Merge "Address API council feedback." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 9e30843..70cda8b 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -71,6 +71,8 @@
     ":android.appwidget.flags-aconfig-java{.generated_srcjars}",
     ":android.webkit.flags-aconfig-java{.generated_srcjars}",
     ":android.provider.flags-aconfig-java{.generated_srcjars}",
+    ":android.chre.flags-aconfig-java{.generated_srcjars}",
+    ":android.speech.flags-aconfig-java{.generated_srcjars}",
 ]
 
 filegroup {
@@ -905,3 +907,23 @@
     aconfig_declarations: "android.provider.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// ContextHub
+java_aconfig_library {
+    name: "android.chre.flags-aconfig-java",
+    aconfig_declarations: "chre_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+// Speech
+aconfig_declarations {
+    name: "android.speech.flags-aconfig",
+    package: "android.speech.flags",
+    srcs: ["core/java/android/speech/flags/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.speech.flags-aconfig-java",
+    aconfig_declarations: "android.speech.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/Android.bp b/Android.bp
index b139b7e..c84de51 100644
--- a/Android.bp
+++ b/Android.bp
@@ -175,9 +175,6 @@
         // 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.
@@ -218,6 +215,7 @@
         "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",
@@ -269,9 +267,6 @@
     ],
     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
@@ -440,9 +435,6 @@
     ],
     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
@@ -486,9 +478,6 @@
     ],
     compile_dex: false,
     headers_only: true,
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 java_library {
@@ -533,7 +522,7 @@
     },
     lint: {
         enabled: false,
-        baseline_filename: "lint-baseline.xml",
+
     },
 }
 
@@ -558,9 +547,6 @@
     ],
     sdk_version: "core_platform",
     apex_available: ["//apex_available:platform"],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 java_library {
@@ -576,9 +562,6 @@
         "calendar-provider-compat-config",
         "contacts-provider-platform-compat-config",
     ],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 platform_compat_config {
@@ -633,9 +616,6 @@
         "rappor",
     ],
     dxflags: ["--core-library"],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 // utility classes statically linked into framework-wifi and dynamically linked
@@ -664,6 +644,7 @@
     lint: {
         baseline_filename: "lint-baseline.xml",
     },
+    apex_available: ["com.android.wifi"],
 }
 
 filegroup {
diff --git a/ProtoLibraries.bp b/ProtoLibraries.bp
index e7adf20..d03bbd2 100644
--- a/ProtoLibraries.bp
+++ b/ProtoLibraries.bp
@@ -34,7 +34,6 @@
         ":ipconnectivity-proto-src",
         ":libstats_atom_enum_protos",
         ":libstats_atom_message_protos",
-        ":libtombstone_proto-src",
         "core/proto/**/*.proto",
         "libs/incident/**/*.proto",
     ],
diff --git a/STABILITY_OWNERS b/STABILITY_OWNERS
new file mode 100644
index 0000000..a7ecb4d
--- /dev/null
+++ b/STABILITY_OWNERS
@@ -0,0 +1,2 @@
+gaillard@google.com
+
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 117faa2..ecfd86c 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -132,18 +132,20 @@
     },
     {
       "name": "vts_treble_vintf_vendor_test"
+    },
+    {
+      "name": "CtsStrictJavaPackagesTestCases"
     }
   ],
   "postsubmit-ravenwood": [
-    // TODO(ravenwood) promote it to presubmit
-    // TODO: Enable it once the infra knows how to run it.
-//    {
-//      "name": "CtsUtilTestCasesRavenwood",
-//      "file_patterns": [
-//        "*Ravenwood*",
-//        "*ravenwood*"
-//      ]
-//    }
+    {
+      "name": "CtsUtilTestCasesRavenwood",
+      "host": true,
+      "file_patterns": [
+        "*Ravenwood*",
+        "*ravenwood*"
+      ]
+    }
   ],
   "postsubmit-managedprofile-stress": [
     {
diff --git a/WEAR_OWNERS b/WEAR_OWNERS
index 4f3bc27..4127f99 100644
--- a/WEAR_OWNERS
+++ b/WEAR_OWNERS
@@ -4,3 +4,9 @@
 adsule@google.com
 andriyn@google.com
 yfz@google.com
+con@google.com
+leetodd@google.com
+sadrul@google.com
+rwmyers@google.com
+nalmalki@google.com
+shijianli@google.com
diff --git a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java
index b7460cd..fa4387b 100644
--- a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java
+++ b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java
@@ -433,6 +433,17 @@
         performMultithreadedReadWriteTest();
     }
 
+    /**
+     * This test measures a multi-threaded read-write environment where there are 2 readers and
+     * 1 writer in the database using WAL journal mode and NORMAL syncMode.
+     */
+    @Test
+    public void testMultithreadedReadWriteWithWalNormal() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
+        insertT1TestDataSet();
+        performMultithreadedReadWriteTest();
+    }
+
     private void doReadLoop(int totalIterations) {
         Random rnd = new Random(0);
         int currentIteration = 0;
@@ -472,7 +483,6 @@
     }
 
     private void doUpdateLoop(int totalIterations) {
-        SQLiteDatabase db = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
         Random rnd = new Random(0);
         int i = 0;
         ContentValues cv = new ContentValues();
@@ -485,24 +495,12 @@
             cv.put("COL_B", "UpdatedValue");
             cv.put("COL_C", i);
             argArray[0] = String.valueOf(id);
-            db.update("T1", cv, "_ID=?", argArray);
+            mDatabase.update("T1", cv, "_ID=?", argArray);
             i++;
             android.os.Trace.endSection();
         }
     }
 
-    /**
-     * This test measures a multi-threaded read-write environment where there are 2 readers and
-     * 1 writer in the database using WAL journal mode and NORMAL syncMode.
-     */
-    @Test
-    public void testMultithreadedReadWriteWithWalNormal() {
-        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
-        insertT1TestDataSet();
-
-        performMultithreadedReadWriteTest();
-    }
-
     private void performMultithreadedReadWriteTest() {
         int totalBGIterations = 10000;
         // Writer - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations
@@ -555,4 +553,3 @@
         createOrOpenTestDatabase(journalMode, syncMode);
     }
 }
-
diff --git a/apct-tests/perftests/permission/Android.bp b/apct-tests/perftests/permission/Android.bp
new file mode 100644
index 0000000..b80a6af
--- /dev/null
+++ b/apct-tests/perftests/permission/Android.bp
@@ -0,0 +1,87 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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: "PermissionServicePerfTests",
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+
+    static_libs: [
+        "platform-compat-test-rules",
+        "androidx.appcompat_appcompat",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "androidx.annotation_annotation",
+        "apct-perftests-utils",
+        "androidx.benchmark_benchmark-common",
+        "androidx.benchmark_benchmark-junit4",
+        "collector-device-lib-platform",
+        "cts-install-lib-java",
+    ],
+
+    libs: ["android.test.base"],
+
+    platform_apis: true,
+
+    test_suites: ["device-tests"],
+
+    data: [
+        ":UsePermissionApp0",
+        ":UsePermissionApp1",
+        ":UsePermissionApp2",
+        ":UsePermissionApp3",
+        ":UsePermissionApp4",
+        ":UsePermissionApp5",
+        ":UsePermissionApp6",
+        ":UsePermissionApp7",
+        ":UsePermissionApp8",
+        ":UsePermissionApp9",
+        ":UsePermissionApp10",
+        ":UsePermissionApp11",
+        ":UsePermissionApp12",
+        ":UsePermissionApp13",
+        ":UsePermissionApp14",
+        ":UsePermissionApp15",
+        ":UsePermissionApp16",
+        ":UsePermissionApp17",
+        ":UsePermissionApp18",
+        ":UsePermissionApp19",
+        ":UsePermissionApp20",
+        ":UsePermissionApp21",
+        ":UsePermissionApp22",
+        ":UsePermissionApp23",
+        ":UsePermissionApp24",
+        ":UsePermissionApp25",
+        ":UsePermissionApp26",
+        ":UsePermissionApp27",
+        ":UsePermissionApp28",
+        ":UsePermissionApp29",
+        ":perfetto_artifacts",
+    ],
+
+    certificate: "platform",
+
+}
diff --git a/apct-tests/perftests/permission/AndroidManifest.xml b/apct-tests/perftests/permission/AndroidManifest.xml
new file mode 100644
index 0000000..fa29ad0
--- /dev/null
+++ b/apct-tests/perftests/permission/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.perftests.permission">
+
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name="android.perftests.utils.PerfTestActivity"
+            android:exported="true">
+          <intent-filter>
+            <action android:name="android.perftests.permission.PERFTEST" />
+          </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.perftests.permission"/>
+
+</manifest>
diff --git a/apct-tests/perftests/permission/AndroidTest.xml b/apct-tests/perftests/permission/AndroidTest.xml
new file mode 100644
index 0000000..07558de
--- /dev/null
+++ b/apct-tests/perftests/permission/AndroidTest.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT 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 PermissionServicePerfTests 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="PermissionServicePerfTests.apk"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="force-queryable" value="false"/>
+        <option name="test-file-name" value="UsePermissionApp0.apk"/>
+        <option name="test-file-name" value="UsePermissionApp1.apk"/>
+        <option name="test-file-name" value="UsePermissionApp2.apk"/>
+        <option name="test-file-name" value="UsePermissionApp3.apk"/>
+        <option name="test-file-name" value="UsePermissionApp4.apk"/>
+        <option name="test-file-name" value="UsePermissionApp5.apk"/>
+        <option name="test-file-name" value="UsePermissionApp6.apk"/>
+        <option name="test-file-name" value="UsePermissionApp7.apk"/>
+        <option name="test-file-name" value="UsePermissionApp8.apk"/>
+        <option name="test-file-name" value="UsePermissionApp9.apk"/>
+        <option name="test-file-name" value="UsePermissionApp10.apk"/>
+        <option name="test-file-name" value="UsePermissionApp11.apk"/>
+        <option name="test-file-name" value="UsePermissionApp12.apk"/>
+        <option name="test-file-name" value="UsePermissionApp13.apk"/>
+        <option name="test-file-name" value="UsePermissionApp14.apk"/>
+        <option name="test-file-name" value="UsePermissionApp15.apk"/>
+        <option name="test-file-name" value="UsePermissionApp16.apk"/>
+        <option name="test-file-name" value="UsePermissionApp17.apk"/>
+        <option name="test-file-name" value="UsePermissionApp18.apk"/>
+        <option name="test-file-name" value="UsePermissionApp19.apk"/>
+        <option name="test-file-name" value="UsePermissionApp20.apk"/>
+        <option name="test-file-name" value="UsePermissionApp21.apk"/>
+        <option name="test-file-name" value="UsePermissionApp22.apk"/>
+        <option name="test-file-name" value="UsePermissionApp23.apk"/>
+        <option name="test-file-name" value="UsePermissionApp24.apk"/>
+        <option name="test-file-name" value="UsePermissionApp25.apk"/>
+        <option name="test-file-name" value="UsePermissionApp26.apk"/>
+        <option name="test-file-name" value="UsePermissionApp27.apk"/>
+        <option name="test-file-name" value="UsePermissionApp28.apk"/>
+        <option name="test-file-name" value="UsePermissionApp29.apk"/>
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.perftests.permission"/>
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/local/PermissionServicePerfTests"/>
+        <option name="collect-on-run-ended-only" value="true"/>
+    </metrics_collector>
+
+    <!-- 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"/>
+        <!--Install the content provider automatically when we push some file in sdcard folder.-->
+        <!--Needed to avoid the installation during the test suite.-->
+        <option name="push-file" key="trace_config_detailed.textproto"
+                value="/sdcard/sample.textproto"/>
+        <option name="push-file" key="UsePermissionApp0.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp0.apk" />
+        <option name="push-file" key="UsePermissionApp1.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp1.apk" />
+        <option name="push-file" key="UsePermissionApp2.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp2.apk" />
+        <option name="push-file" key="UsePermissionApp3.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp3.apk" />
+        <option name="push-file" key="UsePermissionApp4.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp4.apk" />
+        <option name="push-file" key="UsePermissionApp5.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp5.apk" />
+        <option name="push-file" key="UsePermissionApp6.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp6.apk" />
+        <option name="push-file" key="UsePermissionApp7.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp7.apk" />
+        <option name="push-file" key="UsePermissionApp8.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp8.apk" />
+        <option name="push-file" key="UsePermissionApp9.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp9.apk" />
+        <option name="push-file" key="UsePermissionApp10.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp10.apk" />
+        <option name="push-file" key="UsePermissionApp11.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp11.apk" />
+        <option name="push-file" key="UsePermissionApp12.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp12.apk" />
+        <option name="push-file" key="UsePermissionApp13.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp13.apk" />
+        <option name="push-file" key="UsePermissionApp14.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp14.apk" />
+        <option name="push-file" key="UsePermissionApp15.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp15.apk" />
+        <option name="push-file" key="UsePermissionApp16.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp16.apk" />
+        <option name="push-file" key="UsePermissionApp17.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp17.apk" />
+        <option name="push-file" key="UsePermissionApp18.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp18.apk" />
+        <option name="push-file" key="UsePermissionApp19.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp19.apk" />
+        <option name="push-file" key="UsePermissionApp20.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp20.apk" />
+        <option name="push-file" key="UsePermissionApp21.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp21.apk" />
+        <option name="push-file" key="UsePermissionApp22.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp22.apk" />
+        <option name="push-file" key="UsePermissionApp23.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp23.apk" />
+        <option name="push-file" key="UsePermissionApp24.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp24.apk" />
+        <option name="push-file" key="UsePermissionApp25.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp25.apk" />
+        <option name="push-file" key="UsePermissionApp26.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp26.apk" />
+        <option name="push-file" key="UsePermissionApp27.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp27.apk" />
+        <option name="push-file" key="UsePermissionApp28.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp28.apk" />
+        <option name="push-file" key="UsePermissionApp29.apk"
+                value="/data/local/tmp/perftests/UsePermissionApp29.apk" />
+    </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="android.perftests.permission"/>
+        <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"/>
+
+        <!--
+         PackageInstallerBenchmark will break for 5 minutes time out so it changes to 10 minutes
+          -->
+        <option name="test-timeout" value="600000" />
+    </test>
+
+
+</configuration>
diff --git a/apct-tests/perftests/permission/apps/usepermissionapp/Android.bp b/apct-tests/perftests/permission/apps/usepermissionapp/Android.bp
new file mode 100644
index 0000000..1ad20b6
--- /dev/null
+++ b/apct-tests/perftests/permission/apps/usepermissionapp/Android.bp
@@ -0,0 +1,232 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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_helper_app {
+    name: "UsePermissionApp0",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration0",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp1",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration1",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp2",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration2",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp3",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration3",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp4",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration4",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp5",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration5",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp6",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration6",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp7",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration7",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp8",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration8",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp9",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration9",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp10",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration10",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp11",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration11",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp12",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration12",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp13",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration13",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp14",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration14",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp15",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration15",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp16",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration16",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp17",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration17",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp18",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration18",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp19",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration19",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp20",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration20",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp21",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration21",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp22",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration22",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp23",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration23",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp24",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration24",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp25",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration25",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp26",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration26",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp27",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration27",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp28",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration28",
+    ],
+}
+
+android_test_helper_app {
+    name: "UsePermissionApp29",
+    aaptflags: [
+        "--rename-manifest-package android.perftests.appenumeration29",
+    ],
+}
diff --git a/apct-tests/perftests/permission/apps/usepermissionapp/AndroidManifest.xml b/apct-tests/perftests/permission/apps/usepermissionapp/AndroidManifest.xml
new file mode 100644
index 0000000..3bccefd
--- /dev/null
+++ b/apct-tests/perftests/permission/apps/usepermissionapp/AndroidManifest.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.perftests.appenumeration">
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
+    <uses-permission android:name="android.permission.RECORD_BACKGROUND_AUDIO" />
+    <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
+    <uses-permission android:name="android.permission.BACKGROUND_CAMERA" />
+    <uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
+    <uses-permission android:name="android.permission.BODY_SENSORS" />
+    <uses-permission android:name="android.permission.USE_BIOMETRIC" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+    <uses-permission android:name="android.permission.SET_WALLPAPER" />
+    <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
+
+    <queries>
+        <package android:name="android.perftests.appenumeration0" />
+        <package android:name="android.perftests.appenumeration1" />
+        <package android:name="android.perftests.appenumeration2" />
+        <package android:name="android.perftests.appenumeration3" />
+        <package android:name="android.perftests.appenumeration4" />
+        <package android:name="android.perftests.appenumeration5" />
+        <package android:name="android.perftests.appenumeration6" />
+        <package android:name="android.perftests.appenumeration7" />
+        <package android:name="android.perftests.appenumeration8" />
+        <package android:name="android.perftests.appenumeration9" />
+        <package android:name="android.perftests.appenumeration10" />
+        <package android:name="android.perftests.appenumeration11" />
+        <package android:name="android.perftests.appenumeration12" />
+        <package android:name="android.perftests.appenumeration13" />
+        <package android:name="android.perftests.appenumeration14" />
+        <package android:name="android.perftests.appenumeration15" />
+        <package android:name="android.perftests.appenumeration16" />
+        <package android:name="android.perftests.appenumeration17" />
+        <package android:name="android.perftests.appenumeration18" />
+        <package android:name="android.perftests.appenumeration19" />
+        <package android:name="android.perftests.appenumeration20" />
+        <package android:name="android.perftests.appenumeration21" />
+        <package android:name="android.perftests.appenumeration22" />
+        <package android:name="android.perftests.appenumeration23" />
+        <package android:name="android.perftests.appenumeration24" />
+        <package android:name="android.perftests.appenumeration25" />
+        <package android:name="android.perftests.appenumeration26" />
+        <package android:name="android.perftests.appenumeration27" />
+        <package android:name="android.perftests.appenumeration28" />
+        <package android:name="android.perftests.appenumeration29" />
+    </queries>
+
+    <application android:hasCode="false" >
+        <activity android:name="android.perftests.utils.PerfTestActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.perftests.permission.PERFTEST" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/apct-tests/perftests/permission/src/android/perftests/permission/PermissionServicePerfTest.kt b/apct-tests/perftests/permission/src/android/perftests/permission/PermissionServicePerfTest.kt
new file mode 100644
index 0000000..13e67e3
--- /dev/null
+++ b/apct-tests/perftests/permission/src/android/perftests/permission/PermissionServicePerfTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.perftests.permission
+
+import android.Manifest
+import android.os.ParcelFileDescriptor
+import android.os.Trace
+import android.perftests.utils.PerfManualStatusReporter
+import android.perftests.utils.TraceMarkParser
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.AdoptShellPermissionsRule
+import com.android.compatibility.common.util.SystemUtil.eventually
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStreamReader
+import java.util.concurrent.TimeUnit
+import java.util.function.BiConsumer
+
+@RunWith(AndroidJUnit4::class)
+class PermissionServicePerfTest {
+    @get:Rule val mPerfManualStatusReporter = PerfManualStatusReporter()
+    @get:Rule val mAdoptShellPermissionsRule = AdoptShellPermissionsRule(
+        InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+        Manifest.permission.INSTALL_PACKAGES,
+        Manifest.permission.DELETE_PACKAGES
+    )
+    val mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation()
+
+    @Test
+    fun testInstallPackages() {
+        mUiAutomation.executeShellCommand(COMMAND_TRACE_START)
+        eventually { assertThat(Trace.isTagEnabled(TRACE_TAG)).isTrue() }
+        val benchmarkState = mPerfManualStatusReporter.benchmarkState
+        val durations = ArrayList<Long>()
+
+        while (benchmarkState.keepRunning(durations)) {
+            uninstallAllTestApps()
+            installAllTestApps()
+
+            val parser = TraceMarkParser { line -> line.name.contains(PKG_INSTALL_TRACE_PREFIX) }
+            dumpResult(parser) { _, slices ->
+                slices.forEachIndexed { _, slice ->
+                    durations.add(TimeUnit.MICROSECONDS.toNanos(slice.durationInMicroseconds))
+                }
+            }
+        }
+
+        mUiAutomation.executeShellCommand(COMMAND_TRACE_END)
+    }
+
+    private fun installAllTestApps() {
+        for (i in 0..29) {
+            installTestApp(i)
+        }
+    }
+
+    private fun installTestApp(appId: Int) {
+        val apkPath = "$APK_DIR$APK_NAME$appId.apk"
+        runShellCommand("pm install -t $apkPath")
+    }
+
+    private fun uninstallAllTestApps() {
+        for (i in 0..29) {
+            uninstallTestApp(i)
+        }
+    }
+
+    private fun uninstallTestApp(appId: Int) {
+        val packageName = "$PKG_NAME$appId"
+        runShellCommand("pm uninstall $packageName")
+    }
+
+    private fun dumpResult(
+        parser: TraceMarkParser,
+        handler: BiConsumer<String, List<TraceMarkParser.TraceMarkSlice>>
+    ) {
+        parser.reset()
+        try {
+            val inputStream = ParcelFileDescriptor.AutoCloseInputStream(
+                mUiAutomation.executeShellCommand(COMMAND_TRACE_DUMP)
+            )
+            val reader = BufferedReader(InputStreamReader(inputStream))
+            var line = reader.readLine()
+            while (line != null) {
+                parser.visit(line)
+                line = reader.readLine()
+            }
+        } catch (e: IOException) {
+            Log.e(LOG_TAG, "IO error while reading trace dump file.")
+        }
+        parser.forAllSlices(handler)
+    }
+
+    companion object {
+        private val LOG_TAG = PermissionServicePerfTest::class.java.simpleName
+        private const val TRACE_TAG = Trace.TRACE_TAG_PACKAGE_MANAGER
+        private const val PKG_INSTALL_TRACE_PREFIX =
+            "TaggedTracingPermissionManagerServiceImpl#onPackageInstalled"
+        private const val COMMAND_TRACE_START = "atrace --async_start -b 16000 pm"
+        private const val COMMAND_TRACE_END = "atrace --async_stop"
+        private const val COMMAND_TRACE_DUMP = "atrace --async_dump"
+        private const val APK_DIR = "/data/local/tmp/perftests/"
+        private const val APK_NAME = "UsePermissionApp"
+        private const val PKG_NAME = "android.perftests.appenumeration"
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 5a32a02..abf8008 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -115,6 +115,7 @@
 import android.os.ShellCallback;
 import android.os.ShellCommand;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.ThreadLocalWorkSource;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -229,6 +230,9 @@
 
     private static final long TEMPORARY_QUOTA_DURATION = INTERVAL_DAY;
 
+    // System property read on some device configurations to initialize time properly.
+    private static final String TIMEOFFSET_PROPERTY = "persist.sys.time.offset";
+
     private final Intent mBackgroundIntent
             = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND);
 
@@ -2142,6 +2146,9 @@
             // "GMT" if the ID is unrecognized). The parameter ID is used here rather than
             // newZone.getId(). It will be rejected if it is invalid.
             timeZoneWasChanged = SystemTimeZone.setTimeZoneId(tzId, confidence, logInfo);
+
+            final int gmtOffset = newZone.getOffset(mInjector.getCurrentTimeMillis());
+            SystemProperties.set(TIMEOFFSET_PROPERTY, String.valueOf(gmtOffset));
         }
 
         // Clear the default time zone in the system server process. This forces the next call
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 d940e38..b0f378d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -159,6 +159,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -513,6 +514,10 @@
                     if (name == null) {
                         continue;
                     }
+                    if (DEBUG) {
+                        Slog.d(TAG, "DeviceConfig " + name
+                                + " changed to " + properties.getString(name, null));
+                    }
                     switch (name) {
                         case Constants.KEY_ENABLE_API_QUOTAS:
                         case Constants.KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC:
@@ -3507,7 +3512,10 @@
                 }
 
                 final boolean shouldForceBatchJob;
-                if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) {
+                if (job.overrideState > JobStatus.OVERRIDE_NONE) {
+                    // The job should run for some test. Don't force batch it.
+                    shouldForceBatchJob = false;
+                } else if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) {
                     // Never batch expedited or user-initiated jobs, even for RESTRICTED apps.
                     shouldForceBatchJob = false;
                 } else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) {
@@ -4960,6 +4968,8 @@
         Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + namespace + "/" + userId
                 + " " + jobId + " s=" + satisfied + " f=" + force);
 
+        final CountDownLatch delayLatch = new CountDownLatch(1);
+        final JobStatus js;
         try {
             final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
                     userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4968,7 +4978,7 @@
             }
 
             synchronized (mLock) {
-                final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+                js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
                 if (js == null) {
                     return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
                 }
@@ -4979,23 +4989,71 @@
                 // Re-evaluate constraints after the override is set in case one of the overridden
                 // constraints was preventing another constraint from thinking it needed to update.
                 for (int c = mControllers.size() - 1; c >= 0; --c) {
-                    mControllers.get(c).reevaluateStateLocked(uid);
+                    mControllers.get(c).evaluateStateLocked(js);
                 }
 
                 if (!js.isConstraintsSatisfied()) {
-                    js.overrideState = JobStatus.OVERRIDE_NONE;
-                    return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS;
+                    if (js.hasConnectivityConstraint()
+                            && !js.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)
+                            && js.wouldBeReadyWithConstraint(JobStatus.CONSTRAINT_CONNECTIVITY)) {
+                        // Because of how asynchronous the connectivity signals are, JobScheduler
+                        // may not get the connectivity satisfaction signal immediately. In this
+                        // case, wait a few seconds to see if it comes in before saying the
+                        // connectivity constraint isn't satisfied.
+                        mHandler.postDelayed(
+                                checkConstraintRunnableForTesting(
+                                        mHandler, js, delayLatch, 5, 1000),
+                                1000);
+                    } else {
+                        // There's no asynchronous signal to wait for. We can immediately say the
+                        // job's constraints aren't satisfied and return.
+                        js.overrideState = JobStatus.OVERRIDE_NONE;
+                        return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS;
+                    }
+                } else {
+                    delayLatch.countDown();
                 }
-
-                queueReadyJobsForExecutionLocked();
-                maybeRunPendingJobsLocked();
             }
         } catch (RemoteException e) {
             // can't happen
+            return 0;
+        }
+
+        // Choose to block the return until we're sure about the state of the connectivity job
+        // so that tests can expect a reliable state after calling the run command.
+        try {
+            delayLatch.await(7L, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Slog.e(TAG, "Couldn't wait for asynchronous constraint change", e);
+        }
+
+        synchronized (mLock) {
+            if (!js.isConstraintsSatisfied()) {
+                js.overrideState = JobStatus.OVERRIDE_NONE;
+                return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS;
+            }
+
+            queueReadyJobsForExecutionLocked();
+            maybeRunPendingJobsLocked();
         }
         return 0;
     }
 
+    private static Runnable checkConstraintRunnableForTesting(@NonNull final Handler handler,
+            @NonNull final JobStatus js, @NonNull final CountDownLatch latch,
+            final int remainingAttempts, final long delayMs) {
+        return () -> {
+            if (remainingAttempts <= 0 || js.isConstraintsSatisfied()) {
+                latch.countDown();
+                return;
+            }
+            handler.postDelayed(
+                    checkConstraintRunnableForTesting(
+                            handler, js, latch, remainingAttempts - 1, delayMs),
+                    delayMs);
+        };
+    }
+
     // Shell command infrastructure: immediately timeout currently executing jobs
     int executeStopCommand(PrintWriter pw, String pkgName, int userId,
             @Nullable String namespace, boolean hasJobId, int jobId,
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 bdc2246..d39863c 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
@@ -2192,7 +2192,7 @@
      * @return Whether or not this job would be ready to run if it had the specified constraint
      * granted, based on its requirements.
      */
-    boolean wouldBeReadyWithConstraint(int constraint) {
+    public boolean wouldBeReadyWithConstraint(int constraint) {
         return readinessStatusWithConstraint(constraint, true);
     }
 
diff --git a/api/Android.bp b/api/Android.bp
index 363197a..1686943 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -444,3 +444,12 @@
         targets: ["droid"],
     },
 }
+
+phony_rule {
+    name: "checkapi",
+    phony_deps: [
+        "frameworks-base-api-current-compat",
+        "frameworks-base-api-system-current-compat",
+        "frameworks-base-api-module-lib-current-compat",
+    ],
+}
diff --git a/api/Android.mk b/api/Android.mk
deleted file mode 100644
index ce5f995..0000000
--- a/api/Android.mk
+++ /dev/null
@@ -1,2 +0,0 @@
-.PHONY: checkapi
-checkapi: frameworks-base-api-current-compat frameworks-base-api-system-current-compat frameworks-base-api-module-lib-current-compat
diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp
index bcfb68f..6b8c02f 100644
--- a/api/ApiDocs.bp
+++ b/api/ApiDocs.bp
@@ -183,6 +183,7 @@
         "-federationapi AndroidX $(location :current-androidx-api)",
         // doclava contains checks for a few issues that are have been migrated to metalava.
         // disable them in doclava, to avoid mistriggering or double triggering.
+        "-hide 101", // TODO: turn Lint 101 back into an error again
         "-hide 111", // HIDDEN_SUPERCLASS
         "-hide 113", // DEPRECATION_MISMATCH
         "-hide 125", // REQUIRES_PERMISSION
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 28b2d4b..ef1fa60 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -900,10 +900,19 @@
     ],
     api_levels_sdk_type: "system",
     extensions_info_file: ":sdk-extensions-info",
+    dists: [
+        // Make the api-versions.xml file for the system API available in the
+        // sdk build target.
+        {
+            targets: ["sdk"],
+            dest: "api-versions_system.xml",
+            tag: ".api_versions.xml",
+        },
+    ],
 }
 
 // This module can be built with:
-// m out/soong/.intermediates/frameworks/base/api_versions_module_lib/android_common/metalava/api-versions.xml
+// m out/soong/.intermediates/frameworks/base/api/api_versions_module_lib/android_common/metalava/api-versions.xml
 droidstubs {
     name: "api_versions_module_lib",
     srcs: [":android_module_stubs_current_with_test_libs{.jar}"],
diff --git a/api/api.go b/api/api.go
index 2668999..43713aa 100644
--- a/api/api.go
+++ b/api/api.go
@@ -64,6 +64,7 @@
 
 type CombinedApis struct {
 	android.ModuleBase
+	android.DefaultableModuleBase
 
 	properties CombinedApisProperties
 }
@@ -74,6 +75,7 @@
 
 func registerBuildComponents(ctx android.RegistrationContext) {
 	ctx.RegisterModuleType("combined_apis", combinedApisModuleFactory)
+	ctx.RegisterModuleType("combined_apis_defaults", CombinedApisModuleDefaultsFactory)
 }
 
 var PrepareForCombinedApisTest = android.FixtureRegisterWithContext(registerBuildComponents)
@@ -409,6 +411,7 @@
 	module := &CombinedApis{}
 	module.AddProperties(&module.properties)
 	android.InitAndroidModule(module)
+	android.InitDefaultableModule(module)
 	android.AddLoadHook(module, func(ctx android.LoadHookContext) { module.createInternalModules(ctx) })
 	return module
 }
@@ -445,3 +448,16 @@
 	}
 	return s2
 }
+
+// Defaults
+type CombinedApisModuleDefaults struct {
+	android.ModuleBase
+	android.DefaultsModuleBase
+}
+
+func CombinedApisModuleDefaultsFactory() android.Module {
+	module := &CombinedApisModuleDefaults{}
+	module.AddProperties(&CombinedApisProperties{})
+	android.InitDefaultsModule(module)
+	return module
+}
diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md
index e7361fe..b6e4e0d 100644
--- a/cmds/uinput/README.md
+++ b/cmds/uinput/README.md
@@ -7,11 +7,23 @@
   or app (such as the CTS tests via [`UinputDevice`][UinputDevice]).
 * `uinput <filename>` reads commands from a file instead of standard input.
 
+There are also two supported input formats, described in the sections below. The tool will
+automatically detect which format is being used.
+
 [UinputDevice]: https://cs.android.com/android/platform/superproject/main/+/main:cts/libs/input/src/com/android/cts/input/UinputDevice.java
 
-## Command format
+## evemu recording format (recommended)
 
-Input commands should be in JSON format, though the parser is in [lenient mode] to allow comments,
+`uinput` supports the evemu format, as used by the [FreeDesktop project's evemu suite][FreeDesktop].
+This is a simple text-based format compatible with recording and replay tools on other platforms.
+However, it only supports playback of events from one device from a single recording. Recordings can
+be made using the `evemu-record` command on Android or other Linux-based OSes.
+
+[FreeDesktop]: https://gitlab.freedesktop.org/libevdev/evemu
+
+## JSON-like format
+
+The other supported format is JSON-based, though the parser is in [lenient mode] to allow comments,
 and integers can be specified in hexadecimal (e.g. `0xABCD`). The input file (or standard input) can
 contain multiple commands, which will be executed in sequence. Simply add multiple JSON objects to
 the file, one after the other without separators:
@@ -34,9 +46,9 @@
 [lenient mode]: https://developer.android.com/reference/android/util/JsonReader#setLenient(boolean)
 [cts-example-jsons]: https://cs.android.com/android/platform/superproject/main/+/main:cts/tests/tests/hardware/res/raw/
 
-## Command reference
+### Command reference
 
-### `register`
+#### `register`
 
 Register a new uinput device
 
@@ -122,7 +134,7 @@
 
 [struct input_absinfo]: https://cs.android.com/android/platform/superproject/main/+/main:bionic/libc/kernel/uapi/linux/input.h?q=%22struct%20input_absinfo%22
 
-#### Waiting for registration
+##### Waiting for registration
 
 After the command is sent, there will be a delay before the device is set up by the Android input
 stack, and `uinput` does not wait for that process to finish. Any commands sent to the device during
@@ -135,12 +147,12 @@
 
 [onInputDeviceAdded]: https://developer.android.com/reference/android/hardware/input/InputManager.InputDeviceListener.html
 
-#### Unregistering the device
+##### Unregistering the device
 
 As soon as EOF is reached (either in interactive mode, or in file mode), the device that was created
 will be unregistered. There is no explicit command for unregistering a device.
 
-### `delay`
+#### `delay`
 
 Add a delay to command processing
 
@@ -160,7 +172,7 @@
 }
 ```
 
-### `inject`
+#### `inject`
 
 Send an array of uinput event packets to the uinput device
 
@@ -190,7 +202,7 @@
 }
 ```
 
-### `sync`
+#### `sync`
 
 A command used to get a response once the command is processed. When several `inject` and `delay`
 commands are used in a row, the `sync` command can be used to track the progress of the command
diff --git a/core/api/current.txt b/core/api/current.txt
index 5765a22..ac7e6cc 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -49,6 +49,7 @@
     field public static final String BIND_SCREENING_SERVICE = "android.permission.BIND_SCREENING_SERVICE";
     field public static final String BIND_TELECOM_CONNECTION_SERVICE = "android.permission.BIND_TELECOM_CONNECTION_SERVICE";
     field public static final String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE";
+    field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String BIND_TV_AD_SERVICE = "android.permission.BIND_TV_AD_SERVICE";
     field public static final String BIND_TV_INPUT = "android.permission.BIND_TV_INPUT";
     field public static final String BIND_TV_INTERACTIVE_APP = "android.permission.BIND_TV_INTERACTIVE_APP";
     field public static final String BIND_VISUAL_VOICEMAIL_SERVICE = "android.permission.BIND_VISUAL_VOICEMAIL_SERVICE";
@@ -102,6 +103,7 @@
     field public static final String FOREGROUND_SERVICE_HEALTH = "android.permission.FOREGROUND_SERVICE_HEALTH";
     field public static final String FOREGROUND_SERVICE_LOCATION = "android.permission.FOREGROUND_SERVICE_LOCATION";
     field public static final String FOREGROUND_SERVICE_MEDIA_PLAYBACK = "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK";
+    field @FlaggedApi("android.content.pm.introduce_media_processing_type") public static final String FOREGROUND_SERVICE_MEDIA_PROCESSING = "android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING";
     field public static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
     field public static final String FOREGROUND_SERVICE_MICROPHONE = "android.permission.FOREGROUND_SERVICE_MICROPHONE";
     field public static final String FOREGROUND_SERVICE_PHONE_CALL = "android.permission.FOREGROUND_SERVICE_PHONE_CALL";
@@ -1600,6 +1602,7 @@
     field public static final int switchTextOff = 16843628; // 0x101036c
     field public static final int switchTextOn = 16843627; // 0x101036b
     field public static final int syncable = 16842777; // 0x1010019
+    field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly;
     field public static final int tabStripEnabled = 16843453; // 0x10102bd
     field public static final int tabStripLeft = 16843451; // 0x10102bb
     field public static final int tabStripRight = 16843452; // 0x10102bc
@@ -4369,7 +4372,7 @@
     method public final android.media.session.MediaController getMediaController();
     method @NonNull public android.view.MenuInflater getMenuInflater();
     method @NonNull public android.window.OnBackInvokedDispatcher getOnBackInvokedDispatcher();
-    method @Deprecated public final android.app.Activity getParent();
+    method public final android.app.Activity getParent();
     method @Nullable public android.content.Intent getParentActivityIntent();
     method public android.content.SharedPreferences getPreferences(int);
     method @Nullable public android.net.Uri getReferrer();
@@ -4387,7 +4390,7 @@
     method public void invalidateOptionsMenu();
     method public boolean isActivityTransitionRunning();
     method public boolean isChangingConfigurations();
-    method @Deprecated public final boolean isChild();
+    method public final boolean isChild();
     method public boolean isDestroyed();
     method public boolean isFinishing();
     method public boolean isImmersive();
@@ -5315,6 +5318,7 @@
     ctor @Deprecated public AutomaticZenRule(String, android.content.ComponentName, android.net.Uri, int, boolean);
     ctor public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean);
     ctor public AutomaticZenRule(android.os.Parcel);
+    method @FlaggedApi("android.app.modes_api") public boolean canUpdate();
     method public int describeContents();
     method public android.net.Uri getConditionId();
     method @Nullable public android.content.ComponentName getConfigurationActivity();
@@ -12368,7 +12372,6 @@
 
   public final class ModuleInfo implements android.os.Parcelable {
     method public int describeContents();
-    method @FlaggedApi("android.content.pm.provide_info_of_apk_in_apex") @NonNull public java.util.Collection<java.lang.String> getApkInApexPackageNames();
     method @Nullable public CharSequence getName();
     method @Nullable public String getPackageName();
     method public boolean isHidden();
@@ -12817,7 +12820,7 @@
     method public boolean isPackageSuspended();
     method @CheckResult public abstract boolean isPermissionRevokedByPolicy(@NonNull String, @NonNull String);
     method public abstract boolean isSafeMode();
-    method @FlaggedApi("android.content.pm.get_package_info") @WorkerThread public <T> T parseAndroidManifest(@NonNull String, @NonNull java.util.function.Function<android.content.res.XmlResourceParser,T>) throws java.io.IOException;
+    method @FlaggedApi("android.content.pm.get_package_info") @WorkerThread public <T> T parseAndroidManifest(@NonNull java.io.File, @NonNull java.util.function.Function<android.content.res.XmlResourceParser,T>) throws java.io.IOException;
     method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryActivityProperty(@NonNull String);
     method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryApplicationProperty(@NonNull String);
     method @NonNull public abstract java.util.List<android.content.pm.ResolveInfo> queryBroadcastReceivers(@NonNull android.content.Intent, int);
@@ -13287,6 +13290,7 @@
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8
     field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff
     field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2
+    field @FlaggedApi("android.content.pm.introduce_media_processing_type") @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING = 8192; // 0x2000
     field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 32; // 0x20
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_MICROPHONE}, anyOf={android.Manifest.permission.CAPTURE_AUDIO_OUTPUT, android.Manifest.permission.RECORD_AUDIO}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; // 0x80
     field @Deprecated public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0
@@ -18615,6 +18619,7 @@
   }
 
   public final class SyncFence implements java.lang.AutoCloseable android.os.Parcelable {
+    ctor @FlaggedApi("com.android.window.flags.sdk_desired_present_time") public SyncFence(@NonNull android.hardware.SyncFence);
     method public boolean await(@NonNull java.time.Duration);
     method public boolean awaitForever();
     method public void close();
@@ -19730,7 +19735,7 @@
   @FlaggedApi("com.android.internal.camera.flags.concert_mode") public final class LensIntrinsicsSample {
     ctor @FlaggedApi("com.android.internal.camera.flags.concert_mode") public LensIntrinsicsSample(long, @NonNull float[]);
     method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public float[] getLensIntrinsics();
-    method @FlaggedApi("com.android.internal.camera.flags.concert_mode") public long getTimestamp();
+    method @FlaggedApi("com.android.internal.camera.flags.concert_mode") public long getTimestampNanos();
   }
 
   public final class LensShadingMap {
@@ -26291,6 +26296,7 @@
     field public static final int STATE_FAST_FORWARDING = 4; // 0x4
     field public static final int STATE_NONE = 0; // 0x0
     field public static final int STATE_PAUSED = 2; // 0x2
+    field @FlaggedApi("com.android.media.flags.enable_notifying_activity_manager_with_media_session_status_change") public static final int STATE_PLAYBACK_SUPPRESSED = 12; // 0xc
     field public static final int STATE_PLAYING = 3; // 0x3
     field public static final int STATE_REWINDING = 5; // 0x5
     field public static final int STATE_SKIPPING_TO_NEXT = 10; // 0xa
@@ -28955,7 +28961,7 @@
     method public boolean supportsAidPrefixRegistration();
     method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean unsetOffHostForService(@NonNull android.content.ComponentName);
     method public boolean unsetPreferredService(android.app.Activity);
-    field public static final String ACTION_CHANGE_DEFAULT = "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT";
+    field @Deprecated public static final String ACTION_CHANGE_DEFAULT = "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT";
     field public static final String CATEGORY_OTHER = "other";
     field public static final String CATEGORY_PAYMENT = "payment";
     field public static final String EXTRA_CATEGORY = "category";
@@ -33396,7 +33402,7 @@
 
   @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public final class PowerMonitorReadings {
     method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public long getConsumedEnergy(@NonNull android.os.PowerMonitor);
-    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public long getTimestamp(@NonNull android.os.PowerMonitor);
+    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public long getTimestampMillis(@NonNull android.os.PowerMonitor);
     field @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public static final int ENERGY_UNAVAILABLE = -1; // 0xffffffff
   }
 
@@ -33406,6 +33412,7 @@
     method public static final long getElapsedCpuTime();
     method public static final int[] getExclusiveCores();
     method public static final int getGidForName(String);
+    method @FlaggedApi("com.android.sdksandbox.flags.sdk_sandbox_uid_to_app_uid_api") public static final int getSdkSandboxUidForAppUid(int);
     method public static long getStartElapsedRealtime();
     method public static long getStartRequestedElapsedRealtime();
     method public static long getStartRequestedUptimeMillis();
@@ -33989,8 +33996,8 @@
   }
 
   public class SystemHealthManager {
-    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable android.os.Handler, @NonNull java.util.function.Consumer<android.os.PowerMonitorReadings>, @NonNull java.util.function.Consumer<java.lang.RuntimeException>);
-    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable android.os.Handler, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
+    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PowerMonitorReadings,java.lang.RuntimeException>);
+    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
     method public android.os.health.HealthStats takeMyUidSnapshot();
     method public android.os.health.HealthStats takeUidSnapshot(int);
     method public android.os.health.HealthStats[] takeUidSnapshots(int[]);
@@ -41545,6 +41552,8 @@
     field public static final String EXTRA_LANGUAGE_MODEL = "android.speech.extra.LANGUAGE_MODEL";
     field public static final String EXTRA_LANGUAGE_PREFERENCE = "android.speech.extra.LANGUAGE_PREFERENCE";
     field public static final String EXTRA_LANGUAGE_SWITCH_ALLOWED_LANGUAGES = "android.speech.extra.LANGUAGE_SWITCH_ALLOWED_LANGUAGES";
+    field @FlaggedApi("android.speech.flags.multilang_extra_launch") public static final String EXTRA_LANGUAGE_SWITCH_INITIAL_ACTIVE_DURATION_TIME_MILLIS = "android.speech.extra.LANGUAGE_SWITCH_INITIAL_ACTIVE_DURATION_TIME_MILLIS";
+    field @FlaggedApi("android.speech.flags.multilang_extra_launch") public static final String EXTRA_LANGUAGE_SWITCH_MAX_SWITCHES = "android.speech.extra.LANGUAGE_SWITCH_MAX_SWITCHES";
     field public static final String EXTRA_MASK_OFFENSIVE_WORDS = "android.speech.extra.MASK_OFFENSIVE_WORDS";
     field public static final String EXTRA_MAX_RESULTS = "android.speech.extra.MAX_RESULTS";
     field public static final String EXTRA_ONLY_RETURN_LANGUAGE_PREFERENCE = "android.speech.extra.ONLY_RETURN_LANGUAGE_PREFERENCE";
@@ -45336,7 +45345,7 @@
     method public void addOnSubscriptionsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void addSubscriptionsIntoGroup(@NonNull java.util.List<java.lang.Integer>, @NonNull android.os.ParcelUuid);
     method public boolean canManageSubscription(android.telephony.SubscriptionInfo);
-    method @FlaggedApi("com.android.internal.telephony.flags.work_profile_api_split") @NonNull public android.telephony.SubscriptionManager createForAllUserProfiles();
+    method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull public android.telephony.SubscriptionManager createForAllUserProfiles();
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>);
     method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context);
     method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList();
@@ -47288,6 +47297,7 @@
     method public int getLineForOffset(int);
     method public int getLineForVertical(int);
     method public float getLineLeft(int);
+    method @FlaggedApi("com.android.text.flags.inter_character_justification") @IntRange(from=0) public int getLineLetterSpacingUnitCount(@IntRange(from=0) int, boolean);
     method public float getLineMax(int);
     method public float getLineRight(int);
     method @FlaggedApi("com.android.text.flags.use_bounds_for_width") public final float getLineSpacingAmount();
@@ -51794,6 +51804,7 @@
   public static class SurfaceControl.Transaction implements java.io.Closeable android.os.Parcelable {
     ctor public SurfaceControl.Transaction();
     method @NonNull public android.view.SurfaceControl.Transaction addTransactionCommittedListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.SurfaceControl.TransactionCommittedListener);
+    method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction addTransactionCompletedListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.SurfaceControl.TransactionStats>);
     method public void apply();
     method @NonNull public android.view.SurfaceControl.Transaction clearFrameRate(@NonNull android.view.SurfaceControl);
     method @NonNull public android.view.SurfaceControl.Transaction clearTrustedPresentationCallback(@NonNull android.view.SurfaceControl);
@@ -51810,9 +51821,11 @@
     method @NonNull public android.view.SurfaceControl.Transaction setCrop(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect);
     method @NonNull public android.view.SurfaceControl.Transaction setDamageRegion(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Region);
     method @NonNull public android.view.SurfaceControl.Transaction setDataSpace(@NonNull android.view.SurfaceControl, int);
+    method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction setDesiredPresentTime(long);
     method @NonNull public android.view.SurfaceControl.Transaction setExtendedRangeBrightness(@NonNull android.view.SurfaceControl, float, float);
     method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int);
     method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int, int);
+    method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction setFrameTimeline(long);
     method @Deprecated @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int);
     method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int);
     method @NonNull public android.view.SurfaceControl.Transaction setOpaque(@NonNull android.view.SurfaceControl, boolean);
@@ -51828,6 +51841,11 @@
     method public void onTransactionCommitted();
   }
 
+  @FlaggedApi("com.android.window.flags.sdk_desired_present_time") public static final class SurfaceControl.TransactionStats {
+    method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") public long getLatchTime();
+    method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.hardware.SyncFence getPresentFence();
+  }
+
   public static final class SurfaceControl.TrustedPresentationThresholds {
     ctor public SurfaceControl.TrustedPresentationThresholds(@FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @IntRange(from=1) int);
   }
@@ -53973,6 +53991,7 @@
     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";
     field public static final String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
+    field @FlaggedApi("com.android.window.flags.supports_multi_instance_system_ui") public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI";
   }
 
   public static class WindowManager.BadTokenException extends java.lang.RuntimeException {
@@ -56484,6 +56503,7 @@
     field public static final String TYPE_EMAIL = "email";
     field public static final String TYPE_FLIGHT_NUMBER = "flight";
     field public static final String TYPE_OTHER = "other";
+    field @FlaggedApi("android.service.notification.redact_sensitive_notifications_from_untrusted_listeners") public static final String TYPE_OTP_CODE = "otp_code";
     field public static final String TYPE_PHONE = "phone";
     field public static final String TYPE_UNKNOWN = "";
     field public static final String TYPE_URL = "url";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 0d4169f..a532cdb 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -95,7 +95,7 @@
     field public static final String BYPASS_ROLE_QUALIFICATION = "android.permission.BYPASS_ROLE_QUALIFICATION";
     field public static final String CALL_AUDIO_INTERCEPTION = "android.permission.CALL_AUDIO_INTERCEPTION";
     field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED";
-    field public static final String CAMERA_HEADLESS_SYSTEM_USER = "android.permission.CAMERA_HEADLESS_SYSTEM_USER";
+    field @FlaggedApi("com.android.internal.camera.flags.camera_hsum_permission") public static final String CAMERA_HEADLESS_SYSTEM_USER = "android.permission.CAMERA_HEADLESS_SYSTEM_USER";
     field public static final String CAMERA_OPEN_CLOSE_LISTENER = "android.permission.CAMERA_OPEN_CLOSE_LISTENER";
     field public static final String CAPTURE_AUDIO_HOTWORD = "android.permission.CAPTURE_AUDIO_HOTWORD";
     field public static final String CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD = "android.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD";
@@ -473,8 +473,9 @@
     field public static final int config_defaultCallScreening = 17039398; // 0x1040026
     field public static final int config_defaultDialer = 17039395; // 0x1040023
     field public static final int config_defaultNotes = 17039429; // 0x1040045
-    field public static final int config_defaultRetailDemo;
+    field @FlaggedApi("android.permission.flags.retail_demo_role_enabled") public static final int config_defaultRetailDemo;
     field public static final int config_defaultSms = 17039396; // 0x1040024
+    field @FlaggedApi("android.permission.flags.wallet_role_enabled") public static final int config_defaultWallet;
     field public static final int config_devicePolicyManagement = 17039421; // 0x104003d
     field public static final int config_feedbackIntentExtraKey = 17039391; // 0x104001f
     field public static final int config_feedbackIntentNameKey = 17039392; // 0x1040020
@@ -1595,12 +1596,14 @@
     method public int getDensityLevel();
     method @NonNull public java.time.Instant getEndTime();
     method public int getEventType();
+    method @FlaggedApi("android.app.ambient_heart_rate") @IntRange(from=0xffffffff) public int getRatePerMinute();
     method @NonNull public java.time.Instant getStartTime();
     method @NonNull public android.os.PersistableBundle getVendorData();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.ambientcontext.AmbientContextEvent> CREATOR;
     field public static final int EVENT_BACK_DOUBLE_TAP = 3; // 0x3
     field public static final int EVENT_COUGH = 1; // 0x1
+    field @FlaggedApi("android.app.ambient_heart_rate") public static final int EVENT_HEART_RATE = 4; // 0x4
     field public static final int EVENT_SNORE = 2; // 0x2
     field public static final int EVENT_UNKNOWN = 0; // 0x0
     field public static final int EVENT_VENDOR_WEARABLE_START = 100000; // 0x186a0
@@ -1620,6 +1623,7 @@
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setDensityLevel(int);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEndTime(@NonNull java.time.Instant);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEventType(int);
+    method @FlaggedApi("android.app.ambient_heart_rate") @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setRatePerMinute(@IntRange(from=0xffffffff) int);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setStartTime(@NonNull java.time.Instant);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setVendorData(@NonNull android.os.PersistableBundle);
   }
@@ -4141,7 +4145,7 @@
     field public static final int PROTECTION_FLAG_MODULE = 4194304; // 0x400000
     field public static final int PROTECTION_FLAG_OEM = 16384; // 0x4000
     field public static final int PROTECTION_FLAG_RECENTS = 33554432; // 0x2000000
-    field @Deprecated public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000
+    field public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000
     field public static final int PROTECTION_FLAG_ROLE = 67108864; // 0x4000000
     field public static final int PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER = 65536; // 0x10000
     field public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 32768; // 0x8000
@@ -5747,7 +5751,7 @@
     method public void addOnCompleteListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.OnCompleteListener);
     method public void addOnCompleteListener(@NonNull android.hardware.radio.ProgramList.OnCompleteListener);
     method public void close();
-    method @Deprecated @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier);
+    method @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier);
     method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull public java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramInfos(@NonNull android.hardware.radio.ProgramSelector.Identifier);
     method public void registerListCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.ListCallback);
     method public void registerListCallback(@NonNull android.hardware.radio.ProgramList.ListCallback);
@@ -5799,7 +5803,7 @@
     field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5
     field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5
     field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa
-    field @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb
+    field public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb
     field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9
     field public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; // 0x3
     field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int IDENTIFIER_TYPE_HD_STATION_LOCATION = 15; // 0xf
@@ -5807,8 +5811,8 @@
     field @Deprecated public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; // 0x4
     field public static final int IDENTIFIER_TYPE_INVALID = 0; // 0x0
     field public static final int IDENTIFIER_TYPE_RDS_PI = 2; // 0x2
-    field @Deprecated public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd
-    field @Deprecated public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc
+    field public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd
+    field public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc
     field public static final int IDENTIFIER_TYPE_VENDOR_END = 1999; // 0x7cf
     field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = 1999; // 0x7cf
     field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = 1000; // 0x3e8
@@ -5861,7 +5865,7 @@
     field public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8; // 0x8
     field public static final int CONFIG_DAB_FM_LINKING = 7; // 0x7
     field public static final int CONFIG_DAB_FM_SOFT_LINKING = 9; // 0x9
-    field @Deprecated public static final int CONFIG_FORCE_ANALOG = 2; // 0x2
+    field public static final int CONFIG_FORCE_ANALOG = 2; // 0x2
     field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_AM = 11; // 0xb
     field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_FM = 10; // 0xa
     field public static final int CONFIG_FORCE_DIGITAL = 3; // 0x3
@@ -9967,6 +9971,7 @@
   }
 
   public final class CardEmulation {
+    method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public android.nfc.cardemulation.ApduServiceInfo getPreferredPaymentService();
     method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int);
   }
 
@@ -10571,7 +10576,7 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.RECOVERY, android.Manifest.permission.REBOOT}) public static int rebootAndApply(@NonNull android.content.Context, @NonNull String, boolean) throws java.io.IOException;
     method @RequiresPermission(allOf={android.Manifest.permission.RECOVERY, android.Manifest.permission.REBOOT}) public static void rebootWipeAb(android.content.Context, java.io.File, String) throws java.io.IOException;
     method @RequiresPermission(android.Manifest.permission.RECOVERY) public static void scheduleUpdateOnBoot(android.content.Context, java.io.File) throws java.io.IOException;
-    method public static boolean verifyPackageCompatibility(java.io.File) throws java.io.IOException;
+    method @Deprecated public static boolean verifyPackageCompatibility(java.io.File) throws java.io.IOException;
     field public static final int RESUME_ON_REBOOT_REBOOT_ERROR_INVALID_PACKAGE_NAME = 2000; // 0x7d0
     field public static final int RESUME_ON_REBOOT_REBOOT_ERROR_LSKF_NOT_CAPTURED = 3000; // 0xbb8
     field public static final int RESUME_ON_REBOOT_REBOOT_ERROR_PROVIDER_PREPARATION_FAILURE = 5000; // 0x1388
@@ -14679,6 +14684,7 @@
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isLteCdmaEvdoGsmWcdmaEnabled();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isMobileDataPolicyEnabled(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNrDualConnectivityEnabled();
+    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNullCipherNotificationsEnabled();
     method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isOffhook();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isOpportunisticNetworkEnabled();
     method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isPotentialEmergencyNumber(@NonNull String);
@@ -14718,6 +14724,7 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean);
     method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean);
+    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableNullCipherNotifications(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean);
@@ -15125,7 +15132,7 @@
     method public final void notifyDataCallListChanged(java.util.List<android.telephony.data.DataCallResponse>);
     method public final void notifyDataProfileUnthrottled(@NonNull android.telephony.data.DataProfile);
     method public void requestDataCallList(@NonNull android.telephony.data.DataServiceCallback);
-    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestNetworkValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method public void setDataProfile(@NonNull java.util.List<android.telephony.data.DataProfile>, boolean, @NonNull android.telephony.data.DataServiceCallback);
     method public void setInitialAttachApn(@NonNull android.telephony.data.DataProfile, boolean, @NonNull android.telephony.data.DataServiceCallback);
     method public void setupDataCall(int, @NonNull android.telephony.data.DataProfile, boolean, boolean, int, @Nullable android.net.LinkProperties, @NonNull android.telephony.data.DataServiceCallback);
@@ -16332,7 +16339,7 @@
 
   public interface RegistrationManager {
     field public static final int SUGGESTED_ACTION_NONE = 0; // 0x0
-    field @FlaggedApi("com.android.internal.telephony.flags.add_rat_related_suggested_action_to_ims_registration") public static final int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCK = 4; // 0x4
+    field @FlaggedApi("com.android.internal.telephony.flags.add_rat_related_suggested_action_to_ims_registration") public static final int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCKS = 4; // 0x4
     field public static final int SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK = 1; // 0x1
     field public static final int SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK_WITH_TIMEOUT = 2; // 0x2
     field @FlaggedApi("com.android.internal.telephony.flags.add_rat_related_suggested_action_to_ims_registration") public static final int SUGGESTED_ACTION_TRIGGER_RAT_BLOCK = 3; // 0x3
@@ -17030,8 +17037,8 @@
 
   @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class PointingInfo implements android.os.Parcelable {
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int describeContents();
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public float getSatelliteAzimuthDegrees();
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public float getSatelliteElevationDegrees();
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @FloatRange(from=0xffffff4c, to=180) public float getSatelliteAzimuthDegrees();
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @FloatRange(from=0xffffffa6, to=90) public float getSatelliteElevationDegrees();
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void writeToParcel(@NonNull android.os.Parcel, int);
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.PointingInfo> CREATOR;
   }
@@ -17070,7 +17077,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback) throws android.telephony.satellite.SatelliteManager.SatelliteException;
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback);
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteModemStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteProvisionStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteProvisionStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void removeSatelliteAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsDemoModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
@@ -17091,7 +17098,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForNtnSignalStrengthChanged(@NonNull android.telephony.satellite.NtnSignalStrengthCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteCapabilitiesChanged(@NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteDatagram(@NonNull android.telephony.satellite.SatelliteDatagramCallback);
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteModemStateChanged(@NonNull android.telephony.satellite.SatelliteStateCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteModemStateChanged(@NonNull android.telephony.satellite.SatelliteModemStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteProvisionStateChanged(@NonNull android.telephony.satellite.SatelliteProvisionStateCallback);
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; // 0x2
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1; // 0x1
@@ -17161,12 +17168,12 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int getErrorCode();
   }
 
-  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteProvisionStateCallback {
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteProvisionStateChanged(boolean);
+  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteModemStateCallback {
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteModemStateChanged(int);
   }
 
-  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteStateCallback {
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteModemStateChanged(int);
+  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteProvisionStateCallback {
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteProvisionStateChanged(boolean);
   }
 
   @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteTransmissionUpdateCallback {
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index b2a28b2..0505af4 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -2359,8 +2359,8 @@
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.provisionSatelliteService(String,byte[],android.os.CancellationSignal,java.util.concurrent.Executor,java.util.function.Consumer<java.lang.Integer>)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteDatagram(java.util.concurrent.Executor, android.telephony.satellite.SatelliteDatagramCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteDatagram(java.util.concurrent.Executor,android.telephony.satellite.SatelliteDatagramCallback)
-UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteModemStateChanged(java.util.concurrent.Executor, android.telephony.satellite.SatelliteStateCallback):
-    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteModemStateChanged(java.util.concurrent.Executor,android.telephony.satellite.SatelliteStateCallback)
+UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteModemStateChanged(java.util.concurrent.Executor, android.telephony.satellite.SatelliteModemStateCallback):
+    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteModemStateChanged(java.util.concurrent.Executor,android.telephony.satellite.SatelliteModemStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteProvisionStateChanged(java.util.concurrent.Executor, android.telephony.satellite.SatelliteProvisionStateCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteProvisionStateChanged(java.util.concurrent.Executor,android.telephony.satellite.SatelliteProvisionStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#requestIsDemoModeEnabled(java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>):
@@ -2389,8 +2389,8 @@
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback,java.util.concurrent.Executor,java.util.function.Consumer<java.lang.Integer>)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteDatagram(android.telephony.satellite.SatelliteDatagramCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteDatagram(android.telephony.satellite.SatelliteDatagramCallback)
-UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteStateCallback):
-    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteStateCallback)
+UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteModemStateCallback):
+    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteModemStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteProvisionStateChanged(android.telephony.satellite.SatelliteProvisionStateCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteProvisionStateChanged(android.telephony.satellite.SatelliteProvisionStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager.SatelliteException:
@@ -2403,10 +2403,10 @@
     New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback
 UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean)
-UnflaggedApi: android.telephony.satellite.SatelliteStateCallback:
-    New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteStateCallback
-UnflaggedApi: android.telephony.satellite.SatelliteStateCallback#onSatelliteModemStateChanged(int):
-    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteStateCallback.onSatelliteModemStateChanged(int)
+UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback:
+    New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteModemStateCallback
+UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback#onSatelliteModemStateChanged(int):
+    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteModemStateCallback.onSatelliteModemStateChanged(int)
 UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback:
     New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteTransmissionUpdateCallback
 UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback#onReceiveDatagramStateChanged(int, int, int):
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 572be19..bbe03a3 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -284,6 +284,16 @@
     method public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, boolean, int, int);
   }
 
+  public final class AutomaticZenRule implements android.os.Parcelable {
+    method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields();
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_INTERRUPTION_FILTER = 2; // 0x2
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_NAME = 1; // 0x1
+  }
+
+  @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder {
+    method @FlaggedApi("android.app.modes_api") @NonNull public android.app.AutomaticZenRule.Builder setUserModifiedFields(int);
+  }
+
   public class BroadcastOptions extends android.app.ComponentOptions {
     ctor public BroadcastOptions();
     ctor public BroadcastOptions(@NonNull android.os.Bundle);
@@ -1872,6 +1882,7 @@
     method public void setRampingRingerEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void setRs2Value(float);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setTestDeviceConnectionState(@NonNull android.media.AudioDeviceAttributes, boolean);
+    method @FlaggedApi("android.media.audio.focus_exclusive_with_recording") @RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE) public boolean shouldNotificationSoundPlay(@NonNull android.media.AudioAttributes);
   }
 
   public static final class AudioRecord.MetricsConstants {
@@ -2333,6 +2344,9 @@
     field public static final int CPU_LOAD_RESET = 2; // 0x2
     field public static final int CPU_LOAD_RESUME = 3; // 0x3
     field public static final int CPU_LOAD_UP = 0; // 0x0
+    field @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public static final int GPU_LOAD_DOWN = 6; // 0x6
+    field @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public static final int GPU_LOAD_RESET = 7; // 0x7
+    field @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public static final int GPU_LOAD_UP = 5; // 0x5
   }
 
   public final class PowerManager {
@@ -3007,6 +3021,49 @@
     method @Deprecated public boolean isBound();
   }
 
+  @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable {
+    method public int getUserModifiedFields();
+    field public static final int FIELD_DIM_WALLPAPER = 4; // 0x4
+    field public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 16; // 0x10
+    field public static final int FIELD_DISABLE_TAP_TO_WAKE = 32; // 0x20
+    field public static final int FIELD_DISABLE_TILT_TO_WAKE = 64; // 0x40
+    field public static final int FIELD_DISABLE_TOUCH = 128; // 0x80
+    field public static final int FIELD_GRAYSCALE = 1; // 0x1
+    field public static final int FIELD_MAXIMIZE_DOZE = 512; // 0x200
+    field public static final int FIELD_MINIMIZE_RADIO_USAGE = 256; // 0x100
+    field public static final int FIELD_NIGHT_MODE = 8; // 0x8
+    field public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 2; // 0x2
+  }
+
+  @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder {
+    method @NonNull public android.service.notification.ZenDeviceEffects.Builder setUserModifiedFields(int);
+  }
+
+  public final class ZenPolicy implements android.os.Parcelable {
+    method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields();
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_ALLOW_CHANNELS = 8; // 0x8
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_CALLS = 2; // 0x2
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_CONVERSATIONS = 4; // 0x4
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_MESSAGES = 1; // 0x1
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 128; // 0x80
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 32; // 0x20
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 256; // 0x100
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 64; // 0x40
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 512; // 0x200
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_AMBIENT = 32768; // 0x8000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_BADGE = 16384; // 0x4000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1024; // 0x400
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_LIGHTS = 2048; // 0x800
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 65536; // 0x10000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_PEEK = 4096; // 0x1000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 8192; // 0x2000
+  }
+
+  public static final class ZenPolicy.Builder {
+    ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy);
+    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder setUserModifiedFields(int);
+  }
+
 }
 
 package android.service.quickaccesswallet {
@@ -3678,6 +3735,7 @@
 
   public class AnimationUtils {
     method @FlaggedApi("android.view.flags.expected_presentation_time_read_only") public static void lockAnimationClock(long, long);
+    method public static void lockAnimationClock(long);
     method public static void unlockAnimationClock();
   }
 
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index bf26bd0..5e904ef9 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -535,6 +535,10 @@
     Missing nullability on parameter `foreground` in method `isDefaultFocusHighlightNeeded`
 
 
+OptionalBuilderConstructorArgument: android.service.notification.ZenPolicy.Builder#Builder(android.service.notification.ZenPolicy) parameter #0:
+    Builder constructor arguments must be mandatory (i.e. not @Nullable): parameter policy in android.service.notification.ZenPolicy.Builder(android.service.notification.ZenPolicy policy)
+
+
 ProtectedMember: android.app.AppDetailsActivity#onCreate(android.os.Bundle):
     Protected methods not allowed; must be public: method android.app.AppDetailsActivity.onCreate(android.os.Bundle)}
 ProtectedMember: android.view.ViewGroup#resetResolvedDrawables():
@@ -2143,6 +2147,8 @@
     New API must be flagged with @FlaggedApi: field android.service.notification.NotificationRankingUpdate.PARCELABLE_WRITE_RETURN_VALUE
 UnflaggedApi: android.service.notification.NotificationRankingUpdate#isFdNotNullAndClosed():
     New API must be flagged with @FlaggedApi: method android.service.notification.NotificationRankingUpdate.isFdNotNullAndClosed()
+UnflaggedApi: android.service.notification.ZenPolicy.Builder#Builder(android.service.notification.ZenPolicy):
+    New API must be flagged with @FlaggedApi: constructor android.service.notification.ZenPolicy.Builder(android.service.notification.ZenPolicy)
 UnflaggedApi: android.telephony.TelephonyManager#HAL_SERVICE_SATELLITE:
     New API must be flagged with @FlaggedApi: field android.telephony.TelephonyManager.HAL_SERVICE_SATELLITE
 UnflaggedApi: android.telephony.ims.feature.MmTelFeature.MmTelCapabilities:
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index b4a6955..845a346 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -1311,8 +1311,9 @@
         if (!node.mEnded) {
             float durationScale = ValueAnimator.getDurationScale();
             durationScale = durationScale == 0  ? 1 : durationScale;
-            node.mEnded = node.mAnimation.pulseAnimationFrame(
-                    (long) (animPlayTime * durationScale));
+            if (node.mAnimation.pulseAnimationFrame((long) (animPlayTime * durationScale))) {
+                node.mEnded = true;
+            }
         }
     }
 
diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java
index 1e1f155..5840f02 100644
--- a/core/java/android/animation/ObjectAnimator.java
+++ b/core/java/android/animation/ObjectAnimator.java
@@ -25,8 +25,6 @@
 import android.util.Property;
 import android.view.animation.AccelerateDecelerateInterpolator;
 
-import java.lang.ref.WeakReference;
-
 /**
  * This subclass of {@link ValueAnimator} provides support for animating properties on target objects.
  * The constructors of this class take parameters to define the target object that will be animated
@@ -73,11 +71,7 @@
 
     private static final boolean DBG = false;
 
-    /**
-     * A weak reference to the target object on which the property exists, set
-     * in the constructor. We'll cancel the animation if this goes away.
-     */
-    private WeakReference<Object> mTarget;
+    private Object mTarget;
 
     private String mPropertyName;
 
@@ -919,7 +913,7 @@
      */
     @Nullable
     public Object getTarget() {
-        return mTarget == null ? null : mTarget.get();
+        return mTarget;
     }
 
     @Override
@@ -929,7 +923,7 @@
             if (isStarted()) {
                 cancel();
             }
-            mTarget = target == null ? null : new WeakReference<Object>(target);
+            mTarget = target;
             // New target should cause re-initialization prior to starting
             mInitialized = false;
         }
@@ -977,13 +971,6 @@
     @Override
     void animateValue(float fraction) {
         final Object target = getTarget();
-        if (mTarget != null && target == null) {
-            // We lost the target reference, cancel and clean up. Note: we allow null target if the
-            /// target has never been set.
-            cancel();
-            return;
-        }
-
         super.animateValue(fraction);
         int numValues = mValues.length;
         for (int i = 0; i < numValues; ++i) {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 5d4d5e2..2103055 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -24,7 +24,9 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.inMultiWindowMode;
 import static android.os.Process.myUid;
+
 import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext;
+
 import static java.lang.Character.MIN_VALUE;
 
 import android.annotation.AnimRes;
@@ -45,6 +47,7 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.annotation.UiContext;
+import android.app.ActivityOptions.SceneTransitionInfo;
 import android.app.VoiceInteractor.Request;
 import android.app.admin.DevicePolicyManager;
 import android.app.assist.AssistContent;
@@ -930,8 +933,8 @@
     @UnsupportedAppUsage
     final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
 
-    /** The options for scene transition. */
-    ActivityOptions mPendingOptions;
+    /** The scene transition info. */
+    SceneTransitionInfo mSceneTransitionInfo;
 
     /** Whether this activity was launched from a bubble. **/
     boolean mLaunchedFromBubble;
@@ -1174,23 +1177,12 @@
         return mApplication;
     }
 
-    /**
-     * Whether this is a child {@link Activity} of an {@link ActivityGroup}.
-     *
-     * @deprecated {@link ActivityGroup} is deprecated.
-     */
-    @Deprecated
+    /** Is this activity embedded inside of another activity? */
     public final boolean isChild() {
         return mParent != null;
     }
 
-    /**
-     * Returns the parent {@link Activity} if this is a child {@link Activity} of an
-     * {@link ActivityGroup}.
-     *
-     * @deprecated {@link ActivityGroup} is deprecated.
-     */
-    @Deprecated
+    /** Return the parent activity if this view is an embedded child. */
     public final Activity getParent() {
         return mParent;
     }
@@ -3072,7 +3064,7 @@
     }
 
     /**
-     * Request to put the freeform activity into fullscreen. The requester has to be the top-most
+     * Request to put the activity into fullscreen. The requester must be pinned or the top-most
      * activity of the focused display which can be verified using
      * {@link #onTopResumedActivityChanged(boolean)}. The request should also be a response to a
      * user input. When getting fullscreen and receiving corresponding
@@ -5807,10 +5799,9 @@
 
     private Bundle transferSpringboardActivityOptions(@Nullable Bundle options) {
         if (options == null && (mWindow != null && !mWindow.isActive())) {
-            final ActivityOptions activityOptions = getActivityOptions();
-            if (activityOptions != null &&
-                    activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
-                return activityOptions.toBundle();
+            final SceneTransitionInfo info = getSceneTransitionInfo();
+            if (info != null) {
+                return ActivityOptions.makeBasic().setSceneTransitionInfo(info).toBundle();
             }
         }
         return options;
@@ -8079,8 +8070,10 @@
      *
      * @param callback the method to call when all visible activities behind this one have been
      * drawn and it is safe to make this activity translucent again.
-     * @param options activity options delivered to the activity below this one. The options
-     * are retrieved using {@link #getActivityOptions}.
+     * @param options activity options that created from
+     *             {@link ActivityOptions#makeSceneTransitionAnimation} which will be converted to
+     *             {@link SceneTransitionInfo} and delivered to the activity below this one. The
+     *              options are retrieved using {@link #getSceneTransitionInfo}.
      * @return <code>true</code> if Window was opaque and will become translucent or
      * <code>false</code> if window was translucent and no change needed to be made.
      *
@@ -8116,27 +8109,27 @@
     }
 
     /** @hide */
-    public void onNewActivityOptions(ActivityOptions options) {
-        mActivityTransitionState.setEnterActivityOptions(this, options);
+    public void onNewSceneTransitionInfo(ActivityOptions.SceneTransitionInfo info) {
+        mActivityTransitionState.setEnterSceneTransitionInfo(this, info);
         if (!mStopped) {
             mActivityTransitionState.enterReady(this);
         }
     }
 
     /**
-     * Takes the ActivityOptions passed in from the launching activity or passed back
+     * Takes the {@link SceneTransitionInfo} passed in from the launching activity or passed back
      * from an activity launched by this activity in its call to {@link
      * #convertToTranslucent(TranslucentConversionListener, ActivityOptions)}
      *
-     * @return The ActivityOptions passed to {@link #convertToTranslucent}.
+     * @return The {@link SceneTransitionInfo} which based on the ActivityOptions that originally
+     *         passed to {@link #convertToTranslucent}.
      * @hide
      */
-    @UnsupportedAppUsage
-    ActivityOptions getActivityOptions() {
-        final ActivityOptions options = mPendingOptions;
-        // The option only applies once.
-        mPendingOptions = null;
-        return options;
+    SceneTransitionInfo getSceneTransitionInfo() {
+        final SceneTransitionInfo sceneTransitionInfo = mSceneTransitionInfo;
+        // The info only applies once.
+        mSceneTransitionInfo = null;
+        return sceneTransitionInfo;
     }
 
     /**
@@ -8780,7 +8773,7 @@
         mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
                 com.android.internal.R.styleable.Window_windowNoDisplay, false);
         mFragments.dispatchActivityCreated();
-        mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
+        mActivityTransitionState.setEnterSceneTransitionInfo(this, getSceneTransitionInfo());
         dispatchActivityPostCreated(icicle);
         Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
     }
@@ -8798,7 +8791,7 @@
                     + mComponent.getClassName());
         }
         dispatchActivityPreStarted();
-        mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
+        mActivityTransitionState.setEnterSceneTransitionInfo(this, getSceneTransitionInfo());
         mFragments.noteStateNotSaved();
         mCalled = false;
         mFragments.execPendingActions();
diff --git a/core/java/android/app/ActivityOptions.aidl b/core/java/android/app/ActivityOptions.aidl
new file mode 100644
index 0000000..bd5cd88
--- /dev/null
+++ b/core/java/android/app/ActivityOptions.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+/** @hide */
+parcelable ActivityOptions.SceneTransitionInfo;
\ No newline at end of file
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 9c279c3..8af7ed1 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -357,22 +357,7 @@
     private static final String KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT =
             "android:activity.applyNoUserActionFlagForShortcut";
 
-    /**
-     * For Activity transitions, the calling Activity's TransitionListener used to
-     * notify the called Activity when the shared element and the exit transitions
-     * complete.
-     */
-    private static final String KEY_TRANSITION_COMPLETE_LISTENER
-            = "android:activity.transitionCompleteListener";
-
-    private static final String KEY_TRANSITION_IS_RETURNING
-            = "android:activity.transitionIsReturning";
-    private static final String KEY_TRANSITION_SHARED_ELEMENTS
-            = "android:activity.sharedElementNames";
-    private static final String KEY_RESULT_DATA = "android:activity.resultData";
-    private static final String KEY_RESULT_CODE = "android:activity.resultCode";
-    private static final String KEY_EXIT_COORDINATOR_INDEX
-            = "android:activity.exitCoordinatorIndex";
+    private static final String KEY_SCENE_TRANSITION_INFO = "android:activity.sceneTransitionInfo";
 
     /** See {@link SourceInfo}. */
     private static final String KEY_SOURCE_INFO = "android.activity.sourceInfo";
@@ -472,12 +457,7 @@
     private int mHeight;
     private IRemoteCallback mAnimationStartedListener;
     private IRemoteCallback mAnimationFinishedListener;
-    private ResultReceiver mTransitionReceiver;
-    private boolean mIsReturning;
-    private ArrayList<String> mSharedElementNames;
-    private Intent mResultData;
-    private int mResultCode;
-    private int mExitCoordinatorIndex;
+    private SceneTransitionInfo mSceneTransitionInfo;
     private PendingIntent mUsageTimeReport;
     private int mLaunchDisplayId = INVALID_DISPLAY;
     private int mCallerDisplayId = INVALID_DISPLAY;
@@ -1006,8 +986,11 @@
         ExitTransitionCoordinator exit = makeSceneTransitionAnimation(
                 new ActivityExitTransitionCallbacks(activity), activity.mExitTransitionListener,
                 activity.getWindow(), opts, sharedElements);
-        opts.mExitCoordinatorIndex =
-                activity.mActivityTransitionState.addExitTransitionCoordinator(exit);
+        final SceneTransitionInfo info = opts.getSceneTransitionInfo();
+        if (info != null) {
+            info.setExitCoordinatorKey(
+                    activity.mActivityTransitionState.addExitTransitionCoordinator(exit));
+        }
         return opts;
     }
 
@@ -1029,13 +1012,16 @@
         ActivityOptions opts = new ActivityOptions();
         ExitTransitionCoordinator exit = makeSceneTransitionAnimation(
                 exitCallbacks, callback, window, opts, sharedElements);
-        opts.mExitCoordinatorIndex = -1;
+        final SceneTransitionInfo info = opts.getSceneTransitionInfo();
+        if (info != null) {
+            info.setExitCoordinatorKey(-1);
+        }
         return Pair.create(opts, exit);
     }
 
     /**
-     * This method should be called when the
-     * {@link #startSharedElementAnimation(Window, ExitTransitionCallbacks, Pair[])}
+     * This method should be called when the {@link #startSharedElementAnimation(Window,
+     * ExitTransitionCallbacks, SharedElementCallback, Pair[])}
      * animation must be stopped and the Views reset. This can happen if there was an error
      * from startActivity or a springboard activity and the animation should stop and reset.
      *
@@ -1088,9 +1074,11 @@
 
         ExitTransitionCoordinator exit = new ExitTransitionCoordinator(exitCallbacks, window,
                 callback, names, names, views, false);
-        opts.mTransitionReceiver = exit;
-        opts.mSharedElementNames = names;
-        opts.mIsReturning = false;
+        final SceneTransitionInfo info = new SceneTransitionInfo();
+        info.setResultReceiver(exit);
+        info.setSharedElementNames(names);
+        info.setReturning(false);
+        opts.setSceneTransitionInfo(info);
         return exit;
     }
 
@@ -1111,17 +1099,20 @@
             int resultCode, Intent resultData) {
         ActivityOptions opts = new ActivityOptions();
         opts.mAnimationType = ANIM_SCENE_TRANSITION;
-        opts.mSharedElementNames = sharedElementNames;
-        opts.mTransitionReceiver = exitCoordinator;
-        opts.mIsReturning = true;
-        opts.mResultCode = resultCode;
-        opts.mResultData = resultData;
+        final SceneTransitionInfo info = new SceneTransitionInfo();
+        info.setSharedElementNames(sharedElementNames);
+        info.setResultReceiver(exitCoordinator);
+        info.setReturning(true);
+        info.setResultCode(resultCode);
+        info.setResultData(resultData);
         if (activity == null) {
-            opts.mExitCoordinatorIndex = -1;
+            info.setExitCoordinatorKey(-1);
         } else {
-            opts.mExitCoordinatorIndex =
-                    activity.mActivityTransitionState.addExitTransitionCoordinator(exitCoordinator);
+            info.setExitCoordinatorKey(
+                    activity.mActivityTransitionState.addExitTransitionCoordinator(
+                            exitCoordinator));
         }
+        opts.setSceneTransitionInfo(info);
         return opts;
     }
 
@@ -1269,12 +1260,8 @@
                 break;
 
             case ANIM_SCENE_TRANSITION:
-                mTransitionReceiver = opts.getParcelable(KEY_TRANSITION_COMPLETE_LISTENER, android.os.ResultReceiver.class);
-                mIsReturning = opts.getBoolean(KEY_TRANSITION_IS_RETURNING, false);
-                mSharedElementNames = opts.getStringArrayList(KEY_TRANSITION_SHARED_ELEMENTS);
-                mResultData = opts.getParcelable(KEY_RESULT_DATA, android.content.Intent.class);
-                mResultCode = opts.getInt(KEY_RESULT_CODE);
-                mExitCoordinatorIndex = opts.getInt(KEY_EXIT_COORDINATOR_INDEX);
+                mSceneTransitionInfo = opts.getParcelable(KEY_SCENE_TRANSITION_INFO,
+                        SceneTransitionInfo.class);
                 break;
         }
         mLockTaskMode = opts.getBoolean(KEY_LOCK_TASK_MODE, false);
@@ -1437,9 +1424,6 @@
     }
 
     /** @hide */
-    public int getExitCoordinatorKey() { return mExitCoordinatorIndex; }
-
-    /** @hide */
     public void abort() {
         if (mAnimationStartedListener != null) {
             try {
@@ -1450,35 +1434,17 @@
     }
 
     /** @hide */
-    public boolean isReturning() {
-        return mIsReturning;
-    }
-
-    /**
-     * Returns whether or not the ActivityOptions was created with
-     * {@link #startSharedElementAnimation(Window, Pair[])}.
-     *
-     * @hide
-     */
-    boolean isCrossTask() {
-        return mExitCoordinatorIndex < 0;
+    public ActivityOptions setSceneTransitionInfo(SceneTransitionInfo info) {
+        mSceneTransitionInfo = info;
+        return this;
     }
 
     /** @hide */
-    public ArrayList<String> getSharedElementNames() {
-        return mSharedElementNames;
+    public SceneTransitionInfo getSceneTransitionInfo() {
+        return mSceneTransitionInfo;
     }
 
     /** @hide */
-    public ResultReceiver getResultReceiver() { return mTransitionReceiver; }
-
-    /** @hide */
-    public int getResultCode() { return mResultCode; }
-
-    /** @hide */
-    public Intent getResultData() { return mResultData; }
-
-    /** @hide */
     public PendingIntent getUsageTimeReport() {
         return mUsageTimeReport;
     }
@@ -2102,12 +2068,7 @@
             mPackageName = otherOptions.mPackageName;
         }
         mUsageTimeReport = otherOptions.mUsageTimeReport;
-        mTransitionReceiver = null;
-        mSharedElementNames = null;
-        mIsReturning = false;
-        mResultData = null;
-        mResultCode = 0;
-        mExitCoordinatorIndex = 0;
+        mSceneTransitionInfo = null;
         mAnimationType = otherOptions.mAnimationType;
         switch (otherOptions.mAnimationType) {
             case ANIM_CUSTOM:
@@ -2157,14 +2118,9 @@
                 mAnimationStartedListener = otherOptions.mAnimationStartedListener;
                 break;
             case ANIM_SCENE_TRANSITION:
-                mTransitionReceiver = otherOptions.mTransitionReceiver;
-                mSharedElementNames = otherOptions.mSharedElementNames;
-                mIsReturning = otherOptions.mIsReturning;
+                mSceneTransitionInfo = otherOptions.mSceneTransitionInfo;
                 mThumbnail = null;
                 mAnimationStartedListener = null;
-                mResultData = otherOptions.mResultData;
-                mResultCode = otherOptions.mResultCode;
-                mExitCoordinatorIndex = otherOptions.mExitCoordinatorIndex;
                 break;
         }
         mLockTaskMode = otherOptions.mLockTaskMode;
@@ -2240,14 +2196,9 @@
                         != null ? mAnimationStartedListener.asBinder() : null);
                 break;
             case ANIM_SCENE_TRANSITION:
-                if (mTransitionReceiver != null) {
-                    b.putParcelable(KEY_TRANSITION_COMPLETE_LISTENER, mTransitionReceiver);
+                if (mSceneTransitionInfo != null) {
+                    b.putParcelable(KEY_SCENE_TRANSITION_INFO, mSceneTransitionInfo);
                 }
-                b.putBoolean(KEY_TRANSITION_IS_RETURNING, mIsReturning);
-                b.putStringArrayList(KEY_TRANSITION_SHARED_ELEMENTS, mSharedElementNames);
-                b.putParcelable(KEY_RESULT_DATA, mResultData);
-                b.putInt(KEY_RESULT_CODE, mResultCode);
-                b.putInt(KEY_EXIT_COORDINATOR_INDEX, mExitCoordinatorIndex);
                 break;
         }
         if (mLockTaskMode) {
@@ -2607,4 +2558,124 @@
             }
         };
     }
+
+    /**
+     * This class contains necessary information for Activity Scene Transition.
+     *
+     * @hide
+     */
+    public static class SceneTransitionInfo implements Parcelable {
+        private boolean mIsReturning;
+        private int mResultCode;
+        @Nullable
+        private Intent mResultData;
+        @Nullable
+        private ArrayList<String> mSharedElementNames;
+        @Nullable
+        private ResultReceiver mResultReceiver;
+        private int mExitCoordinatorIndex;
+
+        public SceneTransitionInfo() {
+        }
+
+        SceneTransitionInfo(Parcel in) {
+            mIsReturning = in.readBoolean();
+            mResultCode = in.readInt();
+            mResultData = in.readTypedObject(Intent.CREATOR);
+            mSharedElementNames = in.createStringArrayList();
+            mResultReceiver = in.readTypedObject(ResultReceiver.CREATOR);
+            mExitCoordinatorIndex = in.readInt();
+        }
+
+        public static final Creator<SceneTransitionInfo> CREATOR = new Creator<>() {
+            @Override
+            public SceneTransitionInfo createFromParcel(Parcel in) {
+                return new SceneTransitionInfo(in);
+            }
+
+            @Override
+            public SceneTransitionInfo[] newArray(int size) {
+                return new SceneTransitionInfo[size];
+            }
+        };
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeBoolean(mIsReturning);
+            dest.writeInt(mResultCode);
+            dest.writeTypedObject(mResultData, flags);
+            dest.writeStringList(mSharedElementNames);
+            dest.writeTypedObject(mResultReceiver, flags);
+            dest.writeInt(mExitCoordinatorIndex);
+        }
+
+        public void setReturning(boolean isReturning) {
+            mIsReturning = isReturning;
+        }
+
+        public boolean isReturning() {
+            return mIsReturning;
+        }
+
+        public void setResultCode(int resultCode) {
+            mResultCode = resultCode;
+        }
+
+        public int getResultCode() {
+            return mResultCode;
+        }
+
+        public void setResultData(Intent resultData) {
+            mResultData = resultData;
+        }
+
+        @Nullable
+        public Intent getResultData() {
+            return mResultData;
+        }
+
+        public void setSharedElementNames(ArrayList<String> sharedElementNames) {
+            mSharedElementNames = sharedElementNames;
+        }
+
+        @Nullable
+        public ArrayList<String> getSharedElementNames() {
+            return mSharedElementNames;
+        }
+
+        public void setResultReceiver(ResultReceiver resultReceiver) {
+            mResultReceiver = resultReceiver;
+        }
+
+        @Nullable
+        public ResultReceiver getResultReceiver() {
+            return mResultReceiver;
+        }
+
+        public void setExitCoordinatorKey(int exitCoordinatorKey) {
+            mExitCoordinatorIndex = exitCoordinatorKey;
+        }
+
+        public int getExitCoordinatorKey() {
+            return mExitCoordinatorIndex;
+        }
+
+        boolean isCrossTask() {
+            return mExitCoordinatorIndex < 0;
+        }
+
+        @Override
+        public String toString() {
+            return "SceneTransitionInfo, mIsReturning=" + mIsReturning
+                    + ", mResultCode=" + mResultCode + ", mResultData=" + mResultData
+                    + ", mSharedElementNames=" + mSharedElementNames
+                    + ", mTransitionReceiver=" + mResultReceiver
+                    + ", mExitCoordinatorIndex=" + mExitCoordinatorIndex;
+        }
+    }
 }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 7e5326e..949e2ba 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -43,6 +43,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityOptions.SceneTransitionInfo;
 import android.app.RemoteServiceException.BadForegroundServiceNotificationException;
 import android.app.RemoteServiceException.BadUserInitiatedJobNotificationException;
 import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException;
@@ -632,8 +633,8 @@
         @UnsupportedAppUsage
         boolean mPreserveWindow;
 
-        /** The options for scene transition. */
-        ActivityOptions mActivityOptions;
+        /** The scene transition info. */
+        SceneTransitionInfo mSceneTransitionInfo;
 
         /** Whether this activiy was launched from a bubble. */
         boolean mLaunchedFromBubble;
@@ -660,7 +661,7 @@
                 ActivityInfo info, Configuration overrideConfig,
                 String referrer, IVoiceInteractor voiceInteractor, Bundle state,
                 PersistableBundle persistentState, List<ResultInfo> pendingResults,
-                List<ReferrerIntent> pendingNewIntents, ActivityOptions activityOptions,
+                List<ReferrerIntent> pendingNewIntents, SceneTransitionInfo sceneTransitionInfo,
                 boolean isForward, ProfilerInfo profilerInfo, ClientTransactionHandler client,
                 IBinder assistToken, IBinder shareableActivityToken, boolean launchedFromBubble,
                 IBinder taskFragmentToken) {
@@ -680,7 +681,7 @@
             this.profilerInfo = profilerInfo;
             this.overrideConfig = overrideConfig;
             this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo);
-            mActivityOptions = activityOptions;
+            mSceneTransitionInfo = sceneTransitionInfo;
             mLaunchedFromBubble = launchedFromBubble;
             mTaskFragmentToken = taskFragmentToken;
             init();
@@ -1960,9 +1961,9 @@
             sendMessage(H.TRANSLUCENT_CONVERSION_COMPLETE, token, drawComplete ? 1 : 0);
         }
 
-        public void scheduleOnNewActivityOptions(IBinder token, Bundle options) {
-            sendMessage(H.ON_NEW_ACTIVITY_OPTIONS,
-                    new Pair<IBinder, ActivityOptions>(token, ActivityOptions.fromBundle(options)));
+        public void scheduleOnNewSceneTransitionInfo(IBinder token, SceneTransitionInfo info) {
+            sendMessage(H.ON_NEW_SCENE_TRANSITION_INFO,
+                    new Pair<IBinder, SceneTransitionInfo>(token, info));
         }
 
         public void setProcessState(int state) {
@@ -2258,7 +2259,7 @@
         public static final int TRANSLUCENT_CONVERSION_COMPLETE = 144;
         @UnsupportedAppUsage
         public static final int INSTALL_PROVIDER        = 145;
-        public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
+        public static final int ON_NEW_SCENE_TRANSITION_INFO = 146;
         @UnsupportedAppUsage
         public static final int ENTER_ANIMATION_COMPLETE = 149;
         public static final int START_BINDER_TRACKING = 150;
@@ -2314,7 +2315,7 @@
                     case REQUEST_ASSIST_CONTEXT_EXTRAS: return "REQUEST_ASSIST_CONTEXT_EXTRAS";
                     case TRANSLUCENT_CONVERSION_COMPLETE: return "TRANSLUCENT_CONVERSION_COMPLETE";
                     case INSTALL_PROVIDER: return "INSTALL_PROVIDER";
-                    case ON_NEW_ACTIVITY_OPTIONS: return "ON_NEW_ACTIVITY_OPTIONS";
+                    case ON_NEW_SCENE_TRANSITION_INFO: return "ON_NEW_SCENE_TRANSITION_INFO";
                     case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE";
                     case LOCAL_VOICE_INTERACTION_STARTED: return "LOCAL_VOICE_INTERACTION_STARTED";
                     case ATTACH_AGENT: return "ATTACH_AGENT";
@@ -2520,9 +2521,10 @@
                         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                     }
                     break;
-                case ON_NEW_ACTIVITY_OPTIONS:
-                    Pair<IBinder, ActivityOptions> pair = (Pair<IBinder, ActivityOptions>) msg.obj;
-                    onNewActivityOptions(pair.first, pair.second);
+                case ON_NEW_SCENE_TRANSITION_INFO:
+                    Pair<IBinder, SceneTransitionInfo> pair =
+                            (Pair<IBinder, SceneTransitionInfo>) msg.obj;
+                    onNewSceneTransitionInfo(pair.first, pair.second);
                     break;
                 case ENTER_ANIMATION_COMPLETE:
                     handleEnterAnimationComplete((IBinder) msg.obj);
@@ -3921,9 +3923,9 @@
                     activity.setTheme(theme);
                 }
 
-                if (r.mActivityOptions != null) {
-                    activity.mPendingOptions = r.mActivityOptions;
-                    r.mActivityOptions = null;
+                if (r.mSceneTransitionInfo != null) {
+                    activity.mSceneTransitionInfo = r.mSceneTransitionInfo;
+                    r.mSceneTransitionInfo = null;
                 }
                 activity.mLaunchedFromBubble = r.mLaunchedFromBubble;
                 activity.mCalled = false;
@@ -3962,7 +3964,7 @@
 
     @Override
     public void handleStartActivity(ActivityClientRecord r,
-            PendingTransactionActions pendingActions, ActivityOptions activityOptions) {
+            PendingTransactionActions pendingActions, SceneTransitionInfo sceneTransitionInfo) {
         final Activity activity = r.activity;
         if (!r.stopped) {
             throw new IllegalStateException("Can't start activity that is not stopped.");
@@ -3973,8 +3975,8 @@
         }
 
         unscheduleGcIdler();
-        if (activityOptions != null) {
-            activity.mPendingOptions = activityOptions;
+        if (sceneTransitionInfo != null) {
+            activity.mSceneTransitionInfo = sceneTransitionInfo;
         }
 
         // Start
@@ -4349,10 +4351,10 @@
         }
     }
 
-    public void onNewActivityOptions(IBinder token, ActivityOptions options) {
+    public void onNewSceneTransitionInfo(IBinder token, SceneTransitionInfo info) {
         ActivityClientRecord r = mActivities.get(token);
         if (r != null) {
-            r.activity.onNewActivityOptions(options);
+            r.activity.onNewSceneTransitionInfo(info);
         }
     }
 
diff --git a/core/java/android/app/ActivityTransitionState.java b/core/java/android/app/ActivityTransitionState.java
index 6f4bb45..d947a9b 100644
--- a/core/java/android/app/ActivityTransitionState.java
+++ b/core/java/android/app/ActivityTransitionState.java
@@ -15,6 +15,7 @@
  */
 package android.app;
 
+import android.app.ActivityOptions.SceneTransitionInfo;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.ResultReceiver;
@@ -81,9 +82,9 @@
     private EnterTransitionCoordinator mEnterTransitionCoordinator;
 
     /**
-     * ActivityOptions used on entering this Activity.
+     * {@link SceneTransitionInfo} used on entering this Activity.
      */
-    private ActivityOptions mEnterActivityOptions;
+    private SceneTransitionInfo mEnterSceneTransitionInfo;
 
     /**
      * Has an exit transition been started? If so, we don't want to double-exit.
@@ -165,7 +166,7 @@
         }
     }
 
-    public void setEnterActivityOptions(Activity activity, ActivityOptions options) {
+    public void setEnterSceneTransitionInfo(Activity activity, SceneTransitionInfo info) {
         final Window window = activity.getWindow();
         if (window == null) {
             return;
@@ -173,16 +174,15 @@
         // ensure Decor View has been created so that the window features are activated
         window.getDecorView();
         if (window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
-                && options != null && mEnterActivityOptions == null
-                && mEnterTransitionCoordinator == null
-                && options.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
-            mEnterActivityOptions = options;
+                && info != null && mEnterSceneTransitionInfo == null
+                && mEnterTransitionCoordinator == null) {
+            mEnterSceneTransitionInfo = info;
             mIsEnterTriggered = false;
-            if (mEnterActivityOptions.isReturning()) {
+            if (mEnterSceneTransitionInfo.isReturning()) {
                 restoreExitedViews();
-                int result = mEnterActivityOptions.getResultCode();
+                int result = mEnterSceneTransitionInfo.getResultCode();
                 if (result != 0) {
-                    Intent intent = mEnterActivityOptions.getResultData();
+                    Intent intent = mEnterSceneTransitionInfo.getResultData();
                     if (intent != null) {
                         intent.setExtrasClassLoader(activity.getClassLoader());
                     }
@@ -193,25 +193,26 @@
     }
 
     public void enterReady(Activity activity) {
-        if (mEnterActivityOptions == null || mIsEnterTriggered) {
+        if (mEnterSceneTransitionInfo == null || mIsEnterTriggered) {
             return;
         }
         mIsEnterTriggered = true;
         mHasExited = false;
-        ArrayList<String> sharedElementNames = mEnterActivityOptions.getSharedElementNames();
-        ResultReceiver resultReceiver = mEnterActivityOptions.getResultReceiver();
-        final boolean isReturning = mEnterActivityOptions.isReturning();
+        final ArrayList<String> sharedElementNames =
+                mEnterSceneTransitionInfo.getSharedElementNames();
+        ResultReceiver resultReceiver = mEnterSceneTransitionInfo.getResultReceiver();
+        final boolean isReturning = mEnterSceneTransitionInfo.isReturning();
         if (isReturning) {
             restoreExitedViews();
             activity.getWindow().getDecorView().setVisibility(View.VISIBLE);
         }
         getPendingExitNames(); // Set mPendingExitNames before resetting mEnterTransitionCoordinator
         mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,
-                resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),
-                mEnterActivityOptions.isCrossTask());
-        if (mEnterActivityOptions.isCrossTask()) {
-            mExitingFrom = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
-            mExitingTo = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
+                resultReceiver, sharedElementNames, mEnterSceneTransitionInfo.isReturning(),
+                mEnterSceneTransitionInfo.isCrossTask());
+        if (mEnterSceneTransitionInfo.isCrossTask() && sharedElementNames != null) {
+            mExitingFrom = new ArrayList<>(sharedElementNames);
+            mExitingTo = new ArrayList<>(sharedElementNames);
         }
 
         if (!mIsEnterPostponed) {
@@ -248,7 +249,7 @@
         mExitingFrom = null;
         mExitingTo = null;
         mExitingToView = null;
-        mEnterActivityOptions = null;
+        mEnterSceneTransitionInfo = null;
     }
 
     public void onStop(Activity activity) {
@@ -296,7 +297,7 @@
         mExitingToView = null;
         mCalledExitCoordinator = null;
         mEnterTransitionCoordinator = null;
-        mEnterActivityOptions = null;
+        mEnterSceneTransitionInfo = null;
         mExitTransitionCoordinators = null;
     }
 
@@ -386,9 +387,10 @@
                 mExitTransitionCoordinators == null) {
             return;
         }
-        ActivityOptions activityOptions = new ActivityOptions(options);
-        if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
-            int key = activityOptions.getExitCoordinatorKey();
+        final ActivityOptions activityOptions = new ActivityOptions(options);
+        final SceneTransitionInfo info = activityOptions.getSceneTransitionInfo();
+        if (info != null) {
+            int key = info.getExitCoordinatorKey();
             int index = mExitTransitionCoordinators.indexOfKey(key);
             if (index >= 0) {
                 mCalledExitCoordinator = mExitTransitionCoordinators.valueAt(index).get();
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 287d2bd..34c44f9 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -131,6 +131,7 @@
 
 import libcore.util.EmptyArray;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.ref.WeakReference;
@@ -4038,11 +4039,11 @@
     }
 
     @Override
-    public <T> T parseAndroidManifest(@NonNull String apkFilePath,
+    public <T> T parseAndroidManifest(@NonNull File apkFile,
             @NonNull Function<XmlResourceParser, T> parserFunction) throws IOException {
-        Objects.requireNonNull(apkFilePath, "apkFilePath cannot be null");
+        Objects.requireNonNull(apkFile, "apkFile cannot be null");
         Objects.requireNonNull(parserFunction, "parserFunction cannot be null");
-        try (XmlResourceParser xmlResourceParser = getAndroidManifestParser(apkFilePath)) {
+        try (XmlResourceParser xmlResourceParser = getAndroidManifestParser(apkFile)) {
             return parserFunction.apply(xmlResourceParser);
         } catch (IOException e) {
             Log.w(TAG, "Failed to get the android manifest parser", e);
@@ -4050,11 +4051,11 @@
         }
     }
 
-    private static XmlResourceParser getAndroidManifestParser(@NonNull String apkFilePath)
+    private static XmlResourceParser getAndroidManifestParser(@NonNull File apkFile)
             throws IOException {
         ApkAssets apkAssets = null;
         try {
-            apkAssets = ApkAssets.loadFromPath(apkFilePath);
+            apkAssets = ApkAssets.loadFromPath(apkFile.getAbsolutePath());
             return apkAssets.openXml(ApkLiteParseUtils.ANDROID_MANIFEST_FILENAME);
         } finally {
             if (apkAssets != null) {
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index f9ab55e..5b354fc 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -23,6 +23,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.NotificationManager.InterruptionFilter;
 import android.content.ComponentName;
 import android.net.Uri;
@@ -35,6 +36,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Objects;
 
 /**
@@ -111,6 +113,30 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface Type {}
 
+    /** Used to track which rule variables have been modified by the user.
+     * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "FIELD_" }, value = {
+            FIELD_NAME,
+            FIELD_INTERRUPTION_FILTER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ModifiableField {}
+
+    /**
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @TestApi
+    public static final int FIELD_NAME = 1 << 0;
+    /**
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @TestApi
+    public static final int FIELD_INTERRUPTION_FILTER = 1 << 1;
+
     private boolean enabled;
     private String name;
     private @InterruptionFilter int interruptionFilter;
@@ -120,12 +146,14 @@
     private long creationTime;
     private ZenPolicy mZenPolicy;
     private ZenDeviceEffects mDeviceEffects;
+    // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
     private boolean mModified = false;
     private String mPkg;
-    private int mType = TYPE_UNKNOWN;
+    private int mType = Flags.modesApi() ? TYPE_UNKNOWN : 0;
     private int mIconResId;
     private String mTriggerDescription;
     private boolean mAllowManualInvocation;
+    private @ModifiableField int mUserModifiedFields; // Bitwise representation
 
     /**
      * The maximum string length for any string contained in this automatic zen rule. This pertains
@@ -228,6 +256,7 @@
             mIconResId = source.readInt();
             mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
             mType = source.readInt();
+            mUserModifiedFields = source.readInt();
         }
     }
 
@@ -278,6 +307,8 @@
      * Returns whether this rule's name has been modified by the user.
      * @hide
      */
+    // TODO: b/310620812 - Replace with mUserModifiedFields & FIELD_NAME once
+    //  FLAG_MODES_API is inlined.
     public boolean isModified() {
         return mModified;
     }
@@ -475,6 +506,32 @@
         return type;
     }
 
+    /**
+     * Gets the bitmask representing which fields are user modified. Bits are set using
+     * {@link ModifiableField}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @TestApi
+    public @ModifiableField int getUserModifiedFields() {
+        return mUserModifiedFields;
+    }
+
+    /**
+     * Returns {@code true} if the {@link AutomaticZenRule} can be updated.
+     * When this returns {@code false}, calls to
+     * {@link NotificationManager#updateAutomaticZenRule(String, AutomaticZenRule)}) with this rule
+     * will ignore changes to user-configurable fields.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public boolean canUpdate() {
+        // The rule is considered updateable if its bitmask has no user modifications, and
+        // the bitmasks of the policy and device effects have no modification.
+        return mUserModifiedFields == 0
+                && (mZenPolicy == null || mZenPolicy.getUserModifiedFields() == 0)
+                && (mDeviceEffects == null || mDeviceEffects.getUserModifiedFields() == 0);
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -503,6 +560,7 @@
             dest.writeInt(mIconResId);
             dest.writeString(mTriggerDescription);
             dest.writeInt(mType);
+            dest.writeInt(mUserModifiedFields);
         }
     }
 
@@ -524,12 +582,26 @@
                     .append(",allowManualInvocation=").append(mAllowManualInvocation)
                     .append(",iconResId=").append(mIconResId)
                     .append(",triggerDescription=").append(mTriggerDescription)
-                    .append(",type=").append(mType);
+                    .append(",type=").append(mType)
+                    .append(",userModifiedFields=")
+                    .append(modifiedFieldsToString(mUserModifiedFields));
         }
 
         return sb.append(']').toString();
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    private String modifiedFieldsToString(int bitmask) {
+        ArrayList<String> modified = new ArrayList<>();
+        if ((bitmask & FIELD_NAME) != 0) {
+            modified.add("FIELD_NAME");
+        }
+        if ((bitmask & FIELD_INTERRUPTION_FILTER) != 0) {
+            modified.add("FIELD_INTERRUPTION_FILTER");
+        }
+        return "{" + String.join(",", modified) + "}";
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (!(o instanceof AutomaticZenRule)) return false;
@@ -551,7 +623,8 @@
                     && other.mAllowManualInvocation == mAllowManualInvocation
                     && other.mIconResId == mIconResId
                     && Objects.equals(other.mTriggerDescription, mTriggerDescription)
-                    && other.mType == mType;
+                    && other.mType == mType
+                    && other.mUserModifiedFields == mUserModifiedFields;
         }
         return finalEquals;
     }
@@ -561,7 +634,8 @@
         if (Flags.modesApi()) {
             return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
                     configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime,
-                    mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
+                    mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType,
+                    mUserModifiedFields);
         }
         return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
                 configurationActivity, mZenPolicy, mModified, creationTime, mPkg);
@@ -630,6 +704,7 @@
         private boolean mAllowManualInvocation;
         private long mCreationTime;
         private String mPkg;
+        private @ModifiableField int mUserModifiedFields;
 
         public Builder(@NonNull AutomaticZenRule rule) {
             mName = rule.getName();
@@ -646,6 +721,7 @@
             mAllowManualInvocation = rule.isManualInvocationAllowed();
             mCreationTime = rule.getCreationTime();
             mPkg = rule.getPackageName();
+            mUserModifiedFields = rule.mUserModifiedFields;
         }
 
         public Builder(@NonNull String name, @NonNull Uri conditionId) {
@@ -772,6 +848,19 @@
             return this;
         }
 
+        /**
+         * Sets the bitmask representing which fields have been user-modified.
+         * This method should not be used outside of tests. The value of userModifiedFields
+         * should be set based on what values are changed when a rule is populated or updated..
+         * @hide
+         */
+        @FlaggedApi(Flags.FLAG_MODES_API)
+        @TestApi
+        public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
+            mUserModifiedFields = userModifiedFields;
+            return this;
+        }
+
         public @NonNull AutomaticZenRule build() {
             AutomaticZenRule rule = new AutomaticZenRule(mName, mOwner, mConfigurationActivity,
                     mConditionId, mPolicy, mInterruptionFilter, mEnabled);
@@ -782,6 +871,7 @@
             rule.mIconResId = mIconResId;
             rule.mAllowManualInvocation = mAllowManualInvocation;
             rule.setPackageName(mPkg);
+            rule.mUserModifiedFields = mUserModifiedFields;
 
             return rule;
         }
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index 25075e9..b300674 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -17,6 +17,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityOptions.SceneTransitionInfo;
 import android.app.ActivityThread.ActivityClientRecord;
 import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.DestroyActivityItem;
@@ -207,7 +208,7 @@
 
     /** Perform activity start. */
     public abstract void handleStartActivity(@NonNull ActivityClientRecord r,
-            PendingTransactionActions pendingActions, ActivityOptions activityOptions);
+            PendingTransactionActions pendingActions, SceneTransitionInfo sceneTransitionInfo);
 
     /** Get package info. */
     public abstract LoadedApk getPackageInfoNoCheck(ApplicationInfo ai);
diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java
index ac9c497..d1e517b 100644
--- a/core/java/android/app/ForegroundServiceTypePolicy.java
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -30,6 +30,7 @@
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
@@ -577,6 +578,26 @@
     );
 
     /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MEDIA_PROCESSING =
+            new ForegroundServiceTypePolicyInfo(
+                    FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING,
+                    ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+                    ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+                    new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                            new RegularPermission(
+                                    Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING)
+                    }, true),
+                    null /* anyOfPermissions */,
+                    null /* permissionEnforcementFlag */,
+                    true /* permissionEnforcementFlagDefaultValue */,
+                    false /* foregroundOnlyPermission */
+            );
+
+    /**
      * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}.
      *
      * @hide
@@ -1331,6 +1352,8 @@
                     FGS_TYPE_POLICY_SYSTEM_EXEMPTED);
             mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
                     FGS_TYPE_POLICY_SHORT_SERVICE);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING,
+                    FGS_TYPE_POLICY_MEDIA_PROCESSING);
             // TODO (b/271950506): revisit it in the next release.
             // Hide the file management type for now. If anyone uses it, will default to "none".
             mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index a301c18..59e0e99 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.app.ActivityOptions.SceneTransitionInfo;
 import android.app.ContentProviderHolder;
 import android.app.IInstrumentationWatcher;
 import android.app.IUiAutomationConnection;
@@ -114,7 +115,7 @@
     void scheduleCreateBackupAgent(in ApplicationInfo app,
             int backupMode, int userId, int operationType);
     void scheduleDestroyBackupAgent(in ApplicationInfo app, int userId);
-    void scheduleOnNewActivityOptions(IBinder token, in Bundle options);
+    void scheduleOnNewSceneTransitionInfo(IBinder token, in SceneTransitionInfo info);
     void scheduleSuicide();
     void dispatchPackageBroadcast(int cmd, in String[] packages);
     void scheduleCrash(in String msg, int typeId, in Bundle extras);
diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java
index 6b5f19a..1b19ecd 100644
--- a/core/java/android/app/LocalActivityManager.java
+++ b/core/java/android/app/LocalActivityManager.java
@@ -179,7 +179,7 @@
             }
 
             mActivityThread.handleStartActivity(clientRecord, pendingActions,
-                    null /* activityOptions */);
+                    null /* sceneTransitionInfo */);
             r.curState = STARTED;
             
             if (desiredState == RESUMED) {
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index be420de..62db90f 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -744,20 +744,21 @@
                         params, userId, /* getCropped = */ true);
                 Trace.endSection();
 
-                if (pfd != null) {
-                    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);
-                            // Let's do color management
-                            if (cmProxy != null) {
-                                cmProxy.doColorManagement(decoder, info);
-                            }
-                        }));
-                    } catch (OutOfMemoryError | IOException e) {
-                        Log.w(TAG, "Can't decode file", e);
-                    }
+                if (pfd == null) {
+                    return null;
+                }
+                try (InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+                    ImageDecoder.Source src = ImageDecoder.createSource(context.getResources(), is);
+                    return ImageDecoder.decodeBitmap(src, ((decoder, info, source) -> {
+                        // Mutable and hardware config can't be set at the same time.
+                        decoder.setMutableRequired(!hardware);
+                        // Let's do color management
+                        if (cmProxy != null) {
+                            cmProxy.doColorManagement(decoder, info);
+                        }
+                    }));
+                } catch (OutOfMemoryError | IOException e) {
+                    Log.w(TAG, "Can't decode file", e);
                 }
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
diff --git a/core/java/android/app/ambient_context.aconfig b/core/java/android/app/ambient_context.aconfig
new file mode 100644
index 0000000..3f73da2
--- /dev/null
+++ b/core/java/android/app/ambient_context.aconfig
@@ -0,0 +1,8 @@
+package: "android.app"
+
+flag {
+  namespace: "biometrics_integration"
+  name: "ambient_heart_rate"
+  description: "Feature flag for adding heart rate api to ambient context."
+  bug: "318309481"
+}
diff --git a/core/java/android/app/ambientcontext/AmbientContextEvent.java b/core/java/android/app/ambientcontext/AmbientContextEvent.java
index b5c66ff..f94987e 100644
--- a/core/java/android/app/ambientcontext/AmbientContextEvent.java
+++ b/core/java/android/app/ambientcontext/AmbientContextEvent.java
@@ -16,7 +16,9 @@
 
 package android.app.ambientcontext;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.os.Parcelable;
@@ -68,6 +70,14 @@
     public static final int EVENT_BACK_DOUBLE_TAP = 3;
 
     /**
+     * The integer indicating a heart rate measurement was done.
+     *
+     * @see #getRatePerMinute
+     */
+    @Event @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE)
+    public static final int EVENT_HEART_RATE = 4;
+
+    /**
      * Integer indicating the start of wearable vendor defined events that can be detected.
      * These depend on the vendor implementation.
      */
@@ -79,12 +89,16 @@
      */
     public static final String KEY_VENDOR_WEARABLE_EVENT_NAME = "wearable_event_name";
 
+    /** Default value for the rate per minute data field. */
+    private static final int RATE_PER_MINUTE_UNKNOWN = -1;
+
     /** @hide */
     @IntDef(prefix = { "EVENT_" }, value = {
             EVENT_UNKNOWN,
             EVENT_COUGH,
             EVENT_SNORE,
             EVENT_BACK_DOUBLE_TAP,
+            EVENT_HEART_RATE,
             EVENT_VENDOR_WEARABLE_START,
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -170,6 +184,16 @@
         return new PersistableBundle();
     }
 
+    /**
+     * Rate per minute of the event during the start to end time.
+     *
+     * @return the rate per minute, or {@link #RATE_PER_MINUTE_UNKNOWN} if the rate is unknown.
+     */
+    private final @IntRange(from = -1) int mRatePerMinute;
+    private static int defaultRatePerMinute() {
+        return RATE_PER_MINUTE_UNKNOWN;
+    }
+
 
 
     // Code below generated by codegen v1.0.23.
@@ -179,6 +203,8 @@
     //
     // To regenerate run:
     // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java
+    // then manually add @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) back to flagged
+    // APIs.
     //
     // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
     //   Settings > Editor > Code Style > Formatter Control
@@ -191,6 +217,7 @@
         EVENT_COUGH,
         EVENT_SNORE,
         EVENT_BACK_DOUBLE_TAP,
+        EVENT_HEART_RATE,
         EVENT_VENDOR_WEARABLE_START
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -209,6 +236,8 @@
                     return "EVENT_SNORE";
             case EVENT_BACK_DOUBLE_TAP:
                     return "EVENT_BACK_DOUBLE_TAP";
+            case EVENT_HEART_RATE:
+                    return "EVENT_HEART_RATE";
             case EVENT_VENDOR_WEARABLE_START:
                     return "EVENT_VENDOR_WEARABLE_START";
             default: return Integer.toHexString(value);
@@ -255,7 +284,8 @@
             @NonNull Instant endTime,
             @LevelValue int confidenceLevel,
             @LevelValue int densityLevel,
-            @NonNull PersistableBundle vendorData) {
+            @NonNull PersistableBundle vendorData,
+            @IntRange(from = -1) int ratePerMinute) {
         this.mEventType = eventType;
         com.android.internal.util.AnnotationValidations.validate(
                 EventCode.class, null, mEventType);
@@ -274,6 +304,10 @@
         this.mVendorData = vendorData;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mVendorData);
+        this.mRatePerMinute = ratePerMinute;
+        com.android.internal.util.AnnotationValidations.validate(
+                IntRange.class, null, mRatePerMinute,
+                "from", -1);
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -330,6 +364,17 @@
         return mVendorData;
     }
 
+    /**
+     * Rate per minute of the event during the start to end time.
+     *
+     * @return the rate per minute, or {@link #RATE_PER_MINUTE_UNKNOWN} if the rate is unknown.
+     */
+    @DataClass.Generated.Member
+    @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE)
+    public @IntRange(from = -1) int getRatePerMinute() {
+        return mRatePerMinute;
+    }
+
     @Override
     @DataClass.Generated.Member
     public String toString() {
@@ -342,7 +387,8 @@
                 "endTime = " + mEndTime + ", " +
                 "confidenceLevel = " + mConfidenceLevel + ", " +
                 "densityLevel = " + mDensityLevel + ", " +
-                "vendorData = " + mVendorData +
+                "vendorData = " + mVendorData + ", " +
+                "ratePerMinute = " + mRatePerMinute +
         " }";
     }
 
@@ -380,6 +426,7 @@
         dest.writeInt(mConfidenceLevel);
         dest.writeInt(mDensityLevel);
         dest.writeTypedObject(mVendorData, flags);
+        dest.writeInt(mRatePerMinute);
     }
 
     @Override
@@ -399,6 +446,7 @@
         int confidenceLevel = in.readInt();
         int densityLevel = in.readInt();
         PersistableBundle vendorData = (PersistableBundle) in.readTypedObject(PersistableBundle.CREATOR);
+        int ratePerMinute = in.readInt();
 
         this.mEventType = eventType;
         com.android.internal.util.AnnotationValidations.validate(
@@ -418,6 +466,10 @@
         this.mVendorData = vendorData;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mVendorData);
+        this.mRatePerMinute = ratePerMinute;
+        com.android.internal.util.AnnotationValidations.validate(
+                IntRange.class, null, mRatePerMinute,
+                "from", -1);
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -449,6 +501,7 @@
         private @LevelValue int mConfidenceLevel;
         private @LevelValue int mDensityLevel;
         private @NonNull PersistableBundle mVendorData;
+        private @IntRange(from = -1) int mRatePerMinute;
 
         private long mBuilderFieldsSet = 0L;
 
@@ -525,10 +578,22 @@
             return this;
         }
 
+        /**
+         * Rate per minute of the event during the start to end time.
+         */
+        @DataClass.Generated.Member
+        @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE)
+        public @NonNull Builder setRatePerMinute(@IntRange(from = -1) int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x40;
+            mRatePerMinute = value;
+            return this;
+        }
+
         /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull AmbientContextEvent build() {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x40; // Mark builder used
+            mBuilderFieldsSet |= 0x80; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x1) == 0) {
                 mEventType = defaultEventType();
@@ -548,18 +613,22 @@
             if ((mBuilderFieldsSet & 0x20) == 0) {
                 mVendorData = defaultVendorData();
             }
+            if ((mBuilderFieldsSet & 0x40) == 0) {
+                mRatePerMinute = defaultRatePerMinute();
+            }
             AmbientContextEvent o = new AmbientContextEvent(
                     mEventType,
                     mStartTime,
                     mEndTime,
                     mConfidenceLevel,
                     mDensityLevel,
-                    mVendorData);
+                    mVendorData,
+                    mRatePerMinute);
             return o;
         }
 
         private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x40) != 0) {
+            if ((mBuilderFieldsSet & 0x80) != 0) {
                 throw new IllegalStateException(
                         "This Builder should not be reused. Use a new Builder instance instead");
             }
@@ -567,10 +636,10 @@
     }
 
     @DataClass.Generated(
-            time = 1671217108067L,
+            time = 1704895515931L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java",
-            inputSignatures = "public static final  int EVENT_UNKNOWN\npublic static final  int EVENT_COUGH\npublic static final  int EVENT_SNORE\npublic static final  int EVENT_BACK_DOUBLE_TAP\npublic static final  int EVENT_VENDOR_WEARABLE_START\npublic static final  java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\npublic static final  int LEVEL_UNKNOWN\npublic static final  int LEVEL_LOW\npublic static final  int LEVEL_MEDIUM_LOW\npublic static final  int LEVEL_MEDIUM\npublic static final  int LEVEL_MEDIUM_HIGH\npublic static final  int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate static  int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultDensityLevel()\nprivate static  android.os.PersistableBundle defaultVendorData()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
+            inputSignatures = "public static final  int EVENT_UNKNOWN\npublic static final  int EVENT_COUGH\npublic static final  int EVENT_SNORE\npublic static final  int EVENT_BACK_DOUBLE_TAP\npublic static final @android.app.ambientcontext.AmbientContextEvent.Event @android.annotation.FlaggedApi int EVENT_HEART_RATE\npublic static final  int EVENT_VENDOR_WEARABLE_START\npublic static final  java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\nprivate static final  int RATE_PER_MINUTE_UNKNOWN\npublic static final  int LEVEL_UNKNOWN\npublic static final  int LEVEL_LOW\npublic static final  int LEVEL_MEDIUM_LOW\npublic static final  int LEVEL_MEDIUM\npublic static final  int LEVEL_MEDIUM_HIGH\npublic static final  int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate final @android.annotation.IntRange int mRatePerMinute\nprivate static  int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultDensityLevel()\nprivate static  android.os.PersistableBundle defaultVendorData()\nprivate static  int defaultRatePerMinute()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java
index 6371871..a41cb1f 100644
--- a/core/java/android/app/backup/FullBackup.java
+++ b/core/java/android/app/backup/FullBackup.java
@@ -239,7 +239,7 @@
                 Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
             }
 
-            byte[] buffer = new byte[32 * 1024];
+            byte[] buffer = new byte[64 * 1024];
             final long origSize = size;
             FileInputStream in = new FileInputStream(data.getFileDescriptor());
             while (size > 0) {
diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java
index 1190bf6..4d53701 100644
--- a/core/java/android/app/servertransaction/LaunchActivityItem.java
+++ b/core/java/android/app/servertransaction/LaunchActivityItem.java
@@ -23,7 +23,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityClient;
-import android.app.ActivityOptions;
+import android.app.ActivityOptions.SceneTransitionInfo;
 import android.app.ActivityThread;
 import android.app.ActivityThread.ActivityClientRecord;
 import android.app.ClientTransactionHandler;
@@ -73,7 +73,7 @@
     private PersistableBundle mPersistentState;
     private List<ResultInfo> mPendingResults;
     private List<ReferrerIntent> mPendingNewIntents;
-    private ActivityOptions mActivityOptions;
+    private SceneTransitionInfo mSceneTransitionInfo;
     private boolean mIsForward;
     private ProfilerInfo mProfilerInfo;
     private IBinder mAssistToken;
@@ -104,8 +104,8 @@
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
         ActivityClientRecord r = new ActivityClientRecord(mActivityToken, mIntent, mIdent, mInfo,
                 mOverrideConfig, mReferrer, mVoiceInteractor, mState, mPersistentState,
-                mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo,
-                client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
+                mPendingResults, mPendingNewIntents, mSceneTransitionInfo, mIsForward,
+                mProfilerInfo, client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
                 mTaskFragmentToken);
         client.handleLaunchActivity(r, pendingActions, mDeviceId, null /* customIntent */);
         Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
@@ -136,7 +136,7 @@
             @Nullable IVoiceInteractor voiceInteractor, int procState, @Nullable Bundle state,
             @Nullable PersistableBundle persistentState, @Nullable List<ResultInfo> pendingResults,
             @Nullable List<ReferrerIntent> pendingNewIntents,
-            @Nullable ActivityOptions activityOptions,
+            @Nullable SceneTransitionInfo sceneTransitionInfo,
             boolean isForward, @Nullable ProfilerInfo profilerInfo, @NonNull IBinder assistToken,
             @Nullable IActivityClientController activityClientController,
             @NonNull IBinder shareableActivityToken, boolean launchedFromBubble,
@@ -152,7 +152,7 @@
                 persistentState != null ? new PersistableBundle(persistentState) : null,
                 pendingResults != null ? new ArrayList<>(pendingResults) : null,
                 pendingNewIntents != null ? new ArrayList<>(pendingNewIntents) : null,
-                activityOptions, isForward,
+                sceneTransitionInfo, isForward,
                 profilerInfo != null ? new ProfilerInfo(profilerInfo) : null,
                 assistToken, activityClientController, shareableActivityToken,
                 launchedFromBubble, taskFragmentToken);
@@ -193,7 +193,7 @@
         dest.writePersistableBundle(mPersistentState);
         dest.writeTypedList(mPendingResults, flags);
         dest.writeTypedList(mPendingNewIntents, flags);
-        dest.writeBundle(mActivityOptions != null ? mActivityOptions.toBundle() : null);
+        dest.writeTypedObject(mSceneTransitionInfo, flags);
         dest.writeBoolean(mIsForward);
         dest.writeTypedObject(mProfilerInfo, flags);
         dest.writeStrongBinder(mAssistToken);
@@ -213,7 +213,8 @@
                 in.readPersistableBundle(getClass().getClassLoader()),
                 in.createTypedArrayList(ResultInfo.CREATOR),
                 in.createTypedArrayList(ReferrerIntent.CREATOR),
-                ActivityOptions.fromBundle(in.readBundle()), in.readBoolean(),
+                in.readTypedObject(SceneTransitionInfo.CREATOR),
+                in.readBoolean(),
                 in.readTypedObject(ProfilerInfo.CREATOR),
                 in.readStrongBinder(),
                 IActivityClientController.Stub.asInterface(in.readStrongBinder()),
@@ -253,7 +254,7 @@
                 && areBundlesEqualRoughly(mPersistentState, other.mPersistentState)
                 && Objects.equals(mPendingResults, other.mPendingResults)
                 && Objects.equals(mPendingNewIntents, other.mPendingNewIntents)
-                && (mActivityOptions == null) == (other.mActivityOptions == null)
+                && (mSceneTransitionInfo == null) == (other.mSceneTransitionInfo == null)
                 && mIsForward == other.mIsForward
                 && Objects.equals(mProfilerInfo, other.mProfilerInfo)
                 && Objects.equals(mAssistToken, other.mAssistToken)
@@ -276,7 +277,7 @@
         result = 31 * result + getRoughBundleHashCode(mPersistentState);
         result = 31 * result + Objects.hashCode(mPendingResults);
         result = 31 * result + Objects.hashCode(mPendingNewIntents);
-        result = 31 * result + (mActivityOptions != null ? 1 : 0);
+        result = 31 * result + (mSceneTransitionInfo != null ? 1 : 0);
         result = 31 * result + (mIsForward ? 1 : 0);
         result = 31 * result + Objects.hashCode(mProfilerInfo);
         result = 31 * result + Objects.hashCode(mAssistToken);
@@ -325,7 +326,7 @@
                 + ",persistentState=" + mPersistentState
                 + ",pendingResults=" + mPendingResults
                 + ",pendingNewIntents=" + mPendingNewIntents
-                + ",options=" + mActivityOptions
+                + ",sceneTransitionInfo=" + mSceneTransitionInfo
                 + ",profilerInfo=" + mProfilerInfo
                 + ",assistToken=" + mAssistToken
                 + ",shareableActivityToken=" + mShareableActivityToken + "}";
@@ -340,7 +341,7 @@
             int procState, @Nullable Bundle state, @Nullable PersistableBundle persistentState,
             @Nullable List<ResultInfo> pendingResults,
             @Nullable List<ReferrerIntent> pendingNewIntents,
-            @Nullable ActivityOptions activityOptions, boolean isForward,
+            @Nullable SceneTransitionInfo sceneTransitionInfo, boolean isForward,
             @Nullable ProfilerInfo profilerInfo, @Nullable IBinder assistToken,
             @Nullable IActivityClientController activityClientController,
             @Nullable IBinder shareableActivityToken, boolean launchedFromBubble,
@@ -359,7 +360,7 @@
         instance.mPersistentState = persistentState;
         instance.mPendingResults = pendingResults;
         instance.mPendingNewIntents = pendingNewIntents;
-        instance.mActivityOptions = activityOptions;
+        instance.mSceneTransitionInfo = sceneTransitionInfo;
         instance.mIsForward = isForward;
         instance.mProfilerInfo = profilerInfo;
         instance.mAssistToken = assistToken;
diff --git a/core/java/android/app/servertransaction/StartActivityItem.java b/core/java/android/app/servertransaction/StartActivityItem.java
index 8b98b21..a0f93ce 100644
--- a/core/java/android/app/servertransaction/StartActivityItem.java
+++ b/core/java/android/app/servertransaction/StartActivityItem.java
@@ -20,7 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityOptions;
+import android.app.ActivityOptions.SceneTransitionInfo;
 import android.app.ActivityThread.ActivityClientRecord;
 import android.app.ClientTransactionHandler;
 import android.os.IBinder;
@@ -35,13 +35,13 @@
 
     private static final String TAG = "StartActivityItem";
 
-    private ActivityOptions mActivityOptions;
+    private SceneTransitionInfo mSceneTransitionInfo;
 
     @Override
     public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
             @NonNull PendingTransactionActions pendingActions) {
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "startActivityItem");
-        client.handleStartActivity(r, pendingActions, mActivityOptions);
+        client.handleStartActivity(r, pendingActions, mSceneTransitionInfo);
         Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
     }
 
@@ -57,13 +57,13 @@
     /** Obtain an instance initialized with provided params. */
     @NonNull
     public static StartActivityItem obtain(@NonNull IBinder activityToken,
-            @Nullable ActivityOptions activityOptions) {
+            @Nullable SceneTransitionInfo sceneTransitionInfo) {
         StartActivityItem instance = ObjectPool.obtain(StartActivityItem.class);
         if (instance == null) {
             instance = new StartActivityItem();
         }
         instance.setActivityToken(activityToken);
-        instance.mActivityOptions = activityOptions;
+        instance.mSceneTransitionInfo = sceneTransitionInfo;
 
         return instance;
     }
@@ -71,7 +71,7 @@
     @Override
     public void recycle() {
         super.recycle();
-        mActivityOptions = null;
+        mSceneTransitionInfo = null;
         ObjectPool.recycle(this);
     }
 
@@ -81,13 +81,13 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
-        dest.writeBundle(mActivityOptions != null ? mActivityOptions.toBundle() : null);
+        dest.writeTypedObject(mSceneTransitionInfo, flags);
     }
 
     /** Read from Parcel. */
     private StartActivityItem(@NonNull Parcel in) {
         super(in);
-        mActivityOptions = ActivityOptions.fromBundle(in.readBundle());
+        mSceneTransitionInfo = in.readTypedObject(SceneTransitionInfo.CREATOR);
     }
 
     public static final @NonNull Creator<StartActivityItem> CREATOR = new Creator<>() {
@@ -109,21 +109,21 @@
             return false;
         }
         final StartActivityItem other = (StartActivityItem) o;
-        return (mActivityOptions == null) == (other.mActivityOptions == null);
+        return (mSceneTransitionInfo == null) == (other.mSceneTransitionInfo == null);
     }
 
     @Override
     public int hashCode() {
         int result = 17;
         result = 31 * result + super.hashCode();
-        result = 31 * result + (mActivityOptions != null ? 1 : 0);
+        result = 31 * result + (mSceneTransitionInfo != null ? 1 : 0);
         return result;
     }
 
     @Override
     public String toString() {
         return "StartActivityItem{" + super.toString()
-                + ",options=" + mActivityOptions + "}";
+                + ",sceneTransitionInfo=" + mSceneTransitionInfo + "}";
     }
 }
 
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index 2e47fee..ba94077 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -324,7 +324,7 @@
                     break;
                 case ON_START:
                     mTransactionHandler.handleStartActivity(r, mPendingActions,
-                            null /* activityOptions */);
+                            null /* sceneTransitionInfo */);
                     break;
                 case ON_RESUME:
                     mTransactionHandler.handleResumeActivity(r, false /* finalStateRequest */,
diff --git a/core/java/android/app/wearable/OWNERS b/core/java/android/app/wearable/OWNERS
index 073e2d7..497eaf0 100644
--- a/core/java/android/app/wearable/OWNERS
+++ b/core/java/android/app/wearable/OWNERS
@@ -1,3 +1,5 @@
 charliewang@google.com
+hackz@google.com
 oni@google.com
+tomchan@google.com
 volnov@google.com
\ No newline at end of file
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java
index 161fa79..843158c 100644
--- a/core/java/android/companion/AssociationInfo.java
+++ b/core/java/android/companion/AssociationInfo.java
@@ -71,6 +71,12 @@
      * @see CompanionDeviceManager#disassociate(int)
      */
     private final boolean mRevoked;
+    /**
+     * Indicates that the association is waiting for its corresponding companion app to be installed
+     * before it can be added to CDM. This is likely because it was restored onto the device from a
+     * backup.
+     */
+    private final boolean mPending;
     private final long mTimeApprovedMs;
     /**
      * A long value indicates the last time connected reported by selfManaged devices
@@ -88,7 +94,7 @@
             @Nullable String tag, @Nullable MacAddress macAddress,
             @Nullable CharSequence displayName, @Nullable String deviceProfile,
             @Nullable AssociatedDevice associatedDevice, boolean selfManaged,
-            boolean notifyOnDeviceNearby, boolean revoked, long timeApprovedMs,
+            boolean notifyOnDeviceNearby, boolean revoked, boolean pending, long timeApprovedMs,
             long lastTimeConnectedMs, int systemDataSyncFlags) {
         if (id <= 0) {
             throw new IllegalArgumentException("Association ID should be greater than 0");
@@ -109,6 +115,7 @@
         mSelfManaged = selfManaged;
         mNotifyOnDeviceNearby = notifyOnDeviceNearby;
         mRevoked = revoked;
+        mPending = pending;
         mTimeApprovedMs = timeApprovedMs;
         mLastTimeConnectedMs = lastTimeConnectedMs;
         mSystemDataSyncFlags = systemDataSyncFlags;
@@ -236,6 +243,15 @@
     }
 
     /**
+     * @return true if the association is waiting for its corresponding app to be installed
+     * before it can be added to CDM.
+     * @hide
+     */
+    public boolean isPending() {
+        return mPending;
+    }
+
+    /**
      * @return the last time self reported disconnected for selfManaged only.
      * @hide
      */
@@ -318,6 +334,7 @@
                 + ", mAssociatedDevice=" + mAssociatedDevice
                 + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby
                 + ", mRevoked=" + mRevoked
+                + ", mPending=" + mPending
                 + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs)
                 + ", mLastTimeConnectedMs=" + (
                     mLastTimeConnectedMs == Long.MAX_VALUE
@@ -336,6 +353,7 @@
                 && mSelfManaged == that.mSelfManaged
                 && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby
                 && mRevoked == that.mRevoked
+                && mPending == that.mPending
                 && mTimeApprovedMs == that.mTimeApprovedMs
                 && mLastTimeConnectedMs == that.mLastTimeConnectedMs
                 && Objects.equals(mPackageName, that.mPackageName)
@@ -351,7 +369,7 @@
     public int hashCode() {
         return Objects.hash(mId, mUserId, mPackageName, mTag, mDeviceMacAddress, mDisplayName,
                 mDeviceProfile, mAssociatedDevice, mSelfManaged, mNotifyOnDeviceNearby, mRevoked,
-                mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags);
+                mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags);
     }
 
     @Override
@@ -372,6 +390,7 @@
         dest.writeBoolean(mSelfManaged);
         dest.writeBoolean(mNotifyOnDeviceNearby);
         dest.writeBoolean(mRevoked);
+        dest.writeBoolean(mPending);
         dest.writeLong(mTimeApprovedMs);
         dest.writeLong(mLastTimeConnectedMs);
         dest.writeInt(mSystemDataSyncFlags);
@@ -389,6 +408,7 @@
         mSelfManaged = in.readBoolean();
         mNotifyOnDeviceNearby = in.readBoolean();
         mRevoked = in.readBoolean();
+        mPending = in.readBoolean();
         mTimeApprovedMs = in.readLong();
         mLastTimeConnectedMs = in.readLong();
         mSystemDataSyncFlags = in.readInt();
@@ -427,6 +447,7 @@
         private boolean mSelfManaged;
         private boolean mNotifyOnDeviceNearby;
         private boolean mRevoked;
+        private boolean mPending;
         private long mTimeApprovedMs;
         private long mLastTimeConnectedMs;
         private int mSystemDataSyncFlags;
@@ -453,6 +474,31 @@
             mSelfManaged = info.mSelfManaged;
             mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby;
             mRevoked = info.mRevoked;
+            mPending = info.mPending;
+            mTimeApprovedMs = info.mTimeApprovedMs;
+            mLastTimeConnectedMs = info.mLastTimeConnectedMs;
+            mSystemDataSyncFlags = info.mSystemDataSyncFlags;
+        }
+
+        /**
+         * This builder is used specifically to create a new association to be restored to a device
+         * that is potentially using a different user ID from the backed-up device.
+         *
+         * @hide
+         */
+        public Builder(int id, int userId, @NonNull String packageName, AssociationInfo info) {
+            mId = id;
+            mUserId = userId;
+            mPackageName = packageName;
+            mTag = info.mTag;
+            mDeviceMacAddress = info.mDeviceMacAddress;
+            mDisplayName = info.mDisplayName;
+            mDeviceProfile = info.mDeviceProfile;
+            mAssociatedDevice = info.mAssociatedDevice;
+            mSelfManaged = info.mSelfManaged;
+            mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby;
+            mRevoked = info.mRevoked;
+            mPending = info.mPending;
             mTimeApprovedMs = info.mTimeApprovedMs;
             mLastTimeConnectedMs = info.mLastTimeConnectedMs;
             mSystemDataSyncFlags = info.mSystemDataSyncFlags;
@@ -526,6 +572,14 @@
         }
 
         /** @hide */
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setPending(boolean pending) {
+            mPending = pending;
+            return this;
+        }
+
+        /** @hide */
         @TestApi
         @NonNull
         @SuppressLint("MissingGetterMatchingBuilder")
@@ -583,6 +637,7 @@
                     mSelfManaged,
                     mNotifyOnDeviceNearby,
                     mRevoked,
+                    mPending,
                     mTimeApprovedMs,
                     mLastTimeConnectedMs,
                     mSystemDataSyncFlags
diff --git a/core/java/android/companion/datatransfer/PermissionSyncRequest.java b/core/java/android/companion/datatransfer/PermissionSyncRequest.java
index 973fca30..34865f0 100644
--- a/core/java/android/companion/datatransfer/PermissionSyncRequest.java
+++ b/core/java/android/companion/datatransfer/PermissionSyncRequest.java
@@ -48,6 +48,15 @@
     }
 
     /** @hide */
+    @Override
+    public PermissionSyncRequest copyWithNewId(int associationId) {
+        PermissionSyncRequest newRequest = new PermissionSyncRequest(associationId);
+        newRequest.mUserId = this.mUserId;
+        newRequest.mUserConsented = this.mUserConsented;
+        return newRequest;
+    }
+
+    /** @hide */
     @NonNull
     public static final Creator<PermissionSyncRequest> CREATOR =
             new Creator<PermissionSyncRequest>() {
diff --git a/core/java/android/companion/datatransfer/SystemDataTransferRequest.java b/core/java/android/companion/datatransfer/SystemDataTransferRequest.java
index 38a553d..c3a2aa4 100644
--- a/core/java/android/companion/datatransfer/SystemDataTransferRequest.java
+++ b/core/java/android/companion/datatransfer/SystemDataTransferRequest.java
@@ -103,4 +103,15 @@
     public int describeContents() {
         return 0;
     }
+
+    /**
+     * Creates a copy of itself with new association ID.
+     *
+     * This method must be implemented to ensure that backup-and-restore can correctly re-map
+     * the restored requests to the restored associations that can potentially have different
+     * IDs than what was originally backed up.
+     *
+     * @hide
+     */
+    public abstract SystemDataTransferRequest copyWithNewId(int associationId);
 }
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index c7a75ed..e9b94c9 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -41,6 +41,7 @@
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.SQLException;
+import android.multiuser.Flags;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -146,6 +147,7 @@
     private boolean mExported;
     private boolean mNoPerms;
     private boolean mSingleUser;
+    private boolean mSystemUserOnly;
     private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray();
 
     private ThreadLocal<AttributionSource> mCallingAttributionSource;
@@ -377,7 +379,9 @@
                             != PermissionChecker.PERMISSION_GRANTED
                             && getContext().checkUriPermission(userUri, Binder.getCallingPid(),
                             callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                            != PackageManager.PERMISSION_GRANTED) {
+                            != PackageManager.PERMISSION_GRANTED
+                            && !deniedAccessSystemUserOnlyProvider(callingUserId,
+                            mSystemUserOnly)) {
                         FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION,
                                 enumCheckUriPermission,
                                 callingUid, uri.getAuthority(), type);
@@ -865,6 +869,10 @@
     boolean checkUser(int pid, int uid, Context context) {
         final int callingUserId = UserHandle.getUserId(uid);
 
+        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
+            return false;
+        }
+
         if (callingUserId == context.getUserId() || mSingleUser) {
             return true;
         }
@@ -987,6 +995,9 @@
 
         // last chance, check against any uri grants
         final int callingUserId = UserHandle.getUserId(uid);
+        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
+            return PermissionChecker.PERMISSION_HARD_DENIED;
+        }
         final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid))
                 ? maybeAddUserId(uri, callingUserId) : uri;
         if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -2623,6 +2634,7 @@
                 setPathPermissions(info.pathPermissions);
                 mExported = info.exported;
                 mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
+                mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0;
                 setAuthorities(info.authority);
             }
             if (Build.IS_DEBUGGABLE) {
@@ -2756,6 +2768,11 @@
         String auth = uri.getAuthority();
         if (!mSingleUser) {
             int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
+            if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(),
+                    mSystemUserOnly)) {
+                throw new SecurityException("Trying to query a SYSTEM user only content"
+                        + " provider from user:" + mContext.getUserId());
+            }
             if (userId != UserHandle.USER_CURRENT
                     && userId != mContext.getUserId()
                     // Since userId specified in content uri, the provider userId would be
@@ -2929,4 +2946,16 @@
             Trace.traceBegin(traceTag, methodName + subInfo);
         }
     }
+    /**
+     * Return true if access to content provider is denied because it's a SYSTEM user only
+     * provider and the calling user is not the SYSTEM user.
+     *
+     * @param callingUserId UserId of the caller accessing the content provider.
+     * @param systemUserOnly true when the content provider is only available for the SYSTEM user.
+     */
+    private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId,
+            boolean systemUserOnly) {
+        return Flags.enableSystemUserOnlyForServicesAndProviders()
+                && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly);
+    }
 }
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index ee1d117b..d5eee63f 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -8099,7 +8099,7 @@
                         int end = data.indexOf('/', 14);
                         if (end < 0) {
                             // All we have is a package name.
-                            intent.mPackage = data.substring(14);
+                            intent.mPackage = Uri.decodeIfNeeded(data.substring(14));
                             if (!explicitAction) {
                                 intent.setAction(ACTION_MAIN);
                             }
@@ -8107,21 +8107,22 @@
                         } else {
                             // Target the Intent at the given package name always.
                             String authority = null;
-                            intent.mPackage = data.substring(14, end);
+                            intent.mPackage = Uri.decodeIfNeeded(data.substring(14, end));
                             int newEnd;
                             if ((end+1) < data.length()) {
                                 if ((newEnd=data.indexOf('/', end+1)) >= 0) {
                                     // Found a scheme, remember it.
-                                    scheme = data.substring(end+1, newEnd);
+                                    scheme = Uri.decodeIfNeeded(data.substring(end + 1, newEnd));
                                     end = newEnd;
                                     if (end < data.length() && (newEnd=data.indexOf('/', end+1)) >= 0) {
                                         // Found a authority, remember it.
-                                        authority = data.substring(end+1, newEnd);
+                                        authority = Uri.decodeIfNeeded(
+                                                data.substring(end + 1, newEnd));
                                         end = newEnd;
                                     }
                                 } else {
                                     // All we have is a scheme.
-                                    scheme = data.substring(end+1);
+                                    scheme = Uri.decodeIfNeeded(data.substring(end + 1));
                                 }
                             }
                             if (scheme == null) {
@@ -11762,27 +11763,33 @@
                         + this);
             }
             uri.append("android-app://");
-            uri.append(mPackage);
+            uri.append(Uri.encode(mPackage));
             String scheme = null;
             if (mData != null) {
-                scheme = mData.getScheme();
+                // All values here must be wrapped with Uri#encodeIfNotEncoded because it is
+                // possible to exploit the Uri API to return a raw unencoded value, which will
+                // not deserialize properly and may cause the resulting Intent to be transformed
+                // to a malicious value.
+                scheme = Uri.encodeIfNotEncoded(mData.getScheme(), null);
                 if (scheme != null) {
                     uri.append('/');
                     uri.append(scheme);
-                    String authority = mData.getEncodedAuthority();
+                    String authority = Uri.encodeIfNotEncoded(mData.getEncodedAuthority(), null);
                     if (authority != null) {
                         uri.append('/');
                         uri.append(authority);
-                        String path = mData.getEncodedPath();
+
+                        // Multiple path segments are allowed, don't encode the path / separator
+                        String path = Uri.encodeIfNotEncoded(mData.getEncodedPath(), "/");
                         if (path != null) {
                             uri.append(path);
                         }
-                        String queryParams = mData.getEncodedQuery();
+                        String queryParams = Uri.encodeIfNotEncoded(mData.getEncodedQuery(), null);
                         if (queryParams != null) {
                             uri.append('?');
                             uri.append(queryParams);
                         }
-                        String fragment = mData.getEncodedFragment();
+                        String fragment = Uri.encodeIfNotEncoded(mData.getEncodedFragment(), null);
                         if (fragment != null) {
                             uri.append('#');
                             uri.append(fragment);
diff --git a/core/java/android/content/OWNERS b/core/java/android/content/OWNERS
index 90c3d04..a37408b 100644
--- a/core/java/android/content/OWNERS
+++ b/core/java/android/content/OWNERS
@@ -4,6 +4,7 @@
 per-file *Content* = file:/services/core/java/com/android/server/am/OWNERS
 per-file *Sync* = file:/services/core/java/com/android/server/am/OWNERS
 per-file IntentFilter.java = file:/PACKAGE_MANAGER_OWNERS
+per-file UriRelativeFilter* = file:/PACKAGE_MANAGER_OWNERS
 per-file IntentFilter.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file Intent.java = file:/INTENT_OWNERS
 per-file AutofillOptions* = file:/core/java/android/service/autofill/OWNERS
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 30871e9..9fe8af5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -23,7 +23,6 @@
 import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.app.Activity;
-import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
@@ -37,7 +36,6 @@
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Printer;
 import android.window.OnBackInvokedCallback;
@@ -1790,8 +1788,7 @@
      * @hide
      */
     public boolean isChangeEnabled(long changeId) {
-        return CompatChanges.isChangeEnabled(changeId, applicationInfo.packageName,
-                UserHandle.getUserHandleForUid(applicationInfo.uid));
+        return applicationInfo.isChangeEnabled(changeId);
     }
 
     /** @hide */
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 869c621..a8dba51 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -26,6 +26,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.compat.CompatChanges;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -381,10 +382,10 @@
      * start it unless initiated by a user interaction (typically launching its icon
      * from the launcher, could also include user actions like adding it as an app widget,
      * selecting it as a live wallpaper, selecting it as a keyboard, etc). Stopped
-     * applications will not receive broadcasts unless the sender specifies
+     * applications will not receive implicit broadcasts unless the sender specifies
      * {@link android.content.Intent#FLAG_INCLUDE_STOPPED_PACKAGES}.
      *
-     * <p>Applications should avoid launching activies, binding to or starting services, or
+     * <p>Applications should avoid launching activities, binding to or starting services, or
      * otherwise causing a stopped application to run unless initiated by the user.
      *
      * <p>An app can also return to the stopped state by a "force stop".
@@ -2645,6 +2646,17 @@
     }
 
     /**
+     * Checks if a changeId is enabled for the current user
+     * @param changeId The changeId to verify
+     * @return True of the changeId is enabled
+     * @hide
+     */
+    public boolean isChangeEnabled(long changeId) {
+        return CompatChanges.isChangeEnabled(changeId, packageName,
+                UserHandle.getUserHandleForUid(uid));
+    }
+
+    /**
      * @return whether the app has requested exemption from the foreground service restrictions.
      * This does not take any affect for now.
      * @hide
diff --git a/core/java/android/content/pm/ModuleInfo.java b/core/java/android/content/pm/ModuleInfo.java
index a1c8747..c6e93bb 100644
--- a/core/java/android/content/pm/ModuleInfo.java
+++ b/core/java/android/content/pm/ModuleInfo.java
@@ -16,7 +16,6 @@
 
 package android.content.pm;
 
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Parcel;
@@ -122,18 +121,15 @@
         return mApexModuleName;
     }
 
-    /** @hide Sets the list of the package name of APK-in-APEX apps in this module. */
+    /** @hide Set the list of the package names of all APK-in-APEX apps in this module. */
     public ModuleInfo setApkInApexPackageNames(@NonNull Collection<String> apkInApexPackageNames) {
         Objects.requireNonNull(apkInApexPackageNames);
         mApkInApexPackageNames = List.copyOf(apkInApexPackageNames);
         return this;
     }
 
-    /**
-     * Gets the list of the package name of all APK-in-APEX apps in the module.
-     */
+    /** @hide Get the list of the package names of all APK-in-APEX apps in the module. */
     @NonNull
-    @FlaggedApi(android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
     public Collection<String> getApkInApexPackageNames() {
         if (mApkInApexPackageNames == null) {
             return Collections.emptyList();
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 0e131b4..43322641 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -672,6 +672,13 @@
     public @interface UserActionReason {}
 
     /**
+     * The unarchival status is not set.
+     *
+     * @hide
+     */
+    public static final int UNARCHIVAL_STATUS_UNSET = -1;
+
+    /**
      * The unarchival is possible and will commence.
      *
      * <p> Note that this does not mean that the unarchival has completed. This status should be
@@ -736,6 +743,7 @@
      * @hide
      */
     @IntDef(value = {
+            UNARCHIVAL_STATUS_UNSET,
             UNARCHIVAL_OK,
             UNARCHIVAL_ERROR_USER_ACTION_NEEDED,
             UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE,
@@ -2696,8 +2704,6 @@
         public int developmentInstallFlags = 0;
         /** {@hide} */
         public int unarchiveId = -1;
-        /** {@hide} */
-        public IntentSender unarchiveIntentSender;
 
         private final ArrayMap<String, Integer> mPermissionStates;
 
@@ -2750,7 +2756,6 @@
             applicationEnabledSettingPersistent = source.readBoolean();
             developmentInstallFlags = source.readInt();
             unarchiveId = source.readInt();
-            unarchiveIntentSender = source.readParcelable(null, IntentSender.class);
         }
 
         /** {@hide} */
@@ -2785,7 +2790,6 @@
             ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent;
             ret.developmentInstallFlags = developmentInstallFlags;
             ret.unarchiveId = unarchiveId;
-            ret.unarchiveIntentSender = unarchiveIntentSender;
             return ret;
         }
 
@@ -3495,7 +3499,6 @@
                     applicationEnabledSettingPersistent);
             pw.printHexPair("developmentInstallFlags", developmentInstallFlags);
             pw.printPair("unarchiveId", unarchiveId);
-            pw.printPair("unarchiveIntentSender", unarchiveIntentSender);
             pw.println();
         }
 
@@ -3540,7 +3543,6 @@
             dest.writeBoolean(applicationEnabledSettingPersistent);
             dest.writeInt(developmentInstallFlags);
             dest.writeInt(unarchiveId);
-            dest.writeParcelable(unarchiveIntentSender, flags);
         }
 
         public static final Parcelable.Creator<SessionParams>
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 0fb0993..8e5e825 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -11534,14 +11534,14 @@
     }
 
     /**
-     * Retrieve AndroidManifest.xml information for the given application apk path.
+     * Retrieve AndroidManifest.xml information for the given application apk file.
      *
      * <p>Example:
      *
      * <pre><code>
      * Bundle result;
      * try {
-     *     result = getContext().getPackageManager().parseAndroidManifest(apkFilePath,
+     *     result = getContext().getPackageManager().parseAndroidManifest(apkFile,
      *             xmlResourceParser -> {
      *                 Bundle bundle = new Bundle();
      *                 // Search the start tag
@@ -11570,9 +11570,10 @@
      *
      * Note: When the parserFunction is invoked, the client can read the AndroidManifest.xml
      * information by the XmlResourceParser object. After leaving the parserFunction, the
-     * XmlResourceParser object will be closed.
+     * XmlResourceParser object will be closed. The caller should also handle the exception for
+     * calling this method.
      *
-     * @param apkFilePath The path of an application apk file.
+     * @param apkFile The file of an application apk.
      * @param parserFunction The parserFunction will be invoked with the XmlResourceParser object
      *        after getting the AndroidManifest.xml of an application package.
      *
@@ -11583,7 +11584,7 @@
      */
     @FlaggedApi(android.content.pm.Flags.FLAG_GET_PACKAGE_INFO)
     @WorkerThread
-    public <T> T parseAndroidManifest(@NonNull String apkFilePath,
+    public <T> T parseAndroidManifest(@NonNull File apkFile,
             @NonNull Function<XmlResourceParser, T> parserFunction) throws IOException {
         throw new UnsupportedOperationException(
                 "parseAndroidManifest not implemented in subclass");
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
index 012b6c4..cdda12e 100644
--- a/core/java/android/content/pm/PermissionInfo.java
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -273,9 +273,6 @@
      * to the <code>retailDemo</code> value of
      * {@link android.R.attr#protectionLevel}.
      *
-     * @deprecated This flag has been replaced by the retail demo role and is a no-op since Android
-     *             V.
-     *
      * @hide
      */
     @SystemApi
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
index 9e553db..de33fa8 100644
--- a/core/java/android/content/pm/ProviderInfo.java
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -89,6 +89,15 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
+     * Bit in {@link #flags}: If set, this provider will only be available
+     * for the system user.
+     * Set from the android.R.attr#systemUserOnly attribute.
+     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
+     * @hide
+     */
+    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
+
+    /**
      * Bit in {@link #flags}: If set, a single instance of the provider will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 4d704c3..2b378b1 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -17,6 +17,7 @@
 package android.content.pm;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.RequiresPermission;
 import android.os.Parcel;
@@ -100,6 +101,14 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
+     * @hide Bit in {@link #flags}: If set, this service will only be available
+     * for the system user.
+     * Set from the android.R.attr#systemUserOnly attribute.
+     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
+     */
+    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
+
+    /**
      * Bit in {@link #flags}: If set, a single instance of the service will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
@@ -471,6 +480,17 @@
     public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 1 << 12;
 
     /**
+     * Constant corresponding to {@code mediaProcessing} in
+     * the {@link android.R.attr#foregroundServiceType} attribute.
+     * Media processing use cases such as video or photo editing and processing.
+     */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING
+    )
+    @FlaggedApi(Flags.FLAG_INTRODUCE_MEDIA_PROCESSING_TYPE)
+    public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING = 1 << 13;
+
+    /**
      * Constant corresponding to {@code specialUse} in
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * Use cases that can't be categorized into any other foreground service types, but also
@@ -554,6 +574,7 @@
             FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
             FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
             FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT,
+            FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING,
             FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -640,6 +661,8 @@
                 return "shortService";
             case FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT:
                 return "fileManagement";
+            case FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING:
+                return "mediaProcessing";
             case FOREGROUND_SERVICE_TYPE_SPECIAL_USE:
                 return "specialUse";
             default:
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 94bec35..a2cd3e1 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -131,3 +131,18 @@
     bug: "310801107"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "introduce_media_processing_type"
+    namespace: "backstage_power"
+    description: "Add a new FGS type for media processing use cases."
+    bug: "317788011"
+}
+
+flag {
+    name: "encode_app_intent"
+    namespace: "package_manager_service"
+    description: "Feature flag to encode app intent."
+    bug: "281848623"
+}
+
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index c7797c7..1036865 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -70,4 +70,11 @@
     namespace: "profile_experiences"
     description: "Add support for Private Space in resolver sheet"
     bug: "307515485"
-}
\ No newline at end of file
+}
+flag {
+    name: "enable_system_user_only_for_services_and_providers"
+    namespace: "multiuser"
+    description: "Enable systemUserOnly manifest attribute for services and providers."
+    bug: "302354856"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java
index e511469..89f4985 100644
--- a/core/java/android/content/res/Element.java
+++ b/core/java/android/content/res/Element.java
@@ -26,6 +26,8 @@
 
 import com.android.internal.R;
 
+import java.util.Set;
+
 /**
  * Defines the string attribute length and child tag count restrictions for a xml element.
  *
@@ -37,7 +39,11 @@
     private static final int MAX_ATTR_LEN_URL_COMPONENT = 256;
     private static final int MAX_ATTR_LEN_PERMISSION_GROUP = 256;
     private static final int MAX_ATTR_LEN_PACKAGE = 256;
-    private static final int MAX_ATTR_LEN_MIMETYPE = 512;
+    /**
+     * The mime type max length restriction here should match the restriction that is also
+     * placed in {@link android.content.pm.PackageManager#setMimeGroup(String, Set)}
+     */
+    private static final int MAX_ATTR_LEN_MIMETYPE = 255;
     private static final int MAX_ATTR_LEN_NAME = 1024;
     private static final int MAX_ATTR_LEN_PATH = 4000;
     private static final int MAX_ATTR_LEN_VALUE = 32_768;
@@ -103,6 +109,7 @@
     protected static final String TAG_ATTR_HOST = "host";
     protected static final String TAG_ATTR_MANAGE_SPACE_ACTIVITY = "manageSpaceActivity";
     protected static final String TAG_ATTR_MIMETYPE = "mimeType";
+    protected static final String TAG_ATTR_MIMEGROUP = "mimeGroup";
     protected static final String TAG_ATTR_NAME = "name";
     protected static final String TAG_ATTR_PACKAGE = "package";
     protected static final String TAG_ATTR_PATH = "path";
@@ -367,6 +374,7 @@
             case TAG_ATTR_BACKUP_AGENT:
             case TAG_ATTR_CATEGORY:
             case TAG_ATTR_MANAGE_SPACE_ACTIVITY:
+            case TAG_ATTR_MIMEGROUP:
             case TAG_ATTR_NAME:
             case TAG_ATTR_PARENT_ACTIVITY_NAME:
             case TAG_ATTR_PERMISSION:
@@ -520,6 +528,8 @@
                 return MAX_ATTR_LEN_URL_COMPONENT;
             case R.styleable.AndroidManifestData_mimeType:
                 return MAX_ATTR_LEN_MIMETYPE;
+            case R.styleable.AndroidManifestData_mimeGroup:
+                return MAX_ATTR_LEN_NAME;
             case R.styleable.AndroidManifestData_path:
             case R.styleable.AndroidManifestData_pathPattern:
             case R.styleable.AndroidManifestData_pathPrefix:
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 3fcb3da..47ee76e 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -26,6 +26,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
+import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -755,7 +756,10 @@
         @Override
         public void onPendingIntent(PendingIntent pendingIntent) {
             try {
-                mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
+                mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0,
+                        ActivityOptions.makeBasic()
+                            .setPendingIntentBackgroundActivityStartMode(
+                                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle());
             } catch (IntentSender.SendIntentException e) {
                 Log.e(
                         TAG,
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index b96d832..ecffe9e 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -121,8 +121,12 @@
     // The native SQLiteConnection pointer.  (FOR INTERNAL USE ONLY)
     private long mConnectionPtr;
 
+    // Restrict this connection to read-only operations.
     private boolean mOnlyAllowReadOnlyOperations;
 
+    // Allow this connection to treat updates to temporary tables as read-only operations.
+    private boolean mAllowTempTableRetry = Flags.sqliteAllowTempTables();
+
     // The number of times attachCancellationSignal has been called.
     // Because SQLite statement execution can be reentrant, we keep track of how many
     // times we have attempted to attach a cancellation signal to the connection so that
@@ -142,6 +146,7 @@
     private static native void nativeFinalizeStatement(long connectionPtr, long statementPtr);
     private static native int nativeGetParameterCount(long connectionPtr, long statementPtr);
     private static native boolean nativeIsReadOnly(long connectionPtr, long statementPtr);
+    private static native boolean nativeUpdatesTempOnly(long connectionPtr, long statementPtr);
     private static native int nativeGetColumnCount(long connectionPtr, long statementPtr);
     private static native String nativeGetColumnName(long connectionPtr, long statementPtr,
             int index);
@@ -1097,7 +1102,7 @@
         try {
             final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
             final int type = DatabaseUtils.getSqlStatementTypeExtended(sql);
-            final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
+            boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
             statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly,
                     seqNum);
             if (!skipCache && isCacheable(type)) {
@@ -1265,13 +1270,20 @@
 
     /**
      * Verify that the statement is read-only, if the connection only allows read-only
-     * operations.
+     * operations.  If the connection allows updates to temporary tables, then the statement is
+     * read-only if the only updates are to temporary tables.
      * @param statement The statement to check.
      * @throws SQLiteException if the statement could update the database inside a read-only
      * transaction.
      */
     void throwIfStatementForbidden(PreparedStatement statement) {
         if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) {
+            if (mAllowTempTableRetry) {
+                statement.mReadOnly =
+                        nativeUpdatesTempOnly(mConnectionPtr, statement.mStatementPtr);
+                if (statement.mReadOnly) return;
+            }
+
             throw new SQLiteException("Cannot execute this statement because it "
                     + "might modify the database but the connection is read-only.");
         }
diff --git a/core/java/android/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig
index 62a5123..92ef9c2 100644
--- a/core/java/android/database/sqlite/flags.aconfig
+++ b/core/java/android/database/sqlite/flags.aconfig
@@ -7,3 +7,11 @@
      description: "SQLite APIs held back for Android 15"
      bug: "279043253"
 }
+
+flag {
+     name: "sqlite_allow_temp_tables"
+     namespace: "system_performance"
+     is_fixed_read_only: true
+     description: "Permit updates to TEMP tables in read-only transactions"
+     bug: "317993835"
+}
diff --git a/core/java/android/hardware/SyncFence.java b/core/java/android/hardware/SyncFence.java
index d6052cd..c2440fb 100644
--- a/core/java/android/hardware/SyncFence.java
+++ b/core/java/android/hardware/SyncFence.java
@@ -16,6 +16,7 @@
 
 package android.hardware;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.media.Image;
 import android.media.ImageWriter;
@@ -26,6 +27,8 @@
 import android.os.Parcelable;
 import android.os.SystemClock;
 
+import com.android.window.flags.Flags;
+
 import libcore.util.NativeAllocationRegistry;
 
 import java.io.FileDescriptor;
@@ -121,6 +124,19 @@
         }
     }
 
+    /**
+     * Creates a copy of the SyncFence from an existing one.
+     * Both fences must be closed() independently.
+     */
+    @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME)
+    public SyncFence(@NonNull SyncFence other) {
+        this(other.mNativePtr);
+
+        if (mNativePtr != 0) {
+            nIncRef(mNativePtr);
+        }
+    }
+
     private SyncFence() {
         mCloser = () -> {};
     }
@@ -312,4 +328,5 @@
     private static native int nGetFd(long nPtr);
     private static native boolean nWait(long nPtr, long timeout);
     private static native long nGetSignalTime(long nPtr);
+    private static native void nIncRef(long nPtr);
 }
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 3affb73..0cd1c8c 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -79,6 +79,8 @@
 import android.util.Range;
 import android.util.Size;
 
+import com.android.internal.camera.flags.Flags;
+
 import dalvik.annotation.optimization.FastNative;
 import dalvik.system.VMRuntime;
 
@@ -1795,49 +1797,57 @@
             return false;
         }
 
-        long[] tsArray = new long[samples.length];
-        float[] intrinsicsArray = new float[samples.length * 5];
-        for (int i = 0; i < samples.length; i++) {
-            tsArray[i] = samples[i].getTimestamp();
-            System.arraycopy(samples[i].getLensIntrinsics(), 0, intrinsicsArray, 5*i, 5);
+        if (Flags.concertMode()) {
+            long[] tsArray = new long[samples.length];
+            float[] intrinsicsArray = new float[samples.length * 5];
+            for (int i = 0; i < samples.length; i++) {
+                tsArray[i] = samples[i].getTimestampNanos();
+                System.arraycopy(samples[i].getLensIntrinsics(), 0, intrinsicsArray, 5 * i, 5);
 
+            }
+            setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES, intrinsicsArray);
+            setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS, tsArray);
+
+            return true;
+        } else {
+            return false;
         }
-        setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES, intrinsicsArray);
-        setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS, tsArray);
-
-        return true;
     }
 
     private LensIntrinsicsSample[] getLensIntrinsicSamples() {
-        long[] timestamps = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS);
-        float[] intrinsics = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES);
+        if (Flags.concertMode()) {
+            long[] timestamps = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS);
+            float[] intrinsics = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES);
 
-        if (timestamps == null) {
-            if (intrinsics != null) {
-                throw new AssertionError("timestamps is null but intrinsics is not");
+            if (timestamps == null) {
+                if (intrinsics != null) {
+                    throw new AssertionError("timestamps is null but intrinsics is not");
+                }
+
+                return null;
             }
 
+            if (intrinsics == null) {
+                throw new AssertionError("timestamps is not null but intrinsics is");
+            } else if ((intrinsics.length % 5) != 0) {
+                throw new AssertionError("intrinsics are not multiple of 5");
+            }
+
+            if ((intrinsics.length / 5) != timestamps.length) {
+                throw new AssertionError(String.format(
+                        "timestamps has %d entries but intrinsics has %d", timestamps.length,
+                        intrinsics.length / 5));
+            }
+
+            LensIntrinsicsSample[] samples = new LensIntrinsicsSample[timestamps.length];
+            for (int i = 0; i < timestamps.length; i++) {
+                float[] currentIntrinsic = Arrays.copyOfRange(intrinsics, 5 * i, 5 * i + 5);
+                samples[i] = new LensIntrinsicsSample(timestamps[i], currentIntrinsic);
+            }
+            return samples;
+        } else {
             return null;
         }
-
-        if (intrinsics == null) {
-            throw new AssertionError("timestamps is not null but intrinsics is");
-        } else if((intrinsics.length % 5) != 0) {
-            throw new AssertionError("intrinsics are not multiple of 5");
-        }
-
-        if ((intrinsics.length / 5) != timestamps.length) {
-            throw new AssertionError(String.format(
-                    "timestamps has %d entries but intrinsics has %d", timestamps.length,
-                    intrinsics.length / 5));
-        }
-
-        LensIntrinsicsSample[] samples = new LensIntrinsicsSample[timestamps.length];
-        for (int i = 0; i < timestamps.length; i++) {
-            float[] currentIntrinsic = Arrays.copyOfRange(intrinsics, 5*i, 5*i + 5);
-            samples[i] = new LensIntrinsicsSample(timestamps[i], currentIntrinsic);
-        }
-        return samples;
     }
 
     private Capability[] getExtendedSceneModeCapabilities() {
diff --git a/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java b/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java
index 575cbfa..9a4ec5c 100644
--- a/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java
+++ b/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java
@@ -37,16 +37,18 @@
      * Create a new {@link LensIntrinsicsSample}.
      *
      * <p>{@link LensIntrinsicsSample} contains the timestamp and the
-     * {@link CaptureResult#LENS_INTRINSIC_CALIBRATION} sample.
+     * {@link CaptureResult#LENS_INTRINSIC_CALIBRATION} sample.</p>
      *
-     * @param timestamp timestamp of the lens intrinsics sample.
-     * @param lensIntrinsics the lens intrinsic calibration for the sample.
+     * @param timestampNs timestamp in nanoseconds of the lens intrinsics sample. This uses the
+     *                  same time basis as {@link CaptureResult#SENSOR_TIMESTAMP}.
+     * @param lensIntrinsics the lens {@link CaptureResult#LENS_INTRINSIC_CALIBRATION intrinsic}
+     *                      calibration for the sample.
      *
      * @throws IllegalArgumentException if lensIntrinsics length is different from 5
      */
     @FlaggedApi(Flags.FLAG_CONCERT_MODE)
-    public LensIntrinsicsSample(final long timestamp, @NonNull final float[] lensIntrinsics) {
-        mTimestampNs = timestamp;
+    public LensIntrinsicsSample(final long timestampNs, @NonNull final float[] lensIntrinsics) {
+        mTimestampNs = timestampNs;
         Preconditions.checkArgument(lensIntrinsics.length == 5);
         mLensIntrinsics = lensIntrinsics;
     }
@@ -54,18 +56,18 @@
     /**
      * Get the timestamp in nanoseconds.
      *
-     *<p>The timestamps are in the same timebase as and comparable to
+     *<p>The timestamps are in the same time basis as and comparable to
      *{@link CaptureResult#SENSOR_TIMESTAMP android.sensor.timestamp}.</p>
      *
      * @return a long value (guaranteed to be finite)
      */
     @FlaggedApi(Flags.FLAG_CONCERT_MODE)
-    public long getTimestamp() {
+    public long getTimestampNanos() {
         return mTimestampNs;
     }
 
     /**
-     * Get the lens intrinsics calibration
+     * Get the lens {@link CaptureResult#LENS_INTRINSIC_CALIBRATION intrinsics} calibration
      *
      * @return a floating point value (guaranteed to be finite)
      * @see CaptureResult#LENS_INTRINSIC_CALIBRATION
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 134a510..8f0e0c9 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1831,15 +1831,6 @@
         String KEY_POWER_THROTTLING_DATA = "power_throttling_data";
 
         /**
-         * Key for new power controller feature flag. If enabled new DisplayPowerController will
-         * be used.
-         * Read value via {@link android.provider.DeviceConfig#getBoolean(String, String, boolean)}
-         * with {@link android.provider.DeviceConfig#NAMESPACE_DISPLAY_MANAGER} as the namespace.
-         * @hide
-         */
-        String KEY_NEW_POWER_CONTROLLER = "use_newly_structured_display_power_controller";
-
-        /**
          * Key for normal brightness mode controller feature flag.
          * It enables NormalBrightnessModeController.
          * Read value via {@link android.provider.DeviceConfig#getBoolean(String, String, boolean)}
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index 9e09759..56f69a6 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -450,11 +450,14 @@
          * automatically launched upon the display creation. If unset or set to {@code false}, the
          * display will not host any activities upon creation.</p>
          *
-         * <p>Note: setting to {@code true} requires the display to be trusted. If the display is
-         * not trusted, this property is ignored.</p>
+         * <p>Note: setting to {@code true} requires the display to be trusted and to not mirror
+         * content of other displays. If the display is not trusted, or if it mirrors content of
+         * other displays, this property is ignored.</p>
          *
          * @param isHomeSupported whether home activities are supported on the display
          * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
+         * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
+         * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
          * @hide
          */
         @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME)
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index c5167db..a3a2a2e 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -304,11 +304,7 @@
      *
      * @param id primary identifier of a program to fetch
      * @return the program info, or null if there is no such program on the list
-     *
-     * @deprecated Use {@link #getProgramInfos(ProgramSelector.Identifier)} to get all programs
-     * with the given primary identifier
      */
-    @Deprecated
     public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
         Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries;
         synchronized (mLock) {
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 7e5c141..4c95e02 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -312,20 +312,14 @@
     public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
     /**
      * 1: AM, 2:FM
-     * @deprecated use {@link #IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
      */
-    @Deprecated
     public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11;
     /**
      * 32bit primary identifier for SiriusXM Satellite Radio.
-     *
-     * @deprecated SiriusXM Satellite Radio is not supported
      */
     public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12;
     /**
      * 0-999 range
-     *
-     * @deprecated SiriusXM Satellite Radio is not supported
      */
     public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13;
     /**
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index f0f7e8a..41f21ef 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -166,12 +166,7 @@
      * analog handover state managed from the HAL implementation side.
      *
      * <p>Some radio technologies may not support this, i.e. DAB.
-     *
-     * @deprecated Use {@link #CONFIG_FORCE_ANALOG_FM} instead. If {@link #CONFIG_FORCE_ANALOG_FM}
-     * is supported in HAL, {@link RadioTuner#setConfigFlag} and {@link RadioTuner#isConfigFlagSet}
-     * with CONFIG_FORCE_ANALOG will set/get the value of {@link #CONFIG_FORCE_ANALOG_FM}.
      */
-    @Deprecated
     public static final int CONFIG_FORCE_ANALOG = 2;
     /**
      * Forces the digital playback for the supporting radio technology.
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java
index 70de477..05a3e18 100644
--- a/core/java/android/net/Uri.java
+++ b/core/java/android/net/Uri.java
@@ -21,6 +21,7 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Intent;
+import android.content.pm.Flags;
 import android.os.Environment;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -1971,6 +1972,42 @@
     }
 
     /**
+     * Encodes a value it wasn't already encoded.
+     *
+     * @param value string to encode
+     * @param allow characters to allow
+     * @return encoded value
+     * @hide
+     */
+    public static String encodeIfNotEncoded(@Nullable String value, @Nullable String allow) {
+        if (value == null) return null;
+        if (!Flags.encodeAppIntent() || isEncoded(value, allow)) return value;
+        return encode(value, allow);
+    }
+
+    /**
+     * Returns true if the given string is already encoded to safe characters.
+     *
+     * @param value string to check
+     * @param allow characters to allow
+     * @return true if the string is already encoded or false if it should be encoded
+     */
+    private static boolean isEncoded(@Nullable String value, @Nullable String allow) {
+        if (value == null) return true;
+        for (int index = 0; index < value.length(); index++) {
+            char c = value.charAt(index);
+
+            // Allow % because that's the prefix for an encoded character. This method will fail
+            // for decoded strings whose onlyinvalid character is %, but it's assumed that % alone
+            // cannot cause malicious behavior in the framework.
+            if (!isAllowed(c, allow) && c != '%') {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
      * Decodes '%'-escaped octets in the given string using the UTF-8 scheme.
      * Replaces invalid octets with the unicode replacement character
      * ("\\uFFFD").
@@ -1988,6 +2025,18 @@
     }
 
     /**
+     * Decodes a string if it was encoded, indicated by containing a %.
+     * @param value encoded string to decode
+     * @return decoded value
+     * @hide
+     */
+    public static String decodeIfNeeded(@Nullable String value) {
+        if (value == null) return null;
+        if (Flags.encodeAppIntent() && value.contains("%")) return decode(value);
+        return value;
+    }
+
+    /**
      * Support for part implementations.
      */
     static abstract class AbstractPart {
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 70cf973..561db9c 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -102,6 +102,24 @@
     public static final String VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY =
             "vcn_network_selection_wifi_exit_rssi_threshold";
 
+    /**
+     * Key for the interval to poll IpSecTransformState for packet loss monitoring
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY =
+            "vcn_network_selection_poll_ipsec_state_interval_seconds";
+
+    /**
+     * Key for the threshold of IPSec packet loss rate
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY =
+            "vcn_network_selection_ipsec_packet_loss_percent_threshold";
+
     // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
 
     /**
@@ -115,6 +133,20 @@
             "vcn_restricted_transports";
 
     /**
+     * Key for number of seconds to wait before entering safe mode
+     *
+     * <p>A VcnGatewayConnection will enter safe mode when it takes over the configured timeout to
+     * enter {@link ConnectedState}.
+     *
+     * <p>Defaults to 30, unless overridden by carrier config
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY =
+            "vcn_safe_mode_timeout_seconds_key";
+
+    /**
      * Key for maximum number of parallel SAs for tunnel aggregation
      *
      * <p>If set to a value > 1, multiple tunnels will be set up, and inbound traffic will be
@@ -134,7 +166,10 @@
             new String[] {
                 VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
                 VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
+                VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
+                VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
                 VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
+                VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY,
                 VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,
             };
 
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 6956916..7afd721 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -5,4 +5,18 @@
     namespace: "vcn"
     description: "Feature flag for safe mode configurability"
     bug: "276358140"
+}
+
+flag {
+    name: "safe_mode_timeout_config"
+    namespace: "vcn"
+    description: "Feature flag for adjustable safe mode timeout"
+    bug: "317406085"
+}
+
+flag{
+    name: "network_metric_monitor"
+    namespace: "vcn"
+    description: "Feature flag for enabling network metric monitor"
+    bug: "282996138"
 }
\ No newline at end of file
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index 5a40e42..75f5491 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -812,8 +812,8 @@
         if (context == null) {
             throw new IllegalArgumentException("context cannot be null");
         }
-        context = context.getApplicationContext();
-        if (context == null) {
+        Context applicationContext = context.getApplicationContext();
+        if (applicationContext == null) {
             throw new IllegalArgumentException(
                     "context not associated with any application (using a mock context?)");
         }
@@ -1802,7 +1802,7 @@
      * Use {@link #FLAG_LISTEN_DISABLE} to disable listening.
      * Also refer to {@link #resetDiscoveryTechnology(Activity)} to restore these changes.
      * </p>
-     * The pollTech, listenTech parameters can be one or several of below list.
+     * The pollTechnology, listenTechnology parameters can be one or several of below list.
      * <pre>
      *                    Poll                    Listen
      *  Passive A         0x01   (NFC_A)           0x01  (NFC_PASSIVE_A)
@@ -1820,25 +1820,25 @@
      *         NfcAdapter.FLAG_READER_DISABLE, NfcAdapter.FLAG_LISTEN_KEEP);
      * }</pre></p>
      * @param activity The Activity that requests NFC controller to enable specific technologies.
-     * @param pollTech Flags indicating poll technologies.
-     * @param listenTech Flags indicating listen technologies.
+     * @param pollTechnology Flags indicating poll technologies.
+     * @param listenTechnology Flags indicating listen technologies.
      * @throws UnsupportedOperationException if FEATURE_NFC,
      * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF are unavailable.
      */
 
     @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH)
     public void setDiscoveryTechnology(@NonNull Activity activity,
-            @PollTechnology int pollTech, @ListenTechnology int listenTech) {
-        if (listenTech == FLAG_LISTEN_DISABLE) {
+            @PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) {
+        if (listenTechnology == FLAG_LISTEN_DISABLE) {
             synchronized (sLock) {
                 if (!sHasNfcFeature) {
                     throw new UnsupportedOperationException();
                 }
             }
-            mNfcActivityManager.enableReaderMode(activity, null, pollTech, null);
+            mNfcActivityManager.enableReaderMode(activity, null, pollTechnology, null);
             return;
         }
-        if (pollTech == FLAG_READER_DISABLE) {
+        if (pollTechnology == FLAG_READER_DISABLE) {
             synchronized (sLock) {
                 if (!sHasCeFeature) {
                     throw new UnsupportedOperationException();
@@ -1851,7 +1851,7 @@
                 }
             }
         }
-        mNfcActivityManager.setDiscoveryTech(activity, pollTech, listenTech);
+        mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology);
     }
 
     /**
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index ad86d70..81eab71 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -69,7 +69,12 @@
      * specified in {@link #EXTRA_CATEGORY}. There is an optional
      * extra field using {@link Intent#EXTRA_USER} to specify
      * the {@link UserHandle} of the user that owns the app.
+     *
+     * @deprecated Please use {@link android.app.role.RoleManager#createRequestRoleIntent(String)}
+     * with {@link android.app.role.RoleManager#ROLE_WALLET} parameter
+     * and {@link Activity#startActivityForResult(Intent, int)} instead.
      */
+    @Deprecated
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_CHANGE_DEFAULT =
             "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT";
@@ -1084,4 +1089,32 @@
         sService = adapter.getCardEmulationService();
     }
 
+    /**
+     * Returns the {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT} for the given user.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
+    @FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ENABLED)
+    @Nullable
+    public ApduServiceInfo getPreferredPaymentService() {
+        try {
+            return sService.getPreferredPaymentService(mContext.getUser().getIdentifier());
+        } catch (RemoteException e) {
+            // Try one more time
+            recoverService();
+            if (sService == null) {
+                Log.e(TAG, "Failed to recover CardEmulationService.");
+                return null;
+            }
+            try {
+                return sService.getPreferredPaymentService(mContext.getUser().getIdentifier());
+            } catch (RemoteException ee) {
+                Log.e(TAG, "Failed to reach CardEmulationService.");
+                return null;
+            }
+        }
+    }
+
 }
diff --git a/core/java/android/os/AggregateBatteryConsumer.java b/core/java/android/os/AggregateBatteryConsumer.java
index c5f5614..67e2195 100644
--- a/core/java/android/os/AggregateBatteryConsumer.java
+++ b/core/java/android/os/AggregateBatteryConsumer.java
@@ -33,6 +33,7 @@
  *
  * {@hide}
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class AggregateBatteryConsumer extends BatteryConsumer {
     static final int CONSUMER_TYPE_AGGREGATE = 0;
 
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 1a0ce70..b68b94d 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -37,6 +37,7 @@
 import android.service.batterystats.BatteryStatsServiceDumpHistoryProto;
 import android.service.batterystats.BatteryStatsServiceDumpProto;
 import android.telephony.CellSignalStrength;
+import android.telephony.ModemActivityInfo;
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
 import android.text.format.DateFormat;
@@ -83,6 +84,7 @@
  * except where indicated otherwise.
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public abstract class BatteryStats {
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
@@ -463,6 +465,7 @@
     /**
      * State for keeping track of long counting information.
      */
+    @android.ravenwood.annotation.RavenwoodKeepWholeClass
     public static abstract class LongCounter {
 
         /**
@@ -2724,12 +2727,6 @@
      */
     public abstract int getMobileRadioActiveUnknownCount(int which);
 
-    public static final int DATA_CONNECTION_OUT_OF_SERVICE = 0;
-    public static final int DATA_CONNECTION_EMERGENCY_SERVICE =
-            TelephonyManager.getAllNetworkTypes().length + 1;
-    public static final int DATA_CONNECTION_OTHER = DATA_CONNECTION_EMERGENCY_SERVICE + 1;
-
-
     static final String[] DATA_CONNECTION_NAMES = {
         "oos", "gprs", "edge", "umts", "cdma", "evdo_0", "evdo_A",
         "1xrtt", "hsdpa", "hsupa", "hspa", "iden", "evdo_b", "lte",
@@ -2737,9 +2734,28 @@
         "emngcy", "other"
     };
 
+    public static final int DATA_CONNECTION_OUT_OF_SERVICE = 0;
+    public static final int DATA_CONNECTION_EMERGENCY_SERVICE = getEmergencyNetworkConnectionType();
+    public static final int DATA_CONNECTION_OTHER = DATA_CONNECTION_EMERGENCY_SERVICE + 1;
+
     @UnsupportedAppUsage
     public static final int NUM_DATA_CONNECTION_TYPES = DATA_CONNECTION_OTHER + 1;
 
+    @android.ravenwood.annotation.RavenwoodReplace
+    private static int getEmergencyNetworkConnectionType() {
+        int count = TelephonyManager.getAllNetworkTypes().length;
+        if (DATA_CONNECTION_NAMES.length != count + 3) {        // oos, emngcy, other
+            throw new IllegalStateException(
+                    "DATA_CONNECTION_NAMES length does not match network type count. "
+                    + "Expected: " + (count + 3) + ", actual:" + DATA_CONNECTION_NAMES.length);
+        }
+        return count + 1;
+    }
+
+    private static int getEmergencyNetworkConnectionType$ravenwood() {
+        return DATA_CONNECTION_NAMES.length - 2;
+    }
+
     /**
      * Returns the time in microseconds that the phone has been running with
      * the given data connection.
@@ -9015,4 +9031,44 @@
                 (lhs, rhs) -> Double.compare(rhs.millisecondsPerPacket, lhs.millisecondsPerPacket));
         return uidMobileRadioStats;
     }
+
+    @android.ravenwood.annotation.RavenwoodReplace
+    @VisibleForTesting
+    protected static boolean isLowRamDevice() {
+        return ActivityManager.isLowRamDeviceStatic();
+    }
+
+    protected static boolean isLowRamDevice$ravenwood() {
+        return false;
+    }
+
+    @android.ravenwood.annotation.RavenwoodReplace
+    @VisibleForTesting
+    protected static int getCellSignalStrengthLevelCount() {
+        return CellSignalStrength.getNumSignalStrengthLevels();
+    }
+
+    protected static int getCellSignalStrengthLevelCount$ravenwood() {
+        return 5;
+    }
+
+    @android.ravenwood.annotation.RavenwoodReplace
+    @VisibleForTesting
+    protected static int getModemTxPowerLevelCount() {
+        return ModemActivityInfo.getNumTxPowerLevels();
+    }
+
+    protected static int getModemTxPowerLevelCount$ravenwood() {
+        return 5;
+    }
+
+    @android.ravenwood.annotation.RavenwoodReplace
+    @VisibleForTesting
+    protected static boolean isKernelStatsAvailable() {
+        return true;
+    }
+
+    protected static boolean isKernelStatsAvailable$ravenwood() {
+        return false;
+    }
 }
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 511f464..90d82e7 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -56,6 +56,7 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class BatteryUsageStats implements Parcelable, Closeable {
 
     /**
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index 32840d4..203ef47 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -28,6 +28,7 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class BatteryUsageStatsQuery implements Parcelable {
 
     @NonNull
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index a9b7257..5871717 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -1315,9 +1315,7 @@
         if (IS_ENG) return true;
 
         if (IS_TREBLE_ENABLED) {
-            // If we can run this code, the device should already pass AVB.
-            // So, we don't need to check AVB here.
-            int result = VintfObject.verifyWithoutAvb();
+            int result = VintfObject.verifyBuildAtBoot();
 
             if (result != 0) {
                 Slog.e(TAG, "Vendor interface is incompatible, error="
diff --git a/core/java/android/os/ISystemConfig.aidl b/core/java/android/os/ISystemConfig.aidl
index 61b24aa..b7649ba 100644
--- a/core/java/android/os/ISystemConfig.aidl
+++ b/core/java/android/os/ISystemConfig.aidl
@@ -52,4 +52,9 @@
      * @see SystemConfigManager#getDefaultVrComponents
      */
     List<ComponentName> getDefaultVrComponents();
+
+    /**
+     * @see SystemConfigManager#getPreventUserDisablePackages
+     */
+    List<String> getPreventUserDisablePackages();
 }
diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java
index c60f949..fbec518 100644
--- a/core/java/android/os/MessageQueue.java
+++ b/core/java/android/os/MessageQueue.java
@@ -57,6 +57,7 @@
 
     @UnsupportedAppUsage
     Message mMessages;
+    private Message mLast;
     @UnsupportedAppUsage
     private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
     private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
@@ -66,6 +67,10 @@
     // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
     private boolean mBlocked;
 
+    // Tracks the number of async message. We use this in enqueueMessage() to avoid searching the
+    // queue for async messages when inserting a message at the tail.
+    private int mAsyncMessageCount;
+
     // The next barrier token.
     // Barriers are indicated by messages with a null target whose arg1 field carries the token.
     @UnsupportedAppUsage
@@ -364,12 +369,21 @@
                         mBlocked = false;
                         if (prevMsg != null) {
                             prevMsg.next = msg.next;
+                            if (prevMsg.next == null) {
+                                mLast = prevMsg;
+                            }
                         } else {
                             mMessages = msg.next;
+                            if (msg.next == null) {
+                                mLast = null;
+                            }
                         }
                         msg.next = null;
                         if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                         msg.markInUse();
+                        if (msg.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         return msg;
                     }
                 } else {
@@ -492,6 +506,14 @@
             msg.when = when;
             msg.arg1 = token;
 
+            if (Flags.messageQueueTailTracking() && mLast != null && mLast.when <= when) {
+                /* Message goes to tail of list */
+                mLast.next = msg;
+                mLast = msg;
+                msg.next = null;
+                return token;
+            }
+
             Message prev = null;
             Message p = mMessages;
             if (when != 0) {
@@ -500,6 +522,12 @@
                     p = p.next;
                 }
             }
+
+            if (p == null) {
+                /* We reached the tail of the list, or list is empty. */
+                mLast = msg;
+            }
+
             if (prev != null) { // invariant: p == prev.next
                 msg.next = p;
                 prev.next = msg;
@@ -540,9 +568,15 @@
             final boolean needWake;
             if (prev != null) {
                 prev.next = p.next;
+                if (prev.next == null) {
+                    mLast = prev;
+                }
                 needWake = false;
             } else {
                 mMessages = p.next;
+                if (mMessages == null) {
+                    mLast = null;
+                }
                 needWake = mMessages == null || mMessages.target != null;
             }
             p.recycleUnchecked();
@@ -582,24 +616,77 @@
                 msg.next = p;
                 mMessages = msg;
                 needWake = mBlocked;
-            } else {
-                // Inserted within the middle of the queue.  Usually we don't have to wake
-                // up the event queue unless there is a barrier at the head of the queue
-                // and the message is the earliest asynchronous message in the queue.
-                needWake = mBlocked && p.target == null && msg.isAsynchronous();
-                Message prev;
-                for (;;) {
-                    prev = p;
-                    p = p.next;
-                    if (p == null || when < p.when) {
-                        break;
-                    }
-                    if (needWake && p.isAsynchronous()) {
-                        needWake = false;
-                    }
+                if (p == null) {
+                    mLast = mMessages;
                 }
-                msg.next = p; // invariant: p == prev.next
-                prev.next = msg;
+            } else {
+                // Message is to be inserted at tail or middle of queue. Usually we don't have to
+                // wake up the event queue unless there is a barrier at the head of the queue and
+                // the message is the earliest asynchronous message in the queue.
+                //
+                // For readability, we split this portion of the function into two blocks based on
+                // whether tail tracking is enabled. This has a minor implication for the case
+                // where tail tracking is disabled. See the comment below.
+                if (Flags.messageQueueTailTracking()) {
+                    needWake = mBlocked && p.target == null && msg.isAsynchronous()
+                        && mAsyncMessageCount == 0;
+                    if (when >= mLast.when) {
+                        msg.next = null;
+                        mLast.next = msg;
+                        mLast = msg;
+                    } else {
+                        // Inserted within the middle of the queue.
+                        Message prev;
+                        for (;;) {
+                            prev = p;
+                            p = p.next;
+                            if (p == null || when < p.when) {
+                                break;
+                            }
+                        }
+                        if (p == null) {
+                            /* Inserting at tail of queue */
+                            mLast = msg;
+                        }
+                        msg.next = p; // invariant: p == prev.next
+                        prev.next = msg;
+                    }
+                } else {
+                    needWake = mBlocked && p.target == null && msg.isAsynchronous();
+                    Message prev;
+                    for (;;) {
+                        prev = p;
+                        p = p.next;
+                        if (p == null || when < p.when) {
+                            break;
+                        }
+                        if (needWake && p.isAsynchronous()) {
+                            needWake = false;
+                        }
+                    }
+                    msg.next = p; // invariant: p == prev.next
+                    prev.next = msg;
+
+                    /*
+                     * If this block is executing then we have a build without tail tracking -
+                     * specifically: Flags.messageQueueTailTracking() == false. This is determined
+                     * at build time so the flag won't change on us during runtime.
+                     *
+                     * Since we don't want to pepper the code with extra checks, we only check
+                     * for tail tracking when we might use mLast. Otherwise, we continue to update
+                     * mLast as the tail of the list.
+                     *
+                     * In this case however we are not maintaining mLast correctly. Since we never
+                     * use it, this is fine. However, we run the risk of leaking a reference.
+                     * So set mLast to null in this case to avoid any Message leaks. The other
+                     * sites will never use the value so we are safe against null pointer derefs.
+                     */
+                    mLast = null;
+                }
+            }
+
+            if (msg.isAsynchronous()) {
+                mAsyncMessageCount++;
             }
 
             // We can assume mPtr != 0 because mQuitting is false.
@@ -692,10 +779,17 @@
                    && (object == null || p.obj == object)) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -703,8 +797,14 @@
                     if (n.target == h && n.what == what
                         && (object == null || n.obj == object)) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -726,10 +826,17 @@
                    && (object == null || object.equals(p.obj))) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -737,8 +844,14 @@
                     if (n.target == h && n.what == what
                         && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -760,10 +873,17 @@
                    && (object == null || p.obj == object)) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -771,8 +891,14 @@
                     if (n.target == h && n.callback == r
                         && (object == null || n.obj == object)) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -794,10 +920,17 @@
                    && (object == null || object.equals(p.obj))) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -805,8 +938,14 @@
                     if (n.target == h && n.callback == r
                         && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -829,18 +968,31 @@
                     && (object == null || p.obj == object)) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && (object == null || n.obj == object)) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -862,18 +1014,31 @@
                     && (object == null || object.equals(p.obj))) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -890,6 +1055,8 @@
             p = n;
         }
         mMessages = null;
+        mLast = null;
+        mAsyncMessageCount = 0;
     }
 
     private void removeAllFutureMessagesLocked() {
@@ -911,9 +1078,14 @@
                     p = n;
                 }
                 p.next = null;
+                mLast = p;
+
                 do {
                     p = n;
                     n = p.next;
+                    if (p.isAsynchronous()) {
+                        mAsyncMessageCount--;
+                    }
                     p.recycleUnchecked();
                 } while (n != null);
             }
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index d3f2c7a..eb5b511 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -94,4 +94,8 @@
 per-file Temperature.java = file:/THERMAL_OWNERS
 
 # SecurityStateManager
-per-file *SecurityStateManager* = file:/SECURITY_STATE_OWNERS
\ No newline at end of file
+per-file *SecurityStateManager* = file:/SECURITY_STATE_OWNERS
+
+# SystemConfig
+per-file ISystemConfig.aidl = file:/PACKAGE_MANAGER_OWNERS
+per-file SystemConfigManager.java = file:/PACKAGE_MANAGER_OWNERS
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index cc3069f..746278f 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -149,13 +149,47 @@
         @TestApi
         public static final int CPU_LOAD_RESUME = 3;
 
+        /**
+         * This hint indicates an increase in GPU workload intensity. It means that
+         * this hint session needs extra GPU resources to meet the target duration.
+         * This hint must be sent before reporting the actual duration to the session.
+         *
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION)
+        public static final int GPU_LOAD_UP = 5;
+
+        /**
+         * This hint indicates a decrease in GPU workload intensity. It means that
+         * this hint session can reduce GPU resources and still meet the target duration.
+         *
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION)
+        public static final int GPU_LOAD_DOWN = 6;
+
+        /**
+        * This hint indicates an upcoming GPU workload that is completely changed and
+        * unknown. It means that the hint session should reset GPU resources to a known
+        * baseline to prepare for an arbitrary load, and must wake up if inactive.
+         *
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION)
+        public static final int GPU_LOAD_RESET = 7;
+
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
         @IntDef(prefix = {"CPU_LOAD_"}, value = {
             CPU_LOAD_UP,
             CPU_LOAD_DOWN,
             CPU_LOAD_RESET,
-            CPU_LOAD_RESUME
+            CPU_LOAD_RESUME,
+            GPU_LOAD_UP,
+            GPU_LOAD_DOWN
         })
         public @interface Hint {}
 
diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java
index 9c11ad4..164e265 100644
--- a/core/java/android/os/PowerComponents.java
+++ b/core/java/android/os/PowerComponents.java
@@ -40,6 +40,7 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 class PowerComponents {
     private final BatteryConsumer.BatteryConsumerData mData;
 
diff --git a/core/java/android/os/PowerMonitorReadings.java b/core/java/android/os/PowerMonitorReadings.java
index bb677d5..a0ab066 100644
--- a/core/java/android/os/PowerMonitorReadings.java
+++ b/core/java/android/os/PowerMonitorReadings.java
@@ -71,7 +71,7 @@
      */
     @FlaggedApi("com.android.server.power.optimization.power_monitor_api")
     @ElapsedRealtimeLong
-    public long getTimestamp(@NonNull PowerMonitor powerMonitor) {
+    public long getTimestampMillis(@NonNull PowerMonitor powerMonitor) {
         int offset = Arrays.binarySearch(mPowerMonitors, powerMonitor, POWER_MONITOR_COMPARATOR);
         if (offset >= 0) {
             return mTimestampsMs[offset];
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index f952fcf..dd0436c 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -19,6 +19,7 @@
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 
 import android.annotation.ElapsedRealtimeLong;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -37,6 +38,7 @@
 
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.Preconditions;
+import com.android.sdksandbox.flags.Flags;
 
 import dalvik.system.VMRuntime;
 
@@ -1016,11 +1018,30 @@
     @SystemApi(client = MODULE_LIBRARIES)
     @TestApi
     @android.ravenwood.annotation.RavenwoodKeep
+    // TODO(b/318651609): Deprecate once Process#getSdkSandboxUidForAppUid is rolled out to 100%
     public static final int toSdkSandboxUid(int uid) {
         return uid + (FIRST_SDK_SANDBOX_UID - FIRST_APPLICATION_UID);
     }
 
     /**
+     * Returns the sdk sandbox uid corresponding to an app uid.
+     * @see android.app.sdksandbox.SdkSandboxManager
+     *
+     * @param uid the app uid
+     * @return the sdk sandbox uid for the given app uid
+     *
+     * @throws IllegalArgumentException if input is not an app uid
+     */
+    @FlaggedApi(Flags.FLAG_SDK_SANDBOX_UID_TO_APP_UID_API)
+    @android.ravenwood.annotation.RavenwoodKeep
+    public static final int getSdkSandboxUidForAppUid(int uid) {
+        if (!isApplicationUid(uid)) {
+            throw new IllegalArgumentException("Input UID is not an app UID");
+        }
+        return uid + (FIRST_SDK_SANDBOX_UID - FIRST_APPLICATION_UID);
+    }
+
+    /**
      * Returns whether the current process is a sdk sandbox process.
      */
     @android.ravenwood.annotation.RavenwoodKeep
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index f71c269..07f7690 100644
--- a/core/java/android/os/RecoverySystem.java
+++ b/core/java/android/os/RecoverySystem.java
@@ -18,8 +18,6 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -47,11 +45,8 @@
 import android.util.Log;
 import android.view.Display;
 
-import libcore.io.Streams;
-
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileWriter;
 import java.io.IOException;
@@ -75,7 +70,6 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
-import java.util.zip.ZipInputStream;
 
 import sun.security.pkcs.PKCS7;
 import sun.security.pkcs.SignerInfo;
@@ -426,72 +420,43 @@
         } finally {
             raf.close();
         }
-
-        // Additionally verify the package compatibility.
-        if (!readAndVerifyPackageCompatibilityEntry(packageFile)) {
-            throw new SignatureException("package compatibility verification failed");
-        }
     }
 
     /**
      * Verifies the compatibility entry from an {@link InputStream}.
      *
-     * @return the verification result.
+     * @param inputStream The stream that contains the package compatibility info.
+     * @throws IOException Never.
+     * @return {@code true}.
+     * @deprecated This function no longer checks {@code inputStream} and
+     *   unconditionally returns true. Instead, check compatibility when the
+     *   OTA package is generated.
      */
-    @UnsupportedAppUsage
+    @Deprecated
+    @UnsupportedAppUsage(
+            publicAlternatives = "Use {@code true} directly",
+            maxTargetSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     private static boolean verifyPackageCompatibility(InputStream inputStream) throws IOException {
-        ArrayList<String> list = new ArrayList<>();
-        ZipInputStream zis = new ZipInputStream(inputStream);
-        ZipEntry entry;
-        while ((entry = zis.getNextEntry()) != null) {
-            long entrySize = entry.getSize();
-            if (entrySize > Integer.MAX_VALUE || entrySize < 0) {
-                throw new IOException(
-                        "invalid entry size (" + entrySize + ") in the compatibility file");
-            }
-            byte[] bytes = new byte[(int) entrySize];
-            Streams.readFully(zis, bytes);
-            list.add(new String(bytes, UTF_8));
-        }
-        if (list.isEmpty()) {
-            throw new IOException("no entries found in the compatibility file");
-        }
-        return (VintfObject.verify(list.toArray(new String[list.size()])) == 0);
-    }
-
-    /**
-     * Reads and verifies the compatibility entry in an OTA zip package. The compatibility entry is
-     * a zip file (inside the OTA package zip).
-     *
-     * @return {@code true} if the entry doesn't exist or verification passes.
-     */
-    private static boolean readAndVerifyPackageCompatibilityEntry(File packageFile)
-            throws IOException {
-        try (ZipFile zip = new ZipFile(packageFile)) {
-            ZipEntry entry = zip.getEntry("compatibility.zip");
-            if (entry == null) {
-                return true;
-            }
-            InputStream inputStream = zip.getInputStream(entry);
-            return verifyPackageCompatibility(inputStream);
-        }
+        return true;
     }
 
     /**
      * Verifies the package compatibility info against the current system.
      *
      * @param compatibilityFile the {@link File} that contains the package compatibility info.
-     * @throws IOException if there were any errors reading the compatibility file.
-     * @return the compatibility verification result.
+     * @throws IOException Never.
+     * @return {@code true}
+     * @deprecated This function no longer checks {@code compatibilityFile} and
+     *   unconditionally returns true. Instead, check compatibility when the
+     *   OTA package is generated.
      *
      * {@hide}
      */
+    @Deprecated
     @SystemApi
     @SuppressLint("RequiresPermission")
     public static boolean verifyPackageCompatibility(File compatibilityFile) throws IOException {
-        try (InputStream inputStream = new FileInputStream(compatibilityFile)) {
-            return verifyPackageCompatibility(inputStream);
-        }
+        return true;
     }
 
     /**
diff --git a/core/java/android/os/SystemConfigManager.java b/core/java/android/os/SystemConfigManager.java
index 77843d9..21ffbf1 100644
--- a/core/java/android/os/SystemConfigManager.java
+++ b/core/java/android/os/SystemConfigManager.java
@@ -161,4 +161,18 @@
         }
         return Collections.emptyList();
     }
+
+    /**
+     * Return the packages that are prevented from being disabled, where if
+     * disabled it would result in a non-functioning system or similar.
+     * @hide
+     */
+    @NonNull
+    public List<String> getPreventUserDisablePackages() {
+        try {
+            return mInterface.getPreventUserDisablePackages();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index 3eea94e..53af838 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -37,6 +37,7 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class UidBatteryConsumer extends BatteryConsumer {
 
     static final int CONSUMER_TYPE_UID = 1;
@@ -223,6 +224,7 @@
     /**
      * Builder for UidBatteryConsumer.
      */
+    @android.ravenwood.annotation.RavenwoodKeepWholeClass
     public static final class Builder extends BaseBuilder<Builder> {
         private static final String PACKAGE_NAME_UNINITIALIZED = "";
         private final BatteryStats.Uid mBatteryStatsUid;
diff --git a/core/java/android/os/VintfObject.java b/core/java/android/os/VintfObject.java
index 1f11197..4fc5131 100644
--- a/core/java/android/os/VintfObject.java
+++ b/core/java/android/os/VintfObject.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.TestApi;
-import android.util.Slog;
 
 import java.util.Map;
 
@@ -44,44 +43,8 @@
     public static native String[] report();
 
     /**
-     * Verify that the given metadata for an OTA package is compatible with
-     * this device.
-     *
-     * @param packageInfo a list of serialized form of HalManifest's /
-     * CompatibilityMatri'ces (XML).
-     * @return = 0 if success (compatible)
-     *         &gt; 0 if incompatible
-     *         &lt; 0 if any error (mount partition fails, illformed XML, etc.)
-     *
-     * @deprecated Checking compatibility against an OTA package is no longer
-     * supported because the format of VINTF metadata in the OTA package may not
-     * be recognized by the current system.
-     *
-     * <p>
-     * <ul>
-     * <li>This function always returns 0 for non-empty {@code packageInfo}.
-     * </li>
-     * <li>This function returns the result of {@link #verifyWithoutAvb} for
-     * null or empty {@code packageInfo}.</li>
-     * </ul>
-     *
-     * @hide
-     */
-    @Deprecated
-    public static int verify(String[] packageInfo) {
-        if (packageInfo != null && packageInfo.length > 0) {
-            Slog.w(LOG_TAG, "VintfObject.verify() with non-empty packageInfo is deprecated. "
-                    + "Skipping compatibility checks for update package.");
-            return 0;
-        }
-        Slog.w(LOG_TAG, "VintfObject.verify() is deprecated. Call verifyWithoutAvb() instead.");
-        return verifyWithoutAvb();
-    }
-
-    /**
-     * Verify Vintf compatibility on the device without checking AVB
-     * (Android Verified Boot). It is useful to verify a running system
-     * image where AVB check is irrelevant.
+     * Verify Vintf compatibility on the device at boot time. Certain checks
+     * like kernel checks, AVB checks are disabled.
      *
      * @return = 0 if success (compatible)
      *         > 0 if incompatible
@@ -89,7 +52,7 @@
      *
      * @hide
      */
-    public static native int verifyWithoutAvb();
+    public static native int verifyBuildAtBoot();
 
     /**
      * @return a list of HAL names and versions that is supported by this
diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java
index c14810b..f3496e7 100644
--- a/core/java/android/os/ZygoteProcess.java
+++ b/core/java/android/os/ZygoteProcess.java
@@ -425,6 +425,8 @@
                 throw new ZygoteStartFailedEx("Embedded newlines not allowed");
             } else if (arg.indexOf('\r') >= 0) {
                 throw new ZygoteStartFailedEx("Embedded carriage returns not allowed");
+            } else if (arg.indexOf('\u0000') >= 0) {
+                throw new ZygoteStartFailedEx("Embedded nulls not allowed");
             }
         }
 
@@ -965,6 +967,14 @@
             return true;
         }
 
+        for (/* NonNull */ String s : mApiDenylistExemptions) {
+            // indexOf() is intrinsified and faster than contains().
+            if (s.indexOf('\n') >= 0 || s.indexOf('\r') >= 0 || s.indexOf('\u0000') >= 0) {
+                Slog.e(LOG_TAG, "Failed to set API denylist exemptions: Bad character");
+                mApiDenylistExemptions = Collections.emptyList();
+                return false;
+            }
+        }
         try {
             state.mZygoteOutputWriter.write(Integer.toString(mApiDenylistExemptions.size() + 1));
             state.mZygoteOutputWriter.newLine();
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index d646146..0db90bf 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -98,3 +98,11 @@
     description: "Guards StrictMode APIs for detecting restricted network access."
     bug: "317250784"
 }
+
+flag {
+    name: "message_queue_tail_tracking"
+    namespace: "system_performance"
+    description: "track tail of message queue."
+    bug: "305311707"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index 2d53341..322a8e6 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -25,8 +25,8 @@
 import android.os.BatteryStats;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IPowerStatsService;
+import android.os.OutcomeReceiver;
 import android.os.PowerMonitor;
 import android.os.PowerMonitorReadings;
 import android.os.Process;
@@ -39,6 +39,7 @@
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -161,12 +162,12 @@
      * (on-device power rail monitor) rails and modeled energy consumers.  If ODPM is unsupported
      * on this device this method delivers an empty list.
      *
-     * @param handler  optional Handler to deliver the callback. If not supplied, the callback
+     * @param executor optional Handler to deliver the callback. If not supplied, the callback
      *                 may be invoked on an arbitrary thread.
      * @param onResult callback for the result
      */
     @FlaggedApi("com.android.server.power.optimization.power_monitor_api")
-    public void getSupportedPowerMonitors(@Nullable Handler handler,
+    public void getSupportedPowerMonitors(@Nullable Executor executor,
             @NonNull Consumer<List<PowerMonitor>> onResult) {
         final List<PowerMonitor> result;
         synchronized (mPowerMonitorsLock) {
@@ -180,15 +181,15 @@
             }
         }
         if (result != null) {
-            if (handler != null) {
-                handler.post(() -> onResult.accept(result));
+            if (executor != null) {
+                executor.execute(() -> onResult.accept(result));
             } else {
                 onResult.accept(result);
             }
             return;
         }
         try {
-            mPowerStats.getSupportedPowerMonitors(new ResultReceiver(handler) {
+            mPowerStats.getSupportedPowerMonitors(new ResultReceiver(null) {
                 @Override
                 protected void onReceiveResult(int resultCode, Bundle resultData) {
                     PowerMonitor[] array = resultData.getParcelableArray(
@@ -197,7 +198,11 @@
                     synchronized (mPowerMonitorsLock) {
                         mPowerMonitorsInfo = result;
                     }
-                    onResult.accept(result);
+                    if (executor != null) {
+                        executor.execute(()-> onResult.accept(result));
+                    } else {
+                        onResult.accept(result);
+                    }
                 }
             });
         } catch (RemoteException e) {
@@ -213,17 +218,22 @@
      * monitors.
      *
      * @param powerMonitors power monitors to be retrieved.
-     * @param handler       optional Handler to deliver the callbacks. If not supplied, the callback
-     *                      may be invoked on an arbitrary thread.
-     * @param onSuccess     callback for the result
-     * @param onError       callback invoked in case of an error
+     * @param executor      optional Executor to deliver the callbacks. If not supplied, the
+     *                      callback may be invoked on an arbitrary thread.
+     * @param onResult      callback for the result
      */
     @FlaggedApi("com.android.server.power.optimization.power_monitor_api")
     public void getPowerMonitorReadings(@NonNull List<PowerMonitor> powerMonitors,
-            @Nullable Handler handler, @NonNull Consumer<PowerMonitorReadings> onSuccess,
-            @NonNull Consumer<RuntimeException> onError) {
+            @Nullable Executor executor,
+            @NonNull OutcomeReceiver<PowerMonitorReadings, RuntimeException> onResult) {
         if (mPowerStats == null) {
-            onError.accept(new IllegalArgumentException("Unsupported power monitor"));
+            IllegalArgumentException error =
+                    new IllegalArgumentException("Unsupported power monitor");
+            if (executor != null) {
+                executor.execute(() -> onResult.onError(error));
+            } else {
+                onResult.onError(error);
+            }
             return;
         }
 
@@ -235,18 +245,31 @@
             indices[i] = powerMonitorsArray[i].index;
         }
         try {
-            mPowerStats.getPowerMonitorReadings(indices, new ResultReceiver(handler) {
+            mPowerStats.getPowerMonitorReadings(indices, new ResultReceiver(null) {
                 @Override
                 protected void onReceiveResult(int resultCode, Bundle resultData) {
                     if (resultCode == IPowerStatsService.RESULT_SUCCESS) {
-                        onSuccess.accept(new PowerMonitorReadings(powerMonitorsArray,
+                        PowerMonitorReadings result = new PowerMonitorReadings(powerMonitorsArray,
                                 resultData.getLongArray(IPowerStatsService.KEY_ENERGY),
-                                resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS)));
-                    } else if (resultCode == IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR) {
-                        onError.accept(new IllegalArgumentException("Unsupported power monitor"));
+                                resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS));
+                        if (executor != null) {
+                            executor.execute(() -> onResult.onResult(result));
+                        } else {
+                            onResult.onResult(result);
+                        }
                     } else {
-                        onError.accept(new IllegalStateException(
-                                "Unrecognized result code " + resultCode));
+                        RuntimeException error;
+                        if (resultCode == IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR) {
+                            error = new IllegalArgumentException("Unsupported power monitor");
+                        } else {
+                            error = new IllegalStateException(
+                                    "Unrecognized result code " + resultCode);
+                        }
+                        if (executor != null) {
+                            executor.execute(() -> onResult.onError(error));
+                        } else {
+                            onResult.onError(error);
+                        }
                     }
                 }
             });
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index 3ecf74e..54ed73c 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -134,16 +134,16 @@
     @EnforcePermission("MOUNT_UNMOUNT_FILESYSTEMS")
     void setDebugFlags(int flags, int mask) = 60;
     @EnforcePermission("STORAGE_INTERNAL")
-    void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) = 61;
+    void createUserStorageKeys(int userId, boolean ephemeral) = 61;
     @EnforcePermission("STORAGE_INTERNAL")
     void destroyUserStorageKeys(int userId) = 62;
     @EnforcePermission("STORAGE_INTERNAL")
-    void unlockCeStorage(int userId, int serialNumber, in byte[] secret) = 63;
+    void unlockCeStorage(int userId, in byte[] secret) = 63;
     @EnforcePermission("STORAGE_INTERNAL")
     void lockCeStorage(int userId) = 64;
     boolean isCeStorageUnlocked(int userId) = 65;
     @EnforcePermission("STORAGE_INTERNAL")
-    void prepareUserStorage(in String volumeUuid, int userId, int serialNumber, int flags) = 66;
+    void prepareUserStorage(in String volumeUuid, int userId, int flags) = 66;
     @EnforcePermission("STORAGE_INTERNAL")
     void destroyUserStorage(in String volumeUuid, int userId, int flags) = 67;
     @EnforcePermission("STORAGE_INTERNAL")
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 78a12f7..9587db1 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1602,14 +1602,13 @@
      * This is only intended to be called by UserManagerService, as part of creating a user.
      *
      * @param userId ID of the user
-     * @param serialNumber serial number of the user
      * @param ephemeral whether the user is ephemeral
      * @throws RuntimeException on error.  The user's keys already existing is considered an error.
      * @hide
      */
-    public void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) {
+    public void createUserStorageKeys(int userId, boolean ephemeral) {
         try {
-            mStorageManager.createUserStorageKeys(userId, serialNumber, ephemeral);
+            mStorageManager.createUserStorageKeys(userId, ephemeral);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1653,9 +1652,9 @@
     }
 
     /** {@hide} */
-    public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) {
+    public void prepareUserStorage(String volumeUuid, int userId, int flags) {
         try {
-            mStorageManager.prepareUserStorage(volumeUuid, userId, serialNumber, flags);
+            mStorageManager.prepareUserStorage(volumeUuid, userId, flags);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index d09c229..60143cc 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -64,3 +64,17 @@
   description: "enable Permission PREPARE_FACTORY_RESET."
   bug: "302016478"
 }
+
+flag {
+    name: "retail_demo_role_enabled"
+    namespace: "permissions"
+    description: "default retail demo role holder"
+    bug: "274132354"
+}
+
+flag {
+    name: "wallet_role_enabled"
+    namespace: "wallet_integration"
+    description: "This flag is used to enabled the Wallet Role for all users on the device"
+    bug: "283989236"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 84fddcb..47065e1 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -108,10 +108,10 @@
 import java.lang.annotation.Target;
 import java.lang.reflect.Field;
 import java.net.URISyntaxException;
-import java.util.ArrayList;
 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;
@@ -3585,10 +3585,12 @@
                     || applicationInfo.isSignedWithPlatformKey();
         }
 
-        public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix,
-                List<String> names) {
+        private ArrayMap<String, String> getStringsForPrefixStripPrefix(
+                ContentResolver cr, String prefix, String[] names) {
             String namespace = prefix.substring(0, prefix.length() - 1);
             ArrayMap<String, String> keyValues = new ArrayMap<>();
+            int substringLength = prefix.length();
+
             int currentGeneration = -1;
             boolean needsGenerationTracker = false;
 
@@ -3601,22 +3603,30 @@
                                     + " type:" + mUri.getPath()
                                     + " in package:" + cr.getPackageName());
                         }
+                        // When a generation number changes, remove cached values, remove the old
+                        // generation tracker and request a new one
+                        generationTracker.destroy();
+                        mGenerationTrackers.remove(prefix);
                         for (int i = mValues.size() - 1; i >= 0; i--) {
                             String key = mValues.keyAt(i);
                             if (key.startsWith(prefix)) {
                                 mValues.remove(key);
                             }
                         }
+                        needsGenerationTracker = true;
                     } else {
                         boolean prefixCached = mValues.containsKey(prefix);
                         if (prefixCached) {
                             if (DEBUG) {
                                 Log.i(TAG, "Cache hit for prefix:" + prefix);
                             }
-                            if (!names.isEmpty()) {
+                            if (names.length > 0) {
                                 for (String name : names) {
+                                    // mValues can contain "null" values, need to use containsKey.
                                     if (mValues.containsKey(name)) {
-                                        keyValues.put(name, mValues.get(name));
+                                        keyValues.put(
+                                                name.substring(substringLength),
+                                                mValues.get(name));
                                     }
                                 }
                             } else {
@@ -3625,7 +3635,10 @@
                                     // Explicitly exclude the prefix as it is only there to
                                     // signal that the prefix has been cached.
                                     if (key.startsWith(prefix) && !key.equals(prefix)) {
-                                        keyValues.put(key, mValues.get(key));
+                                        String value = mValues.valueAt(i);
+                                        keyValues.put(
+                                                key.substring(substringLength),
+                                                value);
                                     }
                                 }
                             }
@@ -3685,14 +3698,22 @@
                 Map<String, String> flagsToValues =
                         (HashMap) b.getSerializable(Settings.NameValueTable.VALUE, java.util.HashMap.class);
                 // Only the flags requested by the caller
-                if (!names.isEmpty()) {
-                    for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
-                        if (names.contains(flag.getKey())) {
-                            keyValues.put(flag.getKey(), flag.getValue());
+                if (names.length > 0) {
+                    for (String name : names) {
+                        // flagsToValues can contain "null" values, need to use containsKey.
+                        if (flagsToValues.containsKey(name)) {
+                            keyValues.put(
+                                    name.substring(substringLength),
+                                    flagsToValues.get(name));
                         }
                     }
                 } else {
-                    keyValues.putAll(flagsToValues);
+                    keyValues.ensureCapacity(keyValues.size() + flagsToValues.size());
+                    for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
+                        keyValues.put(
+                                flag.getKey().substring(substringLength),
+                                flag.getValue());
+                    }
                 }
 
                 synchronized (NameValueCache.this) {
@@ -10137,8 +10158,12 @@
 
         /**
          * The default NFC payment component
+         *
+         * @deprecated please use {@link android.app.role.RoleManager#getRoleHolders(String)}
+         * with {@link android.app.role.RoleManager#ROLE_WALLET} parameter.
          * @hide
          */
+        @Deprecated
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
         public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component";
 
@@ -12155,6 +12180,36 @@
          */
         public static final String HIDE_PRIVATESPACE_ENTRY_POINT = "hide_privatespace_entry_point";
 
+        /** @hide */
+        public static final int PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK = 0;
+        /** @hide */
+        public static final int PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY = 1;
+        /** @hide */
+        public static final int PRIVATE_SPACE_AUTO_LOCK_NEVER = 2;
+
+        /**
+         * The different auto lock options for private space.
+         *
+         * @hide
+         */
+        @IntDef(prefix = {"PRIVATE_SPACE_AUTO_LOCK_"}, value = {
+                PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK,
+                PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY,
+                PRIVATE_SPACE_AUTO_LOCK_NEVER,
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface PrivateSpaceAutoLockOption {
+        }
+
+
+        /**
+         *  Store auto lock value for private space.
+         *  The possible values are defined in {@link PrivateSpaceAutoLockOption}.
+         *
+         * @hide
+         */
+        public static final String PRIVATE_SPACE_AUTO_LOCK = "private_space_auto_lock";
+
         /**
          * These entries are considered common between the personal and the managed profile,
          * since the managed profile doesn't get to change them.
@@ -12172,6 +12227,7 @@
             CLONE_TO_MANAGED_PROFILE.add(SHOW_IME_WITH_HARD_KEYBOARD);
             CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_BOUNCE_KEYS);
             CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_BUBBLES);
+            CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_HISTORY_ENABLED);
         }
 
         /** @hide */
@@ -19674,6 +19730,15 @@
             @Readable
             public static final String WRIST_DETECTION_AUTO_LOCKING_ENABLED =
                     "wear_wrist_detection_auto_locking_enabled";
+
+            /**
+             * Whether consistent notification blocking experience is enabled.
+             *
+             * @hide
+             */
+            @Readable
+            public static final String CONSISTENT_NOTIFICATION_BLOCKING_ENABLED =
+                    "consistent_notification_blocking_enabled";
         }
     }
 
@@ -19834,21 +19899,15 @@
         @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
         public static Map<String, String> getStrings(@NonNull ContentResolver resolver,
                 @NonNull String namespace, @NonNull List<String> names) {
-            List<String> compositeNames = new ArrayList<>(names.size());
-            for (String name : names) {
-                compositeNames.add(createCompositeName(namespace, name));
+            String[] compositeNames = new String[names.size()];
+            for (int i = 0, size = names.size(); i < size; ++i) {
+                compositeNames[i] = createCompositeName(namespace, names.get(i));
             }
 
             String prefix = createPrefix(namespace);
-            ArrayMap<String, String> rawKeyValues = sNameValueCache.getStringsForPrefix(
+
+            ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix(
                     resolver, prefix, compositeNames);
-            int size = rawKeyValues.size();
-            int substringLength = prefix.length();
-            ArrayMap<String, String> keyValues = new ArrayMap<>(size);
-            for (int i = 0; i < size; ++i) {
-                keyValues.put(rawKeyValues.keyAt(i).substring(substringLength),
-                        rawKeyValues.valueAt(i));
-            }
             return keyValues;
         }
 
@@ -20174,12 +20233,13 @@
         private static String createCompositeName(@NonNull String namespace, @NonNull String name) {
             Preconditions.checkNotNull(namespace);
             Preconditions.checkNotNull(name);
-            return createPrefix(namespace) + name;
+            var sb = new StringBuilder(namespace.length() + 1 + name.length());
+            return sb.append(namespace).append('/').append(name).toString();
         }
 
         private static String createPrefix(@NonNull String namespace) {
             Preconditions.checkNotNull(namespace);
-            return namespace + "/";
+            return namespace + '/';
         }
 
         private static Uri createNamespaceUri(@NonNull String namespace) {
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 7d9c0a3..2d657c2 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -445,6 +445,19 @@
     }
 
     /**
+     * Retrieves the current {@link android.app.Activity} associated with the dream.
+     * This method behaves similarly to calling {@link android.app.Activity#getActivity()}.
+     *
+     * @return The current activity, or null if the dream is not associated with an activity
+     * or not started.
+     *
+     * @hide
+     */
+    public Activity getActivity() {
+        return mActivity;
+    }
+
+    /**
      * Inflates a layout resource and set it to be the content view for this Dream.
      * Behaves similarly to {@link android.app.Activity#setContentView(int)}.
      *
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 92c516c..7658af5 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -42,6 +42,7 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.os.BadParcelableException;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -1056,7 +1057,7 @@
             ParceledListSlice<StatusBarNotification> parceledList = getNotificationInterface()
                     .getActiveNotificationsFromListener(mWrapper, keys, trim);
             return cleanUpNotificationList(parceledList);
-        } catch (android.os.RemoteException ex) {
+        } catch (android.os.RemoteException | BadParcelableException ex) {
             Log.v(TAG, "Unable to contact notification manager", ex);
         }
         return null;
diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java
index 0e82b6c..03ebae5 100644
--- a/core/java/android/service/notification/ZenDeviceEffects.java
+++ b/core/java/android/service/notification/ZenDeviceEffects.java
@@ -17,12 +17,16 @@
 package android.service.notification;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Objects;
 
@@ -33,6 +37,76 @@
 @FlaggedApi(Flags.FLAG_MODES_API)
 public final class ZenDeviceEffects implements Parcelable {
 
+    /** Used to track which rule variables have been modified by the user.
+     * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "FIELD_" }, value = {
+            FIELD_GRAYSCALE,
+            FIELD_SUPPRESS_AMBIENT_DISPLAY,
+            FIELD_DIM_WALLPAPER,
+            FIELD_NIGHT_MODE,
+            FIELD_DISABLE_AUTO_BRIGHTNESS,
+            FIELD_DISABLE_TAP_TO_WAKE,
+            FIELD_DISABLE_TILT_TO_WAKE,
+            FIELD_DISABLE_TOUCH,
+            FIELD_MINIMIZE_RADIO_USAGE,
+            FIELD_MAXIMIZE_DOZE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ModifiableField {}
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_GRAYSCALE = 1 << 0;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 1 << 1;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DIM_WALLPAPER = 1 << 2;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_NIGHT_MODE = 1 << 3;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 1 << 4;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_TAP_TO_WAKE = 1 << 5;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_TILT_TO_WAKE = 1 << 6;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_TOUCH = 1 << 7;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_MINIMIZE_RADIO_USAGE = 1 << 8;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_MAXIMIZE_DOZE = 1 << 9;
+
     private final boolean mGrayscale;
     private final boolean mSuppressAmbientDisplay;
     private final boolean mDimWallpaper;
@@ -45,10 +119,13 @@
     private final boolean mMinimizeRadioUsage;
     private final boolean mMaximizeDoze;
 
+    private final @ModifiableField int mUserModifiedFields; // Bitwise representation
+
     private ZenDeviceEffects(boolean grayscale, boolean suppressAmbientDisplay,
             boolean dimWallpaper, boolean nightMode, boolean disableAutoBrightness,
             boolean disableTapToWake, boolean disableTiltToWake, boolean disableTouch,
-            boolean minimizeRadioUsage, boolean maximizeDoze) {
+            boolean minimizeRadioUsage, boolean maximizeDoze,
+            @ModifiableField int userModifiedFields) {
         mGrayscale = grayscale;
         mSuppressAmbientDisplay = suppressAmbientDisplay;
         mDimWallpaper = dimWallpaper;
@@ -59,6 +136,7 @@
         mDisableTouch = disableTouch;
         mMinimizeRadioUsage = minimizeRadioUsage;
         mMaximizeDoze = maximizeDoze;
+        mUserModifiedFields = userModifiedFields;
     }
 
     @Override
@@ -75,14 +153,15 @@
                 && this.mDisableTiltToWake == that.mDisableTiltToWake
                 && this.mDisableTouch == that.mDisableTouch
                 && this.mMinimizeRadioUsage == that.mMinimizeRadioUsage
-                && this.mMaximizeDoze == that.mMaximizeDoze;
+                && this.mMaximizeDoze == that.mMaximizeDoze
+                && this.mUserModifiedFields == that.mUserModifiedFields;
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode,
                 mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch,
-                mMinimizeRadioUsage, mMaximizeDoze);
+                mMinimizeRadioUsage, mMaximizeDoze, mUserModifiedFields);
     }
 
     @Override
@@ -98,7 +177,43 @@
         if (mDisableTouch) effects.add("disableTouch");
         if (mMinimizeRadioUsage) effects.add("minimizeRadioUsage");
         if (mMaximizeDoze) effects.add("maximizeDoze");
-        return "[" + String.join(", ", effects) + "]";
+        return "[" + String.join(", ", effects) + "]"
+                + " userModifiedFields: " + modifiedFieldsToString(mUserModifiedFields);
+    }
+
+    private String modifiedFieldsToString(int bitmask) {
+        ArrayList<String> modified = new ArrayList<>();
+        if ((bitmask & FIELD_GRAYSCALE) != 0) {
+            modified.add("FIELD_GRAYSCALE");
+        }
+        if ((bitmask & FIELD_SUPPRESS_AMBIENT_DISPLAY) != 0) {
+            modified.add("FIELD_SUPPRESS_AMBIENT_DISPLAY");
+        }
+        if ((bitmask & FIELD_DIM_WALLPAPER) != 0) {
+            modified.add("FIELD_DIM_WALLPAPER");
+        }
+        if ((bitmask & FIELD_NIGHT_MODE) != 0) {
+            modified.add("FIELD_NIGHT_MODE");
+        }
+        if ((bitmask & FIELD_DISABLE_AUTO_BRIGHTNESS) != 0) {
+            modified.add("FIELD_DISABLE_AUTO_BRIGHTNESS");
+        }
+        if ((bitmask & FIELD_DISABLE_TAP_TO_WAKE) != 0) {
+            modified.add("FIELD_DISABLE_TAP_TO_WAKE");
+        }
+        if ((bitmask & FIELD_DISABLE_TILT_TO_WAKE) != 0) {
+            modified.add("FIELD_DISABLE_TILT_TO_WAKE");
+        }
+        if ((bitmask & FIELD_DISABLE_TOUCH) != 0) {
+            modified.add("FIELD_DISABLE_TOUCH");
+        }
+        if ((bitmask & FIELD_MINIMIZE_RADIO_USAGE) != 0) {
+            modified.add("FIELD_MINIMIZE_RADIO_USAGE");
+        }
+        if ((bitmask & FIELD_MAXIMIZE_DOZE) != 0) {
+            modified.add("FIELD_MAXIMIZE_DOZE");
+        }
+        return "{" + String.join(",", modified) + "}";
     }
 
     /**
@@ -194,9 +309,10 @@
     public static final Creator<ZenDeviceEffects> CREATOR = new Creator<ZenDeviceEffects>() {
         @Override
         public ZenDeviceEffects createFromParcel(Parcel in) {
-            return new ZenDeviceEffects(in.readBoolean(), in.readBoolean(), in.readBoolean(),
+            return new ZenDeviceEffects(in.readBoolean(),
                     in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(),
-                    in.readBoolean(), in.readBoolean(), in.readBoolean());
+                    in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(),
+                    in.readBoolean(), in.readInt());
         }
 
         @Override
@@ -205,6 +321,16 @@
         }
     };
 
+    /**
+     * Gets the bitmask representing which fields are user modified. Bits are set using
+     * {@link ModifiableField}.
+     * @hide
+     */
+    @TestApi
+    public @ModifiableField int getUserModifiedFields() {
+        return mUserModifiedFields;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -222,6 +348,7 @@
         dest.writeBoolean(mDisableTouch);
         dest.writeBoolean(mMinimizeRadioUsage);
         dest.writeBoolean(mMaximizeDoze);
+        dest.writeInt(mUserModifiedFields);
     }
 
     /** Builder class for {@link ZenDeviceEffects} objects. */
@@ -238,6 +365,7 @@
         private boolean mDisableTouch;
         private boolean mMinimizeRadioUsage;
         private boolean mMaximizeDoze;
+        private @ModifiableField int mUserModifiedFields;
 
         /**
          * Instantiates a new {@link ZenPolicy.Builder} with all effects set to default (disabled).
@@ -260,6 +388,7 @@
             mDisableTouch = zenDeviceEffects.shouldDisableTouch();
             mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage();
             mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze();
+            mUserModifiedFields = zenDeviceEffects.mUserModifiedFields;
         }
 
         /**
@@ -381,12 +510,24 @@
             return this;
         }
 
+        /**
+         * Sets the bitmask representing which fields are user modified. See the FIELD_ constants.
+         * @hide
+         */
+        @TestApi
+        @NonNull
+        public Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
+            mUserModifiedFields = userModifiedFields;
+            return this;
+        }
+
         /** Builds a {@link ZenDeviceEffects} object based on the builder's state. */
         @NonNull
         public ZenDeviceEffects build() {
-            return new ZenDeviceEffects(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper,
-                    mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake,
-                    mDisableTouch, mMinimizeRadioUsage, mMaximizeDoze);
+            return new ZenDeviceEffects(mGrayscale,
+                    mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness,
+                    mDisableTapToWake, mDisableTiltToWake, mDisableTouch, mMinimizeRadioUsage,
+                    mMaximizeDoze, mUserModifiedFields);
         }
     }
 }
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index fcdc5fe..54248be 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -64,6 +64,7 @@
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
@@ -205,6 +206,7 @@
     private static final String ALLOW_ATT_CONV = "convos";
     private static final String ALLOW_ATT_CONV_FROM = "convosFrom";
     private static final String ALLOW_ATT_CHANNELS = "channels";
+    private static final String USER_MODIFIED_FIELDS = "policyUserModifiedFields";
     private static final String DISALLOW_TAG = "disallow";
     private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
     private static final String STATE_TAG = "state";
@@ -232,6 +234,7 @@
 
     private static final String MANUAL_TAG = "manual";
     private static final String AUTOMATIC_TAG = "automatic";
+    private static final String AUTOMATIC_DELETED_TAG = "deleted";
 
     private static final String RULE_ATT_ID = "ruleId";
     private static final String RULE_ATT_ENABLED = "enabled";
@@ -247,8 +250,10 @@
     private static final String RULE_ATT_MODIFIED = "modified";
     private static final String RULE_ATT_ALLOW_MANUAL = "userInvokable";
     private static final String RULE_ATT_TYPE = "type";
+    private static final String RULE_ATT_USER_MODIFIED_FIELDS = "userModifiedFields";
     private static final String RULE_ATT_ICON = "rule_icon";
     private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
+    private static final String RULE_ATT_DELETION_INSTANT = "deletionInstant";
 
     private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
     private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -261,6 +266,7 @@
     private static final String DEVICE_EFFECT_DISABLE_TOUCH = "zdeDisableTouch";
     private static final String DEVICE_EFFECT_MINIMIZE_RADIO_USAGE = "zdeMinimizeRadioUsage";
     private static final String DEVICE_EFFECT_MAXIMIZE_DOZE = "zdeMaximizeDoze";
+    private static final String DEVICE_EFFECT_USER_MODIFIED_FIELDS = "zdeUserModifiedFields";
 
     @UnsupportedAppUsage
     public boolean allowAlarms = DEFAULT_ALLOW_ALARMS;
@@ -289,6 +295,10 @@
     @UnsupportedAppUsage
     public ArrayMap<String, ZenRule> automaticRules = new ArrayMap<>();
 
+    // Note: Map is *pkg|conditionId* (see deletedRuleKey()) -> ZenRule,
+    // unlike automaticRules (which is id -> rule).
+    public final ArrayMap<String, ZenRule> deletedRules = new ArrayMap<>();
+
     @UnsupportedAppUsage
     public ZenModeConfig() {
     }
@@ -303,15 +313,9 @@
         allowMessagesFrom = source.readInt();
         user = source.readInt();
         manualRule = source.readParcelable(null, ZenRule.class);
-        final int len = source.readInt();
-        if (len > 0) {
-            final String[] ids = new String[len];
-            final ZenRule[] rules = new ZenRule[len];
-            source.readStringArray(ids);
-            source.readTypedArray(rules, ZenRule.CREATOR);
-            for (int i = 0; i < len; i++) {
-                automaticRules.put(ids[i], rules[i]);
-            }
+        readRulesFromParcel(automaticRules, source);
+        if (Flags.modesApi()) {
+            readRulesFromParcel(deletedRules, source);
         }
         allowAlarms = source.readInt() == 1;
         allowMedia = source.readInt() == 1;
@@ -325,6 +329,19 @@
         }
     }
 
+    private static void readRulesFromParcel(ArrayMap<String, ZenRule> ruleMap, Parcel source) {
+        final int len = source.readInt();
+        if (len > 0) {
+            final String[] ids = new String[len];
+            final ZenRule[] rules = new ZenRule[len];
+            source.readStringArray(ids);
+            source.readTypedArray(rules, ZenRule.CREATOR);
+            for (int i = 0; i < len; i++) {
+                ruleMap.put(ids[i], rules[i]);
+            }
+        }
+    }
+
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(allowCalls ? 1 : 0);
@@ -336,19 +353,9 @@
         dest.writeInt(allowMessagesFrom);
         dest.writeInt(user);
         dest.writeParcelable(manualRule, 0);
-        if (!automaticRules.isEmpty()) {
-            final int len = automaticRules.size();
-            final String[] ids = new String[len];
-            final ZenRule[] rules = new ZenRule[len];
-            for (int i = 0; i < len; i++) {
-                ids[i] = automaticRules.keyAt(i);
-                rules[i] = automaticRules.valueAt(i);
-            }
-            dest.writeInt(len);
-            dest.writeStringArray(ids);
-            dest.writeTypedArray(rules, 0);
-        } else {
-            dest.writeInt(0);
+        writeRulesToParcel(automaticRules, dest);
+        if (Flags.modesApi()) {
+            writeRulesToParcel(deletedRules, dest);
         }
         dest.writeInt(allowAlarms ? 1 : 0);
         dest.writeInt(allowMedia ? 1 : 0);
@@ -362,6 +369,23 @@
         }
     }
 
+    private static void writeRulesToParcel(ArrayMap<String, ZenRule> ruleMap, Parcel dest) {
+        if (!ruleMap.isEmpty()) {
+            final int len = ruleMap.size();
+            final String[] ids = new String[len];
+            final ZenRule[] rules = new ZenRule[len];
+            for (int i = 0; i < len; i++) {
+                ids[i] = ruleMap.keyAt(i);
+                rules[i] = ruleMap.valueAt(i);
+            }
+            dest.writeInt(len);
+            dest.writeStringArray(ids);
+            dest.writeTypedArray(rules, 0);
+        } else {
+            dest.writeInt(0);
+        }
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[')
@@ -386,23 +410,26 @@
         } else {
             sb.append(",areChannelsBypassingDnd=").append(areChannelsBypassingDnd);
         }
-        return sb.append(",\nautomaticRules=").append(rulesToString())
-                .append(",\nmanualRule=").append(manualRule)
-                .append(']').toString();
+        sb.append(",\nautomaticRules=").append(rulesToString(automaticRules))
+                .append(",\nmanualRule=").append(manualRule);
+        if (Flags.modesApi()) {
+            sb.append(",\ndeletedRules=").append(rulesToString(deletedRules));
+        }
+        return sb.append(']').toString();
     }
 
-    private String rulesToString() {
-        if (automaticRules.isEmpty()) {
+    private static String rulesToString(ArrayMap<String, ZenRule> ruleList) {
+        if (ruleList.isEmpty()) {
             return "{}";
         }
 
-        StringBuilder buffer = new StringBuilder(automaticRules.size() * 28);
+        StringBuilder buffer = new StringBuilder(ruleList.size() * 28);
         buffer.append("{\n");
-        for (int i = 0; i < automaticRules.size(); i++) {
+        for (int i = 0; i < ruleList.size(); i++) {
             if (i > 0) {
                 buffer.append(",\n");
             }
-            Object value = automaticRules.valueAt(i);
+            Object value = ruleList.valueAt(i);
             buffer.append(value);
         }
         buffer.append('}');
@@ -484,7 +511,9 @@
                 && other.allowConversations == allowConversations
                 && other.allowConversationsFrom == allowConversationsFrom;
         if (Flags.modesApi()) {
-            return eq && other.allowPriorityChannels == allowPriorityChannels;
+            return eq
+                    && Objects.equals(other.deletedRules, deletedRules)
+                    && other.allowPriorityChannels == allowPriorityChannels;
         }
         return eq;
     }
@@ -641,12 +670,20 @@
                             DEFAULT_SUPPRESSED_VISUAL_EFFECTS);
                 } else if (MANUAL_TAG.equals(tag)) {
                     rt.manualRule = readRuleXml(parser);
-                } else if (AUTOMATIC_TAG.equals(tag)) {
+                } else if (AUTOMATIC_TAG.equals(tag)
+                        || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) {
                     final String id = parser.getAttributeValue(null, RULE_ATT_ID);
                     final ZenRule automaticRule = readRuleXml(parser);
                     if (id != null && automaticRule != null) {
                         automaticRule.id = id;
-                        rt.automaticRules.put(id, automaticRule);
+                        if (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag)) {
+                            String deletedRuleKey = deletedRuleKey(automaticRule);
+                            if (deletedRuleKey != null) {
+                                rt.deletedRules.put(deletedRuleKey, automaticRule);
+                            }
+                        } else if (AUTOMATIC_TAG.equals(tag)) {
+                            rt.automaticRules.put(id, automaticRule);
+                        }
                     }
                 } else if (STATE_TAG.equals(tag)) {
                     rt.areChannelsBypassingDnd = safeBoolean(parser,
@@ -657,13 +694,24 @@
         throw new IllegalStateException("Failed to reach END_DOCUMENT");
     }
 
+    /** Generates the map key used for a {@link ZenRule} in {@link #deletedRules}. */
+    @Nullable
+    public static String deletedRuleKey(ZenRule rule) {
+        if (rule.pkg != null && rule.conditionId != null) {
+            return rule.pkg + "|" + rule.conditionId.toString();
+        } else {
+            return null;
+        }
+    }
+
     /**
      * Writes XML of current ZenModeConfig
      * @param out serializer
      * @param version uses XML_VERSION if version is null
      * @throws IOException
      */
-    public void writeXml(TypedXmlSerializer out, Integer version) throws IOException {
+    public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup)
+            throws IOException {
         out.startTag(null, ZEN_TAG);
         out.attribute(null, ZEN_ATT_VERSION, version == null
                 ? Integer.toString(XML_VERSION) : Integer.toString(version));
@@ -704,6 +752,15 @@
             writeRuleXml(automaticRule, out);
             out.endTag(null, AUTOMATIC_TAG);
         }
+        if (Flags.modesApi() && !forBackup) {
+            for (int i = 0; i < deletedRules.size(); i++) {
+                final ZenRule deletedRule = deletedRules.valueAt(i);
+                out.startTag(null, AUTOMATIC_DELETED_TAG);
+                out.attribute(null, RULE_ATT_ID, deletedRule.id);
+                writeRuleXml(deletedRule, out);
+                out.endTag(null, AUTOMATIC_DELETED_TAG);
+            }
+        }
 
         out.startTag(null, STATE_TAG);
         out.attributeBoolean(null, STATE_ATT_CHANNELS_BYPASSING_DND, areChannelsBypassingDnd);
@@ -748,6 +805,12 @@
             rt.iconResName = parser.getAttributeValue(null, RULE_ATT_ICON);
             rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
             rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN);
+            rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0);
+            Long deletionInstant = tryParseLong(
+                    parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null);
+            if (deletionInstant != null) {
+                rt.deletionInstant = Instant.ofEpochMilli(deletionInstant);
+            }
         }
         return rt;
     }
@@ -794,6 +857,11 @@
                 out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription);
             }
             out.attributeInt(null, RULE_ATT_TYPE, rule.type);
+            out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields);
+            if (rule.deletionInstant != null) {
+                out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
+                        rule.deletionInstant.toEpochMilli());
+            }
         }
     }
 
@@ -856,6 +924,7 @@
                 builder.allowChannels(channels);
                 policySet = true;
             }
+            builder.setUserModifiedFields(safeInt(parser, USER_MODIFIED_FIELDS, 0));
         }
 
         if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) {
@@ -968,6 +1037,7 @@
 
         if (Flags.modesApi()) {
             writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getAllowedChannels(), out);
+            out.attributeInt(null, USER_MODIFIED_FIELDS, policy.getUserModifiedFields());
         }
     }
 
@@ -993,6 +1063,7 @@
         }
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
     @Nullable
     private static ZenDeviceEffects readZenDeviceEffectsXml(TypedXmlPullParser parser) {
         ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
@@ -1012,11 +1083,13 @@
                 .setShouldMinimizeRadioUsage(
                         safeBoolean(parser, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, false))
                 .setShouldMaximizeDoze(safeBoolean(parser, DEVICE_EFFECT_MAXIMIZE_DOZE, false))
+                .setUserModifiedFields(safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0))
                 .build();
 
         return deviceEffects.hasEffects() ? deviceEffects : null;
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
     private static void writeZenDeviceEffectsXml(ZenDeviceEffects deviceEffects,
             TypedXmlSerializer out) throws IOException {
         writeBooleanIfTrue(out, DEVICE_EFFECT_DISPLAY_GRAYSCALE,
@@ -1035,6 +1108,8 @@
         writeBooleanIfTrue(out, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE,
                 deviceEffects.shouldMinimizeRadioUsage());
         writeBooleanIfTrue(out, DEVICE_EFFECT_MAXIMIZE_DOZE, deviceEffects.shouldMaximizeDoze());
+        out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
+                deviceEffects.getUserModifiedFields());
     }
 
     private static void writeBooleanIfTrue(TypedXmlSerializer out, String att, boolean value)
@@ -1985,6 +2060,8 @@
         public String triggerDescription;
         public String iconResName;
         public boolean allowManualInvocation;
+        public int userModifiedFields;
+        @Nullable public Instant deletionInstant; // Only set on deleted rules.
 
         public ZenRule() { }
 
@@ -2017,9 +2094,25 @@
                 iconResName = source.readString();
                 triggerDescription = source.readString();
                 type = source.readInt();
+                userModifiedFields = source.readInt();
+                if (source.readInt() == 1) {
+                    deletionInstant = Instant.ofEpochMilli(source.readLong());
+                }
             }
         }
 
+        /**
+         * @see AutomaticZenRule#canUpdate()
+         */
+        @FlaggedApi(Flags.FLAG_MODES_API)
+        public boolean canBeUpdatedByApp() {
+            // The rule is considered updateable if its bitmask has no user modifications, and
+            // the bitmasks of the policy and device effects have no modification.
+            return userModifiedFields == 0
+                    && (zenPolicy == null || zenPolicy.getUserModifiedFields() == 0)
+                    && (zenDeviceEffects == null || zenDeviceEffects.getUserModifiedFields() == 0);
+        }
+
         @Override
         public int describeContents() {
             return 0;
@@ -2064,6 +2157,13 @@
                 dest.writeString(iconResName);
                 dest.writeString(triggerDescription);
                 dest.writeInt(type);
+                dest.writeInt(userModifiedFields);
+                if (deletionInstant != null) {
+                    dest.writeInt(1);
+                    dest.writeLong(deletionInstant.toEpochMilli());
+                } else {
+                    dest.writeInt(0);
+                }
             }
         }
 
@@ -2092,7 +2192,11 @@
                         .append(",allowManualInvocation=").append(allowManualInvocation)
                         .append(",iconResName=").append(iconResName)
                         .append(",triggerDescription=").append(triggerDescription)
-                        .append(",type=").append(type);
+                        .append(",type=").append(type)
+                        .append(",userModifiedFields=").append(userModifiedFields);
+                if (deletionInstant != null) {
+                    sb.append(",deletionInstant=").append(deletionInstant);
+                }
             }
 
             return sb.append(']').toString();
@@ -2151,7 +2255,9 @@
                         && other.allowManualInvocation == allowManualInvocation
                         && Objects.equals(other.iconResName, iconResName)
                         && Objects.equals(other.triggerDescription, triggerDescription)
-                        && other.type == type;
+                        && other.type == type
+                        && other.userModifiedFields == userModifiedFields
+                        && Objects.equals(other.deletionInstant, deletionInstant);
             }
 
             return finalEquals;
@@ -2163,7 +2269,7 @@
                 return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                         component, configurationActivity, pkg, id, enabler, zenPolicy,
                         zenDeviceEffects, modified, allowManualInvocation, iconResName,
-                        triggerDescription, type);
+                        triggerDescription, type, userModifiedFields, deletionInstant);
             }
             return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                     component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index d87e758..91ef11c 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -30,6 +30,11 @@
 /**
  * ZenModeDiff is a utility class meant to encapsulate the diff between ZenModeConfigs and their
  * subcomponents (automatic and manual ZenRules).
+ *
+ * <p>Note that this class is intended to detect <em>meaningful</em> differences, so objects that
+ * are not identical (as per their {@code equals()} implementation) can still produce an empty diff
+ * if only "metadata" fields are updated.
+ *
  * @hide
  */
 public class ZenModeDiff {
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 3c1a279..8477eb7 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -20,6 +20,8 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
 import android.app.Flags;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -32,6 +34,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -41,12 +44,148 @@
  * a device is in Do Not Disturb mode.
  */
 public final class ZenPolicy implements Parcelable {
-    private ArrayList<Integer> mPriorityCategories;
-    private ArrayList<Integer> mVisualEffects;
+
+    /** Used to track which rule variables have been modified by the user.
+     * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "FIELD_" }, value = {
+            FIELD_MESSAGES,
+            FIELD_CALLS,
+            FIELD_CONVERSATIONS,
+            FIELD_ALLOW_CHANNELS,
+            FIELD_PRIORITY_CATEGORY_REMINDERS,
+            FIELD_PRIORITY_CATEGORY_EVENTS,
+            FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS,
+            FIELD_PRIORITY_CATEGORY_ALARMS,
+            FIELD_PRIORITY_CATEGORY_MEDIA,
+            FIELD_PRIORITY_CATEGORY_SYSTEM,
+            FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT,
+            FIELD_VISUAL_EFFECT_LIGHTS,
+            FIELD_VISUAL_EFFECT_PEEK,
+            FIELD_VISUAL_EFFECT_STATUS_BAR,
+            FIELD_VISUAL_EFFECT_BADGE,
+            FIELD_VISUAL_EFFECT_AMBIENT,
+            FIELD_VISUAL_EFFECT_NOTIFICATION_LIST,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ModifiableField {}
+
+    /**
+     * Covers modifications to MESSAGE_SENDERS and PRIORITY_CATEGORY_MESSAGES, which are set at
+     * the same time.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_MESSAGES = 1 << 0;
+    /**
+     * Covers modifications to CALL_SENDERS and PRIORITY_CATEGORY_CALLS, which are set at
+     * the same time.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_CALLS = 1 << 1;
+    /**
+     * Covers modifications to CONVERSATION_SENDERS and PRIORITY_CATEGORY_CONVERSATIONS, which are
+     * set at the same time.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_CONVERSATIONS = 1 << 2;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_ALLOW_CHANNELS = 1 << 3;
+    /**
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_REMINDERS = 1 << 4;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16;
+
+    private List<Integer> mPriorityCategories;
+    private List<Integer> mVisualEffects;
     private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET;
     private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET;
     private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET;
     private @ChannelType int mAllowChannels = CHANNEL_TYPE_UNSET;
+    private final @ModifiableField int mUserModifiedFields; // Bitwise representation
 
     /** @hide */
     @IntDef(prefix = { "PRIORITY_CATEGORY_" }, value = {
@@ -249,6 +388,22 @@
     public ZenPolicy() {
         mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0));
         mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0));
+        mUserModifiedFields = 0;
+    }
+
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public ZenPolicy(List<Integer> priorityCategories, List<Integer> visualEffects,
+                     @PeopleType int priorityMessages, @PeopleType int priorityCalls,
+                     @ConversationSenders int conversationSenders, @ChannelType int allowChannels,
+                     @ModifiableField int userModifiedFields) {
+        mPriorityCategories = priorityCategories;
+        mVisualEffects = visualEffects;
+        mPriorityMessages = priorityMessages;
+        mPriorityCalls = priorityCalls;
+        mConversationSenders = conversationSenders;
+        mAllowChannels = allowChannels;
+        mUserModifiedFields = userModifiedFields;
     }
 
     /**
@@ -473,6 +628,8 @@
      * is not set, it is (@link STATE_UNSET} and will not change the current set policy.
      */
     public static final class Builder {
+        private @ModifiableField int mUserModifiedFields;
+
         private ZenPolicy mZenPolicy;
 
         public Builder() {
@@ -482,9 +639,14 @@
         /**
          * @hide
          */
-        public Builder(ZenPolicy policy) {
+        @SuppressLint("UnflaggedApi")
+        @TestApi
+        public Builder(@Nullable ZenPolicy policy) {
             if (policy != null) {
                 mZenPolicy = policy.copy();
+                if (Flags.modesApi()) {
+                    mUserModifiedFields = policy.mUserModifiedFields;
+                }
             } else {
                 mZenPolicy = new ZenPolicy();
             }
@@ -494,7 +656,15 @@
          * Builds the current ZenPolicy.
          */
         public @NonNull ZenPolicy build() {
-            return mZenPolicy.copy();
+            if (Flags.modesApi()) {
+                return new ZenPolicy(new ArrayList<Integer>(mZenPolicy.mPriorityCategories),
+                        new ArrayList<Integer>(mZenPolicy.mVisualEffects),
+                        mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls,
+                        mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels,
+                        mUserModifiedFields);
+            } else {
+                return mZenPolicy.copy();
+            }
         }
 
         /**
@@ -850,6 +1020,28 @@
             mZenPolicy.mAllowChannels = channelType;
             return this;
         }
+
+        /**
+         * Sets the user modified fields bitmask.
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi(Flags.FLAG_MODES_API)
+        public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
+            mUserModifiedFields = userModifiedFields;
+            return this;
+        }
+    }
+
+    /**
+     Gets the bitmask representing which fields are user modified. Bits are set using
+     * {@link ModifiableField}.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public @ModifiableField int getUserModifiedFields() {
+        return mUserModifiedFields;
     }
 
     @Override
@@ -861,39 +1053,49 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeList(mPriorityCategories);
         dest.writeList(mVisualEffects);
-        dest.writeInt(mPriorityCalls);
         dest.writeInt(mPriorityMessages);
+        dest.writeInt(mPriorityCalls);
         dest.writeInt(mConversationSenders);
         if (Flags.modesApi()) {
             dest.writeInt(mAllowChannels);
+            dest.writeInt(mUserModifiedFields);
         }
     }
 
-    public static final @android.annotation.NonNull Parcelable.Creator<ZenPolicy> CREATOR =
-            new Parcelable.Creator<ZenPolicy>() {
-        @Override
-        public ZenPolicy createFromParcel(Parcel source) {
-            ZenPolicy policy = new ZenPolicy();
-            policy.mPriorityCategories = trimList(
-                    source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
-                    NUM_PRIORITY_CATEGORIES);
-            policy.mVisualEffects = trimList(
-                    source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
-                    NUM_VISUAL_EFFECTS);
-            policy.mPriorityCalls = source.readInt();
-            policy.mPriorityMessages = source.readInt();
-            policy.mConversationSenders = source.readInt();
-            if (Flags.modesApi()) {
-                policy.mAllowChannels = source.readInt();
-            }
-            return policy;
-        }
+    public static final @NonNull Creator<ZenPolicy> CREATOR =
+            new Creator<ZenPolicy>() {
+                @Override
+                public ZenPolicy createFromParcel(Parcel source) {
+                    ZenPolicy policy;
+                    if (Flags.modesApi()) {
+                        policy = new ZenPolicy(
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_PRIORITY_CATEGORIES),
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_VISUAL_EFFECTS),
+                                source.readInt(), source.readInt(), source.readInt(),
+                                source.readInt(), source.readInt()
+                        );
+                    } else {
+                        policy = new ZenPolicy();
+                        policy.mPriorityCategories =
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_PRIORITY_CATEGORIES);
+                        policy.mVisualEffects =
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_VISUAL_EFFECTS);
+                        policy.mPriorityMessages = source.readInt();
+                        policy.mPriorityCalls = source.readInt();
+                        policy.mConversationSenders = source.readInt();
+                    }
+                    return policy;
+                }
 
-        @Override
-        public ZenPolicy[] newArray(int size) {
-            return new ZenPolicy[size];
-        }
-    };
+                @Override
+                public ZenPolicy[] newArray(int size) {
+                    return new ZenPolicy[size];
+                }
+            };
 
     @Override
     public String toString() {
@@ -907,10 +1109,69 @@
                         conversationTypeToString(mConversationSenders));
         if (Flags.modesApi()) {
             sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels));
+            sb.append(", userModifiedFields=")
+                    .append(modifiedFieldsToString(mUserModifiedFields));
         }
         return sb.append('}').toString();
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    private String modifiedFieldsToString(@ModifiableField int bitmask) {
+        ArrayList<String> modified = new ArrayList<>();
+        if ((bitmask & FIELD_MESSAGES) != 0) {
+            modified.add("FIELD_MESSAGES");
+        }
+        if ((bitmask & FIELD_CALLS) != 0) {
+            modified.add("FIELD_CALLS");
+        }
+        if ((bitmask & FIELD_CONVERSATIONS) != 0) {
+            modified.add("FIELD_CONVERSATIONS");
+        }
+        if ((bitmask & FIELD_ALLOW_CHANNELS) != 0) {
+            modified.add("FIELD_ALLOW_CHANNELS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_REMINDERS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_REMINDERS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_EVENTS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_EVENTS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_ALARMS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_ALARMS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_MEDIA) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_MEDIA");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_SYSTEM) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_SYSTEM");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_LIGHTS) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_LIGHTS");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_PEEK) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_PEEK");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_STATUS_BAR) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_STATUS_BAR");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_BADGE) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_BADGE");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_AMBIENT) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_AMBIENT");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_NOTIFICATION_LIST) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_NOTIFICATION_LIST");
+        }
+        return "{" + String.join(",", modified) + "}";
+    }
+
     // Returns a list containing the first maxLength elements of the input list if the list is
     // longer than that size. For the lists in ZenPolicy, this should not happen unless the input
     // is corrupt.
@@ -1066,7 +1327,8 @@
                 && other.mPriorityMessages == mPriorityMessages
                 && other.mConversationSenders == mConversationSenders;
         if (Flags.modesApi()) {
-            return eq && other.mAllowChannels == mAllowChannels;
+            return eq && other.mAllowChannels == mAllowChannels
+                    && other.mUserModifiedFields == mUserModifiedFields;
         }
         return eq;
     }
@@ -1075,13 +1337,13 @@
     public int hashCode() {
         if (Flags.modesApi()) {
             return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls,
-                    mPriorityMessages, mConversationSenders, mAllowChannels);
+                    mPriorityMessages, mConversationSenders, mAllowChannels, mUserModifiedFields);
         }
         return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages,
                 mConversationSenders);
     }
 
-    private @ZenPolicy.State int getZenPolicyPriorityCategoryState(@PriorityCategory int
+    private @State int getZenPolicyPriorityCategoryState(@PriorityCategory int
             category) {
         switch (category) {
             case PRIORITY_CATEGORY_REMINDERS:
@@ -1106,7 +1368,7 @@
         return -1;
     }
 
-    private @ZenPolicy.State int getZenPolicyVisualEffectState(@VisualEffect int effect) {
+    private @State int getZenPolicyVisualEffectState(@VisualEffect int effect) {
         switch (effect) {
             case VISUAL_EFFECT_FULL_SCREEN_INTENT:
                 return getVisualEffectFullScreenIntent();
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index a2ade6a..3008b8d 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -21,3 +21,11 @@
   description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices"
   bug: "306271190"
 }
+
+flag {
+  name: "callstyle_callback_api"
+  namespace: "systemui"
+  description: "Guards the new CallStyleNotificationEventsCallback"
+  bug: "305095040"
+  is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/java/android/service/wearable/OWNERS b/core/java/android/service/wearable/OWNERS
index 073e2d7..eca48b7 100644
--- a/core/java/android/service/wearable/OWNERS
+++ b/core/java/android/service/wearable/OWNERS
@@ -1,3 +1 @@
-charliewang@google.com
-oni@google.com
-volnov@google.com
\ No newline at end of file
+include /core/java/android/app/wearable/OWNERS
\ No newline at end of file
diff --git a/core/java/android/speech/RecognizerIntent.java b/core/java/android/speech/RecognizerIntent.java
index 118d028..1ca7ac7 100644
--- a/core/java/android/speech/RecognizerIntent.java
+++ b/core/java/android/speech/RecognizerIntent.java
@@ -16,6 +16,9 @@
 
 package android.speech;
 
+import static android.speech.flags.Flags.FLAG_MULTILANG_EXTRA_LAUNCH;
+
+import android.annotation.FlaggedApi;
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
@@ -653,4 +656,30 @@
      */
     public static final String EXTRA_LANGUAGE_SWITCH_ALLOWED_LANGUAGES =
             "android.speech.extra.LANGUAGE_SWITCH_ALLOWED_LANGUAGES";
+
+    /**
+     * Optional integer to use for {@link #EXTRA_ENABLE_LANGUAGE_SWITCH}. If set, the language
+     * switch will be deactivated when LANGUAGE_SWITCH_MAX_SWITCHES reached.
+     *
+     * <p> Depending on the recognizer implementation, this flag may have no effect.
+     *
+     * @see #EXTRA_ENABLE_LANGUAGE_SWITCH
+     */
+    @FlaggedApi(FLAG_MULTILANG_EXTRA_LAUNCH)
+    public static final String EXTRA_LANGUAGE_SWITCH_MAX_SWITCHES =
+            "android.speech.extra.LANGUAGE_SWITCH_MAX_SWITCHES";
+
+    /**
+     * Optional integer to use for {@link #EXTRA_ENABLE_LANGUAGE_SWITCH}. If set, the language
+     * switch will only be activated for this value of ms of audio since the START_OF_SPEECH. This
+     * could provide a more stable recognition result when the language switch is only required in
+     * the beginning of the session.
+     *
+     * <p> Depending on the recognizer implementation, this flag may have no effect.
+     *
+     * @see #EXTRA_ENABLE_LANGUAGE_SWITCH
+     */
+    @FlaggedApi(FLAG_MULTILANG_EXTRA_LAUNCH)
+    public static final String EXTRA_LANGUAGE_SWITCH_INITIAL_ACTIVE_DURATION_TIME_MILLIS =
+            "android.speech.extra.LANGUAGE_SWITCH_INITIAL_ACTIVE_DURATION_TIME_MILLIS";
 }
diff --git a/core/java/android/speech/flags/speech_flags.aconfig b/core/java/android/speech/flags/speech_flags.aconfig
new file mode 100644
index 0000000..fd80127
--- /dev/null
+++ b/core/java/android/speech/flags/speech_flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.speech.flags"
+
+flag {
+    name: "multilang_extra_launch"
+    namespace: "machine_learning"
+    description: "Feature flag for adding new extra for multi-lang feature"
+    bug: "312489931"
+}
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index 4c81888..a6d3bb4 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -454,7 +454,7 @@
             line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                     mEllipsizedStart, mEllipsizedStart + mEllipsizedCount, useFallbackLineSpacing);
-            mMax = (int) Math.ceil(line.metrics(null, null, false));
+            mMax = (int) Math.ceil(line.metrics(null, null, false, null));
             TextLine.recycle(line);
         }
 
@@ -603,7 +603,7 @@
                 0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,
                 0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */,
                 useFallbackLineSpacing);
-        fm.width = (int) Math.ceil(line.metrics(fm, fm.mDrawingBounds, false));
+        fm.width = (int) Math.ceil(line.metrics(fm, fm.mDrawingBounds, false, null));
         TextLine.recycle(line);
 
         return fm;
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index c9906cc..eca848a 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -18,6 +18,7 @@
 
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
+import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
 
 import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
@@ -50,8 +51,10 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * A base class that manages text layout in visual elements on
@@ -669,7 +672,8 @@
             int start = previousLineEnd;
             previousLineEnd = getLineStart(lineNum + 1);
             final boolean justify = isJustificationRequired(lineNum);
-            int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
+            int end = getLineVisibleEnd(lineNum, start, previousLineEnd,
+                    true /* trailingSpaceAtLastLineIsVisible */);
             paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
             paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
 
@@ -1056,7 +1060,7 @@
             if (isJustificationRequired(line)) {
                 tl.justify(getJustifyWidth(line));
             }
-            tl.metrics(null, rectF, false);
+            tl.metrics(null, rectF, false, null);
 
             float lineLeft = rectF.left;
             float lineRight = rectF.right;
@@ -1456,7 +1460,7 @@
         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops,
                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                 isFallbackLineSpacingEnabled());
-        float wid = tl.measure(offset - start, trailing, null, null);
+        float wid = tl.measure(offset - start, trailing, null, null, null);
         TextLine.recycle(tl);
 
         if (clamped && wid > mWidth) {
@@ -1792,12 +1796,69 @@
         if (isJustificationRequired(line)) {
             tl.justify(getJustifyWidth(line));
         }
-        final float width = tl.metrics(null, null, mUseBoundsForWidth);
+        final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
         TextLine.recycle(tl);
         return width;
     }
 
     /**
+     * Returns the number of letter spacing unit in the line.
+     *
+     * <p>
+     * This API returns a number of letters that is a target of letter spacing. The letter spacing
+     * won't be added to the middle of the characters that are needed to be treated as a single,
+     * e.g., ligatured or conjunct form. Note that this value is different from the number of]
+     * grapheme clusters that is calculated by {@link BreakIterator#getCharacterInstance(Locale)}.
+     * For example, if the "fi" is ligatured, the ligatured form is treated as single uni and letter
+     * spacing is not added, but it has two separate grapheme cluster.
+     *
+     * <p>
+     * This value is used for calculating the letter spacing amount for the justification because
+     * the letter spacing is applied between clusters. For example, if extra {@code W} pixels needed
+     * to be filled by letter spacing, the amount of letter spacing to be applied is
+     * {@code W}/(letter spacing unit count - 1) px.
+     *
+     * @param line the index of the line
+     * @param includeTrailingWhitespace whether to include trailing whitespace
+     * @return the number of cluster count in the line.
+     */
+    @IntRange(from = 0)
+    @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+    public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line,
+            boolean includeTrailingWhitespace) {
+        final int start = getLineStart(line);
+        final int end = includeTrailingWhitespace ? getLineEnd(line)
+                : getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+                        false  // trailingSpaceAtLastLineIsVisible: Treating trailing whitespaces at
+                               // the last line as a invisible chars for single line justification.
+                );
+
+        final Directions directions = getLineDirections(line);
+        // Returned directions can actually be null
+        if (directions == null) {
+            return 0;
+        }
+        final int dir = getParagraphDirection(line);
+
+        final TextLine tl = TextLine.obtain();
+        final TextPaint paint = mWorkPaint;
+        paint.set(mPaint);
+        paint.setStartHyphenEdit(getStartHyphenEdit(line));
+        paint.setEndHyphenEdit(getEndHyphenEdit(line));
+        tl.set(paint, mText, start, end, dir, directions,
+                false, null, // tab width is not used for cluster counting.
+                getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
+                isFallbackLineSpacingEnabled());
+        if (mLineInfo == null) {
+            mLineInfo = new TextLine.LineInfo();
+        }
+        mLineInfo.setClusterCount(0);
+        tl.metrics(null, null, mUseBoundsForWidth, mLineInfo);
+        TextLine.recycle(tl);
+        return mLineInfo.getClusterCount();
+    }
+
+    /**
      * Returns the signed horizontal extent of the specified line, excluding
      * leading margin.  If full is false, excludes trailing whitespace.
      * @param line the index of the line
@@ -1823,7 +1884,7 @@
         if (isJustificationRequired(line)) {
             tl.justify(getJustifyWidth(line));
         }
-        final float width = tl.metrics(null, null, mUseBoundsForWidth);
+        final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
         TextLine.recycle(tl);
         return width;
     }
@@ -2432,14 +2493,21 @@
      * is not counted) on the specified line.
      */
     public int getLineVisibleEnd(int line) {
-        return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
+        return getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+                true /* trailingSpaceAtLastLineIsVisible */);
     }
 
-    private int getLineVisibleEnd(int line, int start, int end) {
+    private int getLineVisibleEnd(int line, int start, int end,
+            boolean trailingSpaceAtLastLineIsVisible) {
         CharSequence text = mText;
         char ch;
-        if (line == getLineCount() - 1) {
-            return end;
+
+        // Historically, trailing spaces at the last line is counted as visible. However, this
+        // doesn't work well for justification.
+        if (trailingSpaceAtLastLineIsVisible) {
+            if (line == getLineCount() - 1) {
+                return end;
+            }
         }
 
         for (; end > start; end--) {
@@ -2939,7 +3007,7 @@
             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops,
                     0 /* ellipsisStart */, 0 /* ellipsisEnd */,
                     false /* use fallback line spacing. unused */);
-            return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth));
+            return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth, null));
         } finally {
             TextLine.recycle(tl);
             if (mt != null) {
@@ -3337,6 +3405,8 @@
     private boolean mUseBoundsForWidth;
     private @Nullable Paint.FontMetrics mMinimumFontMetrics;
 
+    private TextLine.LineInfo mLineInfo = null;
+
     /** @hide */
     @IntDef(prefix = { "DIR_" }, value = {
             DIR_LEFT_TO_RIGHT,
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index f9abec0..135935c 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -76,6 +76,21 @@
     private RectF mTmpRectForPaintAPI;
     private Rect mTmpRectForPrecompute;
 
+    // Recycling object for Paint APIs. Do not use outside getRunAdvances method.
+    private Paint.RunInfo mRunInfo;
+
+    public static final class LineInfo {
+        private int mClusterCount;
+
+        public int getClusterCount() {
+            return mClusterCount;
+        }
+
+        public void setClusterCount(int clusterCount) {
+            mClusterCount = clusterCount;
+        }
+    };
+
     private boolean mUseFallbackExtent = false;
 
     // The start and end of a potentially existing ellipsis on this text line.
@@ -270,7 +285,7 @@
             // width.
             return;
         }
-        final float width = Math.abs(measure(end, false, null, null));
+        final float width = Math.abs(measure(end, false, null, null, null));
         mAddedWidthForJustify = (justifyWidth - width) / spaces;
         mIsJustifying = true;
     }
@@ -315,10 +330,12 @@
      * @param drawBounds output parameter for drawing bounding box. optional.
      * @param returnDrawWidth true for returning width of the bounding box, false for returning
      *                       total advances.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width of the line
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth) {
+    public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
+            @Nullable LineInfo lineInfo) {
         if (returnDrawWidth) {
             if (drawBounds == null) {
                 if (mTmpRectForMeasure == null) {
@@ -327,7 +344,7 @@
                 drawBounds = mTmpRectForMeasure;
             }
             drawBounds.setEmpty();
-            float w = measure(mLen, false, fmi, drawBounds);
+            float w = measure(mLen, false, fmi, drawBounds, lineInfo);
             float boundsWidth = drawBounds.width();
             if (Math.abs(w) > boundsWidth) {
                 return w;
@@ -337,7 +354,7 @@
                 return Math.signum(w) * boundsWidth;
             }
         } else {
-            return measure(mLen, false, fmi, drawBounds);
+            return measure(mLen, false, fmi, drawBounds, lineInfo);
         }
     }
 
@@ -407,12 +424,13 @@
      *                 the edge of the preceding run's edge. See example above.
      * @param fmi receives metrics information about the requested character, can be null
      * @param drawBounds output parameter for drawing bounding box. optional.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed graphical offset from the leading margin to the requested character edge.
      *         The positive value means the offset is right from the leading edge. The negative
      *         value means the offset is left from the leading edge.
      */
     public float measure(@IntRange(from = 0) int offset, boolean trailing,
-            @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds) {
+            @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) {
         if (offset > mLen) {
             throw new IndexOutOfBoundsException(
                     "offset(" + offset + ") should be less than line limit(" + mLen + ")");
@@ -437,16 +455,16 @@
 
                     if (targetIsInThisSegment && sameDirection) {
                         return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null,
-                                0, h);
+                                0, h, lineInfo);
                     }
 
                     final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds,
-                            null, 0, h);
+                            null, 0, h, lineInfo);
                     h += sameDirection ? segmentWidth : -segmentWidth;
 
                     if (targetIsInThisSegment) {
                         return h + measureRun(segStart, offset, j, runIsRtl, null, null,  null, 0,
-                                h);
+                                h, lineInfo);
                     }
 
                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
@@ -537,7 +555,8 @@
                     final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
 
                     final float segmentWidth =
-                            measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0);
+                            measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0,
+                                    null);
 
                     final float oldh = h;
                     h += sameDirection ? segmentWidth : -segmentWidth;
@@ -578,7 +597,7 @@
     }
 
     /**
-     * @see #measure(int, boolean, FontMetricsInt, RectF)
+     * @see #measure(int, boolean, FontMetricsInt, RectF, LineInfo)
      * @return The measure results for all possible offsets
      */
     @VisibleForTesting
@@ -610,7 +629,7 @@
                     final float previousSegEndHorizontal = measurement[segStart];
                     final float width =
                             measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart,
-                                    0);
+                                    0, null);
                     horizontal += sameDirection ? width : -width;
 
                     float currHorizontal = sameDirection ? oldHorizontal : horizontal;
@@ -675,14 +694,14 @@
             boolean needWidth) {
 
         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0);
+            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null);
             handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
-                    y, bottom, null, null, false, null, 0);
+                    y, bottom, null, null, false, null, 0, null);
             return w;
         }
 
         return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
-                y, bottom, null, null, needWidth, null, 0);
+                y, bottom, null, null, needWidth, null, 0, null);
     }
 
     /**
@@ -698,19 +717,20 @@
      * @param advances receives the advance information about the requested run, can be null.
      * @param advancesIndex the start index to fill in the advance information.
      * @param x horizontal offset of the run.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width from the start of the run to the leading edge
      * of the character at offset, based on the run (not paragraph) direction
      */
     private float measureRun(int start, int offset, int limit, boolean runIsRtl,
             @Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances,
-            int advancesIndex, float x) {
+            int advancesIndex, float x, @Nullable LineInfo lineInfo) {
         if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0);
+            float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null);
             return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi,
-                    drawBounds, true, advances, advancesIndex);
+                    drawBounds, true, advances, advancesIndex, lineInfo);
         }
         return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds,
-                true, advances, advancesIndex);
+                true, advances, advancesIndex, lineInfo);
     }
 
     /**
@@ -729,14 +749,14 @@
             int limit, boolean runIsRtl, float x, boolean needWidth) {
 
         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0);
+            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null);
             handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null,
-                    false, null, 0);
+                    false, null, 0, null);
             return w;
         }
 
         return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null,
-                needWidth, null, 0);
+                needWidth, null, 0, null);
     }
 
 
@@ -1077,16 +1097,35 @@
 
     private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
             boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex,
-            RectF drawingBounds) {
+            RectF drawingBounds, @Nullable LineInfo lineInfo) {
+        if (lineInfo != null) {
+            if (mRunInfo == null) {
+                mRunInfo = new Paint.RunInfo();
+            }
+            mRunInfo.setClusterCount(0);
+        } else {
+            mRunInfo = null;
+        }
         if (mCharsValid) {
-            return wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
-                    runIsRtl, offset, advances, advancesIndex, drawingBounds);
+            float r = wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
+                    runIsRtl, offset, advances, advancesIndex, drawingBounds, mRunInfo);
+            if (lineInfo != null) {
+                lineInfo.setClusterCount(lineInfo.getClusterCount() + mRunInfo.getClusterCount());
+            }
+            return r;
         } else {
             final int delta = mStart;
-            if (mComputed == null || advances != null) {
-                return wp.getRunCharacterAdvance(mText, delta + start, delta + end,
+            // TODO: Add cluster information to the PrecomputedText for better performance of
+            // justification.
+            if (mComputed == null || advances != null || lineInfo != null) {
+                float r = wp.getRunCharacterAdvance(mText, delta + start, delta + end,
                         delta + contextStart, delta + contextEnd, runIsRtl,
-                        delta + offset, advances, advancesIndex, drawingBounds);
+                        delta + offset, advances, advancesIndex, drawingBounds, mRunInfo);
+                if (lineInfo != null) {
+                    lineInfo.setClusterCount(
+                            lineInfo.getClusterCount() + mRunInfo.getClusterCount());
+                }
+                return r;
             } else {
                 if (drawingBounds != null) {
                     if (mTmpRectForPrecompute == null) {
@@ -1120,6 +1159,7 @@
      * @param decorations the list of locations and paremeters for drawing decorations
      * @param advances receives the advance information about the requested run, can be null.
      * @param advancesIndex the start index to fill in the advance information.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width of the run based on the run direction; only
      * valid if needWidth is true
      */
@@ -1128,7 +1168,7 @@
             Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom,
             FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset,
             @Nullable ArrayList<DecorationInfo> decorations,
-            @Nullable float[] advances, int advancesIndex) {
+            @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) {
 
         if (mIsJustifying) {
             wp.setWordSpacing(mAddedWidthForJustify);
@@ -1155,7 +1195,8 @@
                 mTmpRectForPaintAPI = new RectF();
             }
             totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset,
-                    advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI);
+                    advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI,
+                    lineInfo);
             if (drawBounds != null) {
                 if (runIsRtl) {
                     mTmpRectForPaintAPI.offset(x - totalWidth, 0);
@@ -1206,9 +1247,9 @@
                     final int decorationStart = Math.max(info.start, start);
                     final int decorationEnd = Math.min(info.end, offset);
                     float decorationStartAdvance = getRunAdvance(wp, start, end, contextStart,
-                            contextEnd, runIsRtl, decorationStart, null, 0, null);
+                            contextEnd, runIsRtl, decorationStart, null, 0, null, null);
                     float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart,
-                            contextEnd, runIsRtl, decorationEnd, null, 0, null);
+                            contextEnd, runIsRtl, decorationEnd, null, 0, null, null);
                     final float decorationXLeft, decorationXRight;
                     if (runIsRtl) {
                         decorationXLeft = rightX - decorationEndAdvance;
@@ -1377,6 +1418,7 @@
      * @param needWidth true if the width is required
      * @param advances receives the advance information about the requested run, can be null.
      * @param advancesIndex the start index to fill in the advance information.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width of the run based on the run direction; only
      * valid if needWidth is true
      */
@@ -1384,7 +1426,7 @@
             int limit, boolean runIsRtl, Canvas c,
             TextShaper.GlyphsConsumer consumer, float x, int top, int y,
             int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth,
-            @Nullable float[] advances, int advancesIndex) {
+            @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) {
 
         if (measureLimit < start || measureLimit > limit) {
             throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
@@ -1431,7 +1473,7 @@
             wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
             return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top,
                     y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances,
-                    advancesIndex);
+                    advancesIndex, lineInfo);
         }
 
         // Shaping needs to take into account context up to metric boundaries,
@@ -1523,7 +1565,7 @@
                             consumer, x, top, y, bottom, fmi, drawBounds,
                             needWidth || activeEnd < measureLimit,
                             Math.min(activeEnd, mlimit), mDecorations,
-                            advances, advancesIndex + activeStart - start);
+                            advances, advancesIndex + activeStart - start, lineInfo);
 
                     activeStart = j;
                     activePaint.set(wp);
@@ -1551,7 +1593,7 @@
             x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x,
                     top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit,
                     Math.min(activeEnd, mlimit), mDecorations,
-                    advances, advancesIndex + activeStart - start);
+                    advances, advancesIndex + activeStart - start, lineInfo);
         }
 
         return x - originalX;
diff --git a/core/java/android/view/DisplayAddress.java b/core/java/android/view/DisplayAddress.java
index 99e811a..c3c4ab5 100644
--- a/core/java/android/view/DisplayAddress.java
+++ b/core/java/android/view/DisplayAddress.java
@@ -138,6 +138,30 @@
             out.writeLong(mPhysicalDisplayId);
         }
 
+        /**
+         * This method is meant to check to see if the ports match
+         * @param a1 Address to compare
+         * @param a2 Address to compare
+         *
+         * @return true if the arguments have the same port, and at least one does not specify
+         *         a model.
+         */
+        public static boolean isPortMatch(DisplayAddress a1, DisplayAddress a2) {
+            // Both displays must be of type Physical
+            if (!(a1 instanceof Physical && a2 instanceof Physical)) {
+                return false;
+            }
+            Physical p1 = (Physical) a1;
+            Physical p2 = (Physical) a2;
+
+            // If both addresses specify a model, fallback to a basic match check (which
+            // also checks the port).
+            if (p1.getModel() != null && p2.getModel() != null) {
+                return p1.equals(p2);
+            }
+            return p1.getPort() == p2.getPort();
+        }
+
         private Physical(long physicalDisplayId) {
             mPhysicalDisplayId = physicalDisplayId;
         }
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 9bf43a3..1d81be1 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -370,4 +370,14 @@
     boolean transferEmbeddedTouchFocusToHost(IWindow embeddedWindow);
 
     boolean transferHostTouchGestureToEmbedded(IWindow hostWindow, IBinder transferTouchToken);
+
+    /**
+     * Moves the focus to the adjacent window if there is one in the given direction. This can only
+     * move the focus to the window in the same leaf task.
+     *
+     * @param fromWindow The calling window that the focus is moved from.
+     * @param direction The {@link android.view.View.FocusDirection} that the new focus should go.
+     * @return {@code true} if the focus changes. Otherwise, {@code false}.
+     */
+    boolean moveFocusToAdjacentWindow(IWindow fromWindow, int direction);
 }
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index b957b31..674f22c 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -28,6 +28,7 @@
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -71,6 +72,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.VirtualRefBasePtr;
+import com.android.window.flags.Flags;
 
 import dalvik.system.CloseGuard;
 
@@ -277,6 +279,8 @@
     private static native int nativeGetLayerId(long nativeObject);
     private static native void nativeAddTransactionCommittedListener(long nativeObject,
             TransactionCommittedListener listener);
+    private static native void nativeAddTransactionCompletedListener(long nativeObject,
+            Consumer<TransactionStats> listener);
     private static native void nativeSanitize(long transactionObject, int pid, int uid);
     private static native void nativeSetDestinationFrame(long transactionObj, long nativeObject,
             int l, int t, int r, int b);
@@ -290,6 +294,10 @@
     private static native void nativeClearTrustedPresentationCallback(long transactionObj,
             long nativeObject);
     private static native StalledTransactionInfo nativeGetStalledTransactionInfo(int pid);
+    private static native void nativeSetDesiredPresentTime(long transactionObj,
+                                                           long desiredPresentTime);
+    private static native void nativeSetFrameTimeline(long transactionObj,
+                                                           long vsyncId);
 
     /**
      * Transforms that can be applied to buffers as they are displayed to a window.
@@ -2550,6 +2558,50 @@
     }
 
     /**
+     * Transaction stats given to the listener registered in
+     * {@link SurfaceControl.Transaction#addTransactionCompletedListener}
+     */
+    @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME)
+    public static final class TransactionStats {
+        private long mLatchTime;
+        private SyncFence mSyncFence;
+
+        // called from native
+        private TransactionStats(long latchTime, long presentFencePtr) {
+            mLatchTime = latchTime;
+            mSyncFence = new SyncFence(presentFencePtr);
+        }
+
+        /**
+         * Close the TransactionStats. Called by the framework when the listener returns.
+         * @hide
+         */
+        public void close() {
+            mSyncFence.close();
+        }
+
+        /**
+         * Returns the timestamp of when the frame was latched by the framework and queued for
+         * presentation.
+         */
+        @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME)
+        public long getLatchTime() {
+            return mLatchTime;
+        }
+
+        /**
+         * Returns a new SyncFence that signals when the transaction has been presented.
+         * The caller takes ownership of the fence and is responsible for closing
+         * it by calling {@link SyncFence#close}.
+         * If a device does not support present fences, an empty fence will be returned.
+         */
+        @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME)
+        public @NonNull SyncFence getPresentFence() {
+            return new SyncFence(mSyncFence);
+        }
+    };
+
+    /**
      * Threshold values that are sent with
      * {@link Transaction#setTrustedPresentationCallback(SurfaceControl,
      * TrustedPresentationThresholds, Executor, Consumer)}
@@ -4185,12 +4237,35 @@
         }
 
         /**
-         * Sets the frame timeline vsync id received from choreographer
-         * {@link Choreographer#getVsyncId()} that corresponds to the transaction submitted on that
-         * surface control.
+         * Sets the frame timeline to use in SurfaceFlinger.
          *
-         * @hide
+         * A frame timeline should be chosen based on the frame deadline the application
+         * can meet when rendering the frame and the application's desired presentation time.
+         * By setting a frame timeline, SurfaceFlinger tries to present the frame at the
+         * corresponding expected presentation time.
+         *
+         * To receive frame timelines, a callback must be posted to Choreographer using
+         * {@link Choreographer#postVsyncCallback} The vsyncId can then be extracted from the
+         * {@link Choreographer.FrameTimeline#getVsyncId}.
+         *
+         * @param vsyncId The vsync ID received from Choreographer, setting the frame's
+         *                presentation target to the corresponding expected presentation time
+         *                and deadline from the frame to be rendered. A stale or invalid value
+         *                will be ignored.
+         *
          */
+        @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME)
+        @NonNull
+        public Transaction setFrameTimeline(long vsyncId) {
+            if (!Flags.sdkDesiredPresentTime()) {
+                Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled");
+                return this;
+            }
+            nativeSetFrameTimelineVsync(mNativeObject, vsyncId);
+            return this;
+        }
+
+        /** @hide */
         @NonNull
         public Transaction setFrameTimelineVsync(long frameTimelineVsyncId) {
             nativeSetFrameTimelineVsync(mNativeObject, frameTimelineVsyncId);
@@ -4207,6 +4282,9 @@
          * to avoid dropping frames (overwriting transactions), and unable to use timestamps (Which
          * provide a more efficient solution), then this method provides a method to pace your
          * transaction application.
+         * The listener is invoked once the transaction is applied, and never again. Multiple
+         * listeners can be added to the same transaction, however the order the listeners will
+         * be called is not guaranteed.
          *
          * @param executor The executor that the callback should be invoked on.
          * @param listener The callback that will be invoked when the transaction has been
@@ -4223,6 +4301,33 @@
         }
 
         /**
+         * Request to add a TransactionCompletedListener.
+         *
+         * The listener is invoked when transaction is presented, and never again. Multiple
+         * listeners can be added to the same transaction, however the order the listeners will
+         * be called is not guaranteed.
+         *
+         * @param executor The executor that the callback should be invoked on.
+         * @param listener The callback that will be invoked when the transaction has been
+         *                 completed.
+         */
+        @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME)
+        @NonNull
+        public Transaction addTransactionCompletedListener(
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull Consumer<TransactionStats> listener) {
+
+            if (!Flags.sdkDesiredPresentTime()) {
+                Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled");
+                return this;
+            }
+            Consumer<TransactionStats> listenerInner = stats -> executor.execute(
+                                    () -> listener.andThen(TransactionStats::close).accept(stats));
+            nativeAddTransactionCompletedListener(mNativeObject, listenerInner);
+            return this;
+        }
+
+        /**
          * Sets a callback to receive feedback about the presentation of a {@link SurfaceControl}.
          * When the {@link SurfaceControl} is presented according to the passed in
          * {@link TrustedPresentationThresholds}, it is said to "enter the state", and receives the
@@ -4321,6 +4426,30 @@
         }
 
         /**
+         * Specifies a desiredPresentTime for the transaction. The framework will try to present
+         * the transaction at or after the time specified.
+         *
+         * Transactions will not be presented until all of their acquire fences have signaled even
+         * if the app requests an earlier present time.
+         *
+         * If an earlier transaction has a desired present time of x, and a later transaction has
+         * a desired present time that is before x, the later transaction will not preempt the
+         * earlier transaction.
+         *
+         * @param desiredPresentTime The desired time (in CLOCK_MONOTONIC) for the transaction.
+         * @return This transaction
+         */
+        @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME)
+        @NonNull
+        public Transaction setDesiredPresentTime(long desiredPresentTime) {
+            if (!Flags.sdkDesiredPresentTime()) {
+                Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled");
+                return this;
+            }
+            nativeSetDesiredPresentTime(mNativeObject, desiredPresentTime);
+            return this;
+        }
+        /**
          * Writes the transaction to parcel, clearing the transaction as if it had been applied so
          * it can be used to store future transactions. It's the responsibility of the parcel
          * reader to apply the original transaction.
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 2b99e1e..257ecc5 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -33170,6 +33170,10 @@
     public void setFrameContentVelocity(float pixelsPerSecond) {
         if (viewVelocityApi()) {
             mFrameContentVelocity = Math.abs(pixelsPerSecond);
+
+            if (sToolkitMetricsForFrameRateDecisionFlagValue) {
+                Trace.setCounter("Set frame velocity", (long) mFrameContentVelocity);
+            }
         }
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 8529b4e..c66f3c8 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.pm.ActivityInfo.OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS;
 import static android.graphics.HardwareRenderer.SYNC_CONTEXT_IS_STOPPED;
 import static android.graphics.HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND;
@@ -6943,7 +6944,11 @@
         }
 
         private int doOnBackKeyEvent(KeyEvent keyEvent) {
-            OnBackInvokedCallback topCallback = getOnBackInvokedDispatcher().getTopCallback();
+            WindowOnBackInvokedDispatcher dispatcher = getOnBackInvokedDispatcher();
+            OnBackInvokedCallback topCallback = dispatcher.getTopCallback();
+            if (dispatcher.isDispatching()) {
+                return FINISH_NOT_HANDLED;
+            }
             if (topCallback instanceof OnBackAnimationCallback) {
                 final OnBackAnimationCallback animationCallback =
                         (OnBackAnimationCallback) topCallback;
@@ -7232,7 +7237,7 @@
         }
 
         private boolean performFocusNavigation(KeyEvent event) {
-            int direction = 0;
+            @FocusDirection int direction = 0;
             switch (event.getKeyCode()) {
                 case KeyEvent.KEYCODE_DPAD_LEFT:
                     if (event.hasNoModifiers()) {
@@ -7284,6 +7289,8 @@
                                             isFastScrolling));
                             return true;
                         }
+                    } else if (moveFocusToAdjacentWindow(direction)) {
+                        return true;
                     }
 
                     // Give the focused view a last chance to handle the dpad key.
@@ -7293,12 +7300,26 @@
                 } else {
                     if (mView.restoreDefaultFocus()) {
                         return true;
+                    } else if (moveFocusToAdjacentWindow(direction)) {
+                        return true;
                     }
                 }
             }
             return false;
         }
 
+        private boolean moveFocusToAdjacentWindow(@FocusDirection int direction) {
+            if (getConfiguration().windowConfiguration.getWindowingMode()
+                    != WINDOWING_MODE_MULTI_WINDOW) {
+                return false;
+            }
+            try {
+                return mWindowSession.moveFocusToAdjacentWindow(mWindow, direction);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
+
         private boolean performKeyboardGroupNavigation(int direction) {
             final View focused = mView.findFocus();
             if (focused == null && mView.restoreDefaultFocus()) {
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 61cf126..d8fa415 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1260,7 +1260,7 @@
      * <p>When this compat override is enabled the min aspect ratio given in the app's manifest can
      * be overridden by the device manufacturer using their discretion to improve display
      * compatibility unless the app's manifest value is higher. This treatment will also apply if
-     * no min aspect ratio value is provided in the manifest. These treatments can apply only in
+     * no min aspect ratio value is provided in the manifest. These treatments can apply either in
      * specific cases (e.g. device is in portrait) or each time the app is displayed on screen.
      *
      * <p>Setting this property to {@code false} informs the system that the app must be
@@ -1494,6 +1494,30 @@
             "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED";
 
     /**
+     * Activity or Application level {@link android.content.pm.PackageManager.Property
+     * PackageManager.Property} for an app to declare that System UI should be shown for this
+     * app/component to allow it to be launched as multiple instances.  This property only affects
+     * SystemUI behavior and does _not_ affect whether a component can actually be launched into
+     * multiple instances, which is determined by the Activity's {@code launchMode} or the launching
+     * Intent's flags.  If the property is set on the Application, then all components within that
+     * application will use that value unless specified per component.
+     *
+     * The value must be a boolean string.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;activity&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
+     *     android:value="true|false"/&gt;
+     * &lt;/activity&gt;
+     * </pre>
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI)
+    public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI =
+            "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI";
+
+    /**
      * Request for app's keyboard shortcuts to be retrieved asynchronously.
      *
      * @param receiver The callback to be triggered when the result is ready.
@@ -3163,6 +3187,12 @@
         public static final int PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS = 1 << 10;
 
         /**
+         * Flag to indicate that the window is forcibly to go edge-to-edge.
+         * @hide
+         */
+        public static final int PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED = 1 << 11;
+
+        /**
          * Flag to indicate that the window frame should be the requested frame adding the display
          * cutout frame. This will only be applied if a specific size smaller than the parent frame
          * is given, and the window is covering the display cutout. The extended frame will not be
@@ -3338,6 +3368,7 @@
                 PRIVATE_FLAG_SYSTEM_ERROR,
                 PRIVATE_FLAG_OPTIMIZE_MEASURE,
                 PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
+                PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED,
                 PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
                 PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY,
                 PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME,
@@ -3400,6 +3431,10 @@
                         equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
                         name = "DISABLE_WALLPAPER_TOUCH_EVENTS"),
                 @ViewDebug.FlagToString(
+                        mask = PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED,
+                        equals = PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED,
+                        name = "EDGE_TO_EDGE_ENFORCED"),
+                @ViewDebug.FlagToString(
                         mask = PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
                         equals = PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
                         name = "LAYOUT_SIZE_EXTENDED_BY_CUTOUT"),
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index d6ac562..b95e459 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -30,6 +30,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.MergedConfiguration;
+import android.view.View.FocusDirection;
 import android.view.WindowInsets.Type.InsetsType;
 import android.window.ClientWindowFrames;
 import android.window.OnBackInvokedCallbackInfo;
@@ -665,6 +666,13 @@
         return false;
     }
 
+    @Override
+    public boolean moveFocusToAdjacentWindow(IWindow fromWindow, @FocusDirection int direction) {
+        Log.e(TAG, "Received request to moveFocusToAdjacentWindow on"
+                + " WindowlessWindowManager. We shouldn't get here!");
+        return false;
+    }
+
     void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) {
         IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder();
         IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder();
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 48f8f1b..c7355c1 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -99,3 +99,10 @@
     description: "Feature flag for system pinch zoom gesture detector and related opt-out apis"
     bug: "283323770"
 }
+
+flag {
+    name: "support_system_pinch_zoom_opt_out_apis"
+    namespace: "accessibility"
+    description: "Feature flag for declaring system pinch zoom opt-out apis"
+    bug: "315089687"
+}
diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java
index 70d8abe..57a3b76 100644
--- a/core/java/android/view/animation/AnimationUtils.java
+++ b/core/java/android/view/animation/AnimationUtils.java
@@ -109,6 +109,40 @@
     }
 
     /**
+     * Locks AnimationUtils{@link #currentAnimationTimeMillis()} to a fixed value for the current
+     * thread. This is used by {@link android.view.Choreographer} to ensure that all accesses
+     * during a vsync update are synchronized to the timestamp of the vsync.
+     *
+     * It is also exposed to tests to allow for rapid, flake-free headless testing.
+     *
+     * Must be followed by a call to {@link #unlockAnimationClock()} to allow time to
+     * progress. Failing to do this will result in stuck animations, scrolls, and flings.
+     *
+     * Note that time is not allowed to "rewind" and must perpetually flow forward. So the
+     * lock may fail if the time is in the past from a previously returned value, however
+     * time will be frozen for the duration of the lock. The clock is a thread-local, so
+     * ensure that {@link #lockAnimationClock(long)}, {@link #unlockAnimationClock()}, and
+     * {@link #currentAnimationTimeMillis()} are all called on the same thread.
+     *
+     * This is also not reference counted in any way. Any call to {@link #unlockAnimationClock()}
+     * will unlock the clock for everyone on the same thread. It is therefore recommended
+     * for tests to use their own thread to ensure that there is no collision with any existing
+     * {@link android.view.Choreographer} instance.
+     *
+     * Have to add the method back because of b/307888459.
+     * Remove this method once the lockAnimationClock(long, long) change
+     * is landed to aosp/android14-tests-dev branch.
+     *
+     * @hide
+     */
+    @TestApi
+    public static void lockAnimationClock(long vsyncMillis) {
+        AnimationState state = sAnimationState.get();
+        state.animationClockLocked = true;
+        state.currentVsyncTimeMillis = vsyncMillis;
+    }
+
+    /**
      * Frees the time lock set in place by {@link #lockAnimationClock(long)}. Must be called
      * to allow the animation clock to self-update.
      *
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index bb49679..dbeffc8 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -1469,7 +1469,8 @@
         if (infos.size() == 0) {
             throw new IllegalArgumentException("No VirtualViewInfo found");
         }
-        if (isCredmanRequested(view) && mIsFillAndSaveDialogDisabledForCredentialManager) {
+        if (shouldSuppressDialogsForCredman(view)
+                && mIsFillAndSaveDialogDisabledForCredentialManager) {
             if (sDebug) {
                 Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:"
                         + view.getAutofillId().toString());
@@ -1493,7 +1494,7 @@
      * @hide
      */
     public void notifyViewEnteredForFillDialog(View v) {
-        if (isCredmanRequested(v)
+        if (shouldSuppressDialogsForCredman(v)
                 && mIsFillAndSaveDialogDisabledForCredentialManager) {
             if (sDebug) {
                 Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:"
@@ -3390,19 +3391,39 @@
         }
     }
 
-    private boolean isCredmanRequested(View view) {
+    private boolean shouldSuppressDialogsForCredman(View view) {
         if (view == null) {
             return false;
         }
+        // isCredential field indicates that the developer might be calling Credman, and we should
+        // suppress autofill dialogs. But it is not a good enough indicator that there is a valid
+        // credman option.
         if (view.isCredential()) {
             return true;
         }
+        return containsAutofillHintPrefix(view, View.AUTOFILL_HINT_CREDENTIAL_MANAGER);
+    }
+
+    private boolean isCredmanRequested(View view) {
+        if (view == null) {
+            return false;
+        }
         String[] hints = view.getAutofillHints();
         if (hints == null) {
             return false;
         }
+        // if hint starts with 'credential=', then we assume that there is a valid
+        // credential option set by the client.
+        return containsAutofillHintPrefix(view, View.AUTOFILL_HINT_CREDENTIAL_MANAGER + "=");
+    }
+
+    private boolean containsAutofillHintPrefix(View view, String prefix) {
+        String[] hints = view.getAutofillHints();
+        if (hints == null) {
+            return false;
+        }
         for (String hint : hints) {
-            if (hint != null && hint.startsWith(View.AUTOFILL_HINT_CREDENTIAL_MANAGER)) {
+            if (hint != null && hint.startsWith(prefix)) {
                 return true;
             }
         }
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index a74b06a..9f9b7b4 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -1,6 +1,13 @@
 package: "android.view.flags"
 
 flag {
+     name: "enable_surface_native_alloc_registration"
+     namespace: "toolkit"
+     description: "Feature flag for registering surfaces with the VM for faster cleanup"
+     bug: "306193257"
+}
+
+flag {
     name: "enable_use_measure_cache_during_force_layout"
     namespace: "toolkit"
     description: "Enables using the measure cache during a view force layout from the second "
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index feccc6b..3bc02a6 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -354,7 +354,11 @@
      * @hide
      */
     public static void ensureDefaultInstanceForDefaultDisplayIfNecessary() {
-        forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());
+        // Skip this call if we are in system_server, as the system code should not use this
+        // deprecated instance.
+        if (!ActivityThread.isSystem()) {
+            forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());
+        }
     }
 
     private static final Object sLock = new Object();
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index ef50045..1d2f653 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -16,6 +16,9 @@
 
 package android.view.textclassifier;
 
+import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -108,6 +111,9 @@
     String TYPE_DATE_TIME = "datetime";
     /** Flight number in IATA format. */
     String TYPE_FLIGHT_NUMBER = "flight";
+    /** One-time login codes */
+    @FlaggedApi(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
+    String TYPE_OTP_CODE = "otp_code";
     /**
      * Word that users may be interested to look up for meaning.
      * @hide
@@ -126,7 +132,8 @@
             TYPE_DATE,
             TYPE_DATE_TIME,
             TYPE_FLIGHT_NUMBER,
-            TYPE_DICTIONARY
+            TYPE_DICTIONARY,
+            TYPE_OTP_CODE
     })
     @interface EntityType {}
 
diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java
index bdaad2b..473b814 100644
--- a/core/java/android/window/SplashScreenView.java
+++ b/core/java/android/window/SplashScreenView.java
@@ -47,6 +47,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
+import android.view.WindowManager;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
@@ -337,7 +338,14 @@
                         "SplashScreenView");
                 ImageView imageView = new ImageView(viewContext);
                 imageView.setBackground(mIconDrawable);
-                viewHost.setView(imageView, mIconSize, mIconSize);
+                final int windowFlag = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+                final WindowManager.LayoutParams lp =
+                        new WindowManager.LayoutParams(mIconSize, mIconSize,
+                                WindowManager.LayoutParams.TYPE_APPLICATION, windowFlag,
+                                PixelFormat.TRANSPARENT);
+                viewHost.setView(imageView, lp);
                 SurfaceControlViewHost.SurfacePackage surfacePackage = viewHost.getSurfacePackage();
                 surfaceView.setChildSurfacePackage(surfacePackage);
                 view.mSurfacePackage = surfacePackage;
diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java
index e62d5c9..64fe66e 100644
--- a/core/java/android/window/TransitionFilter.java
+++ b/core/java/android/window/TransitionFilter.java
@@ -212,7 +212,9 @@
                         continue;
                     }
                 }
-                if (!matchesTopActivity(change.getTaskInfo())) continue;
+                if (!matchesTopActivity(change.getTaskInfo(), change.getActivityComponent())) {
+                    continue;
+                }
                 if (mModes != null) {
                     boolean pass = false;
                     for (int m = 0; m < mModes.length; ++m) {
@@ -234,11 +236,15 @@
             return false;
         }
 
-        private boolean matchesTopActivity(ActivityManager.RunningTaskInfo info) {
+        private boolean matchesTopActivity(ActivityManager.RunningTaskInfo taskInfo,
+                @Nullable ComponentName activityComponent) {
             if (mTopActivity == null) return true;
-            if (info == null) return false;
-            final ComponentName component = info.topActivity;
-            return mTopActivity.equals(component);
+            if (activityComponent != null) {
+                return mTopActivity.equals(activityComponent);
+            } else if (taskInfo != null) {
+                return mTopActivity.equals(taskInfo.topActivity);
+            }
+            return false;
         }
 
         /** Check if the request matches this filter. It may generate false positives */
@@ -247,7 +253,7 @@
             if (mActivityType == ACTIVITY_TYPE_UNDEFINED) return true;
             return request.getTriggerTask() != null
                     && request.getTriggerTask().getActivityType() == mActivityType
-                    && matchesTopActivity(request.getTriggerTask());
+                    && matchesTopActivity(request.getTriggerTask(), null /* activityCmp */);
         }
 
         @Override
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 7c9340e..bceb872 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -44,6 +44,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.content.ComponentName;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
@@ -635,6 +636,7 @@
         private @ColorInt int mBackgroundColor;
         private SurfaceControl mSnapshot = null;
         private float mSnapshotLuma;
+        private ComponentName mActivityComponent = null;
 
         public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) {
             mContainer = container;
@@ -663,6 +665,7 @@
             mBackgroundColor = in.readInt();
             mSnapshot = in.readTypedObject(SurfaceControl.CREATOR);
             mSnapshotLuma = in.readFloat();
+            mActivityComponent = in.readTypedObject(ComponentName.CREATOR);
         }
 
         private Change localRemoteCopy() {
@@ -685,6 +688,7 @@
             out.mBackgroundColor = mBackgroundColor;
             out.mSnapshot = mSnapshot != null ? new SurfaceControl(mSnapshot, "localRemote") : null;
             out.mSnapshotLuma = mSnapshotLuma;
+            out.mActivityComponent = mActivityComponent;
             return out;
         }
 
@@ -780,6 +784,11 @@
             mSnapshotLuma = luma;
         }
 
+        /** Sets the component-name of the container. Container must be an Activity. */
+        public void setActivityComponent(@Nullable ComponentName component) {
+            mActivityComponent = component;
+        }
+
         /** @return the container that is changing. May be null if non-remotable (eg. activity) */
         @Nullable
         public WindowContainerToken getContainer() {
@@ -913,6 +922,12 @@
             return mSnapshotLuma;
         }
 
+        /** @return the component-name of this container (if it is an activity). */
+        @Nullable
+        public ComponentName getActivityComponent() {
+            return mActivityComponent;
+        }
+
         /** @hide */
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -936,6 +951,7 @@
             dest.writeInt(mBackgroundColor);
             dest.writeTypedObject(mSnapshot, flags);
             dest.writeFloat(mSnapshotLuma);
+            dest.writeTypedObject(mActivityComponent, flags);
         }
 
         @NonNull
@@ -994,6 +1010,10 @@
             if (mLastParent != null) {
                 sb.append(" lastParent="); sb.append(mLastParent);
             }
+            if (mActivityComponent != null) {
+                sb.append(" component=");
+                sb.append(mActivityComponent.flattenToShortString());
+            }
             sb.append('}');
             return sb.toString();
         }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 6a8ca33..86804c6 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -174,6 +174,21 @@
         }
     }
 
+    /**
+     * Indicates if the dispatcher is actively dispatching to a callback.
+     */
+    public boolean isDispatching() {
+        return mIsDispatching;
+    }
+
+    private void onStartDispatching() {
+        mIsDispatching = true;
+    }
+
+    private void onStopDispatching() {
+        mIsDispatching = false;
+    }
+
     private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) {
         boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
         if (isInProgress && callback instanceof OnBackAnimationCallback) {
@@ -231,7 +246,7 @@
                                     .ImeOnBackInvokedCallback
                                 ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback)
                                         callback).getIOnBackInvokedCallback()
-                                : new OnBackInvokedCallbackWrapper(callback);
+                                : new OnBackInvokedCallbackWrapper(callback, this);
                 callbackInfo = new OnBackInvokedCallbackInfo(
                         iCallback,
                         priority,
@@ -258,6 +273,7 @@
 
     @NonNull
     private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+    private boolean mIsDispatching = false;
 
     /**
      * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it
@@ -317,18 +333,33 @@
             }
         }
         final CallbackRef mCallbackRef;
+        /**
+         * The dispatcher this callback is registered with.
+         * This can be null for callbacks on {@link ImeOnBackInvokedDispatcher} because they are
+         * forwarded and registered on the app's {@link WindowOnBackInvokedDispatcher}. */
+        @Nullable
+        private final WindowOnBackInvokedDispatcher mDispatcher;
 
-        OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) {
+        OnBackInvokedCallbackWrapper(
+                @NonNull OnBackInvokedCallback callback,
+                WindowOnBackInvokedDispatcher dispatcher) {
             mCallbackRef = new CallbackRef(callback, true /* useWeakRef */);
+            mDispatcher = dispatcher;
         }
 
-        OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) {
+        OnBackInvokedCallbackWrapper(
+                @NonNull OnBackInvokedCallback callback,
+                boolean useWeakRef) {
             mCallbackRef = new CallbackRef(callback, useWeakRef);
+            mDispatcher = null;
         }
 
         @Override
         public void onBackStarted(BackMotionEvent backEvent) {
             Handler.getMain().post(() -> {
+                if (mDispatcher != null) {
+                    mDispatcher.onStartDispatching();
+                }
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
                     mProgressAnimator.onBackStarted(backEvent, event ->
@@ -353,6 +384,9 @@
         @Override
         public void onBackCancelled() {
             Handler.getMain().post(() -> {
+                if (mDispatcher != null) {
+                    mDispatcher.onStopDispatching();
+                }
                 mProgressAnimator.onBackCancelled(() -> {
                     final OnBackAnimationCallback callback = getBackAnimationCallback();
                     if (callback != null) {
@@ -365,6 +399,9 @@
         @Override
         public void onBackInvoked() throws RemoteException {
             Handler.getMain().post(() -> {
+                if (mDispatcher != null) {
+                    mDispatcher.onStopDispatching();
+                }
                 boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
                 mProgressAnimator.reset();
                 final OnBackInvokedCallback callback = mCallbackRef.get();
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 56df493..3794c5d 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -63,4 +63,12 @@
     description: "Enable trustedPresentationListener on windows public API"
     is_fixed_read_only: true
     bug: "278027319"
-}
\ No newline at end of file
+}
+
+flag {
+    namespace: "window_surfaces"
+    name: "sdk_desired_present_time"
+    description: "Feature flag for the new SDK API to set desired present time"
+    is_fixed_read_only: true
+    bug: "295038072"
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 3366a7e..bb16ad2 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -72,7 +72,7 @@
     name: "predictive_back_system_animations"
     namespace: "systemui"
     description: "Predictive back for system animations"
-    bug: "309545085"
+    bug: "319421778"
     is_fixed_read_only: true
 }
 
@@ -82,4 +82,12 @@
     description: "Enable record activity snapshot by default"
     bug: "259497289"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "supports_multi_instance_system_ui"
+    namespace: "multitasking"
+    description: "Feature flag to enable a multi-instance system ui component property."
+    bug: "262864589"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
index 1bd0982..eeea17b 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -68,10 +68,10 @@
         // TODO b/291899544: for released flags, use resource config values
         /** Value used by polite notif. feature */
         public static final Flag NOTIF_COOLDOWN_T1 = devFlag(
-                "persist.debug.sysui.notification.notif_cooldown_t1", 5000);
+                "persist.debug.sysui.notification.notif_cooldown_t1", 60000);
         /** Value used by polite notif. feature */
         public static final Flag NOTIF_COOLDOWN_T2 = devFlag(
-                "persist.debug.sysui.notification.notif_cooldown_t2", 3000);
+                "persist.debug.sysui.notification.notif_cooldown_t2", 5000);
         /** Value used by polite notif. feature */
         public static final Flag NOTIF_VOLUME1 = devFlag(
                 "persist.debug.sysui.notification.notif_volume1", 30);
@@ -80,12 +80,6 @@
         /** Value used by polite notif. feature. -1 to ignore the counter */
         public static final Flag NOTIF_COOLDOWN_COUNTER_RESET = devFlag(
                 "persist.debug.sysui.notification.notif_cooldown_counter_reset", 10);
-        /**
-         * Value used by polite notif. feature: cooldown behavior/strategy. Valid values: rule1,
-         * rule2
-         */
-        public static final Flag NOTIF_COOLDOWN_RULE = devFlag(
-                "persist.debug.sysui.notification.notif_cooldown_rule", "rule1");
 
         /** b/303716154: For debugging only: use short bitmap duration. */
         public static final Flag DEBUG_SHORT_BITMAP_DURATION = devFlag(
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 1330e16..7a79e0f 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -977,11 +977,16 @@
     /**
      * @return true if there is more than 100MB free disk space left.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     private boolean hasFreeDiskSpace() {
         final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
         return stats.getAvailableBytes() > MIN_FREE_SPACE;
     }
 
+    private boolean hasFreeDiskSpace$ravenwood() {
+        return true;
+    }
+
     @VisibleForTesting
     public List<String> getFilesNames() {
         List<String> names = new ArrayList<>();
diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java
index 2298cbd..ab982f5 100644
--- a/core/java/com/android/internal/os/PowerProfile.java
+++ b/core/java/com/android/internal/os/PowerProfile.java
@@ -50,6 +50,7 @@
  * Customize the XML file for different devices.
  * [hidden]
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class PowerProfile {
 
     public static final String TAG = "PowerProfile";
@@ -321,6 +322,13 @@
     private int mCpuPowerBracketCount;
 
     @VisibleForTesting
+    public PowerProfile() {
+        synchronized (sLock) {
+            initLocked();
+        }
+    }
+
+    @VisibleForTesting
     @UnsupportedAppUsage
     public PowerProfile(Context context) {
         this(context, false);
@@ -358,6 +366,10 @@
         if (sPowerItemMap.size() == 0 && sPowerArrayMap.size() == 0) {
             readPowerValuesFromXml(context, xmlId);
         }
+        initLocked();
+    }
+
+    private void initLocked() {
         initCpuClusters();
         initCpuScalingPolicies();
         initCpuPowerBrackets();
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
index 5d82d04..12aff1c 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
@@ -29,6 +29,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
+import android.multiuser.Flags;
 import android.os.Build;
 import android.os.PatternMatcher;
 import android.util.Slog;
@@ -126,6 +127,10 @@
                     .setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestProvider_singleUser, sa));
 
+            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
+                provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY,
+                        R.styleable.AndroidManifestProvider_systemUserOnly, sa));
+            }
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestProvider_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
index a1dd19a3..4ac542f8 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
@@ -29,6 +29,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
+import android.multiuser.Flags;
 import android.os.Build;
 
 import com.android.internal.R;
@@ -105,6 +106,11 @@
                             | flag(ServiceInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestService_singleUser, sa)));
 
+            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
+                service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY,
+                        R.styleable.AndroidManifestService_systemUserOnly, sa));
+            }
+
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestService_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl
index 36b7ee5..d62c8f3 100644
--- a/core/java/com/android/internal/policy/IKeyguardService.aidl
+++ b/core/java/com/android/internal/policy/IKeyguardService.aidl
@@ -130,4 +130,10 @@
      * Note that it's called only if the device is interactive.
      */
     void onSystemKeyPressed(int keycode);
+
+    /**
+     * Requests to show the keyguard immediately without locking the device. Keyguard will show
+     * whether a screen lock was configured or not (including if screen lock is SWIPE or NONE).
+     */
+    void showDismissibleKeyguard();
 }
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 31910ac..4e3b64c 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -31,6 +31,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
 
@@ -46,6 +47,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources.Theme;
@@ -294,9 +296,9 @@
     private int mFrameResource = 0;
 
     private int mTextColor = 0;
-    int mStatusBarColor = 0;
-    int mNavigationBarColor = 0;
-    int mNavigationBarDividerColor = 0;
+    int mStatusBarColor = Color.TRANSPARENT;
+    int mNavigationBarColor = Color.TRANSPARENT;
+    int mNavigationBarDividerColor = Color.TRANSPARENT;
     private boolean mForcedStatusBarColor = false;
     private boolean mForcedNavigationBarColor = false;
 
@@ -388,11 +390,9 @@
         mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
         mAllowFloatingWindowsFillScreen = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_allowFloatingWindowsFillScreen);
-        mEdgeToEdgeEnforced =
-                context.getApplicationInfo().targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
-                        || (CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
-                                && Flags.enforceEdgeToEdge());
+        mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(context.getApplicationInfo(), true /* local */);
         if (mEdgeToEdgeEnforced) {
+            getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
             mDecorFitsSystemWindows = false;
         }
     }
@@ -431,6 +431,22 @@
         mActivityConfigCallback = activityConfigCallback;
     }
 
+    /**
+     * Returns whether the given application is enforced to go edge-to-edge.
+     *
+     * @param info The application to query.
+     * @param local Whether this is called from the process of the given application.
+     * @return {@code true} if edge-to-edge is enforced. Otherwise, {@code false}.
+     */
+    public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local) {
+        return info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
+                || (Flags.enforceEdgeToEdge() && (local
+                        // Calling this doesn't require a permission.
+                        ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
+                        // Calling this requires permissions.
+                        : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)));
+    }
+
     @Override
     public final void setContainer(Window container) {
         super.setContainer(container);
@@ -2548,17 +2564,10 @@
         final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;
         final boolean targetPreQ = targetSdk < Build.VERSION_CODES.Q;
 
-        if (!mForcedStatusBarColor) {
-            final int statusBarCompatibleColor = context.getColor(R.color.status_bar_compatible);
-            final int statusBarDefaultColor = context.getColor(R.color.status_bar_default);
-            final int statusBarColor = a.getColor(R.styleable.Window_statusBarColor,
-                    statusBarDefaultColor);
-
-            mStatusBarColor = statusBarColor == statusBarDefaultColor && !mEdgeToEdgeEnforced
-                    ? statusBarCompatibleColor
-                    : statusBarColor;
+        if (!mForcedStatusBarColor && !mEdgeToEdgeEnforced) {
+            mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, Color.BLACK);
         }
-        if (!mForcedNavigationBarColor) {
+        if (!mForcedNavigationBarColor && !mEdgeToEdgeEnforced) {
             final int navBarCompatibleColor = context.getColor(R.color.navigation_bar_compatible);
             final int navBarDefaultColor = context.getColor(R.color.navigation_bar_default);
             final int navBarColor = a.getColor(R.styleable.Window_navigationBarColor,
@@ -2566,7 +2575,6 @@
 
             mNavigationBarColor =
                     navBarColor == navBarDefaultColor
-                            && !mEdgeToEdgeEnforced
                             && !context.getResources().getBoolean(
                                     R.bool.config_navBarDefaultTransparent)
                     ? navBarCompatibleColor
@@ -2575,7 +2583,7 @@
             mNavigationBarDividerColor = a.getColor(R.styleable.Window_navigationBarDividerColor,
                     Color.TRANSPARENT);
         }
-        if (!targetPreQ) {
+        if (!targetPreQ && !mEdgeToEdgeEnforced) {
             mEnsureStatusBarContrastWhenTransparent = a.getBoolean(
                     R.styleable.Window_enforceStatusBarContrast, false);
             mEnsureNavigationBarContrastWhenTransparent = a.getBoolean(
@@ -3899,6 +3907,9 @@
 
     @Override
     public void setStatusBarColor(int color) {
+        if (mEdgeToEdgeEnforced) {
+            return;
+        }
         if (mStatusBarColor == color && mForcedStatusBarColor) {
             return;
         }
@@ -3920,6 +3931,9 @@
 
     @Override
     public void setNavigationBarColor(int color) {
+        if (mEdgeToEdgeEnforced) {
+            return;
+        }
         if (mNavigationBarColor == color && mForcedNavigationBarColor) {
             return;
         }
@@ -3936,6 +3950,9 @@
 
     @Override
     public void setNavigationBarDividerColor(int navigationBarDividerColor) {
+        if (mEdgeToEdgeEnforced) {
+            return;
+        }
         mNavigationBarDividerColor = navigationBarDividerColor;
         if (mDecor != null) {
             mDecor.updateColorViews(null, false /* animate */);
@@ -3949,6 +3966,9 @@
 
     @Override
     public void setStatusBarContrastEnforced(boolean ensureContrast) {
+        if (mEdgeToEdgeEnforced) {
+            return;
+        }
         mEnsureStatusBarContrastWhenTransparent = ensureContrast;
         if (mDecor != null) {
             mDecor.updateColorViews(null, false /* animate */);
@@ -3962,6 +3982,9 @@
 
     @Override
     public void setNavigationBarContrastEnforced(boolean enforceContrast) {
+        if (mEdgeToEdgeEnforced) {
+            return;
+        }
         mEnsureNavigationBarContrastWhenTransparent = enforceContrast;
         if (mDecor != null) {
             mDecor.updateColorViews(null, false /* animate */);
@@ -4031,6 +4054,9 @@
 
     @Override
     public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
+        if (mEdgeToEdgeEnforced) {
+            return;
+        }
         mDecorFitsSystemWindows = decorFitsSystemWindows;
         applyDecorFitsSystemWindows();
     }
diff --git a/core/java/com/android/internal/power/ModemPowerProfile.java b/core/java/com/android/internal/power/ModemPowerProfile.java
index a555ae3..b15c10e 100644
--- a/core/java/com/android/internal/power/ModemPowerProfile.java
+++ b/core/java/com/android/internal/power/ModemPowerProfile.java
@@ -39,6 +39,7 @@
 /**
  * ModemPowerProfile for handling the modem element in the power_profile.xml
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class ModemPowerProfile {
     private static final String TAG = "ModemPowerProfile";
 
diff --git a/core/java/com/android/server/OWNERS b/core/java/com/android/server/OWNERS
deleted file mode 100644
index 1c2d19d..0000000
--- a/core/java/com/android/server/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-per-file SystemConfig.java = file:/PACKAGE_MANAGER_OWNERS
diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp
index 893cc98..6f1c763 100644
--- a/core/jni/android_database_SQLiteConnection.cpp
+++ b/core/jni/android_database_SQLiteConnection.cpp
@@ -82,10 +82,16 @@
     const String8 path;
     const String8 label;
 
+    // The prepared statement used to determine which tables are updated by a statement.  This
+    // is is initially null.  It is set non-null on first use.
+    sqlite3_stmt* tableQuery;
+
     volatile bool canceled;
 
     SQLiteConnection(sqlite3* db, int openFlags, const String8& path, const String8& label) :
-        db(db), openFlags(openFlags), path(path), label(label), canceled(false) { }
+            db(db), openFlags(openFlags), path(path), label(label), tableQuery(nullptr),
+            canceled(false) { }
+
 };
 
 // Called each time a statement begins execution, when tracing is enabled.
@@ -188,6 +194,9 @@
 
     if (connection) {
         ALOGV("Closing connection %p", connection->db);
+        if (connection->tableQuery != nullptr) {
+            sqlite3_finalize(connection->tableQuery);
+        }
         int err = sqlite3_close(connection->db);
         if (err != SQLITE_OK) {
             // This can happen if sub-objects aren't closed first.  Make sure the caller knows.
@@ -419,6 +428,46 @@
     return sqlite3_stmt_readonly(statement) != 0;
 }
 
+static jboolean nativeUpdatesTempOnly(JNIEnv* env, jclass,
+        jlong connectionPtr, jlong statementPtr) {
+    sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+    SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+
+    int result = SQLITE_OK;
+    if (connection->tableQuery == nullptr) {
+        static char const* sql =
+                "SELECT COUNT(*) FROM tables_used(?) WHERE schema != 'temp' AND wr != 0";
+        result = sqlite3_prepare_v2(connection->db, sql, -1, &connection->tableQuery, nullptr);
+        if (result != SQLITE_OK) {
+            ALOGE("failed to compile query table: %s",
+                  sqlite3_errstr(sqlite3_extended_errcode(connection->db)));
+            return false;
+        }
+    }
+
+    // A temporary, to simplify the code.
+    sqlite3_stmt* query = connection->tableQuery;
+    sqlite3_reset(query);
+    sqlite3_clear_bindings(query);
+    result = sqlite3_bind_text(query, 1, sqlite3_sql(statement), -1, SQLITE_STATIC);
+    if (result != SQLITE_OK) {
+        ALOGE("tables bind pointer returns %s", sqlite3_errstr(result));
+        return false;
+    }
+    result = sqlite3_step(query);
+    if (result != SQLITE_ROW) {
+        ALOGE("tables query error: %d/%s", result, sqlite3_errstr(result));
+        // Make sure the query is no longer bound to the statement.
+        sqlite3_clear_bindings(query);
+        return false;
+    }
+
+    int count = sqlite3_column_int(query, 0);
+    // Make sure the query is no longer bound to the statement SQL string.
+    sqlite3_clear_bindings(query);
+    return count == 0;
+}
+
 static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jlong connectionPtr,
         jlong statementPtr) {
     sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
@@ -915,6 +964,8 @@
             (void*)nativeGetParameterCount },
     { "nativeIsReadOnly", "(JJ)Z",
             (void*)nativeIsReadOnly },
+    { "nativeUpdatesTempOnly", "(JJ)Z",
+            (void*)nativeUpdatesTempOnly },
     { "nativeGetColumnCount", "(JJ)I",
             (void*)nativeGetColumnCount },
     { "nativeGetColumnName", "(JJI)Ljava/lang/String;",
diff --git a/core/jni/android_hardware_SyncFence.cpp b/core/jni/android_hardware_SyncFence.cpp
index b996653..6e94616 100644
--- a/core/jni/android_hardware_SyncFence.cpp
+++ b/core/jni/android_hardware_SyncFence.cpp
@@ -66,6 +66,10 @@
     return fromJlong<Fence>(jPtr)->getSignalTime();
 }
 
+static void SyncFence_incRef(JNIEnv*, jobject, jlong jPtr) {
+    fromJlong<Fence>(jPtr)->incStrong((void*)SyncFence_incRef);
+}
+
 // ----------------------------------------------------------------------------
 // JNI Glue
 // ----------------------------------------------------------------------------
@@ -80,6 +84,7 @@
         { "nGetFd", "(J)I", (void*) SyncFence_getFd },
         { "nWait",  "(JJ)Z", (void*) SyncFence_wait },
         { "nGetSignalTime", "(J)J", (void*) SyncFence_getSignalTime },
+        { "nIncRef", "(J)V", (void*) SyncFence_incRef },
 };
 // clang-format on
 
diff --git a/core/jni/android_os_VintfObject.cpp b/core/jni/android_os_VintfObject.cpp
index 1baea2a..a5b2f65 100644
--- a/core/jni/android_os_VintfObject.cpp
+++ b/core/jni/android_os_VintfObject.cpp
@@ -46,6 +46,7 @@
 using vintf::Version;
 using vintf::VintfObject;
 using vintf::Vndk;
+using vintf::CheckFlags::ENABLE_ALL_CHECKS;
 
 template<typename V>
 static inline jobjectArray toJavaStringArray(JNIEnv* env, const V& v) {
@@ -93,12 +94,16 @@
     return toJavaStringArray(env, cStrings);
 }
 
-static jint android_os_VintfObject_verifyWithoutAvb(JNIEnv* env, jclass) {
+static jint android_os_VintfObject_verifyBuildAtBoot(JNIEnv* env, jclass) {
     std::string error;
-    int32_t status = VintfObject::GetInstance()->checkCompatibility(&error,
-            ::android::vintf::CheckFlags::DISABLE_AVB_CHECK);
+    // Use temporary VintfObject, not the shared instance, to release memory
+    // after check.
+    int32_t status =
+            VintfObject::Builder()
+                    .build()
+                    ->checkCompatibility(&error, ENABLE_ALL_CHECKS.disableAvb().disableKernel());
     if (status)
-        LOG(WARNING) << "VintfObject.verifyWithoutAvb() returns " << status << ": " << error;
+        LOG(WARNING) << "VintfObject.verifyBuildAtBoot() returns " << status << ": " << error;
     return status;
 }
 
@@ -170,7 +175,7 @@
 
 static const JNINativeMethod gVintfObjectMethods[] = {
         {"report", "()[Ljava/lang/String;", (void*)android_os_VintfObject_report},
-        {"verifyWithoutAvb", "()I", (void*)android_os_VintfObject_verifyWithoutAvb},
+        {"verifyBuildAtBoot", "()I", (void*)android_os_VintfObject_verifyBuildAtBoot},
         {"getHalNamesAndVersions", "()[Ljava/lang/String;",
          (void*)android_os_VintfObject_getHalNamesAndVersions},
         {"getSepolicyVersion", "()Ljava/lang/String;",
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index db42246..55326da 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -231,6 +231,16 @@
 
 static struct {
     jclass clazz;
+    jmethodID accept;
+} gConsumerClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID ctor;
+} gTransactionStatsClassInfo;
+
+static struct {
+    jclass clazz;
     jmethodID ctor;
     jfieldID format;
     jfieldID alphaInterpretation;
@@ -317,6 +327,52 @@
     }
 };
 
+class TransactionCompletedListenerWrapper {
+public:
+    explicit TransactionCompletedListenerWrapper(JNIEnv* env, jobject object) {
+        env->GetJavaVM(&mVm);
+        mTransactionCompletedListenerObject = env->NewGlobalRef(object);
+        LOG_ALWAYS_FATAL_IF(!mTransactionCompletedListenerObject, "Failed to make global ref");
+    }
+
+    ~TransactionCompletedListenerWrapper() {
+        getenv()->DeleteGlobalRef(mTransactionCompletedListenerObject);
+    }
+
+    void callback(nsecs_t latchTime, const sp<Fence>& presentFence,
+                  const std::vector<SurfaceControlStats>& /*stats*/) {
+        JNIEnv* env = getenv();
+        // Adding a strong reference for java SyncFence
+        presentFence->incStrong(0);
+
+        jobject stats =
+                env->NewObject(gTransactionStatsClassInfo.clazz, gTransactionStatsClassInfo.ctor,
+                               latchTime, presentFence.get());
+        env->CallVoidMethod(mTransactionCompletedListenerObject, gConsumerClassInfo.accept, stats);
+        env->DeleteLocalRef(stats);
+        DieIfException(env, "Uncaught exception in TransactionCompletedListener.");
+    }
+
+    static void transactionCallbackThunk(void* context, nsecs_t latchTime,
+                                         const sp<Fence>& presentFence,
+                                         const std::vector<SurfaceControlStats>& stats) {
+        TransactionCompletedListenerWrapper* listener =
+                reinterpret_cast<TransactionCompletedListenerWrapper*>(context);
+        listener->callback(latchTime, presentFence, stats);
+        delete listener;
+    }
+
+private:
+    jobject mTransactionCompletedListenerObject;
+    JavaVM* mVm;
+
+    JNIEnv* getenv() {
+        JNIEnv* env;
+        mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
+        return env;
+    }
+};
+
 class WindowInfosReportedListenerWrapper : public gui::BnWindowInfosReportedListener {
 public:
     explicit WindowInfosReportedListenerWrapper(JNIEnv* env, jobject listener) {
@@ -1879,10 +1935,16 @@
 
     FrameTimelineInfo ftInfo;
     ftInfo.vsyncId = frameTimelineVsyncId;
-    ftInfo.inputEventId = android::os::IInputConstants::INVALID_INPUT_EVENT_ID;
     transaction->setFrameTimelineInfo(ftInfo);
 }
 
+static void nativeSetDesiredPresentTime(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                        jlong desiredPresentTime) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+
+    transaction->setDesiredPresentTime(desiredPresentTime);
+}
+
 static void nativeAddTransactionCommittedListener(JNIEnv* env, jclass clazz, jlong transactionObj,
                                                   jobject transactionCommittedListenerObject) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -1894,6 +1956,17 @@
                                                  context);
 }
 
+static void nativeAddTransactionCompletedListener(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                                  jobject transactionCompletedListenerObject) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+
+    void* context =
+            new TransactionCompletedListenerWrapper(env, transactionCompletedListenerObject);
+    transaction->addTransactionCompletedCallback(TransactionCompletedListenerWrapper::
+                                                         transactionCallbackThunk,
+                                                 context);
+}
+
 static void nativeSetTrustedPresentationCallback(JNIEnv* env, jclass clazz, jlong transactionObj,
                                                  jlong nativeObject,
                                                  jlong trustedPresentationCallbackObject,
@@ -2318,6 +2391,8 @@
             (void*)nativeSurfaceFlushJankData },
     {"nativeAddTransactionCommittedListener", "(JLandroid/view/SurfaceControl$TransactionCommittedListener;)V",
             (void*) nativeAddTransactionCommittedListener },
+    {"nativeAddTransactionCompletedListener", "(JLjava/util/function/Consumer;)V",
+            (void*) nativeAddTransactionCompletedListener },
     {"nativeSetTrustedPresentationCallback", "(JJJLandroid/view/SurfaceControl$TrustedPresentationThresholds;)V",
             (void*) nativeSetTrustedPresentationCallback },
     {"nativeClearTrustedPresentationCallback", "(JJ)V",
@@ -2337,6 +2412,8 @@
     {"getNativeTrustedPresentationCallbackFinalizer", "()J", (void*)getNativeTrustedPresentationCallbackFinalizer },
     {"nativeGetStalledTransactionInfo", "(I)Landroid/gui/StalledTransactionInfo;",
             (void*) nativeGetStalledTransactionInfo },
+    {"nativeSetDesiredPresentTime", "(JJ)V",
+            (void*) nativeSetDesiredPresentTime },
         // clang-format on
 };
 
@@ -2539,6 +2616,16 @@
     gTransactionCommittedListenerClassInfo.onTransactionCommitted =
             GetMethodIDOrDie(env, transactionCommittedListenerClazz, "onTransactionCommitted",
                              "()V");
+    jclass consumerClazz = FindClassOrDie(env, "java/util/function/Consumer");
+    gConsumerClassInfo.clazz = MakeGlobalRefOrDie(env, consumerClazz);
+    gConsumerClassInfo.accept =
+            GetMethodIDOrDie(env, consumerClazz, "accept", "(Ljava/lang/Object;)V");
+
+    jclass transactionStatsClazz =
+            FindClassOrDie(env, "android/view/SurfaceControl$TransactionStats");
+    gTransactionStatsClassInfo.clazz = MakeGlobalRefOrDie(env, transactionStatsClazz);
+    gTransactionStatsClassInfo.ctor =
+            GetMethodIDOrDie(env, gTransactionStatsClassInfo.clazz, "<init>", "(JJ)V");
 
     jclass displayDecorationSupportClazz =
             FindClassOrDie(env, "android/hardware/graphics/common/DisplayDecorationSupport");
diff --git a/core/jni/hwbinder/EphemeralStorage.cpp b/core/jni/hwbinder/EphemeralStorage.cpp
index 95bb42e..ef0750c 100644
--- a/core/jni/hwbinder/EphemeralStorage.cpp
+++ b/core/jni/hwbinder/EphemeralStorage.cpp
@@ -164,7 +164,7 @@
             }
 
             default:
-                CHECK(!"Should not be here");
+                CHECK(!"Should not be here") << "Item type: " << item.mType;
         }
     }
 
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index db391f7..a854e36 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -18,6 +18,7 @@
 per-file apphibernationservice.proto = file:/core/java/android/apphibernation/OWNERS
 per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com
 per-file background_install_control.proto = wenhaowang@google.com,georgechan@google.com,billylau@google.com
+per-file android/content/intent.proto = file:/PACKAGE_MANAGER_OWNERS
 
 # Biometrics
 jaggies@google.com
@@ -31,5 +32,3 @@
 
 # Accessibility
 pweaver@google.com
-hongmingjin@google.com
-cbrower@google.com
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index e65bfab..5f3f641 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1709,6 +1709,7 @@
 
     <!-- @SystemApi Allows camera access by Headless System User 0 when device is running in
             HSUM Mode.
+         @FlaggedApi("com.android.internal.camera.flags.camera_hsum_permission")
     @hide -->
     <permission android:name="android.permission.CAMERA_HEADLESS_SYSTEM_USER"
         android:permissionGroup="android.permission-group.UNDEFINED"
@@ -5213,6 +5214,14 @@
     <permission android:name="android.permission.BIND_REMOTE_DISPLAY"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by a android.media.tv.ad.TvAdService to ensure that only the system can
+         bind to it.
+         <p>Protection level: signature|privileged
+         @FlaggedApi("android.media.tv.flags.enable_ad_service_fw")
+    -->
+    <permission android:name="android.permission.BIND_TV_AD_SERVICE"
+                android:protectionLevel="signature|privileged" />
+
     <!-- Must be required by a {@link android.media.tv.TvInputService}
          to ensure that only the system can bind to it.
          <p>Protection level: signature|privileged
@@ -7146,6 +7155,16 @@
         android:label="@string/permlab_foregroundServiceFileManagement"
         android:protectionLevel="normal|instant" />
 
+    <!-- @FlaggedApi("android.content.pm.introduce_media_processing_type")
+         Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "mediaProcessing".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"
+                android:description="@string/permdesc_foregroundServiceMediaProcessing"
+                android:label="@string/permlab_foregroundServiceMediaProcessing"
+                android:protectionLevel="normal|instant" />
+
     <!-- Allows a regular application to use {@link android.app.Service#startForeground
          Service.startForeground} with the type "specialUse".
          <p>Protection level: normal|appop|instant
diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml
index af30532..31acd9a 100644
--- a/core/res/res/values-watch/config.xml
+++ b/core/res/res/values-watch/config.xml
@@ -90,4 +90,7 @@
          {@link MotionEvent#AXIS_SCROLL} generated by {@link InputDevice#SOURCE_ROTARY_ENCODER}
          devices. -->
     <bool name="config_viewRotaryEncoderHapticScrollFedbackEnabled">true</bool>
+
+    <!-- If this is true, allow wake from theater mode from motion. -->
+    <bool name="config_allowTheaterModeWakeFromMotion">true</bool>
 </resources>
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index d1143c4..6019524 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -301,9 +301,7 @@
             granted to the system companion device manager service -->
         <flag name="companion" value="0x800000" />
         <!-- Additional flag from base permission type: this permission will be granted to the
-             retail demo app, as defined by the OEM.
-             This flag has been replaced by the retail demo role and is a no-op since Android V.
-          -->
+             retail demo app, as defined by the OEM. -->
         <flag name="retailDemo" value="0x1000000" />
         <!-- Additional flag from base permission type: this permission will be granted to the
              recents app. -->
@@ -508,6 +506,12 @@
          receivers, and providers; it can not be used with activities. -->
     <attr name="singleUser" format="boolean" />
 
+    <!-- If set to true, only a single instance of this component will
+    run and be available for the SYSTEM user. Non SYSTEM users will not be
+    allowed to access the component if this flag is enabled.
+    This flag can be used with services, receivers, providers and activities. -->
+    <attr name="systemUserOnly" format="boolean" />
+
     <!-- Specify a specific process that the associated code is to run in.
          Use with the application tag (to supply a default process for all
          application components), or with the activity, receiver, service,
@@ -1746,6 +1750,12 @@
             TODO: b/258855262 mark this field as {@code hide} once this bug is fixed.
             <flag name="fileManagement" value="0x1000" />
         -->
+        <!-- Media processing use cases such as video or photo editing and processing.
+            <p>Requires the app to hold the permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PROCESSING} in order to use
+            this type.
+        -->
+        <flag name="mediaProcessing" value="0x2000" />
         <!-- Use cases that can't be categorized into any other foreground service types, but also
             can't use @link android.app.job.JobInfo.Builder} APIs.
             See {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE} for the
@@ -2855,6 +2865,7 @@
              Context.createAttributionContext() using the first attribution tag
              contained here. -->
         <attr name="attributionTags" />
+        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- Attributes that can be supplied in an AndroidManifest.xml
@@ -3013,6 +3024,7 @@
              ignored when the process is bound into a shared isolated process by a client.
         -->
         <attr name="allowSharedIsolatedProcess" format="boolean" />
+        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- @hide The <code>apex-system-service</code> tag declares an apex system service
@@ -3140,7 +3152,7 @@
         <attr name="uiOptions" />
         <attr name="parentActivityName" />
         <attr name="singleUser" />
-        <!-- @hide This broadcast receiver or activity will only receive broadcasts for the
+        <!-- This broadcast receiver or activity will only receive broadcasts for the
              system user-->
         <attr name="systemUserOnly" format="boolean" />
         <attr name="persistableMode" />
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index eddd81e..53a6270 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -568,10 +568,6 @@
     <color name="side_fps_button_color">#00677E</color>
 
     <!-- Color for system bars -->
-    <color name="status_bar_compatible">@android:color/black</color>
-    <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent
-         color is set by the framework or not. -->
-    <color name="status_bar_default">#00808080</color>
     <color name="navigation_bar_compatible">@android:color/black</color>
     <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent
          color is set by the framework or not. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 806be94..5be29a6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2349,6 +2349,8 @@
     <string name="config_defaultCallRedirection" translatable="false"></string>
     <!-- The name of the package that will hold the call screening role by default. -->
     <string name="config_defaultCallScreening" translatable="false"></string>
+    <!-- The name of the package that will hold the wallet role by default. -->
+    <string name="config_defaultWallet" translatable="false" />
     <!-- The name of the package that will hold the system gallery role. -->
     <string name="config_systemGallery" translatable="false">com.android.gallery3d</string>
     <!-- The names of the packages that will hold the automotive projection role. -->
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 8d80af4..5346454 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -212,6 +212,36 @@
     <bool name="config_send_satellite_datagram_to_modem_in_demo_mode">false</bool>
     <java-symbol type="bool" name="config_send_satellite_datagram_to_modem_in_demo_mode" />
 
+    <!-- List of country codes where oem-enabled satellite services are either allowed or disallowed
+         by the device. Each country code is a lowercase 2 character ISO-3166-1 alpha-2.
+         -->
+    <string-array name="config_oem_enabled_satellite_country_codes">
+    </string-array>
+    <java-symbol type="array" name="config_oem_enabled_satellite_country_codes" />
+
+    <!-- The file storing S2-cell-based satellite access restriction of the countries defined by
+         config_oem_enabled_satellite_countries. -->
+    <string name="config_oem_enabled_satellite_s2cell_file"></string>
+    <java-symbol type="string" name="config_oem_enabled_satellite_s2cell_file" />
+
+    <!-- Whether to treat the countries defined by the config_oem_enabled_satellite_countries
+         as satellite-allowed areas. The default true value means the countries defined by
+         config_oem_enabled_satellite_countries will be treated as satellite-allowed areas.
+         -->
+    <bool name="config_oem_enabled_satellite_access_allow">true</bool>
+    <java-symbol type="bool" name="config_oem_enabled_satellite_access_allow" />
+
+    <!-- The time duration in seconds which is used to decide whether the Location returned from
+         LocationManager#getLastKnownLocation is fresh.
+
+         The Location is considered fresh if the duration from the Location's elapsed real time to
+         the current elapsed real time is less than this config. If the Location is considered
+         fresh, it will be used as the current location by Telephony to decide whether satellite
+         services should be allowed.
+         -->
+    <integer name="config_oem_enabled_satellite_location_fresh_duration">600</integer>
+    <java-symbol type="integer" name="config_oem_enabled_satellite_location_fresh_duration" />
+
     <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks
          will not perform handover if the target transport is out of service, or VoPS not
          supported. The network will be torn down on the source transport, and will be
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 540967d..7b5c49c 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -119,6 +119,8 @@
     <public name="optional"/>
     <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
     <public name="adServiceTypes" />
+    <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") -->
+    <public name="systemUserOnly"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01bc0000">
@@ -128,8 +130,10 @@
   </staging-public-group>
 
   <staging-public-group type="string" first-id="0x01ba0000">
-    <!-- @hide @SystemApi -->
+    <!-- @hide @SystemApi @FlaggedApi("android.permission.flags.retail_demo_role_enabled") -->
     <public name="config_defaultRetailDemo" />
+    <!-- @hide @SystemApi @FlaggedApi("android.permission.flags.wallet_role_enabled") -->
+    <public name="config_defaultWallet" />
   </staging-public-group>
 
   <staging-public-group type="dimen" first-id="0x01b90000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d2fb9e1..f285806 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1248,6 +1248,11 @@
     <string name="permdesc_foregroundServiceFileManagement">Allows the app to make use of foreground services with the type \"fileManagement\"</string>
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceMediaProcessing">run foreground service with the type \"mediaProcessing\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceMediaProcessing">Allows the app to make use of foreground services with the type \"mediaProcessing\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_foregroundServiceSpecialUse">run foreground service with the type \"specialUse\"</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_foregroundServiceSpecialUse">Allows the app to make use of foreground services with the type \"specialUse\"</string>
@@ -1820,7 +1825,7 @@
     <!-- Message shown during fingerprint acquisision when the fingerprint cannot be recognized -->
     <string name="fingerprint_acquired_partial">Press firmly on the sensor</string>
     <!-- Message shown during fingerprint acquisision when the fingerprint cannot be recognized -->
-    <string name="fingerprint_acquired_insufficient">Can\u2019t recognize fingerprint. Try again.</string>
+    <string name="fingerprint_acquired_insufficient">Fingerprint not recognized. Try again.</string>
     <!-- Message shown during fingerprint acquisision when the fingerprint sensor needs cleaning -->
     <string name="fingerprint_acquired_imager_dirty">Clean fingerprint sensor and try again</string>
     <string name="fingerprint_acquired_imager_dirty_alt">Clean sensor and try again</string>
@@ -1954,7 +1959,7 @@
     <!-- Message shown during face acquisition when the sensor needs to be recalibrated [CHAR LIMIT=50] -->
     <string name="face_acquired_recalibrate">Please re-enroll your face.</string>
     <!-- Message shown during face enrollment when a different person's face is detected [CHAR LIMIT=50] -->
-    <string name="face_acquired_too_different">Can\u2019t recognize face. Try again.</string>
+    <string name="face_acquired_too_different">Face not recognized. Try again.</string>
     <!-- Message shown during face enrollment when the face is too similar to a previous acquisition [CHAR LIMIT=50] -->
     <string name="face_acquired_too_similar">Change the position of your head slightly</string>
     <!-- Message shown during acqusition when the user's face is turned too far left or right [CHAR LIMIT=50] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b0a4c16..d12ef2b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3095,8 +3095,6 @@
   <java-symbol type="bool" name="config_navBarDefaultTransparent" />
   <java-symbol type="color" name="navigation_bar_default"/>
   <java-symbol type="color" name="navigation_bar_compatible"/>
-  <java-symbol type="color" name="status_bar_default"/>
-  <java-symbol type="color" name="status_bar_compatible"/>
 
   <!-- EditText suggestion popup. -->
   <java-symbol type="id" name="suggestionWindowContainer" />
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index d5d67ab..bdbf96b 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -190,7 +190,7 @@
         <item name="windowTranslucentStatus">false</item>
         <item name="windowTranslucentNavigation">false</item>
         <item name="windowDrawsSystemBarBackgrounds">false</item>
-        <item name="statusBarColor">@color/status_bar_default</item>
+        <item name="statusBarColor">@color/black</item>
         <item name="navigationBarColor">@color/navigation_bar_default</item>
         <item name="windowActionBarFullscreenDecorLayout">@layout/screen_action_bar</item>
         <item name="windowContentTransitions">false</item>
diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
index 43266a5..cb3f99c 100644
--- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
+++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
@@ -17,6 +17,7 @@
 package android.animation;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.util.PollingCheck;
@@ -343,6 +344,20 @@
     }
 
     @Test
+    public void childAnimatorCancelsDuringUpdate_animatorSetIsEnded() throws Throwable {
+        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                animation.cancel();
+            }
+        });
+        mActivity.runOnUiThread(() -> {
+            mSet1.start();
+            assertFalse(mSet1.isRunning());
+        });
+    }
+
+    @Test
     public void reentrantStart() throws Throwable {
         CountDownLatch latch = new CountDownLatch(3);
         AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
index 1925588..9d85b65 100644
--- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
+++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.fail;
 
@@ -26,6 +28,8 @@
 import android.os.Parcel;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenPolicy;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -226,4 +230,66 @@
 
         assertThrows(IllegalArgumentException.class, () -> builder.setType(100));
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testCanUpdate_nullPolicyAndDeviceEffects() {
+        AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
+                Uri.parse("uri://short"));
+
+        AutomaticZenRule rule = builder.setUserModifiedFields(0)
+                .setZenPolicy(null)
+                .setDeviceEffects(null)
+                .build();
+
+        assertThat(rule.canUpdate()).isTrue();
+
+        rule = builder.setUserModifiedFields(1).build();
+        assertThat(rule.canUpdate()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testCanUpdate_policyModified() {
+        ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0);
+        ZenPolicy policy = policyBuilder.build();
+
+        AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
+                Uri.parse("uri://short"));
+        AutomaticZenRule rule = builder.setUserModifiedFields(0)
+                .setZenPolicy(policy)
+                .setDeviceEffects(null).build();
+
+        // Newly created ZenPolicy is not user modified.
+        assertThat(policy.getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.canUpdate()).isTrue();
+
+        policy = policyBuilder.setUserModifiedFields(1).build();
+        assertThat(policy.getUserModifiedFields()).isEqualTo(1);
+        rule = builder.setZenPolicy(policy).build();
+        assertThat(rule.canUpdate()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testCanUpdate_deviceEffectsModified() {
+        ZenDeviceEffects.Builder deviceEffectsBuilder =
+                new ZenDeviceEffects.Builder().setUserModifiedFields(0);
+        ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
+
+        AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
+                Uri.parse("uri://short"));
+        AutomaticZenRule rule = builder.setUserModifiedFields(0)
+                .setZenPolicy(null)
+                .setDeviceEffects(deviceEffects).build();
+
+        // Newly created ZenDeviceEffects is not user modified.
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.canUpdate()).isTrue();
+
+        deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build();
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1);
+        rule = builder.setDeviceEffects(deviceEffects).build();
+        assertThat(rule.canUpdate()).isFalse();
+    }
 }
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 723c081..a796a0f 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -171,7 +171,8 @@
 
     @Test
     public void testRecycleStartActivityItem() {
-        testRecycle(() -> StartActivityItem.obtain(mActivityToken, ActivityOptions.makeBasic()));
+        testRecycle(() -> StartActivityItem.obtain(mActivityToken,
+                new ActivityOptions.SceneTransitionInfo()));
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
index c0e2a49..3823033 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
@@ -255,9 +255,9 @@
             return LaunchActivityItem.obtain(mActivityToken, mIntent, mIdent, mInfo,
                     mCurConfig, mOverrideConfig, mDeviceId, mReferrer, mVoiceInteractor,
                     mProcState, mState, mPersistentState, mPendingResults, mPendingNewIntents,
-                    mActivityOptions, mIsForward, mProfilerInfo, mAssistToken,
-                    null /* activityClientController */, mShareableActivityToken,
-                    mLaunchedFromBubble, mTaskFragmentToken);
+                    mActivityOptions != null ? mActivityOptions.getSceneTransitionInfo() : null,
+                    mIsForward, mProfilerInfo, mAssistToken, null /* activityClientController */,
+                    mShareableActivityToken, mLaunchedFromBubble, mTaskFragmentToken);
         }
     }
 }
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index 07921bf..952cdd9 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -262,7 +262,7 @@
     public void testStart() {
         // Write to parcel
         StartActivityItem item = StartActivityItem.obtain(mActivityToken,
-                ActivityOptions.makeBasic());
+                new ActivityOptions.SceneTransitionInfo());
         writeAndPrepareForReading(item);
 
         // Read from parcel and assert
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
index 3fc08ee..e118c98d 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -26,6 +26,9 @@
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.test.AndroidTestCase;
 import android.util.Log;
 
@@ -35,6 +38,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -53,6 +57,10 @@
 @SmallTest
 public class SQLiteDatabaseTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String TAG = "SQLiteDatabaseTest";
 
     private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
@@ -247,6 +255,7 @@
 
         final String query = "--comment\nSELECT count(*) from t1";
 
+        database.beginTransactionReadOnly();
         try {
             for (int i = count; i > 0; i--) {
                 ticker.arriveAndAwaitAdvance();
@@ -260,6 +269,7 @@
         } catch (Throwable t) {
             errors.add(t);
         } finally {
+            database.endTransaction();
             ticker.arriveAndDeregister();
         }
     }
@@ -347,4 +357,50 @@
 
         assertTrue("ReadThread failed with errors: " + errors, errors.isEmpty());
     }
+
+    @RequiresFlagsEnabled(Flags.FLAG_SQLITE_ALLOW_TEMP_TABLES)
+    @Test
+    public void testTempTable() {
+        boolean allowed;
+        allowed = true;
+        mDatabase.beginTransactionReadOnly();
+        try {
+            mDatabase.execSQL("CREATE TEMP TABLE t1 (i int, j int);");
+            mDatabase.execSQL("INSERT INTO t1 (i, j) VALUES (2, 20)");
+            mDatabase.execSQL("INSERT INTO t1 (i, j) VALUES (3, 30)");
+
+            final String sql = "SELECT i FROM t1 WHERE j = 30";
+            try (SQLiteRawStatement s = mDatabase.createRawStatement(sql)) {
+                assertTrue(s.step());
+                assertEquals(3, s.getColumnInt(0));
+            }
+
+        } catch (SQLiteException e) {
+            allowed = false;
+        } finally {
+            mDatabase.endTransaction();
+        }
+        assertTrue(allowed);
+
+        // Repeat the test on the main schema.
+        allowed = true;
+        mDatabase.beginTransactionReadOnly();
+        try {
+            mDatabase.execSQL("CREATE TABLE t2 (i int, j int);");
+            mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (2, 20)");
+            mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (3, 30)");
+
+            final String sql = "SELECT i FROM t2 WHERE j = 30";
+            try (SQLiteRawStatement s = mDatabase.createRawStatement(sql)) {
+                assertTrue(s.step());
+                assertEquals(3, s.getColumnInt(0));
+            }
+
+        } catch (SQLiteException e) {
+            allowed = false;
+        } finally {
+            mDatabase.endTransaction();
+        }
+        assertFalse(allowed);
+    }
 }
diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java
index bf56df1..0dec756 100644
--- a/core/tests/coretests/src/android/graphics/PaintTest.java
+++ b/core/tests/coretests/src/android/graphics/PaintTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertNotEquals;
 
 import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
 
 import androidx.test.filters.SmallTest;
 
@@ -362,4 +363,44 @@
         //    = 30
         assertEquals(30.0f, p.getUnderlineThickness(), 0.5f);
     }
+
+    private int getClusterCount(Paint p, String text) {
+        Paint.RunInfo runInfo = new Paint.RunInfo();
+        p.getRunCharacterAdvance(text, 0, text.length(), 0, text.length(), false, 0, null, 0, null,
+                runInfo);
+        int ccByString = runInfo.getClusterCount();
+        runInfo.setClusterCount(0);
+        char[] buf = new char[text.length()];
+        TextUtils.getChars(text, 0, text.length(), buf, 0);
+        p.getRunCharacterAdvance(buf, 0, buf.length, 0, buf.length, false, 0, null, 0, null,
+                runInfo);
+        int ccByChars = runInfo.getClusterCount();
+        assertEquals(ccByChars, ccByString);
+        return ccByChars;
+    }
+
+    public void testCluster() {
+        final Paint p = new Paint();
+        p.setTextSize(100);
+
+        // Regular String
+        assertEquals(1, getClusterCount(p, "A"));
+        assertEquals(2, getClusterCount(p, "AB"));
+
+        // Ligature is in the same cluster
+        assertEquals(1, getClusterCount(p, "fi"));  // Ligature
+        p.setFontFeatureSettings("'liga' off");
+        assertEquals(2, getClusterCount(p, "fi"));  // Ligature is disabled
+        p.setFontFeatureSettings("");
+
+        // Combining character
+        assertEquals(1, getClusterCount(p, "\u0061\u0300"));  // A + COMBINING GRAVE ACCENT
+
+        // BiDi
+        final String rtlStr = "\u05D0\u05D1\u05D2";
+        final String ltrStr = "abc";
+        assertEquals(3, getClusterCount(p, rtlStr));
+        assertEquals(6, getClusterCount(p, rtlStr + ltrStr));
+        assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr));
+    }
 }
diff --git a/core/tests/coretests/src/android/os/BuildTest.java b/core/tests/coretests/src/android/os/BuildTest.java
index 3162e6d..2d3e123 100644
--- a/core/tests/coretests/src/android/os/BuildTest.java
+++ b/core/tests/coretests/src/android/os/BuildTest.java
@@ -45,7 +45,8 @@
     public final RavenwoodRule mRavenwood = new RavenwoodRule();
 
     @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+                SetFlagsRule.DefaultInitValueType.NULL_DEFAULT);
 
     /**
      * Asserts that a String is non-null and non-empty.  If it is not,
diff --git a/core/tests/coretests/src/android/text/TextLineTest.java b/core/tests/coretests/src/android/text/TextLineTest.java
index 34842a0..a31992c 100644
--- a/core/tests/coretests/src/android/text/TextLineTest.java
+++ b/core/tests/coretests/src/android/text/TextLineTest.java
@@ -50,11 +50,11 @@
         tl.set(paint, line, 0, line.length(), Layout.DIR_LEFT_TO_RIGHT,
                 Layout.DIRS_ALL_LEFT_TO_RIGHT, false /* hasTabs */, null /* tabStops */,
                 0, 0 /* no ellipsis */, false /* useFallbackLinespace */);
-        final float originalWidth = tl.metrics(null, null, false);
+        final float originalWidth = tl.metrics(null, null, false, null);
         final float expandedWidth = 2 * originalWidth;
 
         tl.justify(expandedWidth);
-        final float newWidth = tl.metrics(null, null, false);
+        final float newWidth = tl.metrics(null, null, false, null);
         TextLine.recycle(tl);
         return Math.abs(newWidth - expandedWidth) < 0.5;
     }
@@ -128,7 +128,7 @@
     private void assertMeasurements(final TextLine tl, final int length, boolean trailing,
             final float[] expected) {
         for (int offset = 0; offset <= length; ++offset) {
-            assertEquals(expected[offset], tl.measure(offset, trailing, null, null), 0.0f);
+            assertEquals(expected[offset], tl.measure(offset, trailing, null, null, null), 0.0f);
         }
 
         final boolean[] trailings = new boolean[length + 1];
@@ -318,7 +318,7 @@
         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
                 false /* hasTabs */, null /* tabStops */, 9, 12,
                 false /* useFallbackLineSpacing */);
-        tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
+        tl.measure(text.length(), false /* trailing */, null /* fmi */, null, null);
 
         assertFalse(span.mIsUsed);
     }
@@ -335,7 +335,7 @@
         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
                 false /* hasTabs */, null /* tabStops */, 9, 12,
                 false /* useFallbackLineSpacing */);
-        tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
+        tl.measure(text.length(), false /* trailing */, null /* fmi */, null, null);
 
         assertTrue(span.mIsUsed);
     }
@@ -352,7 +352,7 @@
         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
                 false /* hasTabs */, null /* tabStops */, 9, 12,
                 false /* useFallbackLineSpacing */);
-        tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
+        tl.measure(text.length(), false /* trailing */, null /* fmi */, null, null);
         assertTrue(span.mIsUsed);
     }
 
diff --git a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java
index c9536b9..533b799 100644
--- a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java
@@ -22,7 +22,6 @@
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
@@ -34,7 +33,6 @@
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-@IgnoreUnderRavenwood(blockedBy = LongArrayMultiStateCounter.class)
 public class LongArrayMultiStateCounterTest {
     @Rule
     public final RavenwoodRule mRavenwood = new RavenwoodRule();
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index 43683ff..ce2543a 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -56,6 +56,7 @@
         <permission name="android.permission.REAL_GET_TASKS"/>
         <permission name="android.permission.REQUEST_NETWORK_SCORES"/>
         <permission name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE"/>
+        <permission name="android.permission.SATELLITE_COMMUNICATION"/>
         <permission name="android.permission.SET_WALLPAPER_DIM_AMOUNT"/>
         <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" />
         <permission name="android.permission.START_ACTIVITY_AS_CALLER"/>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index b9efe65..a1ea2b8 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -134,6 +134,7 @@
         <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
         <permission name="android.permission.DUMP"/>
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+        <permission name="android.permission.LOCATION_BYPASS"/>
         <permission name="android.permission.LOCAL_MAC_ADDRESS"/>
         <permission name="android.permission.MANAGE_USERS"/>
         <permission name="android.permission.MANAGE_SUBSCRIPTION_PLANS" />
@@ -149,6 +150,7 @@
         <permission name="android.permission.REGISTER_CALL_PROVIDER"/>
         <permission name="android.permission.REGISTER_SIM_SUBSCRIPTION"/>
         <permission name="android.permission.REGISTER_STATS_PULL_ATOM"/>
+        <permission name="android.permission.SATELLITE_COMMUNICATION"/>
         <permission name="android.permission.SEND_RESPOND_VIA_MESSAGE"/>
         <permission name="android.permission.SHUTDOWN"/>
         <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/>
diff --git a/data/keyboards/Android.bp b/data/keyboards/Android.bp
new file mode 100644
index 0000000..f15c153
--- /dev/null
+++ b/data/keyboards/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+genrule {
+    name: "validate_framework_keymaps",
+    srcs: [
+        "*.kl",
+        "*.kcm",
+        "*.idc",
+    ],
+    tools: ["validatekeymaps"],
+    out: ["stamp"],
+    cmd: "$(location validatekeymaps) -q $(in) " +
+        "&& touch $(out)",
+    dist: {
+        targets: ["droidcore"],
+    },
+}
diff --git a/data/keyboards/Android.mk b/data/keyboards/Android.mk
deleted file mode 100644
index 6ae8800..0000000
--- a/data/keyboards/Android.mk
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright (C) 2010 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This makefile performs build time validation of framework keymap files.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(LOCAL_PATH)/common.mk
-
-# Validate all key maps.
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := validate_framework_keymaps
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-intermediates := $(call intermediates-dir-for,ETC,$(LOCAL_MODULE),,COMMON)
-LOCAL_BUILT_MODULE := $(intermediates)/stamp
-
-validatekeymaps := $(HOST_OUT_EXECUTABLES)/validatekeymaps$(HOST_EXECUTABLE_SUFFIX)
-$(LOCAL_BUILT_MODULE): PRIVATE_VALIDATEKEYMAPS := $(validatekeymaps)
-$(LOCAL_BUILT_MODULE) : $(framework_keylayouts) $(framework_keycharmaps) $(framework_keyconfigs) | $(validatekeymaps)
-	$(hide) $(PRIVATE_VALIDATEKEYMAPS) -q $^
-	$(hide) mkdir -p $(dir $@) && touch $@
-
-# Run validatekeymaps uncondionally for platform build.
-droidcore : $(LOCAL_BUILT_MODULE)
-
-# Reset temp vars.
-validatekeymaps :=
-framework_keylayouts :=
-framework_keycharmaps :=
-framework_keyconfigs :=
diff --git a/data/keyboards/common.mk b/data/keyboards/Vendor_0957_Product_0031.idc
similarity index 61%
rename from data/keyboards/common.mk
rename to data/keyboards/Vendor_0957_Product_0031.idc
index d75b691..f0320c8 100644
--- a/data/keyboards/common.mk
+++ b/data/keyboards/Vendor_0957_Product_0031.idc
@@ -1,4 +1,4 @@
-# Copyright (C) 2010 The Android Open Source Project
+# Copyright 2024 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -11,12 +11,11 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+#
 
-# This is the list of framework provided keylayouts and key character maps to include.
-# Used by Android.mk and keyboards.mk.
+# Input Device Configuration file for Google Reference RCU Remote.
+# PID 0031 is for old GPIO pin
 
-framework_keylayouts := $(wildcard $(LOCAL_PATH)/*.kl)
-
-framework_keycharmaps := $(wildcard $(LOCAL_PATH)/*.kcm)
-
-framework_keyconfigs := $(wildcard $(LOCAL_PATH)/*.idc)
+# Basic Parameters
+keyboard.doNotWakeByDefault = 1
+audio.mic = 1
\ No newline at end of file
diff --git a/data/keyboards/Vendor_0957_Product_0031.kl b/data/keyboards/Vendor_0957_Product_0031.kl
new file mode 100644
index 0000000..b47ee58
--- /dev/null
+++ b/data/keyboards/Vendor_0957_Product_0031.kl
@@ -0,0 +1,82 @@
+# Copyright 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Key Layout file for Google Reference RCU Remote with customizable button.
+#
+
+key 116   TV_POWER      WAKE
+key 217   ASSIST        WAKE
+key 423   MACRO_1       WAKE
+
+key 103   DPAD_UP
+key 108   DPAD_DOWN
+key 105   DPAD_LEFT
+key 106   DPAD_RIGHT
+key 353   DPAD_CENTER
+
+key 158   BACK
+key 172   HOME          WAKE
+
+key 113   VOLUME_MUTE
+key 114   VOLUME_DOWN
+key 115   VOLUME_UP
+
+key 2     1
+key 3     2
+key 4     3
+key 5     4
+key 6     5
+key 7     6
+key 8     7
+key 9     8
+key 10    9
+key 11    0
+
+# custom keys
+key usage 0x000c01BB    TV_INPUT
+
+key usage 0x000c0185    TV_TELETEXT
+key usage 0x000c0061    CAPTIONS
+
+key usage 0x000c01BD    INFO
+key usage 0x000c0037    PERIOD
+
+key usage 0x000c0069    PROG_RED
+key usage 0x000c006A    PROG_GREEN
+key usage 0x000c006C    PROG_YELLOW
+key usage 0x000c006B    PROG_BLUE
+key usage 0x000c00B4    MEDIA_SKIP_BACKWARD
+key usage 0x000c00CD    MEDIA_PLAY_PAUSE
+key usage 0x000c00B2    MEDIA_RECORD
+key usage 0x000c00B3    MEDIA_SKIP_FORWARD
+
+key usage 0x000c022A    BOOKMARK
+key usage 0x000c01A2    ALL_APPS
+key usage 0x000c019C    PROFILE_SWITCH
+
+key usage 0x000c0096    SETTINGS
+key usage 0x000c009F    NOTIFICATION
+
+key usage 0x000c008D    GUIDE
+key usage 0x000c0089    TV
+
+key usage 0x000c0187    FEATURED_APP_1    WAKE #FreeTv
+
+key usage 0x000c009C    CHANNEL_UP
+key usage 0x000c009D    CHANNEL_DOWN
+
+key usage 0x000c0077    BUTTON_3     WAKE #YouTube
+key usage 0x000c0078    BUTTON_4     WAKE #Netflix
+key usage 0x000c0079    BUTTON_6     WAKE
+key usage 0x000c007A    BUTTON_7     WAKE
\ No newline at end of file
diff --git a/data/keyboards/common.mk b/data/keyboards/Vendor_0957_Product_0034.idc
similarity index 60%
copy from data/keyboards/common.mk
copy to data/keyboards/Vendor_0957_Product_0034.idc
index d75b691..52ed0bc 100644
--- a/data/keyboards/common.mk
+++ b/data/keyboards/Vendor_0957_Product_0034.idc
@@ -1,4 +1,4 @@
-# Copyright (C) 2010 The Android Open Source Project
+# Copyright 2024 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -11,12 +11,13 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+#
 
-# This is the list of framework provided keylayouts and key character maps to include.
-# Used by Android.mk and keyboards.mk.
+# Input Device Configuration file for Google Reference RCU Remote.
+# PID 0034 is for new GPIO pin PCB.
 
-framework_keylayouts := $(wildcard $(LOCAL_PATH)/*.kl)
-
-framework_keycharmaps := $(wildcard $(LOCAL_PATH)/*.kcm)
-
-framework_keyconfigs := $(wildcard $(LOCAL_PATH)/*.idc)
+# Basic Parameters
+keyboard.layout = Vendor_0957_Product_0031
+# The reason why we set is follow https://docs.partner.android.com/tv/build/gtv/boot-resume
+keyboard.doNotWakeByDefault = 1
+audio.mic = 1
\ No newline at end of file
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index f10cdb8..c5a2f98 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -65,6 +65,8 @@
     private long mNativeShader;
     private long mNativeColorFilter;
 
+    private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric");
+
     // Use a Holder to allow static initialization of Paint in the boot image.
     private static class NoImagePreloadHolder {
         public static final NativeAllocationRegistry sRegistry =
@@ -2474,6 +2476,19 @@
         nGetFontMetricsInt(mNativePaint, metrics, true);
     }
 
+    /** @hide */
+    public static final class RunInfo {
+        private int mClusterCount = 0;
+
+        public int getClusterCount() {
+            return mClusterCount;
+        }
+
+        public void setClusterCount(int clusterCount) {
+            mClusterCount = clusterCount;
+        }
+    }
+
     /**
      * Return the recommend line spacing based on the current typeface and
      * text size.
@@ -3320,7 +3335,7 @@
             int contextEnd, boolean isRtl, int offset,
             @Nullable float[] advances, int advancesIndex) {
         return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
-                advances, advancesIndex, null);
+                advances, advancesIndex, null, null);
     }
 
     /**
@@ -3339,12 +3354,14 @@
      * @param advances the array that receives the computed character advances
      * @param advancesIndex the start index from which the advances array is filled
      * @param drawBounds the output parameter for the bounding box of drawing text, optional
+     * @param runInfo the output parameter for storing run information.
      * @return width measurement between start and offset
-     * @hide
+     * @hide TODO: Reorganize APIs
      */
     public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart,
             int contextEnd, boolean isRtl, int offset,
-            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds) {
+            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+            @Nullable RunInfo runInfo) {
         if (text == null) {
             throw new IllegalArgumentException("text cannot be null");
         }
@@ -3370,11 +3387,19 @@
         }
 
         if (end == start) {
+            if (runInfo != null) {
+                runInfo.setClusterCount(0);
+            }
             return 0.0f;
         }
 
-        return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
-                isRtl, offset, advances, advancesIndex, drawBounds);
+        if (sIsRobolectric) {
+            return nGetRunCharacterAdvance(mNativePaint, text, start, end,
+                    contextStart, contextEnd, isRtl, offset, advances, advancesIndex, drawBounds);
+        } else {
+            return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
+                    isRtl, offset, advances, advancesIndex, drawBounds, runInfo);
+        }
     }
 
     /**
@@ -3402,7 +3427,7 @@
             int contextStart, int contextEnd, boolean isRtl, int offset,
             @Nullable float[] advances, int advancesIndex) {
         return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
-                advances, advancesIndex, null);
+                advances, advancesIndex, null, null);
     }
 
     /**
@@ -3418,12 +3443,14 @@
      * @param advances the array that receives the computed character advances
      * @param advancesIndex the start index from which the advances array is filled
      * @param drawBounds the output parameter for the bounding box of drawing text, optional
+     * @param runInfo an optional output parameter for filling run information.
      * @return width measurement between start and offset
-     * @hide
+     * @hide  TODO: Reorganize APIs
      */
     public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end,
             int contextStart, int contextEnd, boolean isRtl, int offset,
-            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds) {
+            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+            @Nullable RunInfo runInfo) {
         if (text == null) {
             throw new IllegalArgumentException("text cannot be null");
         }
@@ -3456,7 +3483,7 @@
         TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
         final float result = getRunCharacterAdvance(buf, start - contextStart, end - contextStart,
                 0, contextEnd - contextStart, isRtl, offset - contextStart,
-                advances, advancesIndex, drawBounds);
+                advances, advancesIndex, drawBounds, runInfo);
         TemporaryBuffer.recycle(buf);
         return result;
     }
@@ -3574,7 +3601,7 @@
             int contextStart, int contextEnd, boolean isRtl, int offset);
     private static native float nGetRunCharacterAdvance(long paintPtr, char[] text, int start,
             int end, int contextStart, int contextEnd, boolean isRtl, int offset, float[] advances,
-            int advancesIndex, RectF drawingBounds);
+            int advancesIndex, RectF drawingBounds, RunInfo runInfo);
     private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end,
             int contextStart, int contextEnd, boolean isRtl, float advance);
     private static native void nGetFontMetricsIntForText(long paintPtr, char[] text,
@@ -3729,4 +3756,11 @@
     private static native void nSetTextSize(long paintPtr, float textSize);
     @CriticalNative
     private static native boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr);
+
+
+    // Following Native methods are kept for old Robolectric JNI signature used by
+    // SystemUIGoogleRoboRNGTests
+    private static native float nGetRunCharacterAdvance(long paintPtr, char[] text,
+            int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset,
+            float[] advances, int advancesIndex, RectF drawingBounds);
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 592f9a5..80afb16d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -382,9 +382,13 @@
         if (splitAttributes == null) {
             return TaskFragmentAnimationParams.DEFAULT;
         }
-        return new TaskFragmentAnimationParams.Builder()
-                // TODO(b/263047900): Update extensions API.
-                // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
-                .build();
+        final AnimationBackground animationBackground = splitAttributes.getAnimationBackground();
+        if (animationBackground instanceof AnimationBackground.ColorBackground colorBackground) {
+            return new TaskFragmentAnimationParams.Builder()
+                    .setAnimationBackgroundColor(colorBackground.getColor())
+                    .build();
+        } else {
+            return TaskFragmentAnimationParams.DEFAULT;
+        }
     }
 }
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 6f356fa..8b7fd10 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -893,8 +893,7 @@
         return new SplitAttributes.Builder()
                 .setSplitType(splitTypeToUpdate)
                 .setLayoutDirection(splitAttributes.getLayoutDirection())
-                // TODO(b/263047900): Update extensions API.
-                // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
+                .setAnimationBackground(splitAttributes.getAnimationBackground())
                 .build();
     }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
index 60beb0b..f471af0 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
@@ -25,6 +25,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.window.extensions.embedding.AnimationBackground;
 import androidx.window.extensions.embedding.SplitAttributes;
 
 import org.junit.Before;
@@ -70,7 +71,7 @@
                 .isEqualTo(SplitAttributes.LayoutDirection.LOCALE);
         assertThat(splitAttributes.getSplitType())
                 .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f));
-        // TODO(b/263047900): Update extensions API.
-        // assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0);
+        assertThat(splitAttributes.getAnimationBackground())
+                .isEqualTo(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT);
     }
 }
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 5ad144d..45540e0 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -175,3 +175,74 @@
     plugins: ["dagger2-compiler"],
     use_resource_processor: true,
 }
+
+android_app {
+    name: "WindowManagerShellRobolectric",
+    platform_apis: true,
+    static_libs: [
+        "WindowManager-Shell",
+    ],
+    manifest: "multivalentTests/AndroidManifestRobolectric.xml",
+    use_resource_processor: true,
+}
+
+android_robolectric_test {
+    name: "WMShellRobolectricTests",
+    instrumentation_for: "WindowManagerShellRobolectric",
+    upstream: true,
+    java_resource_dirs: [
+        "multivalentTests/robolectric/config",
+    ],
+    srcs: [
+        "multivalentTests/src/**/*.kt",
+    ],
+    static_libs: [
+        "junit",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "mockito-robolectric-prebuilt",
+        "mockito-kotlin2",
+        "truth",
+    ],
+}
+
+android_test {
+    name: "WMShellMultivalentTestsOnDevice",
+    srcs: [
+        "multivalentTests/src/**/*.kt",
+    ],
+    static_libs: [
+        "WindowManager-Shell",
+        "junit",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "frameworks-base-testutils",
+        "mockito-kotlin2",
+        "mockito-target-extended-minus-junit4",
+        "truth",
+        "platform-test-annotations",
+        "platform-test-rules",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+    optimize: {
+        enabled: false,
+    },
+    test_suites: ["device-tests"],
+    platform_apis: true,
+    certificate: "platform",
+    aaptflags: [
+        "--extra-packages",
+        "com.android.wm.shell",
+    ],
+    manifest: "multivalentTests/AndroidManifest.xml",
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
new file mode 100644
index 0000000..f8f8338
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
@@ -0,0 +1,13 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.multivalenttests">
+
+    <application android:debuggable="true" android:supportsRtl="true" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="Multivalent tests for WindowManager-Shell"
+        android:targetPackage="com.android.wm.shell.multivalenttests">
+    </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml
new file mode 100644
index 0000000..ffcd7d4
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.multivalenttests">
+</manifest>
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidTest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidTest.xml
new file mode 100644
index 0000000..36fe8ec
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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 Tests for WindowManagerShellLib">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="WMShellMultivalentTestsOnDevice.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="framework-base-presubmit" />
+    <option name="test-tag" value="WMShellMultivalentTestsOnDevice" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.wm.shell.multivalenttests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/libs/WindowManager/Shell/multivalentTests/robolectric/config/robolectric.properties b/libs/WindowManager/Shell/multivalentTests/robolectric/config/robolectric.properties
new file mode 100644
index 0000000..7a0527c
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/robolectric/config/robolectric.properties
@@ -0,0 +1,2 @@
+sdk=NEWEST_SDK
+
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
new file mode 100644
index 0000000..ea7c6ed
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.graphics.Insets
+import android.graphics.PointF
+import android.graphics.Rect
+import android.os.UserHandle
+import android.view.WindowManager
+import androidx.test.core.app.ApplicationProvider
+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.MAX_HEIGHT
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests operations and the resulting state managed by [BubblePositioner]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubblePositionerTest {
+
+    private lateinit var positioner: BubblePositioner
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val defaultDeviceConfig =
+        DeviceConfig(
+            windowBounds = Rect(0, 0, 1000, 2000),
+            isLargeScreen = false,
+            isSmallTablet = false,
+            isLandscape = false,
+            isRtl = false,
+            insets = Insets.of(0, 0, 0, 0)
+        )
+
+    @Before
+    fun setUp() {
+        val windowManager = context.getSystemService(WindowManager::class.java)
+        positioner = BubblePositioner(context, windowManager)
+    }
+
+    @Test
+    fun testUpdate() {
+        val insets = Insets.of(10, 20, 5, 15)
+        val screenBounds = Rect(0, 0, 1000, 1200)
+        val availableRect = Rect(screenBounds)
+        availableRect.inset(insets)
+        positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds))
+        assertThat(positioner.availableRect).isEqualTo(availableRect)
+        assertThat(positioner.isLandscape).isFalse()
+        assertThat(positioner.isLargeScreen).isFalse()
+        assertThat(positioner.insets).isEqualTo(insets)
+    }
+
+    @Test
+    fun testShowBubblesVertically_phonePortrait() {
+        positioner.update(defaultDeviceConfig)
+        assertThat(positioner.showBubblesVertically()).isFalse()
+    }
+
+    @Test
+    fun testShowBubblesVertically_phoneLandscape() {
+        positioner.update(defaultDeviceConfig.copy(isLandscape = true))
+        assertThat(positioner.isLandscape).isTrue()
+        assertThat(positioner.showBubblesVertically()).isTrue()
+    }
+
+    @Test
+    fun testShowBubblesVertically_tablet() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
+        assertThat(positioner.showBubblesVertically()).isTrue()
+    }
+
+    /** If a resting position hasn't been set, calling it will return the default position. */
+    @Test
+    fun testGetRestingPosition_returnsDefaultPosition() {
+        positioner.update(defaultDeviceConfig)
+        val restingPosition = positioner.getRestingPosition()
+        val defaultPosition = positioner.defaultStartPosition
+        assertThat(restingPosition).isEqualTo(defaultPosition)
+    }
+
+    /** If a resting position has been set, it'll return that instead of the default position. */
+    @Test
+    fun testGetRestingPosition_returnsRestingPosition() {
+        positioner.update(defaultDeviceConfig)
+        val restingPosition = PointF(100f, 100f)
+        positioner.restingPosition = restingPosition
+        assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition)
+    }
+
+    /** Test that the default resting position on phone is in upper left. */
+    @Test
+    fun testGetRestingPosition_bubble_onPhone() {
+        positioner.update(defaultDeviceConfig)
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testGetRestingPosition_bubble_onPhone_RTL() {
+        positioner.update(defaultDeviceConfig.copy(isRtl = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    /** Test that the default resting position on tablet is middle left. */
+    @Test
+    fun testGetRestingPosition_chatBubble_onTablet() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testGetRestingPosition_chatBubble_onTablet_RTL() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    /** Test that the default resting position on tablet is middle right. */
+    @Test
+    fun testGetDefaultPosition_appBubble_onTablet() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
+        assertThat(startPosition.x).isEqualTo(allowableStackRegion.right)
+        assertThat(startPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testGetRestingPosition_appBubble_onTablet_RTL() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
+        assertThat(startPosition.x).isEqualTo(allowableStackRegion.left)
+        assertThat(startPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testHasUserModifiedDefaultPosition_false() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
+        positioner.restingPosition = positioner.defaultStartPosition
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
+    }
+
+    @Test
+    fun testHasUserModifiedDefaultPosition_true() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
+        positioner.restingPosition = PointF(0f, 100f)
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue()
+    }
+
+    @Test
+    fun testGetExpandedViewHeight_max() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT)
+    }
+
+    @Test
+    fun testGetExpandedViewHeight_customHeight_valid() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+        val minHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
+        val bubble =
+            Bubble(
+                "key",
+                ShortcutInfo.Builder(context, "id").build(),
+                minHeight + 100 /* desiredHeight */,
+                0 /* desiredHeightResId */,
+                "title",
+                0 /* taskId */,
+                null /* locus */,
+                true /* isDismissable */,
+                directExecutor()) {}
+
+        // Ensure the height is the same as the desired value
+        assertThat(positioner.getExpandedViewHeight(bubble))
+            .isEqualTo(bubble.getDesiredHeight(context))
+    }
+
+    @Test
+    fun testGetExpandedViewHeight_customHeight_tooSmall() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val bubble =
+            Bubble(
+                "key",
+                ShortcutInfo.Builder(context, "id").build(),
+                10 /* desiredHeight */,
+                0 /* desiredHeightResId */,
+                "title",
+                0 /* taskId */,
+                null /* locus */,
+                true /* isDismissable */,
+                directExecutor()) {}
+
+        // Ensure the height is the same as the desired value
+        val minHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
+        assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight)
+    }
+
+    @Test
+    fun testGetMaxExpandedViewHeight_onLargeTablet() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val manageButtonHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
+        val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
+        val expandedViewPadding =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+        val expectedHeight =
+            1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2
+        assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */))
+            .isEqualTo(expectedHeight)
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_largeScreen_true() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isTrue()
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_largeScreen_landscape_false() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isFalse()
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_smallTablet_false() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                isSmallTablet = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isFalse()
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_phone_false() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isFalse()
+    }
+
+    @Test
+    fun testExpandedViewY_phoneLandscape() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height so it'll always be top aligned
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_phonePortrait() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // Always top aligned in phone portrait
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_smallTabletLandscape() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isSmallTablet = true,
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height which is always top aligned on small tablets
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_smallTabletPortrait() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isSmallTablet = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height which is always top aligned on small tablets
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_largeScreenLandscape() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height which is always top aligned on landscape, large tablet
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_largeScreenPortrait() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        val manageButtonHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
+        val manageButtonPlusMargin =
+            manageButtonHeight +
+                2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin)
+        val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
+
+        val expectedExpandedViewY =
+            positioner.availableRect.bottom -
+                manageButtonPlusMargin -
+                positioner.getExpandedViewHeightForLargeScreen() -
+                pointerWidth
+
+        // Bubbles are bottom aligned on portrait, large tablet
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(expectedExpandedViewY)
+    }
+
+    private val defaultYPosition: Float
+        /**
+         * Calculates the Y position bubbles should be placed based on the config. Based on the
+         * calculations in [BubblePositioner.getDefaultStartPosition] and
+         * [BubbleStackView.RelativeStackPosition].
+         */
+        get() {
+            val isTablet = positioner.isLargeScreen
+
+            // On tablet the position is centered, on phone it is an offset from the top.
+            val desiredY =
+                if (isTablet) {
+                    positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f
+                } else {
+                    context.resources
+                        .getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y)
+                        .toFloat()
+                }
+            // Since we're visually centering the bubbles on tablet, use total screen height rather
+            // than the available height.
+            val height =
+                if (isTablet) {
+                    positioner.screenRect.height()
+                } else {
+                    positioner.availableRect.height()
+                }
+            val offsetPercent = (desiredY / height).coerceIn(0f, 1f)
+            val allowableStackRegion =
+                positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+            return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
+        }
+}
diff --git a/libs/WindowManager/Shell/multivalentTestsForDevice b/libs/WindowManager/Shell/multivalentTestsForDevice
new file mode 120000
index 0000000..20ee34a
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTestsForDevice
@@ -0,0 +1 @@
+multivalentTests
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/multivalentTestsForDeviceless b/libs/WindowManager/Shell/multivalentTestsForDeviceless
new file mode 120000
index 0000000..20ee34a
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTestsForDeviceless
@@ -0,0 +1 @@
+multivalentTests
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
index 681a52b..e04ab81 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
@@ -21,4 +21,9 @@
     android:orientation="vertical"
     android:id="@+id/bubble_bar_expanded_view">
 
+    <com.android.wm.shell.bubbles.bar.BubbleBarHandleView
+        android:id="@+id/bubble_bar_handle_view"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content" />
+
 </com.android.wm.shell.bubbles.bar.BubbleBarExpandedView>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index ac75c73..44ee561 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -20,6 +20,7 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
 import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
+import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
@@ -330,6 +331,11 @@
             if (!animation.hasExtension()) {
                 continue;
             }
+            if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT)
+                    && adapter.mChange.getActivityComponent() != null) {
+                // Skip edge extension for translucent activity.
+                continue;
+            }
             final TransitionInfo.Change change = adapter.mChange;
             if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) {
                 // Need to screenshot after startTransaction is applied otherwise activity
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 a498236..bb433db 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
@@ -176,6 +176,10 @@
     private StatusBarCustomizer mCustomizer;
     private boolean mTrackingLatency;
 
+    // Keep previous navigation type before remove mBackNavigationInfo.
+    @BackNavigationInfo.BackTargetType
+    private int mPreviousNavigationType;
+
     public BackAnimationController(
             @NonNull ShellInit shellInit,
             @NonNull ShellController shellController,
@@ -403,8 +407,8 @@
         mCurrentTracker.updateStartLocation();
         // Dispatch onBackStarted, only to app callbacks.
         // System callbacks will receive onBackStarted when the remote animation starts.
-        if (!shouldDispatchToAnimator()) {
-            tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
+        if (!shouldDispatchToAnimator() && mActiveCallback != null) {
+            tryDispatchAppOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
         }
     }
 
@@ -507,7 +511,7 @@
             mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
             // App is handling back animation. Cancel system animation latency tracking.
             cancelLatencyTracking();
-            tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
+            tryDispatchAppOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
         }
     }
 
@@ -551,14 +555,24 @@
                 && mBackNavigationInfo.isPrepareRemoteAnimation();
     }
 
-    private void tryDispatchOnBackStarted(IOnBackInvokedCallback callback,
+    private void tryDispatchAppOnBackStarted(
+            IOnBackInvokedCallback callback,
             BackMotionEvent backEvent) {
-        if (callback == null || mOnBackStartDispatched) {
+        if (mOnBackStartDispatched && callback != null) {
+            return;
+        }
+        dispatchOnBackStarted(callback, backEvent);
+        mOnBackStartDispatched = true;
+    }
+
+    private void dispatchOnBackStarted(
+            IOnBackInvokedCallback callback,
+            BackMotionEvent backEvent) {
+        if (callback == null) {
             return;
         }
         try {
             callback.onBackStarted(backEvent);
-            mOnBackStartDispatched = true;
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackStarted error: ", e);
         }
@@ -861,6 +875,7 @@
         mShellBackAnimationRegistry.resetDefaultCrossActivity();
         cancelLatencyTracking();
         if (mBackNavigationInfo != null) {
+            mPreviousNavigationType = mBackNavigationInfo.getType();
             mBackNavigationInfo.onBackNavigationFinished(triggerBack);
             mBackNavigationInfo = null;
         }
@@ -940,9 +955,17 @@
 
                                     if (apps.length >= 1) {
                                         mCurrentTracker.updateStartLocation();
-                                        tryDispatchOnBackStarted(
-                                                mActiveCallback,
-                                                mCurrentTracker.createStartEvent(apps[0]));
+                                        BackMotionEvent startEvent =
+                                                mCurrentTracker.createStartEvent(apps[0]);
+                                        // {@code mActiveCallback} is the callback from
+                                        // the BackAnimationRunners and not a real app-side
+                                        // callback. We also dispatch to the app-side callback
+                                        // (which should be a system callback with PRIORITY_SYSTEM)
+                                        // to keep consistent with app registered callbacks.
+                                        dispatchOnBackStarted(mActiveCallback, startEvent);
+                                        tryDispatchAppOnBackStarted(
+                                                mBackNavigationInfo.getOnBackInvokedCallback(),
+                                                startEvent);
                                     }
 
                                     // Dispatch the first progress after animation start for
@@ -965,7 +988,9 @@
                         mShellExecutor.execute(
                                 () -> {
                                     if (!mShellBackAnimationRegistry.cancel(
-                                            mBackNavigationInfo.getType())) {
+                                            mBackNavigationInfo != null
+                                                    ? mBackNavigationInfo.getType()
+                                                    : mPreviousNavigationType)) {
                                         return;
                                     }
                                     if (!mBackGestureStarted) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 80fc3a8..ac2a1c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -136,6 +136,9 @@
         mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
         mStartTaskRect.offsetTo(0, 0);
 
+        // inset bottom in case of pinned taskbar being present
+        mStartTaskRect.inset(0, 0, 0, mClosingTarget.contentInsets.bottom);
+
         // Draw background.
         mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
                 BACKGROUNDCOLOR, mTransaction);
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 470a825..ca28312 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
@@ -1177,14 +1177,17 @@
             if (mStackAnimationController.isStackOnLeftSide()) {
                 int availableRectOffsetX =
                         mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left;
-                animate().translationX(-(mBubbleSize + availableRectOffsetX)).start();
+                mBubbleContainer
+                        .animate()
+                        .translationX(-(mBubbleSize + availableRectOffsetX))
+                        .start();
             } else {
                 int availableRectOffsetX =
                         mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right;
-                animate().translationX(mBubbleSize - availableRectOffsetX).start();
+                mBubbleContainer.animate().translationX(mBubbleSize - availableRectOffsetX).start();
             }
         } else {
-            animate().translationX(0).start();
+            mBubbleContainer.animate().translationX(0).start();
         }
     };
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index d073f1d..66c0c96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -70,7 +70,7 @@
     private @Nullable Supplier<Rect> mLayerBoundsSupplier;
     private @Nullable Listener mListener;
 
-    private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext());
+    private BubbleBarHandleView mHandleView;
     private @Nullable TaskView mTaskView;
     private @Nullable BubbleOverflowContainerView mOverflowView;
 
@@ -111,7 +111,7 @@
         setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
         mCaptionHeight = context.getResources().getDimensionPixelSize(
                 R.dimen.bubble_bar_expanded_view_caption_height);
-        addView(mHandleView);
+        mHandleView = findViewById(R.id.bubble_bar_handle_view);
         applyThemeAttrs();
         setClipToOutline(true);
         setOutlineProvider(new ViewOutlineProvider() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
index 0693543..662f325 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.common.split;
 
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
@@ -24,19 +24,27 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
 
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.PendingIntent;
-import android.content.Context;
+import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
 
+import java.util.Arrays;
+import java.util.List;
+
 /** Helper utility class for split screen components to use. */
 public class SplitScreenUtils {
     /** Reverse the split position. */
@@ -135,4 +143,28 @@
             return isLandscape;
         }
     }
+
+    /** Returns the component from a PendingIntent */
+    @Nullable
+    public static ComponentName getComponent(@Nullable PendingIntent pendingIntent) {
+        if (pendingIntent == null || pendingIntent.getIntent() == null) {
+            return null;
+        }
+        return pendingIntent.getIntent().getComponent();
+    }
+
+    /** Returns the component from a shortcut */
+    @Nullable
+    public static ComponentName getShortcutComponent(@NonNull String packageName, String shortcutId,
+            @NonNull UserHandle user, @NonNull LauncherApps launcherApps) {
+        LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery();
+        query.setPackage(packageName);
+        query.setShortcutIds(Arrays.asList(shortcutId));
+        query.setQueryFlags(FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED);
+        List<ShortcutInfo> shortcuts = launcherApps.getShortcuts(query, user);
+        ShortcutInfo info = shortcuts != null && shortcuts.size() > 0
+                ? shortcuts.get(0)
+                : null;
+        return info != null ? info.getActivity() : null;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 0ef047f..36f06e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -210,7 +210,6 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
-            RecentsTransitionHandler recentsTransitionHandler,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
         if (DesktopModeStatus.isEnabled()) {
             return new DesktopModeWindowDecorViewModel(
@@ -226,7 +225,6 @@
                     syncQueue,
                     transitions,
                     desktopTasksController,
-                    recentsTransitionHandler,
                     rootTaskDisplayAreaOrganizer);
         }
         return new CaptionWindowDecorViewModel(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 4a1bcaa..b1c43c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -320,9 +320,8 @@
     }
 
     /** Move a task with given `taskId` to fullscreen */
-    fun moveToFullscreen(taskId: Int, windowDecor: DesktopModeWindowDecoration) {
+    fun moveToFullscreen(taskId: Int) {
         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
-            windowDecor.incrementRelayoutBlock()
             moveToFullscreenWithAnimation(task, task.positionInParent)
         }
     }
@@ -906,20 +905,17 @@
      * @param position position of surface when drag ends.
      * @param inputCoordinate the coordinates of the motion event
      * @param taskBounds the updated bounds of the task being dragged.
-     * @param windowDecor the window decoration for the task being dragged
      */
     fun onDragPositioningEnd(
         taskInfo: RunningTaskInfo,
         position: Point,
         inputCoordinate: PointF,
-        taskBounds: Rect,
-        windowDecor: DesktopModeWindowDecoration
+        taskBounds: Rect
     ) {
         if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
             return
         }
         if (taskBounds.top <= transitionAreaHeight) {
-            windowDecor.incrementRelayoutBlock()
             moveToFullscreenWithAnimation(taskInfo, position)
         }
         if (inputCoordinate.x <= transitionAreaWidth) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index 9debb25..0218493 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -54,8 +54,6 @@
         taskId: Int,
         windowDecoration: DesktopModeWindowDecoration
     ) {
-        // Pause relayout until the transition animation finishes.
-        windowDecoration.incrementRelayoutBlock()
         transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this)
         taskToDecorationMap.put(taskId, windowDecoration)
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 6b6a7bc..ffcc526 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -112,7 +112,6 @@
                     onChangeTransitionReady(change, startT, finishT);
                     break;
             }
-            mWindowDecorViewModel.onTransitionReady(transition, info, change);
         }
         mTransitionToTaskInfo.put(transition, taskInfoList);
     }
@@ -153,8 +152,6 @@
 
     @Override
     public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
-        mWindowDecorViewModel.onTransitionMerged(merged, playing);
-
         final List<ActivityManager.RunningTaskInfo> infoOfMerged =
                 mTransitionToTaskInfo.get(merged);
         if (infoOfMerged == null) {
@@ -178,7 +175,6 @@
         final List<ActivityManager.RunningTaskInfo> taskInfo =
                 mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList());
         mTransitionToTaskInfo.remove(transition);
-        mWindowDecorViewModel.onTransitionFinished(transition);
         for (int i = 0; i < taskInfo.size(); ++i) {
             mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i));
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index 3906599..8b3de62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -52,9 +52,10 @@
      * @param componentName ComponentName represents the Activity
      * @param destinationBounds the destination bounds the PiP window lands into
      * @param overlay an optional overlay to fade out after entering PiP
+     * @param appBounds the bounds used to set the buffer size of the optional content overlay
      */
     oneway void stopSwipePipToHome(int taskId, in ComponentName componentName,
-            in Rect destinationBounds, in SurfaceControl overlay) = 2;
+            in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds) = 2;
 
     /**
      * Notifies the swiping Activity to PiP onto home transition is aborted
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 3635165..a9a3f78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -334,6 +334,16 @@
     @Nullable
     SurfaceControl mPipOverlay;
 
+    /**
+     * The app bounds used for the buffer size of the
+     * {@link com.android.wm.shell.pip.PipContentOverlay.PipAppIconOverlay}.
+     *
+     * Note that this is empty if the overlay is removed or if it's some other type of overlay
+     * defined in {@link PipContentOverlay}.
+     */
+    @NonNull
+    final Rect mAppBounds = new Rect();
+
     public PipTaskOrganizer(Context context,
             @NonNull SyncTransactionQueue syncTransactionQueue,
             @NonNull PipTransitionState pipTransitionState,
@@ -464,15 +474,15 @@
      * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards.
      */
     public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
-            SurfaceControl overlay) {
+            SurfaceControl overlay, Rect appBounds) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "stopSwipePipToHome: %s, state=%s", componentName, mPipTransitionState);
+                "stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState);
         // do nothing if there is no startSwipePipToHome being called before
         if (!mPipTransitionState.getInSwipePipToHomeTransition()) {
             return;
         }
         mPipBoundsState.setBounds(destinationBounds);
-        mPipOverlay = overlay;
+        setContentOverlay(overlay, appBounds);
         if (ENABLE_SHELL_TRANSITIONS && overlay != null) {
             // With Shell transition, the overlay was attached to the remote transition leash, which
             // will be removed when the current transition is finished, so we need to reparent it
@@ -1888,7 +1898,7 @@
                         "%s: trying to remove overlay (%s) which is not local reference (%s)",
                         TAG, surface, mPipOverlay);
             }
-            mPipOverlay = null;
+            clearContentOverlay();
         }
         if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
             // Avoid double removal, which is fatal.
@@ -1905,6 +1915,20 @@
         if (callback != null) callback.run();
     }
 
+    void clearContentOverlay() {
+        mPipOverlay = null;
+        mAppBounds.setEmpty();
+    }
+
+    void setContentOverlay(@Nullable SurfaceControl leash, @NonNull Rect appBounds) {
+        mPipOverlay = leash;
+        if (mPipOverlay != null) {
+            mAppBounds.set(appBounds);
+        } else {
+            mAppBounds.setEmpty();
+        }
+    }
+
     private void resetShadowRadius() {
         if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
             // mLeash is undefined when in PipTransitionState.UNDEFINED
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index f5f15d8..8e375a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -141,8 +141,6 @@
     /** Whether the PIP window has fade out for fixed rotation. */
     private boolean mHasFadeOut;
 
-    private Rect mInitBounds = new Rect();
-
     /** Used for setting transform to a transaction from animator. */
     private final PipAnimationController.PipTransactionHandler mTransactionConsumer =
             new PipAnimationController.PipTransactionHandler() {
@@ -465,12 +463,13 @@
                     mSurfaceTransactionHelper.crop(tx, leash, destinationBounds)
                             .resetScale(tx, leash, destinationBounds)
                             .round(tx, leash, true /* applyCornerRadius */);
-                    if (mPipOrganizer.mPipOverlay != null && !mInitBounds.isEmpty()) {
+                    final Rect appBounds = mPipOrganizer.mAppBounds;
+                    if (mPipOrganizer.mPipOverlay != null && !appBounds.isEmpty()) {
                         // Resetting the scale for pinned task while re-adjusting its crop,
                         // also scales the overlay. So we need to update the overlay leash too.
                         Rect overlayBounds = new Rect(destinationBounds);
                         final int overlaySize = PipContentOverlay.PipAppIconOverlay
-                                .getOverlaySize(mInitBounds, destinationBounds);
+                                .getOverlaySize(appBounds, destinationBounds);
 
                         overlayBounds.offsetTo(
                                 (destinationBounds.width() - overlaySize) / 2,
@@ -479,7 +478,6 @@
                                 mPipOrganizer.mPipOverlay, overlayBounds);
                     }
                 }
-                mInitBounds.setEmpty();
                 wct.setBoundsChangeTransaction(taskInfo.token, tx);
             }
             final int displayRotation = taskInfo.getConfiguration().windowConfiguration
@@ -617,7 +615,7 @@
         // if overlay is present remove it immediately, as exit transition came before it faded out
         if (mPipOrganizer.mPipOverlay != null) {
             startTransaction.remove(mPipOrganizer.mPipOverlay);
-            clearPipOverlay();
+            mPipOrganizer.clearContentOverlay();
         }
         if (pipChange == null) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -951,9 +949,6 @@
         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
         final Rect currentBounds = pipChange.getStartAbsBounds();
 
-        // Cache the start bounds for overlay manipulations as a part of finishCallback.
-        mInitBounds.set(currentBounds);
-
         int rotationDelta = deltaRotation(startRotation, endRotation);
         Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
                 taskInfo.pictureInPictureParams, currentBounds, destinationBounds);
@@ -992,10 +987,12 @@
                     0 /* startingAngle */, rotationDelta);
             if (sourceHintRect == null) {
                 // We use content overlay when there is no source rect hint to enter PiP use bounds
-                // animation.
+                // animation. We also temporarily disallow app icon overlay and use color overlay
+                // instead when in fixed rotation enter PiP in button nav with no sourceRectHint.
+                // TODO(b/319286295): Fix App Icon Overlay animation in fixed rotation in btn nav.
                 // TODO(b/272819817): cleanup the null-check and extra logging.
                 final boolean hasTopActivityInfo = taskInfo.topActivityInfo != null;
-                if (hasTopActivityInfo) {
+                if (hasTopActivityInfo && mFixedRotationState != FIXED_ROTATION_TRANSITION) {
                     animator.setAppIconContentOverlay(
                             mContext, currentBounds, destinationBounds, taskInfo.topActivityInfo,
                             mPipBoundsState.getLauncherState().getAppIconSizePx());
@@ -1022,7 +1019,7 @@
         } else {
             throw new RuntimeException("Unrecognized animation type: " + enterAnimationType);
         }
-        mPipOrganizer.mPipOverlay = animator.getContentOverlayLeash();
+        mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds);
         animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(mEnterExitAnimationDuration);
@@ -1073,10 +1070,6 @@
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                     "%s: SwipePipToHome should not use fixed rotation %d", TAG, mEndFixedRotation);
         }
-        Rect appBounds = pipTaskInfo.configuration.windowConfiguration.getAppBounds();
-        if (mFixedRotationState == FIXED_ROTATION_CALLBACK && appBounds != null) {
-            mInitBounds.set(appBounds);
-        }
         final SurfaceControl swipePipToHomeOverlay = mPipOrganizer.mPipOverlay;
         if (swipePipToHomeOverlay != null) {
             // Launcher fade in the overlay on top of the fullscreen Task. It is possible we
@@ -1106,7 +1099,7 @@
         sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
         if (swipePipToHomeOverlay != null) {
             mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay,
-                    this::clearPipOverlay /* callback */, false /* withStartDelay */);
+                    null /* callback */, false /* withStartDelay */);
         }
         mPipTransitionState.setInSwipePipToHomeTransition(false);
     }
@@ -1250,10 +1243,6 @@
         mPipMenuController.updateMenuBounds(destinationBounds);
     }
 
-    private void clearPipOverlay() {
-        mPipOrganizer.mPipOverlay = null;
-    }
-
     @Override
     public void dump(PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 63f20fd..238e6b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -982,8 +982,9 @@
     }
 
     private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
-            SurfaceControl overlay) {
-        mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay);
+            SurfaceControl overlay, Rect appBounds) {
+        mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay,
+                appBounds);
     }
 
     private void abortSwipePipToHome(int taskId, ComponentName componentName) {
@@ -1280,13 +1281,13 @@
 
         @Override
         public void stopSwipePipToHome(int taskId, ComponentName componentName,
-                Rect destinationBounds, SurfaceControl overlay) {
+                Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
             if (overlay != null) {
                 overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
             }
             executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
                     (controller) -> controller.stopSwipePipToHome(
-                            taskId, componentName, destinationBounds, overlay));
+                            taskId, componentName, destinationBounds, overlay, appBounds));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 7b57097..880d952 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -23,12 +23,15 @@
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.common.split.SplitScreenUtils.getComponent;
+import static com.android.wm.shell.common.split.SplitScreenUtils.getShortcutComponent;
 import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit;
 import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
 import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage;
@@ -47,6 +50,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -171,6 +176,8 @@
     private final ShellTaskOrganizer mTaskOrganizer;
     private final SyncTransactionQueue mSyncQueue;
     private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final LauncherApps mLauncherApps;
     private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
     private final ShellExecutor mMainExecutor;
     private final SplitScreenImpl mImpl = new SplitScreenImpl();
@@ -186,7 +193,8 @@
     private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
     private final Optional<DesktopTasksController> mDesktopTasksController;
     private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
-    private final String[] mAppsSupportMultiInstances;
+    // A static allow list of apps which support multi-instance
+    private final String[] mAppsSupportingMultiInstance;
 
     @VisibleForTesting
     StageCoordinator mStageCoordinator;
@@ -220,6 +228,8 @@
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mContext = context;
+        mPackageManager = context.getPackageManager();
+        mLauncherApps = context.getSystemService(LauncherApps.class);
         mRootTDAOrganizer = rootTDAOrganizer;
         mMainExecutor = mainExecutor;
         mDisplayController = displayController;
@@ -242,7 +252,7 @@
 
         // TODO(255224696): Remove the config once having a way for client apps to opt-in
         //                  multi-instances split.
-        mAppsSupportMultiInstances = mContext.getResources()
+        mAppsSupportingMultiInstance = mContext.getResources()
                 .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
     }
 
@@ -266,12 +276,15 @@
             WindowDecorViewModel windowDecorViewModel,
             DesktopTasksController desktopTasksController,
             ShellExecutor mainExecutor,
-            StageCoordinator stageCoordinator) {
+            StageCoordinator stageCoordinator,
+            String[] appsSupportingMultiInstance) {
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mContext = context;
+        mPackageManager = context.getPackageManager();
+        mLauncherApps = context.getSystemService(LauncherApps.class);
         mRootTDAOrganizer = rootTDAOrganizer;
         mMainExecutor = mainExecutor;
         mDisplayController = displayController;
@@ -288,8 +301,7 @@
         mStageCoordinator = stageCoordinator;
         mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
         shellInit.addInitCallback(this::onInit, this);
-        mAppsSupportMultiInstances = mContext.getResources()
-                .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
+        mAppsSupportingMultiInstance = appsSupportingMultiInstance;
     }
 
     public SplitScreen asSplitScreen() {
@@ -588,7 +600,8 @@
 
         if (samePackage(packageName, getPackageName(reverseSplitPosition(position)),
                 user.getIdentifier(), getUserId(reverseSplitPosition(position)))) {
-            if (supportMultiInstancesSplit(packageName)) {
+            if (supportsMultiInstanceSplit(getShortcutComponent(packageName, shortcutId, user,
+                    mLauncherApps))) {
                 activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else if (isSplitScreenVisible()) {
@@ -609,7 +622,7 @@
                 activityOptions.toBundle(), user);
     }
 
-    void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
+    void startShortcutAndTaskWithLegacyTransition(@NonNull ShortcutInfo shortcutInfo,
             @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
             RemoteAnimationAdapter adapter, InstanceId instanceId) {
@@ -621,7 +634,7 @@
         final int userId1 = shortcutInfo.getUserId();
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(shortcutInfo.getPackage())) {
+            if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) {
                 activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
@@ -640,7 +653,7 @@
                 instanceId);
     }
 
-    void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
+    void startShortcutAndTask(@NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options1,
             int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
             @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition,
             InstanceId instanceId) {
@@ -653,7 +666,7 @@
         final int userId1 = shortcutInfo.getUserId();
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) {
                 activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
@@ -692,7 +705,7 @@
         final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent))) {
                 fillInIntent = new Intent();
                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -722,7 +735,7 @@
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         boolean setSecondIntentMultipleTask = false;
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent))) {
                 setSecondIntentMultipleTask = true;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
@@ -757,7 +770,7 @@
         final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
         final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) {
                 fillInIntent1 = new Intent();
                 fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 fillInIntent2 = new Intent();
@@ -794,7 +807,7 @@
                 ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic();
         boolean setSecondIntentMultipleTask = false;
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) {
                 fillInIntent1 = new Intent();
                 fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 setSecondIntentMultipleTask = true;
@@ -856,7 +869,7 @@
             return;
         }
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(intent))) {
                 // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
                 // the split and there is no reusable background task.
                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
@@ -915,16 +928,63 @@
         return taskInfo != null ? taskInfo.userId : -1;
     }
 
+    /**
+     * Returns whether a specific component desires to be launched in multiple instances for
+     * split screen.
+     */
     @VisibleForTesting
-    boolean supportMultiInstancesSplit(String packageName) {
-        if (packageName != null) {
-            for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
-                if (mAppsSupportMultiInstances[i].equals(packageName)) {
-                    return true;
-                }
+    boolean supportsMultiInstanceSplit(@Nullable ComponentName componentName) {
+        if (componentName == null || componentName.getPackageName() == null) {
+            // TODO(b/262864589): Handle empty component case
+            return false;
+        }
+
+        // Check the pre-defined allow list
+        final String packageName = componentName.getPackageName();
+        for (int i = 0; i < mAppsSupportingMultiInstance.length; i++) {
+            if (mAppsSupportingMultiInstance[i].equals(packageName)) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "application=%s in allowlist supports multi-instance", packageName);
+                return true;
             }
         }
 
+        // Check the activity property first
+        try {
+            final PackageManager.Property activityProp = mPackageManager.getProperty(
+                    PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName);
+            // If the above call doesn't throw a NameNotFoundException, then the activity property
+            // should override the application property value
+            if (activityProp.isBoolean()) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "activity=%s supports multi-instance", componentName);
+                return activityProp.getBoolean();
+            } else {
+                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Warning: property=%s for activity=%s has non-bool type=%d",
+                        PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName,
+                        activityProp.getType());
+            }
+        } catch (PackageManager.NameNotFoundException nnfe) {
+            // Not specified in the activity, fall through
+        }
+
+        // Check the application property otherwise
+        try {
+            final PackageManager.Property appProp = mPackageManager.getProperty(
+                    PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName);
+            if (appProp.isBoolean()) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "application=%s supports multi-instance", packageName);
+                return appProp.getBoolean();
+            } else {
+                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Warning: property=%s for application=%s has non-bool type=%d",
+                        PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, appProp.getType());
+            }
+        } catch (PackageManager.NameNotFoundException nnfe) {
+            // Not specified in either application or activity
+        }
         return false;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 7dec12a..5de8a9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -388,6 +388,7 @@
 
     IBinder startResizeTransition(WindowContainerTransaction wct,
             Transitions.TransitionHandler handler,
+            @Nullable TransitionConsumedCallback consumedCallback,
             @Nullable TransitionFinishedCallback finishCallback) {
         if (mPendingResize != null) {
             mPendingResize.cancel(null);
@@ -396,13 +397,14 @@
         }
 
         IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler);
-        setResizeTransition(transition, finishCallback);
+        setResizeTransition(transition, consumedCallback, finishCallback);
         return transition;
     }
 
     void setResizeTransition(@NonNull IBinder transition,
+            @Nullable TransitionConsumedCallback consumedCallback,
             @Nullable TransitionFinishedCallback finishCallback) {
-        mPendingResize = new TransitSession(transition, null /* consumedCb */, finishCallback);
+        mPendingResize = new TransitSession(transition, consumedCallback, finishCallback);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  splitTransition "
                 + " deduced Resize split screen");
     }
@@ -479,18 +481,20 @@
     private void startFadeAnimation(@NonNull SurfaceControl leash, boolean show) {
         final float end = show ? 1.f : 0.f;
         final float start = 1.f - end;
-        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
         final ValueAnimator va = ValueAnimator.ofFloat(start, end);
         va.setDuration(FADE_DURATION);
         va.setInterpolator(show ? ALPHA_IN : ALPHA_OUT);
         va.addUpdateListener(animation -> {
             float fraction = animation.getAnimatedFraction();
+            final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
             transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
             transaction.apply();
+            mTransactionPool.release(transaction);
         });
         va.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
+                final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
                 transaction.setAlpha(leash, end);
                 transaction.apply();
                 mTransactionPool.release(transaction);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 96e57e7..0781a9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2236,8 +2236,11 @@
         sendOnBoundsChanged();
         if (ENABLE_SHELL_TRANSITIONS) {
             mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart");
-            mSplitTransitions.startResizeTransition(wct, this, (finishWct, t) ->
-                    mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish"));
+            mSplitTransitions.startResizeTransition(wct, this, (aborted) -> {
+                mSplitLayout.setDividerInteractive(true, false, "onSplitResizeConsumed");
+            }, (finishWct, t) -> {
+                mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish");
+            });
         } else {
             // Only need screenshot for legacy case because shell transition should screenshot
             // itself during transition.
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 f58aeac..a666e20 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
@@ -73,6 +73,7 @@
 import com.android.internal.graphics.palette.Palette;
 import com.android.internal.graphics.palette.Quantizer;
 import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
+import com.android.internal.policy.PhoneWindow;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.IconProvider;
@@ -245,16 +246,19 @@
         } else {
             windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
         }
-        params.layoutInDisplayCutoutMode = a.getInt(
-                R.styleable.Window_windowLayoutInDisplayCutoutMode,
-                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS);
-        params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
-        a.recycle();
 
         final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
         final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
                 ? windowInfo.targetActivityInfo
                 : taskInfo.topActivityInfo;
+        params.layoutInDisplayCutoutMode = a.getInt(
+                R.styleable.Window_windowLayoutInDisplayCutoutMode,
+                PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */)
+                        ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                        : params.layoutInDisplayCutoutMode);
+        params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
+        a.recycle();
+
         final int displayId = taskInfo.displayId;
         // Assumes it's safe to show starting windows of launched apps while
         // the keyguard is being hidden. This is okay because starting windows never show
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index 0eb7c2d..a7843e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -293,11 +293,7 @@
     private class ShellInterfaceImpl implements ShellInterface {
         @Override
         public void onInit() {
-            try {
-                mMainExecutor.executeBlocking(() -> ShellController.this.handleInit());
-            } catch (InterruptedException e) {
-                throw new RuntimeException("Failed to initialize the Shell in 2s", e);
-            }
+            mMainExecutor.execute(ShellController.this::handleInit);
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index 34c015f..84f21f6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -330,7 +330,7 @@
                     continue;
                 }
                 if (isHide) {
-                    if (pending.mType == TRANSIT_TO_BACK) {
+                    if (pending != null && pending.mType == TRANSIT_TO_BACK) {
                         // TO_BACK is only used when setting the task view visibility immediately,
                         // so in that case we can also hide the surface immediately
                         startTransaction.hide(chg.getLeash());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 9f20f49..bf783e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -21,9 +21,9 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
@@ -84,7 +84,7 @@
     private UnfoldTransitionHandler mUnfoldHandler;
     private ActivityEmbeddingController mActivityEmbeddingController;
 
-    private class MixedTransition {
+    private static class MixedTransition {
         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
 
         /** Both the display and split-state (enter/exit) is changing */
@@ -124,6 +124,16 @@
         int mAnimType = ANIM_TYPE_DEFAULT;
         final IBinder mTransition;
 
+        private final Transitions mPlayer;
+        private final DefaultMixedHandler mMixedHandler;
+        private final PipTransitionController mPipHandler;
+        private final RecentsTransitionHandler mRecentsHandler;
+        private final StageCoordinator mSplitHandler;
+        private final KeyguardTransitionHandler mKeyguardHandler;
+        private final DesktopTasksController mDesktopTasksController;
+        private final UnfoldTransitionHandler mUnfoldHandler;
+        private final ActivityEmbeddingController mActivityEmbeddingController;
+
         Transitions.TransitionHandler mLeftoversHandler = null;
         TransitionInfo mInfo = null;
         WindowContainerTransaction mFinishWCT = null;
@@ -144,9 +154,408 @@
          */
         int mInFlightSubAnimations = 0;
 
-        MixedTransition(int type, IBinder transition) {
+        MixedTransition(int type, IBinder transition, Transitions player,
+                DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+                RecentsTransitionHandler recentsHandler, StageCoordinator splitHandler,
+                KeyguardTransitionHandler keyguardHandler,
+                DesktopTasksController desktopTasksController,
+                UnfoldTransitionHandler unfoldHandler,
+                ActivityEmbeddingController activityEmbeddingController) {
             mType = type;
             mTransition = transition;
+            mPlayer = player;
+            mMixedHandler = mixedHandler;
+            mPipHandler = pipHandler;
+            mRecentsHandler = recentsHandler;
+            mSplitHandler = splitHandler;
+            mKeyguardHandler = keyguardHandler;
+            mDesktopTasksController = desktopTasksController;
+            mUnfoldHandler = unfoldHandler;
+            mActivityEmbeddingController = activityEmbeddingController;
+
+            switch (type) {
+                case TYPE_RECENTS_DURING_DESKTOP:
+                case TYPE_RECENTS_DURING_KEYGUARD:
+                case TYPE_RECENTS_DURING_SPLIT:
+                    mLeftoversHandler = mRecentsHandler;
+                    break;
+                case TYPE_UNFOLD:
+                    mLeftoversHandler = mUnfoldHandler;
+                    break;
+                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                case TYPE_ENTER_PIP_FROM_SPLIT:
+                case TYPE_KEYGUARD:
+                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                default:
+                    break;
+            }
+        }
+
+        boolean startAnimation(
+                @NonNull IBinder transition, @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            switch (mType) {
+                case TYPE_ENTER_PIP_FROM_SPLIT:
+                    return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+                            finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                    return animateEnterPipFromActivityEmbedding(
+                            info, startTransaction, finishTransaction, finishCallback);
+                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+                    return false;
+                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                    final boolean handledToPip = animateOpenIntentWithRemoteAndPip(
+                            info, startTransaction, finishTransaction, finishCallback);
+                    // Consume the transition on remote handler if the leftover handler already
+                    // handle this transition. And if it cannot, the transition will be handled by
+                    // remote handler, so don't consume here.
+                    // Need to check leftOverHandler as it may change in
+                    // #animateOpenIntentWithRemoteAndPip
+                    if (handledToPip && mHasRequestToRemote
+                            && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+                        mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
+                                transition, false, null);
+                    }
+                    return handledToPip;
+                case TYPE_RECENTS_DURING_SPLIT:
+                    for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                        final TransitionInfo.Change change = info.getChanges().get(i);
+                        // Pip auto-entering info might be appended to recent transition like
+                        // pressing home-key in 3-button navigation. This offers split handler the
+                        // opportunity to handle split to pip animation.
+                        if (mPipHandler.isEnteringPip(change, info.getType())
+                                && mSplitHandler.getSplitItemPosition(change.getLastParent())
+                                != SPLIT_POSITION_UNDEFINED) {
+                            return animateEnterPipFromSplit(
+                                    this, info, startTransaction, finishTransaction, finishCallback,
+                                    mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+                        }
+                    }
+
+                    return animateRecentsDuringSplit(
+                            info, startTransaction, finishTransaction, finishCallback);
+                case TYPE_KEYGUARD:
+                    return animateKeyguard(this, info, startTransaction, finishTransaction,
+                            finishCallback, mKeyguardHandler, mPipHandler);
+                case TYPE_RECENTS_DURING_KEYGUARD:
+                    return animateRecentsDuringKeyguard(
+                            info, startTransaction, finishTransaction, finishCallback);
+                case TYPE_RECENTS_DURING_DESKTOP:
+                    return animateRecentsDuringDesktop(
+                            info, startTransaction, finishTransaction, finishCallback);
+                case TYPE_UNFOLD:
+                    return animateUnfold(
+                            info, startTransaction, finishTransaction, finishCallback);
+                default:
+                    throw new IllegalStateException(
+                            "Starting mixed animation without a known mixed type? " + mType);
+            }
+        }
+
+        private boolean animateEnterPipFromActivityEmbedding(
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+                    + "entering PIP from an Activity Embedding window");
+            // Split into two transitions (wct)
+            TransitionInfo.Change pipChange = null;
+            final TransitionInfo everythingElse =
+                    subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                TransitionInfo.Change change = info.getChanges().get(i);
+                if (mPipHandler.isEnteringPip(change, info.getType())) {
+                    if (pipChange != null) {
+                        throw new IllegalStateException("More than 1 pip-entering changes in one"
+                                + " transition? " + info);
+                    }
+                    pipChange = change;
+                    // going backwards, so remove-by-index is fine.
+                    everythingElse.getChanges().remove(i);
+                }
+            }
+
+            final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+                --mInFlightSubAnimations;
+                joinFinishArgs(wct);
+                if (mInFlightSubAnimations > 0) return;
+                finishCallback.onTransitionFinished(mFinishWCT);
+            };
+
+            if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
+                // Fallback to dispatching to other handlers.
+                return false;
+            }
+
+            // PIP window should always be on the highest Z order.
+            if (pipChange != null) {
+                mInFlightSubAnimations = 2;
+                mPipHandler.startEnterAnimation(
+                        pipChange,
+                        startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
+                        finishTransaction,
+                        finishCB);
+            } else {
+                mInFlightSubAnimations = 1;
+            }
+
+            mActivityEmbeddingController.startAnimation(mTransition, everythingElse,
+                    startTransaction, finishTransaction, finishCB);
+            return true;
+        }
+
+        private boolean animateOpenIntentWithRemoteAndPip(
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            TransitionInfo.Change pipChange = null;
+            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                TransitionInfo.Change change = info.getChanges().get(i);
+                if (mPipHandler.isEnteringPip(change, info.getType())) {
+                    if (pipChange != null) {
+                        throw new IllegalStateException("More than 1 pip-entering changes in one"
+                                + " transition? " + info);
+                    }
+                    pipChange = change;
+                    info.getChanges().remove(i);
+                }
+            }
+            Transitions.TransitionFinishCallback finishCB = (wct) -> {
+                --mInFlightSubAnimations;
+                joinFinishArgs(wct);
+                if (mInFlightSubAnimations > 0) return;
+                finishCallback.onTransitionFinished(mFinishWCT);
+            };
+            if (pipChange == null) {
+                if (mLeftoversHandler != null) {
+                    mInFlightSubAnimations = 1;
+                    if (mLeftoversHandler.startAnimation(
+                            mTransition, info, startTransaction, finishTransaction, finishCB)) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
+                    + " animation because remote-animation likely doesn't support it");
+            // Split the transition into 2 parts: the pip part and the rest.
+            mInFlightSubAnimations = 2;
+            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+            // we need a separate one to send over to launcher.
+            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+
+            mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
+
+            // Dispatch the rest of the transition normally.
+            if (mLeftoversHandler != null
+                    && mLeftoversHandler.startAnimation(
+                            mTransition, info, startTransaction, finishTransaction, finishCB)) {
+                return true;
+            }
+            mLeftoversHandler = mPlayer.dispatchTransition(mTransition, info,
+                    startTransaction, finishTransaction, finishCB, mMixedHandler);
+            return true;
+        }
+
+        private boolean animateRecentsDuringSplit(
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            // Split-screen is only interested in the recents transition finishing (and merging), so
+            // just wrap finish and start recents animation directly.
+            Transitions.TransitionFinishCallback finishCB = (wct) -> {
+                mInFlightSubAnimations = 0;
+                // If pair-to-pair switching, the post-recents clean-up isn't needed.
+                wct = wct != null ? wct : new WindowContainerTransaction();
+                if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
+                    mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+                } else {
+                    // notify pair-to-pair recents animation finish
+                    mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
+                }
+                mSplitHandler.onTransitionAnimationComplete();
+                finishCallback.onTransitionFinished(wct);
+            };
+            mInFlightSubAnimations = 1;
+            mSplitHandler.onRecentsInSplitAnimationStart(info);
+            final boolean handled = mLeftoversHandler.startAnimation(mTransition, info,
+                    startTransaction, finishTransaction, finishCB);
+            if (!handled) {
+                mSplitHandler.onRecentsInSplitAnimationCanceled();
+            }
+            return handled;
+        }
+
+        private boolean animateRecentsDuringKeyguard(
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            if (mInfo == null) {
+                mInfo = info;
+                mFinishT = finishTransaction;
+                mFinishCB = finishCallback;
+            }
+            return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
+        }
+
+        private boolean animateRecentsDuringDesktop(
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            Transitions.TransitionFinishCallback finishCB = wct -> {
+                mInFlightSubAnimations--;
+                if (mInFlightSubAnimations == 0) {
+                    finishCallback.onTransitionFinished(wct);
+                }
+            };
+
+            mInFlightSubAnimations++;
+            boolean consumed = mRecentsHandler.startAnimation(
+                    mTransition, info, startTransaction, finishTransaction, finishCB);
+            if (!consumed) {
+                mInFlightSubAnimations--;
+                return false;
+            }
+            if (mDesktopTasksController != null) {
+                mDesktopTasksController.syncSurfaceState(info, finishTransaction);
+                return true;
+            }
+
+            return false;
+        }
+
+        private boolean animateUnfold(
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+                mInFlightSubAnimations--;
+                if (mInFlightSubAnimations > 0) return;
+                finishCallback.onTransitionFinished(wct);
+            };
+            mInFlightSubAnimations = 1;
+            // Sync pip state.
+            if (mPipHandler != null) {
+                mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+            }
+            if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
+                mSplitHandler.updateSurfaces(startTransaction);
+            }
+            return mUnfoldHandler.startAnimation(
+                    mTransition, info, startTransaction, finishTransaction, finishCB);
+        }
+
+        void mergeAnimation(
+                @NonNull IBinder transition, @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            switch (mType) {
+                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+                    // queue since no actual animation.
+                    break;
+                case TYPE_ENTER_PIP_FROM_SPLIT:
+                    if (mAnimType == ANIM_TYPE_GOING_HOME) {
+                        boolean ended = mSplitHandler.end();
+                        // If split couldn't end (because it is remote), then don't end everything
+                        // else since we have to play out the animation anyways.
+                        if (!ended) return;
+                        mPipHandler.end();
+                        if (mLeftoversHandler != null) {
+                            mLeftoversHandler.mergeAnimation(
+                                    transition, info, t, mergeTarget, finishCallback);
+                        }
+                    } else {
+                        mPipHandler.end();
+                    }
+                    break;
+                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                    mPipHandler.end();
+                    mActivityEmbeddingController.mergeAnimation(transition, info, t, mergeTarget,
+                            finishCallback);
+                    break;
+                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                    mPipHandler.end();
+                    if (mLeftoversHandler != null) {
+                        mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+                                finishCallback);
+                    }
+                    break;
+                case TYPE_RECENTS_DURING_SPLIT:
+                    if (mSplitHandler.isPendingEnter(transition)) {
+                        // Recents -> enter-split means that we are switching from one pair to
+                        // another pair.
+                        mAnimType = ANIM_TYPE_PAIR_TO_PAIR;
+                    }
+                    mLeftoversHandler.mergeAnimation(
+                            transition, info, t, mergeTarget, finishCallback);
+                    break;
+                case TYPE_KEYGUARD:
+                    mKeyguardHandler.mergeAnimation(
+                            transition, info, t, mergeTarget, finishCallback);
+                    break;
+                case TYPE_RECENTS_DURING_KEYGUARD:
+                    if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
+                        DefaultMixedHandler.handoverTransitionLeashes(mInfo, info, t, mFinishT);
+                        if (animateKeyguard(this, info, t, mFinishT, mFinishCB, mKeyguardHandler,
+                                mPipHandler)) {
+                            finishCallback.onTransitionFinished(null);
+                        }
+                    }
+                    mLeftoversHandler.mergeAnimation(
+                            transition, info, t, mergeTarget, finishCallback);
+                    break;
+                case TYPE_RECENTS_DURING_DESKTOP:
+                    mLeftoversHandler.mergeAnimation(
+                            transition, info, t, mergeTarget, finishCallback);
+                    break;
+                case TYPE_UNFOLD:
+                    mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "Playing a mixed transition with unknown type? " + mType);
+            }
+        }
+
+        void onTransitionConsumed(
+                @NonNull IBinder transition, boolean aborted,
+                @Nullable SurfaceControl.Transaction finishT) {
+            switch (mType) {
+                case TYPE_ENTER_PIP_FROM_SPLIT:
+                    mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+                    break;
+                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                    mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+                    mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
+                    break;
+                case TYPE_RECENTS_DURING_SPLIT:
+                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                case TYPE_RECENTS_DURING_DESKTOP:
+                    mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+                    break;
+                case TYPE_KEYGUARD:
+                    mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
+                    break;
+                case TYPE_UNFOLD:
+                    mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
+                    break;
+                default:
+                    break;
+            }
+
+            if (mHasRequestToRemote) {
+                mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
+                        transition, aborted, finishT);
+            }
         }
 
         boolean startSubAnimation(Transitions.TransitionHandler handler, TransitionInfo info,
@@ -175,7 +584,6 @@
             joinFinishArgs(wct);
 
             if (mInFlightSubAnimations == 0) {
-                mActiveTransitions.remove(MixedTransition.this);
                 mFinishCB.onTransitionFinished(mFinishWCT);
             }
         }
@@ -236,8 +644,8 @@
                 throw new IllegalStateException("Unexpected remote transition in"
                         + "pip-enter-from-split request");
             }
-            mActiveTransitions.add(new MixedTransition(MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT,
-                    transition));
+            mActiveTransitions.add(createMixedTransition(
+                    MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition));
 
             WindowContainerTransaction out = new WindowContainerTransaction();
             mPipHandler.augmentRequest(transition, request, out);
@@ -248,7 +656,7 @@
                 mActivityEmbeddingController != null)) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                     " Got a PiP-enter request from an Activity Embedding split");
-            mActiveTransitions.add(new MixedTransition(
+            mActiveTransitions.add(createMixedTransition(
                     MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING, transition));
             // Postpone transition splitting to later.
             WindowContainerTransaction out = new WindowContainerTransaction();
@@ -267,7 +675,7 @@
             if (handler == null) {
                 return null;
             }
-            final MixedTransition mixed = new MixedTransition(
+            final MixedTransition mixed = createMixedTransition(
                     MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
@@ -293,7 +701,7 @@
                         mPlayer.getRemoteTransitionHandler(),
                         new WindowContainerTransaction());
             }
-            final MixedTransition mixed = new MixedTransition(
+            final MixedTransition mixed = createMixedTransition(
                     MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
@@ -302,10 +710,8 @@
             final WindowContainerTransaction wct =
                     mUnfoldHandler.handleRequest(transition, request);
             if (wct != null) {
-                final MixedTransition mixed = new MixedTransition(
-                        MixedTransition.TYPE_UNFOLD, transition);
-                mixed.mLeftoversHandler = mUnfoldHandler;
-                mActiveTransitions.add(mixed);
+                mActiveTransitions.add(createMixedTransition(
+                        MixedTransition.TYPE_UNFOLD, transition));
             }
             return wct;
         }
@@ -331,31 +737,31 @@
     private void setRecentsTransitionDuringSplit(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "Split-Screen is foreground, so treat it as Mixed.");
-        final MixedTransition mixed = new MixedTransition(
-                MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
-        mixed.mLeftoversHandler = mRecentsHandler;
-        mActiveTransitions.add(mixed);
+        mActiveTransitions.add(createMixedTransition(
+                MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition));
     }
 
     private void setRecentsTransitionDuringKeyguard(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "keyguard is visible, so treat it as Mixed.");
-        final MixedTransition mixed = new MixedTransition(
-                MixedTransition.TYPE_RECENTS_DURING_KEYGUARD, transition);
-        mixed.mLeftoversHandler = mRecentsHandler;
-        mActiveTransitions.add(mixed);
+        mActiveTransitions.add(createMixedTransition(
+                MixedTransition.TYPE_RECENTS_DURING_KEYGUARD, transition));
     }
 
     private void setRecentsTransitionDuringDesktop(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "desktop mode is active, so treat it as Mixed.");
-        final MixedTransition mixed = new MixedTransition(
-                MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition);
-        mixed.mLeftoversHandler = mRecentsHandler;
-        mActiveTransitions.add(mixed);
+        mActiveTransitions.add(createMixedTransition(
+                MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition));
     }
 
-    private TransitionInfo subCopy(@NonNull TransitionInfo info,
+    private MixedTransition createMixedTransition(int type, IBinder transition) {
+        return new MixedTransition(type, transition, mPlayer, this, mPipHandler, mRecentsHandler,
+                mSplitHandler, mKeyguardHandler, mDesktopTasksController, mUnfoldHandler,
+                mActivityEmbeddingController);
+    }
+
+    private static TransitionInfo subCopy(@NonNull TransitionInfo info,
             @WindowManager.TransitionType int newType, boolean withChanges) {
         final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
         out.setTrack(info.getTrack());
@@ -372,12 +778,12 @@
         return out;
     }
 
-    private boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
+    private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
         return change.getTaskInfo() != null
                 && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
     }
 
-    private boolean isWallpaper(@NonNull TransitionInfo.Change change) {
+    private static boolean isWallpaper(@NonNull TransitionInfo.Change change) {
         return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
     }
 
@@ -399,10 +805,15 @@
         if (KeyguardTransitionHandler.handles(info)) {
             if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) {
                 final MixedTransition keyguardMixed =
-                        new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
+                        createMixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
                 mActiveTransitions.add(keyguardMixed);
-                final boolean hasAnimateKeyguard = animateKeyguard(keyguardMixed, info,
-                        startTransaction, finishTransaction, finishCallback);
+                Transitions.TransitionFinishCallback callback = wct -> {
+                    mActiveTransitions.remove(keyguardMixed);
+                    finishCallback.onTransitionFinished(wct);
+                };
+                final boolean hasAnimateKeyguard = animateKeyguard(
+                        keyguardMixed, info, startTransaction, finishTransaction, callback,
+                        mKeyguardHandler, mPipHandler);
                 if (hasAnimateKeyguard) {
                     ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                             "Converting mixed transition into a keyguard transition");
@@ -420,173 +831,27 @@
 
         if (mixed == null) return false;
 
-        if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
-            return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING) {
-            return animateEnterPipFromActivityEmbedding(mixed, info, startTransaction,
-                    finishTransaction, finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
-            return false;
-        } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
-            final boolean handledToPip = animateOpenIntentWithRemoteAndPip(mixed, info,
-                    startTransaction, finishTransaction, finishCallback);
-            // Consume the transition on remote handler if the leftover handler already handle this
-            // transition. And if it cannot, the transition will be handled by remote handler, so
-            // don't consume here.
-            // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
-            if (handledToPip && mixed.mHasRequestToRemote
-                    && mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
-                mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
-            }
-            return handledToPip;
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
-            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                final TransitionInfo.Change change = info.getChanges().get(i);
-                // Pip auto-entering info might be appended to recent transition like pressing
-                // home-key in 3-button navigation. This offers split handler the opportunity to
-                // handle split to pip animation.
-                if (mPipHandler.isEnteringPip(change, info.getType())
-                        && mSplitHandler.getSplitItemPosition(change.getLastParent())
-                        != SPLIT_POSITION_UNDEFINED) {
-                    return animateEnterPipFromSplit(mixed, info, startTransaction,
-                            finishTransaction, finishCallback);
-                }
-            }
-
-            return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
-            return animateKeyguard(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) {
-            return animateRecentsDuringKeyguard(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
-            return animateRecentsDuringDesktop(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
-            return animateUnfold(mixed, info, startTransaction, finishTransaction, finishCallback);
-        } else {
-            mActiveTransitions.remove(mixed);
-            throw new IllegalStateException("Starting mixed animation without a known mixed type? "
-                    + mixed.mType);
-        }
-    }
-
-    private boolean animateEnterPipFromActivityEmbedding(@NonNull MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
-                + "entering PIP from an Activity Embedding window");
-        // Split into two transitions (wct)
-        TransitionInfo.Change pipChange = null;
-        final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
-        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-            TransitionInfo.Change change = info.getChanges().get(i);
-            if (mPipHandler.isEnteringPip(change, info.getType())) {
-                if (pipChange != null) {
-                    throw new IllegalStateException("More than 1 pip-entering changes in one"
-                            + " transition? " + info);
-                }
-                pipChange = change;
-                // going backwards, so remove-by-index is fine.
-                everythingElse.getChanges().remove(i);
-            }
-        }
-
-        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
-            --mixed.mInFlightSubAnimations;
-            mixed.joinFinishArgs(wct);
-            if (mixed.mInFlightSubAnimations > 0) return;
-            mActiveTransitions.remove(mixed);
-            finishCallback.onTransitionFinished(mixed.mFinishWCT);
+        final MixedTransition chosenTransition = mixed;
+        Transitions.TransitionFinishCallback callback = wct -> {
+            mActiveTransitions.remove(chosenTransition);
+            finishCallback.onTransitionFinished(wct);
         };
 
-        if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
-            // Fallback to dispatching to other handlers.
-            return false;
+        boolean handled = chosenTransition.startAnimation(
+                transition, info, startTransaction, finishTransaction, callback);
+        if (!handled) {
+            mActiveTransitions.remove(chosenTransition);
         }
-
-        // PIP window should always be on the highest Z order.
-        if (pipChange != null) {
-            mixed.mInFlightSubAnimations = 2;
-            mPipHandler.startEnterAnimation(
-                    pipChange, startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
-                    finishTransaction,
-                    finishCB);
-        } else {
-            mixed.mInFlightSubAnimations = 1;
-        }
-
-        mActivityEmbeddingController.startAnimation(mixed.mTransition, everythingElse,
-                startTransaction, finishTransaction, finishCB);
-        return true;
+        return handled;
     }
 
-    private boolean animateOpenIntentWithRemoteAndPip(@NonNull MixedTransition mixed,
+    private static boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
             @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        TransitionInfo.Change pipChange = null;
-        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-            TransitionInfo.Change change = info.getChanges().get(i);
-            if (mPipHandler.isEnteringPip(change, info.getType())) {
-                if (pipChange != null) {
-                    throw new IllegalStateException("More than 1 pip-entering changes in one"
-                            + " transition? " + info);
-                }
-                pipChange = change;
-                info.getChanges().remove(i);
-            }
-        }
-        Transitions.TransitionFinishCallback finishCB = (wct) -> {
-            --mixed.mInFlightSubAnimations;
-            mixed.joinFinishArgs(wct);
-            if (mixed.mInFlightSubAnimations > 0) return;
-            mActiveTransitions.remove(mixed);
-            finishCallback.onTransitionFinished(mixed.mFinishWCT);
-        };
-        if (pipChange == null) {
-            if (mixed.mLeftoversHandler != null) {
-                mixed.mInFlightSubAnimations = 1;
-                if (mixed.mLeftoversHandler.startAnimation(mixed.mTransition,
-                        info, startTransaction, finishTransaction, finishCB)) {
-                    return true;
-                }
-            }
-            mActiveTransitions.remove(mixed);
-            return false;
-        }
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
-                        + " animation because remote-animation likely doesn't support it");
-        // Split the transition into 2 parts: the pip part and the rest.
-        mixed.mInFlightSubAnimations = 2;
-        // make a new startTransaction because pip's startEnterAnimation "consumes" it so
-        // we need a separate one to send over to launcher.
-        SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
-
-        mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
-
-        // Dispatch the rest of the transition normally.
-        if (mixed.mLeftoversHandler != null
-                && mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info,
-                    startTransaction, finishTransaction, finishCB)) {
-            return true;
-        }
-        mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, info,
-                startTransaction, finishTransaction, finishCB, this);
-        return true;
-    }
-
-    private boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
+            @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
                 + "entering PIP while Split-Screen is foreground.");
         TransitionInfo.Change pipChange = null;
@@ -595,7 +860,7 @@
         boolean homeIsOpening = false;
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             TransitionInfo.Change change = info.getChanges().get(i);
-            if (mPipHandler.isEnteringPip(change, info.getType())) {
+            if (pipHandler.isEnteringPip(change, info.getType())) {
                 if (pipChange != null) {
                     throw new IllegalStateException("More than 1 pip-entering changes in one"
                             + " transition? " + info);
@@ -611,7 +876,6 @@
         }
         if (pipChange == null) {
             // um, something probably went wrong.
-            mActiveTransitions.remove(mixed);
             return false;
         }
         final boolean isGoingHome = homeIsOpening;
@@ -619,13 +883,12 @@
             --mixed.mInFlightSubAnimations;
             mixed.joinFinishArgs(wct);
             if (mixed.mInFlightSubAnimations > 0) return;
-            mActiveTransitions.remove(mixed);
             if (isGoingHome) {
-                mSplitHandler.onTransitionAnimationComplete();
+                splitHandler.onTransitionAnimationComplete();
             }
             finishCallback.onTransitionFinished(mixed.mFinishWCT);
         };
-        if (isGoingHome || mSplitHandler.getSplitItemPosition(pipChange.getLastParent())
+        if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent())
                 != SPLIT_POSITION_UNDEFINED) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
                     + "since entering-PiP caused us to leave split and return home.");
@@ -641,7 +904,7 @@
             // we need a separate one to send over to launcher.
             SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
             @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
-            if (mSplitHandler.isSplitScreenVisible()) {
+            if (splitHandler.isSplitScreenVisible()) {
                 // The non-going home case, we could be pip-ing one of the split stages and keep
                 // showing the other
                 for (int i = info.getChanges().size() - 1; i >= 0; --i) {
@@ -651,7 +914,7 @@
                         continue;
                     }
                     @SplitScreen.StageType int splitItemStage =
-                            mSplitHandler.getSplitItemStage(change.getLastParent());
+                            splitHandler.getSplitItemStage(change.getLastParent());
                     if (splitItemStage != STAGE_TYPE_UNDEFINED) {
                         topStageToKeep = splitItemStage;
                         break;
@@ -659,7 +922,7 @@
                 }
             }
             // Let split update internal state for dismiss.
-            mSplitHandler.prepareDismissAnimation(topStageToKeep,
+            splitHandler.prepareDismissAnimation(topStageToKeep,
                     EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
                     finishTransaction);
 
@@ -673,13 +936,13 @@
                 }
             }
 
-            mPipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
-            mPipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
+            pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
+            pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
                     finishCB);
             // Dispatch the rest of the transition normally. This will most-likely be taken by
             // recents or default handler.
-            mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse,
-                    otherStartT, finishTransaction, finishCB, this);
+            mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
+                    otherStartT, finishTransaction, finishCB, mixedHandler);
         } else {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
                     + "forward animation to Pip-Handler.");
@@ -687,7 +950,7 @@
             // new pip task is spawned). In this case, we don't actually exit split so we can
             // just let pip transition handle the animation verbatim.
             mixed.mInFlightSubAnimations = 1;
-            mPipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction,
+            pipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction,
                     finishCB);
         }
         return true;
@@ -724,10 +987,15 @@
     public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             Transitions.TransitionFinishCallback finishCallback) {
-        final MixedTransition mixed = new MixedTransition(
+        final MixedTransition mixed = createMixedTransition(
                 MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition);
         mActiveTransitions.add(mixed);
-        return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback);
+        Transitions.TransitionFinishCallback callback = wct -> {
+            mActiveTransitions.remove(mixed);
+            finishCallback.onTransitionFinished(wct);
+        };
+        return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback, mPlayer, this,
+                mPipHandler, mSplitHandler);
     }
 
     /**
@@ -750,7 +1018,7 @@
         }
         if (displayPart.getChanges().isEmpty()) return false;
         unlinkMissingParents(everythingElse);
-        final MixedTransition mixed = new MixedTransition(
+        final MixedTransition mixed = createMixedTransition(
                 MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
         mActiveTransitions.add(mixed);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
@@ -779,116 +1047,22 @@
         return true;
     }
 
-    private boolean animateRecentsDuringSplit(@NonNull final MixedTransition mixed,
+    private static boolean animateKeyguard(@NonNull final MixedTransition mixed,
             @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        // Split-screen is only interested in the recents transition finishing (and merging), so
-        // just wrap finish and start recents animation directly.
-        Transitions.TransitionFinishCallback finishCB = (wct) -> {
-            mixed.mInFlightSubAnimations = 0;
-            mActiveTransitions.remove(mixed);
-            // If pair-to-pair switching, the post-recents clean-up isn't needed.
-            wct = wct != null ? wct : new WindowContainerTransaction();
-            if (mixed.mAnimType != MixedTransition.ANIM_TYPE_PAIR_TO_PAIR) {
-                mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
-            } else {
-                // notify pair-to-pair recents animation finish
-                mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
-            }
-            mSplitHandler.onTransitionAnimationComplete();
-            finishCallback.onTransitionFinished(wct);
-        };
-        mixed.mInFlightSubAnimations = 1;
-        mSplitHandler.onRecentsInSplitAnimationStart(info);
-        final boolean handled = mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info,
-                startTransaction, finishTransaction, finishCB);
-        if (!handled) {
-            mSplitHandler.onRecentsInSplitAnimationCanceled();
-            mActiveTransitions.remove(mixed);
-        }
-        return handled;
-    }
-
-    private boolean animateKeyguard(@NonNull final MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull KeyguardTransitionHandler keyguardHandler,
+            PipTransitionController pipHandler) {
         if (mixed.mFinishT == null) {
             mixed.mFinishT = finishTransaction;
             mixed.mFinishCB = finishCallback;
         }
         // Sync pip state.
-        if (mPipHandler != null) {
-            mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+        if (pipHandler != null) {
+            pipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
         }
-        return mixed.startSubAnimation(mKeyguardHandler, info, startTransaction, finishTransaction);
-    }
-
-    private boolean animateRecentsDuringKeyguard(@NonNull final MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        if (mixed.mInfo == null) {
-            mixed.mInfo = info;
-            mixed.mFinishT = finishTransaction;
-            mixed.mFinishCB = finishCallback;
-        }
-        return mixed.startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
-    }
-
-    private boolean animateRecentsDuringDesktop(@NonNull final MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        Transitions.TransitionFinishCallback finishCB = wct -> {
-            mixed.mInFlightSubAnimations--;
-            if (mixed.mInFlightSubAnimations == 0) {
-                mActiveTransitions.remove(mixed);
-                finishCallback.onTransitionFinished(wct);
-            }
-        };
-
-        mixed.mInFlightSubAnimations++;
-        boolean consumed = mRecentsHandler.startAnimation(
-                mixed.mTransition, info, startTransaction, finishTransaction, finishCB);
-        if (!consumed) {
-            mixed.mInFlightSubAnimations--;
-            return false;
-        }
-        if (mDesktopTasksController != null) {
-            mDesktopTasksController.syncSurfaceState(info, finishTransaction);
-            return true;
-        }
-
-        return false;
-    }
-
-    private boolean animateUnfold(@NonNull final MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
-            mixed.mInFlightSubAnimations--;
-            if (mixed.mInFlightSubAnimations > 0) return;
-            mActiveTransitions.remove(mixed);
-            finishCallback.onTransitionFinished(wct);
-        };
-        mixed.mInFlightSubAnimations = 1;
-        // Sync pip state.
-        if (mPipHandler != null) {
-            mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
-        }
-        if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
-            mSplitHandler.updateSurfaces(startTransaction);
-        }
-        return mUnfoldHandler.startAnimation(
-                mixed.mTransition, info, startTransaction, finishTransaction, finishCB);
+        return mixed.startSubAnimation(keyguardHandler, info, startTransaction, finishTransaction);
     }
 
     /** Use to when split use intent to enter, check if this enter transition should be mixed or
@@ -932,65 +1106,13 @@
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         for (int i = 0; i < mActiveTransitions.size(); ++i) {
             if (mActiveTransitions.get(i).mTransition != mergeTarget) continue;
+
             MixedTransition mixed = mActiveTransitions.get(i);
             if (mixed.mInFlightSubAnimations <= 0) {
                 // Already done, so no need to end it.
                 return;
             }
-            if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
-                // queue since no actual animation.
-            } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
-                if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) {
-                    boolean ended = mSplitHandler.end();
-                    // If split couldn't end (because it is remote), then don't end everything else
-                    // since we have to play out the animation anyways.
-                    if (!ended) return;
-                    mPipHandler.end();
-                    if (mixed.mLeftoversHandler != null) {
-                        mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
-                                finishCallback);
-                    }
-                } else {
-                    mPipHandler.end();
-                }
-            } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING) {
-                mPipHandler.end();
-                mActivityEmbeddingController.mergeAnimation(transition, info, t, mergeTarget,
-                        finishCallback);
-            } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
-                mPipHandler.end();
-                if (mixed.mLeftoversHandler != null) {
-                    mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
-                            finishCallback);
-                }
-            } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
-                if (mSplitHandler.isPendingEnter(transition)) {
-                    // Recents -> enter-split means that we are switching from one pair to
-                    // another pair.
-                    mixed.mAnimType = MixedTransition.ANIM_TYPE_PAIR_TO_PAIR;
-                }
-                mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
-                        finishCallback);
-            } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
-                mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
-            } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) {
-                if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
-                    handoverTransitionLeashes(mixed, info, t, mixed.mFinishT);
-                    if (animateKeyguard(mixed, info, t, mixed.mFinishT, mixed.mFinishCB)) {
-                        finishCallback.onTransitionFinished(null);
-                    }
-                }
-                mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
-                        finishCallback);
-            } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
-                mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
-                        finishCallback);
-            } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
-                mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
-            } else {
-                throw new IllegalStateException("Playing a mixed transition with unknown type? "
-                        + mixed.mType);
-            }
+            mixed.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
         }
     }
 
@@ -1003,46 +1125,30 @@
             mixed = mActiveTransitions.remove(i);
             break;
         }
-        if (mixed == null) return;
-        if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
-            mPipHandler.onTransitionConsumed(transition, aborted, finishT);
-        } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING) {
-            mPipHandler.onTransitionConsumed(transition, aborted, finishT);
-            mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
-            mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
-        } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
-            mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
-        } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
-            mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
-            mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
-        } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
-            mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
-        }
-        if (mixed.mHasRequestToRemote) {
-            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+        if (mixed != null) {
+            mixed.onTransitionConsumed(transition, aborted, finishT);
         }
     }
 
     /**
-     * Update an incoming {@link TransitionInfo} with the leashes from an ongoing
-     * {@link MixedTransition} so that it can take over some parts of the animation without
+     * Update an incoming {@link TransitionInfo} with the leashes from an existing
+     * {@link TransitionInfo} so that it can take over some parts of the animation without
      * reparenting to new transition roots.
      */
-    private static void handoverTransitionLeashes(@NonNull MixedTransition mixed,
-            @NonNull TransitionInfo info,
+    private static void handoverTransitionLeashes(
+            @NonNull TransitionInfo from,
+            @NonNull TransitionInfo to,
             @NonNull SurfaceControl.Transaction startT,
             @NonNull SurfaceControl.Transaction finishT) {
 
         // Show the roots in case they contain new changes not present in the original transition.
-        for (int j = info.getRootCount() - 1; j >= 0; --j) {
-            startT.show(info.getRoot(j).getLeash());
+        for (int j = to.getRootCount() - 1; j >= 0; --j) {
+            startT.show(to.getRoot(j).getLeash());
         }
 
         // Find all of the leashes from the original transition.
         Map<WindowContainerToken, TransitionInfo.Change> originalChanges = new ArrayMap<>();
-        for (TransitionInfo.Change oldChange : mixed.mInfo.getChanges()) {
+        for (TransitionInfo.Change oldChange : from.getChanges()) {
             if (oldChange.getContainer() != null) {
                 originalChanges.put(oldChange.getContainer(), oldChange);
             }
@@ -1050,9 +1156,10 @@
 
         // Merge the animation leashes by re-using the original ones if we see the same container
         // in the new transition and the old.
-        for (TransitionInfo.Change newChange : info.getChanges()) {
+        for (TransitionInfo.Change newChange : to.getChanges()) {
             if (originalChanges.containsKey(newChange.getContainer())) {
-                final TransitionInfo.Change oldChange = originalChanges.get(newChange.getContainer());
+                final TransitionInfo.Change oldChange = originalChanges.get(
+                        newChange.getContainer());
                 startT.reparent(newChange.getLeash(), null);
                 newChange.setLeash(oldChange.getLeash());
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index cebc400..1a793a1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -23,13 +23,11 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.os.Handler;
-import android.os.IBinder;
 import android.util.SparseArray;
 import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
-import android.window.TransitionInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
@@ -80,16 +78,6 @@
     }
 
     @Override
-    public void onTransitionReady(IBinder transition, TransitionInfo info,
-            TransitionInfo.Change change) {}
-
-    @Override
-    public void onTransitionMerged(IBinder merged, IBinder playing) {}
-
-    @Override
-    public void onTransitionFinished(IBinder transition) {}
-
-    @Override
     public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
         mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 61a8e9b..d07c646 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -22,7 +22,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.WindowInsets.Type.statusBars;
-import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
@@ -43,7 +42,6 @@
 import android.graphics.Region;
 import android.hardware.input.InputManager;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Looper;
 import android.util.SparseArray;
 import android.view.Choreographer;
@@ -59,8 +57,6 @@
 import android.view.SurfaceControl.Transaction;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.WindowManager;
-import android.window.TransitionInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
@@ -80,8 +76,6 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
-import com.android.wm.shell.recents.RecentsTransitionHandler;
-import com.android.wm.shell.recents.RecentsTransitionStateListener;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -115,7 +109,6 @@
     private final DisplayController mDisplayController;
     private final SyncTransactionQueue mSyncQueue;
     private final Optional<DesktopTasksController> mDesktopTasksController;
-    private final RecentsTransitionHandler mRecentsTransitionHandler;
     private boolean mTransitionDragActive;
 
     private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
@@ -154,7 +147,6 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
-            RecentsTransitionHandler recentsTransitionHandler,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer
     ) {
         this(
@@ -170,7 +162,6 @@
                 syncQueue,
                 transitions,
                 desktopTasksController,
-                recentsTransitionHandler,
                 new DesktopModeWindowDecoration.Factory(),
                 new InputMonitorFactory(),
                 SurfaceControl.Transaction::new,
@@ -191,7 +182,6 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
-            RecentsTransitionHandler recentsTransitionHandler,
             DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
             InputMonitorFactory inputMonitorFactory,
             Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -207,7 +197,6 @@
         mSyncQueue = syncQueue;
         mTransitions = transitions;
         mDesktopTasksController = desktopTasksController;
-        mRecentsTransitionHandler = recentsTransitionHandler;
         mShellCommandHandler = shellCommandHandler;
         mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
         mInputMonitorFactory = inputMonitorFactory;
@@ -219,12 +208,6 @@
 
     private void onInit() {
         mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener);
-        mRecentsTransitionHandler.addTransitionStateListener(new RecentsTransitionStateListener() {
-            @Override
-            public void onTransitionStarted(IBinder transition) {
-                blockRelayoutOnTransitionStarted(transition);
-            }
-        });
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(),
                 new DesktopModeOnInsetsChangedListener());
@@ -264,48 +247,6 @@
     }
 
     @Override
-    public void onTransitionReady(
-            @NonNull IBinder transition,
-            @NonNull TransitionInfo info,
-            @NonNull TransitionInfo.Change change) {
-        if (change.getMode() == WindowManager.TRANSIT_CHANGE
-                && (info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE
-                || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
-                || info.getType() == Transitions.TRANSIT_MOVE_TO_DESKTOP)) {
-            mWindowDecorByTaskId.get(change.getTaskInfo().taskId)
-                    .addTransitionPausingRelayout(transition);
-        } else if (change.getMode() == WindowManager.TRANSIT_TO_BACK
-                && info.getType() == Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
-                && change.getTaskInfo() != null) {
-            final DesktopModeWindowDecoration decor =
-                    mWindowDecorByTaskId.get(change.getTaskInfo().taskId);
-            if (decor != null) {
-                decor.addTransitionPausingRelayout(transition);
-            }
-        } else if (change.getMode() == WindowManager.TRANSIT_TO_FRONT
-                && ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0)
-                && change.getTaskInfo() != null) {
-            blockRelayoutOnTransitionStarted(transition);
-        }
-    }
-
-    @Override
-    public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
-        for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
-            final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
-            decor.mergeTransitionPausingRelayout(merged, playing);
-        }
-    }
-
-    @Override
-    public void onTransitionFinished(@NonNull IBinder transition) {
-        for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
-            final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
-            decor.removeTransitionPausingRelayout(transition);
-        }
-    }
-
-    @Override
     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
         if (decoration == null) return;
@@ -365,16 +306,6 @@
         }
     }
 
-    private void blockRelayoutOnTransitionStarted(IBinder transition) {
-        // Block relayout on window decorations originating from #onTaskInfoChanges until the
-        // animation completes to avoid interfering with the transition animation.
-        for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
-            final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
-            decor.incrementRelayoutBlock();
-            decor.addTransitionPausingRelayout(transition);
-        }
-    }
-
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             DragDetector.MotionEventHandler {
@@ -425,7 +356,6 @@
                     // App sometimes draws before the insets from WindowDecoration#relayout have
                     // been added, so they must be added here
                     mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct);
-                    decoration.incrementRelayoutBlock();
                     mDesktopTasksController.get().moveToDesktop(decoration, mTaskId, wct);
                     closeOtherSplitTask(mTaskId);
                 }
@@ -436,7 +366,7 @@
                     mSplitScreenController.moveTaskToFullscreen(mTaskId);
                 } else {
                     mDesktopTasksController.ifPresent(c ->
-                            c.moveToFullscreen(mTaskId, mWindowDecorByTaskId.get(mTaskId)));
+                            c.moveToFullscreen(mTaskId));
                 }
             } else if (id == R.id.split_screen_button) {
                 decoration.closeHandleMenu();
@@ -604,7 +534,7 @@
                     mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo,
                             position,
                             new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
-                            newTaskBounds, mWindowDecorByTaskId.get(mTaskId)));
+                            newTaskBounds));
                     mIsDragging = false;
                     return true;
                 }
@@ -812,20 +742,8 @@
                                     mContext, mDragToDesktopAnimationStartBounds,
                                     relevantDecor.mTaskInfo, relevantDecor.mTaskSurface);
                             mDesktopTasksController.ifPresent(
-                                    c -> {
-                                        final int taskId = relevantDecor.mTaskInfo.taskId;
-                                        relevantDecor.incrementRelayoutBlock();
-                                        if (isTaskInSplitScreen(taskId)) {
-                                            final DesktopModeWindowDecoration otherDecor =
-                                                    mWindowDecorByTaskId.get(
-                                                            getOtherSplitTask(taskId).taskId);
-                                            if (otherDecor != null) {
-                                                otherDecor.incrementRelayoutBlock();
-                                            }
-                                        }
-                                        c.startDragToDesktop(relevantDecor.mTaskInfo,
-                                                mMoveToDesktopAnimator, relevantDecor);
-                                    });
+                                    c -> c.startDragToDesktop(relevantDecor.mTaskInfo,
+                                            mMoveToDesktopAnimator, relevantDecor));
                         }
                     }
                     if (mMoveToDesktopAnimator != null) {
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 d08b655..5f77192 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
@@ -36,7 +36,6 @@
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
-import android.os.IBinder;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.MotionEvent;
@@ -62,8 +61,6 @@
 import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder;
 import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder;
 
-import java.util.HashSet;
-import java.util.Set;
 import java.util.function.Supplier;
 
 /**
@@ -104,8 +101,6 @@
 
     private ExclusionRegionListener mExclusionRegionListener;
 
-    private final Set<IBinder> mTransitionsPausingRelayout = new HashSet<>();
-    private int mRelayoutBlock;
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
 
     DesktopModeWindowDecoration(
@@ -179,13 +174,6 @@
 
     @Override
     void relayout(ActivityManager.RunningTaskInfo taskInfo) {
-        // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell
-        // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl
-        // and interferes with the transition animation that is playing at the same time.
-        if (mRelayoutBlock > 0) {
-            return;
-        }
-
         final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
         // The crop and position of the task should only be set when a task is fluid resizing. In
         // all other cases, it is expected that the transition handler positions and crops the task
@@ -737,16 +725,6 @@
         return exclusionRegion;
     }
 
-    /**
-     * If transition exists in mTransitionsPausingRelayout, remove the transition and decrement
-     * mRelayoutBlock
-     */
-    void removeTransitionPausingRelayout(IBinder transition) {
-        if (mTransitionsPausingRelayout.remove(transition)) {
-            mRelayoutBlock--;
-        }
-    }
-
     @Override
     int getCaptionHeightId(@WindowingMode int windowingMode) {
         return getCaptionHeightIdStatic(windowingMode);
@@ -767,35 +745,10 @@
         return R.id.desktop_mode_caption;
     }
 
-    /**
-     * Add transition to mTransitionsPausingRelayout
-     */
-    void addTransitionPausingRelayout(IBinder transition) {
-        mTransitionsPausingRelayout.add(transition);
-    }
-
-    /**
-     * If two transitions merge and the merged transition is in mTransitionsPausingRelayout,
-     * remove the merged transition from the set and add the transition it was merged into.
-     */
-    public void mergeTransitionPausingRelayout(IBinder merged, IBinder playing) {
-        if (mTransitionsPausingRelayout.remove(merged)) {
-            mTransitionsPausingRelayout.add(playing);
-        }
-    }
-
-    /**
-     * Increase mRelayoutBlock, stopping relayout if mRelayoutBlock is now greater than 0.
-     */
-    public void incrementRelayoutBlock() {
-        mRelayoutBlock++;
-    }
-
     @Override
     public String toString() {
         return "{"
                 + "mPositionInParent=" + mPositionInParent + ", "
-                + "mRelayoutBlock=" + mRelayoutBlock + ", "
                 + "taskId=" + mTaskInfo.taskId + ", "
                 + "windowingMode=" + windowingModeToString(mTaskInfo.getWindowingMode()) + ", "
                 + "isFocused=" + isFocused()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index ae1a3d9..01a6012 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -17,9 +17,7 @@
 package com.android.wm.shell.windowdecor;
 
 import android.app.ActivityManager;
-import android.os.IBinder;
 import android.view.SurfaceControl;
-import android.window.TransitionInfo;
 
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -103,34 +101,4 @@
      * @param taskInfo the info of the task
      */
     void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo);
-
-    /**
-     * Notifies that a shell transition is about to start. If the transition is of type
-     * TRANSIT_ENTER_DESKTOP, it will save that transition to unpause relayout for the transitioning
-     * task after the transition has ended.
-     *
-     * @param transition the ready transaction
-     * @param info of Transition to check if relayout needs to be paused for a task
-     * @param change a change in the given transition
-     */
-    default void onTransitionReady(IBinder transition, TransitionInfo info,
-            TransitionInfo.Change change) {}
-
-    /**
-     * Notifies that a shell transition is about to merge with another to give the window
-     * decoration a chance to prepare for this merge.
-     *
-     * @param merged the transaction being merged
-     * @param playing the transaction being merged into
-     */
-    default void onTransitionMerged(IBinder merged, IBinder playing) {}
-
-    /**
-     * Notifies that a shell transition is about to finish  to give the window decoration a chance
-     * to clean up.
-     *
-     * @param transaction
-     */
-    default void onTransitionFinished(IBinder transaction) {}
-
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
index 182a908..be77171 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
@@ -101,7 +101,8 @@
     override fun pipLayerReduces() {
         flicker.assertLayers {
             val pipLayerList =
-                this.layers { standardAppHelper.layerMatchesAnyOf(it) && it.isVisible }
+                this.layers { standardAppHelper.packageNameMatcher.layerMatchesAnyOf(it)
+                        && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
             }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
deleted file mode 100644
index 6ebee73..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
+++ /dev/null
@@ -1,602 +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.wm.shell.bubbles;
-
-import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
-import static org.mockito.Mockito.mock;
-
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Insets;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.UserHandle;
-import android.testing.AndroidTestingRunner;
-import android.view.WindowManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests operations and the resulting state managed by {@link BubblePositioner}.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class BubblePositionerTest extends ShellTestCase {
-
-    private BubblePositioner mPositioner;
-
-    @Before
-    public void setUp() {
-        WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-        mPositioner = new BubblePositioner(mContext, windowManager);
-    }
-
-    @Test
-    public void testUpdate() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1000, 1200);
-        Rect availableRect = new Rect(screenBounds);
-        availableRect.inset(insets);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.getAvailableRect()).isEqualTo(availableRect);
-        assertThat(mPositioner.isLandscape()).isFalse();
-        assertThat(mPositioner.isLargeScreen()).isFalse();
-        assertThat(mPositioner.getInsets()).isEqualTo(insets);
-    }
-
-    @Test
-    public void testShowBubblesVertically_phonePortrait() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.showBubblesVertically()).isFalse();
-    }
-
-    @Test
-    public void testShowBubblesVertically_phoneLandscape() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLandscape().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.isLandscape()).isTrue();
-        assertThat(mPositioner.showBubblesVertically()).isTrue();
-    }
-
-    @Test
-    public void testShowBubblesVertically_tablet() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.showBubblesVertically()).isTrue();
-    }
-
-    /** If a resting position hasn't been set, calling it will return the default position. */
-    @Test
-    public void testGetRestingPosition_returnsDefaultPosition() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        PointF restingPosition = mPositioner.getRestingPosition();
-        PointF defaultPosition = mPositioner.getDefaultStartPosition();
-
-        assertThat(restingPosition).isEqualTo(defaultPosition);
-    }
-
-    /** If a resting position has been set, it'll return that instead of the default position. */
-    @Test
-    public void testGetRestingPosition_returnsRestingPosition() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        PointF restingPosition = new PointF(100, 100);
-        mPositioner.setRestingPosition(restingPosition);
-
-        assertThat(mPositioner.getRestingPosition()).isEqualTo(restingPosition);
-    }
-
-    /** Test that the default resting position on phone is in upper left. */
-    @Test
-    public void testGetRestingPosition_bubble_onPhone() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testGetRestingPosition_bubble_onPhone_RTL() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    /** Test that the default resting position on tablet is middle left. */
-    @Test
-    public void testGetRestingPosition_chatBubble_onTablet() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testGetRestingPosition_chatBubble_onTablet_RTL() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    /** Test that the default resting position on tablet is middle right. */
-    @Test
-    public void testGetDefaultPosition_appBubble_onTablet() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
-
-        assertThat(startPosition.x).isEqualTo(allowableStackRegion.right);
-        assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testGetRestingPosition_appBubble_onTablet_RTL() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
-
-        assertThat(startPosition.x).isEqualTo(allowableStackRegion.left);
-        assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testHasUserModifiedDefaultPosition_false() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
-
-        mPositioner.setRestingPosition(mPositioner.getDefaultStartPosition());
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
-    }
-
-    @Test
-    public void testHasUserModifiedDefaultPosition_true() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
-
-        mPositioner.setRestingPosition(new PointF(0, 100));
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isTrue();
-    }
-
-    @Test
-    public void testGetExpandedViewHeight_max() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT);
-    }
-
-    @Test
-    public void testGetExpandedViewHeight_customHeight_valid() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        final int minHeight = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_expanded_default_height);
-        Bubble bubble = new Bubble("key",
-                mock(ShortcutInfo.class),
-                minHeight + 100 /* desiredHeight */,
-                0 /* desiredHeightResId */,
-                "title",
-                0 /* taskId */,
-                null /* locus */,
-                true /* isDismissable */,
-                directExecutor(),
-                mock(Bubbles.BubbleMetadataFlagListener.class));
-
-        // Ensure the height is the same as the desired value
-        assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(
-                bubble.getDesiredHeight(mContext));
-    }
-
-
-    @Test
-    public void testGetExpandedViewHeight_customHeight_tooSmall() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Bubble bubble = new Bubble("key",
-                mock(ShortcutInfo.class),
-                10 /* desiredHeight */,
-                0 /* desiredHeightResId */,
-                "title",
-                0 /* taskId */,
-                null /* locus */,
-                true /* isDismissable */,
-                directExecutor(),
-                mock(Bubbles.BubbleMetadataFlagListener.class));
-
-        // Ensure the height is the same as the minimum value
-        final int minHeight = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_expanded_default_height);
-        assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight);
-    }
-
-    @Test
-    public void testGetMaxExpandedViewHeight_onLargeTablet() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        int manageButtonHeight =
-                mContext.getResources().getDimensionPixelSize(R.dimen.bubble_manage_button_height);
-        int pointerWidth = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_pointer_width);
-        int expandedViewPadding = mContext.getResources().getDimensionPixelSize(R
-                .dimen.bubble_expanded_view_padding);
-        float expectedHeight = 1800 - 2 * 20 - manageButtonHeight - pointerWidth
-                - expandedViewPadding * 2;
-        assertThat(((float) mPositioner.getMaxExpandedViewHeight(false /* isOverflow */)))
-                .isWithin(0.1f).of(expectedHeight);
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_largeScreen_true() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isTrue();
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_largeScreen_false() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isFalse();
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_smallTablet_false() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setSmallTablet()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isFalse();
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_phone_false() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isFalse();
-    }
-
-    @Test
-    public void testExpandedViewY_phoneLandscape() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height so it'll always be top aligned
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_phonePortrait() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // Always top aligned in phone portrait
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_smallTabletLandscape() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setSmallTablet()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height which is always top aligned on small tablets
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_smallTabletPortrait() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setSmallTablet()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height which is always top aligned on small tablets
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_largeScreenLandscape() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height which is always top aligned on landscape, large tablet
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_largeScreenPortrait() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        int manageButtonHeight =
-                mContext.getResources().getDimensionPixelSize(R.dimen.bubble_manage_button_height);
-        int manageButtonPlusMargin = manageButtonHeight + 2
-                * mContext.getResources().getDimensionPixelSize(
-                        R.dimen.bubble_manage_button_margin);
-        int pointerWidth = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_pointer_width);
-
-        final float expectedExpandedViewY = mPositioner.getAvailableRect().bottom
-                - manageButtonPlusMargin
-                - mPositioner.getExpandedViewHeightForLargeScreen()
-                - pointerWidth;
-
-        // Bubbles are bottom aligned on portrait, large tablet
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(expectedExpandedViewY);
-    }
-
-    /**
-     * Calculates the Y position bubbles should be placed based on the config. Based on
-     * the calculations in {@link BubblePositioner#getDefaultStartPosition()} and
-     * {@link BubbleStackView.RelativeStackPosition}.
-     */
-    private float getDefaultYPosition() {
-        final boolean isTablet = mPositioner.isLargeScreen();
-
-        // On tablet the position is centered, on phone it is an offset from the top.
-        final float desiredY = isTablet
-                ? mPositioner.getScreenRect().height() / 2f - (mPositioner.getBubbleSize() / 2f)
-                : mContext.getResources().getDimensionPixelOffset(
-                        R.dimen.bubble_stack_starting_offset_y);
-        // Since we're visually centering the bubbles on tablet, use total screen height rather
-        // than the available height.
-        final float height = isTablet
-                ? mPositioner.getScreenRect().height()
-                : mPositioner.getAvailableRect().height();
-        float offsetPercent = desiredY / height;
-        offsetPercent = Math.max(0f, Math.min(1f, offsetPercent));
-        final RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent;
-    }
-
-    /**
-     * Sets up window manager to return config values based on what you need for the test.
-     * By default it sets up a portrait phone without any insets.
-     */
-    private static class ConfigBuilder {
-        private Rect mScreenBounds = new Rect(0, 0, 1000, 2000);
-        private boolean mIsLargeScreen = false;
-        private boolean mIsSmallTablet = false;
-        private boolean mIsLandscape = false;
-        private boolean mIsRtl = false;
-        private Insets mInsets = Insets.of(0, 0, 0, 0);
-
-        public ConfigBuilder setScreenBounds(Rect screenBounds) {
-            mScreenBounds = screenBounds;
-            return this;
-        }
-
-        public ConfigBuilder setLargeScreen() {
-            mIsLargeScreen = true;
-            return this;
-        }
-
-        public ConfigBuilder setSmallTablet() {
-            mIsSmallTablet = true;
-            return this;
-        }
-
-        public ConfigBuilder setLandscape() {
-            mIsLandscape = true;
-            return this;
-        }
-
-        public ConfigBuilder setRtl() {
-            mIsRtl = true;
-            return this;
-        }
-
-        public ConfigBuilder setInsets(Insets insets) {
-            mInsets = insets;
-            return this;
-        }
-
-        private DeviceConfig build() {
-            return new DeviceConfig(mIsLargeScreen, mIsSmallTablet, mIsLandscape, mIsRtl,
-                    mScreenBounds, mInsets);
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt
new file mode 100644
index 0000000..955660c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.split
+
+import android.content.ComponentName
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.wm.shell.ShellTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class SplitScreenUtilsTests : ShellTestCase() {
+
+    @Test
+    fun getShortcutComponent_nullShortcuts() {
+        val launcherApps = mock(LauncherApps::class.java).also {
+            `when`(it.getShortcuts(any(), any())).thenReturn(null)
+        }
+        assertEquals(null, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE,
+                TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps))
+    }
+
+    @Test
+    fun getShortcutComponent_noShortcuts() {
+        val launcherApps = mock(LauncherApps::class.java).also {
+            `when`(it.getShortcuts(any(), any())).thenReturn(ArrayList<ShortcutInfo>())
+        }
+        assertEquals(null, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE,
+                TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps))
+    }
+
+    @Test
+    fun getShortcutComponent_validShortcut() {
+        val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
+        val shortcutInfo = ShortcutInfo.Builder(context, "id").setActivity(component).build()
+        val launcherApps = mock(LauncherApps::class.java).also {
+            `when`(it.getShortcuts(any(), any())).thenReturn(arrayListOf(shortcutInfo))
+        }
+        assertEquals(component, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE,
+                TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps))
+    }
+
+    companion object {
+        val TEST_PACKAGE = "com.android.wm.shell.common.split"
+        val TEST_ACTIVITY = "TestActivity";
+        val TEST_SHORTCUT_ID = "test_shortcut_1"
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 94c862b..9249b0a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -393,7 +393,7 @@
     fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() {
         val task = setUpFreeformTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
-        controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration)
+        controller.moveToFullscreen(task.taskId)
         val wct = getLatestExitDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
             .isEqualTo(WINDOWING_MODE_UNDEFINED)
@@ -403,7 +403,7 @@
     fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() {
         val task = setUpFreeformTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
-        controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration)
+        controller.moveToFullscreen(task.taskId)
         val wct = getLatestExitDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
                 .isEqualTo(WINDOWING_MODE_FULLSCREEN)
@@ -411,7 +411,7 @@
 
     @Test
     fun moveToFullscreen_nonExistentTask_doesNothing() {
-        controller.moveToFullscreen(999, desktopModeWindowDecoration)
+        controller.moveToFullscreen(999)
         verifyWCTNotExecuted()
     }
 
@@ -420,7 +420,7 @@
         val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
         val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
 
-        controller.moveToFullscreen(taskDefaultDisplay.taskId, desktopModeWindowDecoration)
+        controller.moveToFullscreen(taskDefaultDisplay.taskId)
 
         with(getLatestExitDesktopWct()) {
             assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 855b7ee..12a5594 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
+import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
@@ -36,6 +37,8 @@
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -46,8 +49,10 @@
 import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 
 import androidx.test.annotation.UiThreadTest;
@@ -55,6 +60,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
@@ -91,6 +97,10 @@
 @RunWith(AndroidJUnit4.class)
 public class SplitScreenControllerTests extends ShellTestCase {
 
+    private static final String TEST_PACKAGE = "com.android.wm.shell.splitscreen";
+    private static final String TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.splitscreen.fake";
+    private static final String TEST_ACTIVITY = "TestActivity";
+
     @Mock ShellInit mShellInit;
     @Mock ShellCommandHandler mShellCommandHandler;
     @Mock ShellTaskOrganizer mTaskOrganizer;
@@ -118,6 +128,8 @@
     public void setup() {
         assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
         MockitoAnnotations.initMocks(this);
+        String[] appsSupportingMultiInstance = mContext.getResources()
+                .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
         mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
                 mMainExecutor));
         mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
@@ -125,7 +137,8 @@
                 mRootTDAOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
                 mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
-                mDesktopTasksController, mMainExecutor, mStageCoordinator));
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                appsSupportingMultiInstance));
     }
 
     @Test
@@ -200,7 +213,7 @@
 
     @Test
     public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() {
-        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doReturn(true).when(mSplitScreenController).supportsMultiInstanceSplit(any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
@@ -237,12 +250,13 @@
 
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
                 isNull());
-        verify(mSplitScreenController, never()).supportMultiInstancesSplit(any());
+        verify(mSplitScreenController, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator, never()).switchSplitPosition(any());
     }
 
     @Test
     public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
+        doReturn(true).when(mSplitScreenController).supportsMultiInstanceSplit(any());
         doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
@@ -259,14 +273,14 @@
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
                 SPLIT_POSITION_TOP_OR_LEFT, null);
-        verify(mSplitScreenController, never()).supportMultiInstancesSplit(any());
+        verify(mSplitScreenController, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
                 isNull());
     }
 
     @Test
     public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() {
-        doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doReturn(false).when(mSplitScreenController).supportsMultiInstanceSplit(any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
@@ -283,6 +297,130 @@
         verify(mStageCoordinator).switchSplitPosition(anyString());
     }
 
+    @Test
+    public void supportsMultiInstanceSplit_inStaticAllowList() {
+        String[] allowList = { TEST_PACKAGE };
+        SplitScreenController controller = new SplitScreenController(mContext, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                allowList);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        assertEquals(true, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_notInStaticAllowList() {
+        String[] allowList = { TEST_PACKAGE };
+        SplitScreenController controller = new SplitScreenController(mContext, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                allowList);
+        ComponentName component = new ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY);
+        assertEquals(false, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_activityPropertyTrue()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        PackageManager.Property activityProp = new PackageManager.Property("", true, "", "");
+        doReturn(activityProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component));
+        PackageManager.Property appProp = new PackageManager.Property("", false, "", "");
+        doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        // Expect activity property to override application property
+        assertEquals(true, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_activityPropertyFalseApplicationPropertyTrue()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        PackageManager.Property activityProp = new PackageManager.Property("", false, "", "");
+        doReturn(activityProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component));
+        PackageManager.Property appProp = new PackageManager.Property("", true, "", "");
+        doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        // Expect activity property to override application property
+        assertEquals(false, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_noActivityPropertyApplicationPropertyTrue()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty(
+                eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component));
+        PackageManager.Property appProp = new PackageManager.Property("", true, "", "");
+        doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        // Expect fall through to app property
+        assertEquals(true, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_noActivityOrAppProperty()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty(
+                eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component));
+        doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty(
+                eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        assertEquals(false, controller.supportsMultiInstanceSplit(component));
+    }
+
     private Intent createStartIntent(String activityName) {
         Intent intent = new Intent();
         intent.setComponent(new ComponentName(mContext, activityName));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index e22bf3d..e9da258 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -64,6 +64,7 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.IApplicationThread;
 import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
@@ -420,6 +421,30 @@
     }
 
     @Test
+    public void testTransitionFilterActivityComponent() {
+        TransitionFilter filter = new TransitionFilter();
+        ComponentName cmpt = new ComponentName("testpak", "testcls");
+        filter.mRequirements =
+                new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
+        filter.mRequirements[0].mTopActivity = cmpt;
+        filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+
+        final RunningTaskInfo taskInf = createTaskInfo(1);
+        final TransitionInfo openTask = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN, taskInf).build();
+        assertFalse(filter.matches(openTask));
+
+        taskInf.topActivity = cmpt;
+        final TransitionInfo openTaskCmpt = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN, taskInf).build();
+        assertTrue(filter.matches(openTaskCmpt));
+
+        final TransitionInfo openAct = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN, cmpt).build();
+        assertTrue(filter.matches(openAct));
+    }
+
+    @Test
     public void testRegisteredRemoteTransition() {
         Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
index 8343858..b8939e6f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.mock;
 
 import android.app.ActivityManager;
+import android.content.ComponentName;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -50,20 +51,34 @@
     }
 
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
-            @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) {
+            @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo,
+            ComponentName activityComponent) {
         final TransitionInfo.Change change = new TransitionInfo.Change(
                 taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */));
         change.setMode(mode);
         change.setFlags(flags);
         change.setTaskInfo(taskInfo);
+        change.setActivityComponent(activityComponent);
         return addChange(change);
     }
 
+    /** Add a change to the TransitionInfo */
+    public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
+            @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) {
+        return addChange(mode, flags, taskInfo, null /* activityComponent */);
+    }
+
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
             ActivityManager.RunningTaskInfo taskInfo) {
         return addChange(mode, TransitionInfo.FLAG_NONE, taskInfo);
     }
 
+    /** Add a change to the TransitionInfo */
+    public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
+            ComponentName activityComponent) {
+        return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent);
+    }
+
     public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) {
         return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 883c24e..f84685a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -27,7 +27,6 @@
 import android.hardware.display.DisplayManager
 import android.hardware.display.VirtualDisplay
 import android.os.Handler
-import android.os.IBinder
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.Choreographer
@@ -40,8 +39,6 @@
 import android.view.SurfaceView
 import android.view.WindowInsets.Type.navigationBars
 import android.view.WindowInsets.Type.statusBars
-import android.view.WindowManager
-import android.window.TransitionInfo
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
@@ -53,8 +50,6 @@
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.desktopmode.DesktopTasksController
-import com.android.wm.shell.recents.RecentsTransitionHandler
-import com.android.wm.shell.recents.RecentsTransitionStateListener
 import com.android.wm.shell.sysui.KeyguardChangeListener
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
@@ -100,7 +95,6 @@
     @Mock private lateinit var mockShellController: ShellController
     @Mock private lateinit var mockShellExecutor: ShellExecutor
     @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
-    @Mock private lateinit var mockRecentsTransitionHandler: RecentsTransitionHandler
     @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
 
     private val transactionFactory = Supplier<SurfaceControl.Transaction> {
@@ -127,7 +121,6 @@
                 mockSyncQueue,
                 mockTransitions,
                 Optional.of(mockDesktopTasksController),
-                mockRecentsTransitionHandler,
                 mockDesktopModeWindowDecorFactory,
                 mockInputMonitorFactory,
                 transactionFactory,
@@ -275,48 +268,6 @@
     }
 
     @Test
-    fun testRelayoutBlockedDuringRecentsTransition() {
-        val recentsCaptor = argumentCaptor<RecentsTransitionStateListener>()
-        verify(mockRecentsTransitionHandler).addTransitionStateListener(recentsCaptor.capture())
-
-        val transition = mock(IBinder::class.java)
-        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
-        val decoration = setUpMockDecorationForTask(task)
-
-        // Make sure a window decorations exists first by launching a freeform task.
-        onTaskOpening(task)
-        // Now call back when a Recents transition starts.
-        recentsCaptor.firstValue.onTransitionStarted(transition)
-
-        verify(decoration).incrementRelayoutBlock()
-        verify(decoration).addTransitionPausingRelayout(transition)
-    }
-
-    @Test
-    fun testRelayoutBlockedDuringKeyguardTransition() {
-        val transition = mock(IBinder::class.java)
-        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
-        val decoration = setUpMockDecorationForTask(task)
-        val transitionInfo = mock(TransitionInfo::class.java)
-        val transitionChange = mock(TransitionInfo.Change::class.java)
-        val taskInfo = mock(RunningTaskInfo()::class.java)
-
-        // Replicate a keyguard going away transition for a task
-        whenever(transitionInfo.getFlags())
-                .thenReturn(WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY)
-        whenever(transitionChange.getMode()).thenReturn(WindowManager.TRANSIT_TO_FRONT)
-        whenever(transitionChange.getTaskInfo()).thenReturn(taskInfo)
-
-        // Make sure a window decorations exists first by launching a freeform task.
-        onTaskOpening(task)
-        // OnTransition ready is called when a keyguard going away transition happens
-        desktopModeWindowDecorViewModel
-                .onTransitionReady(transition, transitionInfo, transitionChange)
-
-        verify(decoration).incrementRelayoutBlock()
-        verify(decoration).addTransitionPausingRelayout(transition)
-    }
-    @Test
     fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() {
         val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true)
         val decoration = setUpMockDecorationForTask(task)
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index 2f28363..77800a3 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -31,6 +31,12 @@
     ],
 }
 
+cc_aconfig_library {
+    name: "backup_flags_cc_lib",
+    host_supported: true,
+    aconfig_declarations: "backup_flags",
+}
+
 cc_defaults {
     name: "libandroidfw_defaults",
     cpp_std: "gnu++2b",
@@ -115,7 +121,10 @@
                 "libutils",
                 "libz",
             ],
-            static_libs: ["libziparchive_for_incfs"],
+            static_libs: [
+                "libziparchive_for_incfs",
+                "backup_flags_cc_lib",
+            ],
             static: {
                 enabled: false,
             },
diff --git a/libs/androidfw/BackupHelpers.cpp b/libs/androidfw/BackupHelpers.cpp
index 1a6a952..a1e7c2f 100644
--- a/libs/androidfw/BackupHelpers.cpp
+++ b/libs/androidfw/BackupHelpers.cpp
@@ -36,6 +36,9 @@
 #include <utils/KeyedVector.h>
 #include <utils/String8.h>
 
+#include <com_android_server_backup.h>
+namespace backup_flags = com::android::server::backup;
+
 namespace android {
 
 #define MAGIC0 0x70616e53 // Snap
@@ -214,7 +217,7 @@
 {
     LOGP("write_update_file %s (%s) : mode 0%o\n", realFilename, key.c_str(), mode);
 
-    const int bufsize = 4*1024;
+    const int bufsize = backup_flags::enable_max_size_writes_to_pipes() ? (64*1024) : (4*1024);
     int err;
     int amt;
     int fileSize;
@@ -550,7 +553,8 @@
     }
 
     // read/write up to this much at a time.
-    const size_t BUFSIZE = 32 * 1024;
+    const size_t BUFSIZE = backup_flags::enable_max_size_writes_to_pipes() ? (64*1024) : (32*1024);
+
     char* buf = (char *)calloc(1,BUFSIZE);
     const size_t PAXHEADER_OFFSET = 512;
     const size_t PAXHEADER_SIZE = 512;
@@ -726,7 +730,7 @@
 
 
 
-#define RESTORE_BUF_SIZE (8*1024)
+const size_t RESTORE_BUF_SIZE = backup_flags::enable_max_size_writes_to_pipes() ? 64*1024 : 8*1024;
 
 RestoreHelperBase::RestoreHelperBase()
 {
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 6c3172a..d58c872 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -56,6 +56,7 @@
 
 bool Properties::debugLayersUpdates = false;
 bool Properties::debugOverdraw = false;
+bool Properties::debugTraceGpuResourceCategories = false;
 bool Properties::showDirtyRegions = false;
 bool Properties::skipEmptyFrames = true;
 bool Properties::useBufferAge = true;
@@ -151,10 +152,12 @@
 
     skpCaptureEnabled = debuggingEnabled && base::GetBoolProperty(PROPERTY_CAPTURE_SKP_ENABLED, false);
 
-    SkAndroidFrameworkTraceUtil::setEnableTracing(
-            base::GetBoolProperty(PROPERTY_SKIA_TRACING_ENABLED, false));
+    bool skiaBroadTracing = base::GetBoolProperty(PROPERTY_SKIA_TRACING_ENABLED, false);
+    SkAndroidFrameworkTraceUtil::setEnableTracing(skiaBroadTracing);
     SkAndroidFrameworkTraceUtil::setUsePerfettoTrackEvents(
             base::GetBoolProperty(PROPERTY_SKIA_USE_PERFETTO_TRACK_EVENTS, false));
+    debugTraceGpuResourceCategories =
+            base::GetBoolProperty(PROPERTY_TRACE_GPU_RESOURCES, skiaBroadTracing);
 
     runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false);
 
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index bca57e9..b956fac 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -143,6 +143,15 @@
 #define PROPERTY_CAPTURE_SKP_ENABLED "debug.hwui.capture_skp_enabled"
 
 /**
+ * Might split Skia's GPU resource utilization into separate tracing tracks (slow).
+ *
+ * Aggregate total and purgeable numbers will still be reported under a "misc" track when this is
+ * disabled, they just won't be split into distinct categories. Results may vary depending on GPU
+ * backend/API, and the category mappings defined in ATraceMemoryDump's hardcoded sResourceMap.
+ */
+#define PROPERTY_TRACE_GPU_RESOURCES "debug.hwui.trace_gpu_resources"
+
+/**
  * Allows broad recording of Skia drawing commands.
  *
  * If disabled, a very minimal set of trace events *may* be recorded.
@@ -254,6 +263,7 @@
 
     static bool debugLayersUpdates;
     static bool debugOverdraw;
+    static bool debugTraceGpuResourceCategories;
     static bool showDirtyRegions;
     // TODO: Remove after stabilization period
     static bool skipEmptyFrames;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index c156c46..72ddecc 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -22,6 +22,13 @@
 }
 
 flag {
+  name: "high_contrast_text_small_text_rect"
+  namespace: "accessibility"
+  description: "Draw a solid rectangle background behind text instead of a stroke outline"
+  bug: "186567103"
+}
+
+flag {
   name: "hdr_10bit_plus"
   namespace: "core_graphics"
   description: "Use 10101010 and FP16 formats for HDR-UI when available"
diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp
index 80b6c03..e9f4b81c 100644
--- a/libs/hwui/hwui/Canvas.cpp
+++ b/libs/hwui/hwui/Canvas.cpp
@@ -18,6 +18,7 @@
 
 #include <SkFontMetrics.h>
 #include <SkRRect.h>
+#include <minikin/MinikinRect.h>
 
 #include "FeatureFlags.h"
 #include "MinikinUtils.h"
@@ -107,7 +108,13 @@
     // care of all alignment.
     paint.setTextAlign(Paint::kLeft_Align);
 
-    DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance());
+    minikin::MinikinRect bounds;
+    // We only need the bounds to draw a rectangular background in high contrast mode. Let's save
+    // the cycles otherwise.
+    if (flags::high_contrast_text_small_text_rect() && isHighContrastText()) {
+        MinikinUtils::getBounds(&paint, bidiFlags, typeface, text, textSize, &bounds);
+    }
+    DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance(), bounds);
     MinikinUtils::forFontRun(layout, &paint, f);
 
     if (text_feature::fix_double_underline()) {
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index 8f99990..ba65439 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -33,6 +33,8 @@
 
 namespace android {
 
+inline constexpr int kHighContrastTextBorderWidth = 4;
+
 static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness,
                               const Paint& paint, Canvas* canvas) {
     const SkScalar strokeWidth = fmax(thickness, 1.0f);
@@ -45,15 +47,26 @@
     paint->setShader(nullptr);
     paint->setColorFilter(nullptr);
     paint->setLooper(nullptr);
-    paint->setStrokeWidth(4 + 0.04 * paint->getSkFont().getSize());
+    paint->setStrokeWidth(kHighContrastTextBorderWidth + 0.04 * paint->getSkFont().getSize());
     paint->setStrokeJoin(SkPaint::kRound_Join);
     paint->setLooper(nullptr);
 }
 
 class DrawTextFunctor {
 public:
+    /**
+     * Creates a Functor to draw the given text layout.
+     *
+     * @param layout
+     * @param canvas
+     * @param paint
+     * @param x
+     * @param y
+     * @param totalAdvance
+     * @param bounds bounds of the text. Only required if high contrast text mode is enabled.
+     */
     DrawTextFunctor(const minikin::Layout& layout, Canvas* canvas, const Paint& paint, float x,
-                    float y, float totalAdvance)
+                    float y, float totalAdvance, const minikin::MinikinRect& bounds)
             : layout(layout)
             , canvas(canvas)
             , paint(paint)
@@ -61,7 +74,8 @@
             , y(y)
             , totalAdvance(totalAdvance)
             , underlinePosition(0)
-            , underlineThickness(0) {}
+            , underlineThickness(0)
+            , bounds(bounds) {}
 
     void operator()(size_t start, size_t end) {
         auto glyphFunc = [&](uint16_t* text, float* positions) {
@@ -91,7 +105,16 @@
             Paint outlinePaint(paint);
             simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint);
             outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style);
-            canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
+            if (flags::high_contrast_text_small_text_rect()) {
+                auto bgBounds(bounds);
+                auto padding = kHighContrastTextBorderWidth + 0.1f * paint.getSkFont().getSize();
+                bgBounds.offset(x, y);
+                canvas->drawRect(bgBounds.mLeft - padding, bgBounds.mTop - padding,
+                                 bgBounds.mRight + padding, bgBounds.mBottom + padding,
+                                 outlinePaint);
+            } else {
+                canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
+            }
 
             // inner
             gDrawTextBlobMode = DrawTextBlobMode::HctInner;
@@ -146,6 +169,7 @@
     float totalAdvance;
     float underlinePosition;
     float underlineThickness;
+    const minikin::MinikinRect& bounds;
 };
 
 }  // namespace android
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 7552b56d..833069f 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -96,7 +96,7 @@
 float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags,
                                 const Typeface* typeface, const uint16_t* buf, size_t start,
                                 size_t count, size_t bufSize, float* advances,
-                                minikin::MinikinRect* bounds) {
+                                minikin::MinikinRect* bounds, uint32_t* clusterCount) {
     minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface);
     const minikin::U16StringPiece textBuf(buf, bufSize);
     const minikin::Range range(start, start + count);
@@ -104,7 +104,7 @@
     const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
 
     return minikin::Layout::measureText(textBuf, range, bidiFlags, minikinPaint, startHyphen,
-                                        endHyphen, advances, bounds);
+                                        endHyphen, advances, bounds, clusterCount);
 }
 
 minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin::Bidi bidiFlags,
diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h
index 61bc881..f8574ee 100644
--- a/libs/hwui/hwui/MinikinUtils.h
+++ b/libs/hwui/hwui/MinikinUtils.h
@@ -53,7 +53,7 @@
 
     static float measureText(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface,
                              const uint16_t* buf, size_t start, size_t count, size_t bufSize,
-                             float* advances, minikin::MinikinRect* bounds);
+                             float* advances, minikin::MinikinRect* bounds, uint32_t* clusterCount);
 
     static minikin::MinikinExtent getFontExtent(const Paint* paint, minikin::Bidi bidiFlags,
                                                 const Typeface* typeface, const uint16_t* buf,
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 7cc4866..8315c4c 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -247,6 +247,9 @@
 static jfieldID gFontMetricsInt_bottom;
 static jfieldID gFontMetricsInt_leading;
 
+static jclass gRunInfo_class;
+static jfieldID gRunInfo_clusterCount;
+
 ///////////////////////////////////////////////////////////////////////////////
 
 void GraphicsJNI::get_jrect(JNIEnv* env, jobject obj, int* L, int* T, int* R, int* B)
@@ -511,6 +514,10 @@
     return descent - ascent + leading;
 }
 
+void GraphicsJNI::set_cluster_count_to_run_info(JNIEnv* env, jobject runInfo, jint clusterCount) {
+    env->SetIntField(runInfo, gRunInfo_clusterCount, clusterCount);
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////
 
 jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, BitmapRegionDecoderWrapper* bitmap) {
@@ -834,5 +841,10 @@
     gFontMetricsInt_bottom = GetFieldIDOrDie(env, gFontMetricsInt_class, "bottom", "I");
     gFontMetricsInt_leading = GetFieldIDOrDie(env, gFontMetricsInt_class, "leading", "I");
 
+    gRunInfo_class = FindClassOrDie(env, "android/graphics/Paint$RunInfo");
+    gRunInfo_class = MakeGlobalRefOrDie(env, gRunInfo_class);
+
+    gRunInfo_clusterCount = GetFieldIDOrDie(env, gRunInfo_class, "mClusterCount", "I");
+
     return 0;
 }
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index b9fff36..b0a1074 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -77,6 +77,8 @@
     static SkRect* jrect_to_rect(JNIEnv*, jobject jrect, SkRect*);
     static void rect_to_jrectf(const SkRect&, JNIEnv*, jobject jrectf);
 
+    static void set_cluster_count_to_run_info(JNIEnv* env, jobject runInfo, jint clusterCount);
+
     static void set_jpoint(JNIEnv*, jobject jrect, int x, int y);
 
     static SkIPoint* jpoint_to_ipoint(JNIEnv*, jobject jpoint, SkIPoint* point);
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index d84b73d..58d9d8b 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -114,7 +114,7 @@
 
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(&paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, 0,
-                                  count, count, advancesArray.get(), nullptr);
+                                  count, count, advancesArray.get(), nullptr, nullptr);
 
         for (int i = 0; i < count; i++) {
             // traverse in the given direction
@@ -206,7 +206,7 @@
         }
         const float advance = MinikinUtils::measureText(
                 paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, start, count,
-                contextCount, advancesArray.get(), nullptr);
+                contextCount, advancesArray.get(), nullptr, nullptr);
         if (advances) {
             env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get());
         }
@@ -244,7 +244,7 @@
         minikin::Bidi bidiFlags = dir == 1 ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(paint, bidiFlags, typeface, text, start, count, start + count,
-                                  advancesArray.get(), nullptr);
+                                  advancesArray.get(), nullptr, nullptr);
         size_t result = minikin::GraphemeBreak::getTextRunCursor(advancesArray.get(), text,
                 start, count, offset, moveOpt);
         return static_cast<jint>(result);
@@ -508,7 +508,7 @@
     static jfloat doRunAdvance(JNIEnv* env, const Paint* paint, const Typeface* typeface,
                                const jchar buf[], jint start, jint count, jint bufSize,
                                jboolean isRtl, jint offset, jfloatArray advances,
-                               jint advancesIndex, SkRect* drawBounds) {
+                               jint advancesIndex, SkRect* drawBounds, uint32_t* clusterCount) {
         if (advances) {
             size_t advancesLength = env->GetArrayLength(advances);
             if ((size_t)(count + advancesIndex) > advancesLength) {
@@ -519,9 +519,9 @@
         minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
         minikin::MinikinRect bounds;
         if (offset == start + count && advances == nullptr) {
-            float result =
-                    MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
-                                              bufSize, nullptr, drawBounds ? &bounds : nullptr);
+            float result = MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
+                                                     bufSize, nullptr,
+                                                     drawBounds ? &bounds : nullptr, clusterCount);
             if (drawBounds) {
                 copyMinikinRectToSkRect(bounds, drawBounds);
             }
@@ -529,7 +529,8 @@
         }
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
-                                  advancesArray.get(), drawBounds ? &bounds : nullptr);
+                                  advancesArray.get(), drawBounds ? &bounds : nullptr,
+                                  clusterCount);
 
         if (drawBounds) {
             copyMinikinRectToSkRect(bounds, drawBounds);
@@ -549,7 +550,7 @@
         ScopedCharArrayRO textArray(env, text);
         jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
                                      start - contextStart, end - start, contextEnd - contextStart,
-                                     isRtl, offset - contextStart, nullptr, 0, nullptr);
+                                     isRtl, offset - contextStart, nullptr, 0, nullptr, nullptr);
         return result;
     }
 
@@ -558,27 +559,41 @@
                                                         jint contextStart, jint contextEnd,
                                                         jboolean isRtl, jint offset,
                                                         jfloatArray advances, jint advancesIndex,
-                                                        jobject drawBounds) {
+                                                        jobject drawBounds, jobject runInfo) {
         const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
         const Typeface* typeface = paint->getAndroidTypeface();
         ScopedCharArrayRO textArray(env, text);
         SkRect skDrawBounds;
+        uint32_t clusterCount = 0;
         jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
                                      start - contextStart, end - start, contextEnd - contextStart,
                                      isRtl, offset - contextStart, advances, advancesIndex,
-                                     drawBounds ? &skDrawBounds : nullptr);
+                                     drawBounds ? &skDrawBounds : nullptr, &clusterCount);
         if (drawBounds != nullptr) {
             GraphicsJNI::rect_to_jrectf(skDrawBounds, env, drawBounds);
         }
+        if (runInfo) {
+            GraphicsJNI::set_cluster_count_to_run_info(env, runInfo, clusterCount);
+        }
         return result;
     }
 
+    // This method is kept for old Robolectric JNI signature used by SystemUIGoogleRoboRNGTests.
+    static jfloat getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric(
+            JNIEnv* env, jclass cls, jlong paintHandle, jcharArray text, jint start, jint end,
+            jint contextStart, jint contextEnd, jboolean isRtl, jint offset, jfloatArray advances,
+            jint advancesIndex, jobject drawBounds) {
+        return getRunCharacterAdvance___CIIIIZI_FI_F(env, cls, paintHandle, text, start, end,
+                                                     contextStart, contextEnd, isRtl, offset,
+                                                     advances, advancesIndex, drawBounds, nullptr);
+    }
+
     static jint doOffsetForAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[],
             jint start, jint count, jint bufSize, jboolean isRtl, jfloat advance) {
         minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
-                                  advancesArray.get(), nullptr);
+                                  advancesArray.get(), nullptr, nullptr);
         return minikin::getOffsetForAdvance(advancesArray.get(), buf, start, count, advance);
     }
 
@@ -1145,8 +1160,11 @@
          (void*)PaintGlue::getCharArrayBounds},
         {"nHasGlyph", "(JILjava/lang/String;)Z", (void*)PaintGlue::hasGlyph},
         {"nGetRunAdvance", "(J[CIIIIZI)F", (void*)PaintGlue::getRunAdvance___CIIIIZI_F},
-        {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
+        {"nGetRunCharacterAdvance",
+         "(J[CIIIIZI[FILandroid/graphics/RectF;Landroid/graphics/Paint$RunInfo;)F",
          (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
+        {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
+         (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric},
         {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
         {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
          (void*)PaintGlue::getFontMetricsIntForText___C},
diff --git a/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp b/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp
index 234f42d..756b937 100644
--- a/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp
+++ b/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp
@@ -20,6 +20,8 @@
 
 #include <cstring>
 
+#include "GrDirectContext.h"
+
 namespace android {
 namespace uirenderer {
 namespace skiapipeline {
@@ -114,8 +116,16 @@
 
 /**
  * logTraces reads from mCurrentValues and logs the counters with ATRACE.
+ *
+ * gpuMemoryIsAlreadyInDump must be true if GrDirectContext::dumpMemoryStatistics(...) was called
+ * with this tracer, false otherwise. Leaving this false allows this function to quickly query total
+ * and purgable GPU memory without the caller having to spend time in
+ * GrDirectContext::dumpMemoryStatistics(...) first, which iterates over every resource in the GPU
+ * cache. This can save significant time, but buckets all GPU memory into a single "misc" track,
+ * which may be a loss of granularity depending on the GPU backend and the categories defined in
+ * sResourceMap.
  */
-void ATraceMemoryDump::logTraces() {
+void ATraceMemoryDump::logTraces(bool gpuMemoryIsAlreadyInDump, GrDirectContext* grContext) {
     // Accumulate data from last dumpName
     recordAndResetCountersIfNeeded("");
     uint64_t hwui_all_frame_memory = 0;
@@ -126,6 +136,20 @@
             ATRACE_INT64((std::string("Purgeable ") + it.first).c_str(), it.second.purgeableMemory);
         }
     }
+
+    if (!gpuMemoryIsAlreadyInDump && grContext) {
+        // Total GPU memory
+        int gpuResourceCount;
+        size_t gpuResourceBytes;
+        grContext->getResourceCacheUsage(&gpuResourceCount, &gpuResourceBytes);
+        hwui_all_frame_memory += (uint64_t)gpuResourceBytes;
+        ATRACE_INT64("HWUI Misc Memory", gpuResourceBytes);
+
+        // Purgable subset of GPU memory
+        size_t purgeableGpuResourceBytes = grContext->getResourceCachePurgeableBytes();
+        ATRACE_INT64("Purgeable HWUI Misc Memory", purgeableGpuResourceBytes);
+    }
+
     ATRACE_INT64("HWUI All Memory", hwui_all_frame_memory);
 }
 
diff --git a/libs/hwui/pipeline/skia/ATraceMemoryDump.h b/libs/hwui/pipeline/skia/ATraceMemoryDump.h
index 4592711..777d1a2 100644
--- a/libs/hwui/pipeline/skia/ATraceMemoryDump.h
+++ b/libs/hwui/pipeline/skia/ATraceMemoryDump.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <GrDirectContext.h>
 #include <SkString.h>
 #include <SkTraceMemoryDump.h>
 
@@ -50,7 +51,7 @@
 
     void startFrame();
 
-    void logTraces();
+    void logTraces(bool gpuMemoryIsAlreadyInDump, GrDirectContext* grContext);
 
 private:
     std::string mLastDumpName;
diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp
index 30d4612..eb4d494 100644
--- a/libs/hwui/renderthread/CacheManager.cpp
+++ b/libs/hwui/renderthread/CacheManager.cpp
@@ -269,13 +269,14 @@
     cancelDestroyContext();
     mFrameCompletions.next() = systemTime(CLOCK_MONOTONIC);
     if (ATRACE_ENABLED()) {
+        ATRACE_NAME("dumpingMemoryStatistics");
         static skiapipeline::ATraceMemoryDump tracer;
         tracer.startFrame();
         SkGraphics::DumpMemoryStatistics(&tracer);
-        if (mGrContext) {
+        if (mGrContext && Properties::debugTraceGpuResourceCategories) {
             mGrContext->dumpMemoryStatistics(&tracer);
         }
-        tracer.logTraces();
+        tracer.logTraces(Properties::debugTraceGpuResourceCategories, mGrContext.get());
     }
 }
 
diff --git a/libs/hwui/tests/unit/UnderlineTest.cpp b/libs/hwui/tests/unit/UnderlineTest.cpp
index c70a304..9911bfa 100644
--- a/libs/hwui/tests/unit/UnderlineTest.cpp
+++ b/libs/hwui/tests/unit/UnderlineTest.cpp
@@ -103,8 +103,9 @@
     // Create minikin::Layout
     std::unique_ptr<Typeface> typeface(makeTypeface());
     minikin::Layout layout = doLayout(text, *paint, typeface.get());
+    minikin::MinikinRect bounds;
 
-    DrawTextFunctor f(layout, &canvas, *paint, 0, 0, layout.getAdvance());
+    DrawTextFunctor f(layout, &canvas, *paint, 0, 0, layout.getAdvance(), bounds);
     MinikinUtils::forFontRun(layout, paint, f);
     return f;
 }
diff --git a/lint-baseline.xml b/lint-baseline.xml
index 79b2155..660884a 100644
--- a/lint-baseline.xml
+++ b/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="7.1.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NonUserGetterCalled"
@@ -433,7 +433,7 @@
     <issue
         id="NonUserGetterCalled"
         message="`android.provider.Settings.System#getInt()` called from system process. Please call `android.provider.Settings.System#getIntForUser()` instead. "
-        errorLine1="        boolean cap = System.getInt(resolver, System.TEXT_AUTO_CAPS, 1) > 0;"
+        errorLine1="        boolean cap = System.getInt(resolver, System.TEXT_AUTO_CAPS, 1) &gt; 0;"
         errorLine2="                             ~~~~~~">
         <location
             file="frameworks/base/core/java/android/text/method/TextKeyListener.java"
@@ -444,7 +444,7 @@
     <issue
         id="NonUserGetterCalled"
         message="`android.provider.Settings.System#getInt()` called from system process. Please call `android.provider.Settings.System#getIntForUser()` instead. "
-        errorLine1="        boolean text = System.getInt(resolver, System.TEXT_AUTO_REPLACE, 1) > 0;"
+        errorLine1="        boolean text = System.getInt(resolver, System.TEXT_AUTO_REPLACE, 1) &gt; 0;"
         errorLine2="                              ~~~~~~">
         <location
             file="frameworks/base/core/java/android/text/method/TextKeyListener.java"
@@ -455,7 +455,7 @@
     <issue
         id="NonUserGetterCalled"
         message="`android.provider.Settings.System#getInt()` called from system process. Please call `android.provider.Settings.System#getIntForUser()` instead. "
-        errorLine1="        boolean period = System.getInt(resolver, System.TEXT_AUTO_PUNCTUATE, 1) > 0;"
+        errorLine1="        boolean period = System.getInt(resolver, System.TEXT_AUTO_PUNCTUATE, 1) &gt; 0;"
         errorLine2="                                ~~~~~~">
         <location
             file="frameworks/base/core/java/android/text/method/TextKeyListener.java"
@@ -466,7 +466,7 @@
     <issue
         id="NonUserGetterCalled"
         message="`android.provider.Settings.System#getInt()` called from system process. Please call `android.provider.Settings.System#getIntForUser()` instead. "
-        errorLine1="        boolean pw = System.getInt(resolver, System.TEXT_SHOW_PASSWORD, 1) > 0;"
+        errorLine1="        boolean pw = System.getInt(resolver, System.TEXT_SHOW_PASSWORD, 1) &gt; 0;"
         errorLine2="                            ~~~~~~">
         <location
             file="frameworks/base/core/java/android/text/method/TextKeyListener.java"
@@ -562,4 +562,4 @@
             column="74"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/location/api/current.txt b/location/api/current.txt
index 0c23d8c..c55676b 100644
--- a/location/api/current.txt
+++ b/location/api/current.txt
@@ -414,7 +414,7 @@
     field public static final int TYPE_GPS_L5CNAV = 259; // 0x103
     field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L1 = 1795; // 0x703
     field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L5 = 1794; // 0x702
-    field @Deprecated public static final int TYPE_IRN_L5CA = 1793; // 0x701
+    field public static final int TYPE_IRN_L5CA = 1793; // 0x701
     field public static final int TYPE_QZS_L1CA = 1025; // 0x401
     field public static final int TYPE_SBS = 513; // 0x201
     field public static final int TYPE_UNKNOWN = 0; // 0x0
diff --git a/location/java/android/location/GnssNavigationMessage.java b/location/java/android/location/GnssNavigationMessage.java
index 5e3f803..7a667ae 100644
--- a/location/java/android/location/GnssNavigationMessage.java
+++ b/location/java/android/location/GnssNavigationMessage.java
@@ -78,9 +78,7 @@
     public static final int TYPE_GAL_F = 0x0602;
     /**
      * NavIC L5 C/A message contained in the structure.
-     * @deprecated deprecated.
      */
-    @Deprecated
     public static final int TYPE_IRN_L5CA = 0x0701;
     /** NavIC L5 message contained in the structure. */
     @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1)
diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
index ee2510f..0d5af50 100644
--- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
+++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
@@ -19,6 +19,7 @@
 import android.Manifest;
 import android.annotation.RequiresPermission;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.location.LocationManager;
 import android.os.SystemClock;
 import android.telephony.TelephonyCallback;
@@ -26,6 +27,8 @@
 import android.telephony.emergency.EmergencyNumber;
 import android.util.Log;
 
+import com.android.internal.telephony.flags.Flags;
+
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -139,8 +142,20 @@
                 (mCallEndElapsedRealtimeMillis > 0)
                         && ((SystemClock.elapsedRealtime() - mCallEndElapsedRealtimeMillis)
                         < emergencyExtensionMillis);
-        boolean isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
-        boolean isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+        boolean isInEmergencyCallback = false;
+        boolean isInEmergencySmsMode = false;
+        if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+            isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+            isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+        } else {
+            PackageManager pm = mContext.getPackageManager();
+            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
+                isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+            }
+            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
+                isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+            }
+        }
         return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
                 || isInEmergencySmsMode;
     }
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index a5a69f9..4918289 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -21,6 +21,7 @@
 import static android.content.Context.DEVICE_ID_DEFAULT;
 import static android.media.audio.Flags.autoPublicVolumeApiHardening;
 import static android.media.audio.Flags.automaticBtDeviceType;
+import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
 import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
 import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
@@ -10081,6 +10082,28 @@
         }
     }
 
+    /**
+     * @hide
+     * Checks whether a notification sound should be played or not, as reported by the state
+     * of the audio framework. Querying whether playback should proceed is favored over
+     * playing and letting the sound be muted or not.
+     * @param aa the {@link AudioAttributes} of the notification about to maybe play
+     * @return true if the audio framework state is such that the notification should be played
+     *    because at time of checking, and the notification will be heard,
+     *    false otherwise
+     */
+    @TestApi
+    @FlaggedApi(FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING)
+    @RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE)
+    public boolean shouldNotificationSoundPlay(@NonNull final AudioAttributes aa) {
+        final IAudioService service = getService();
+        try {
+            return service.shouldNotificationSoundPlay(Objects.requireNonNull(aa));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     //====================================================================
     // Mute await connection
 
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 5c268d4..2eec9b3 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -775,4 +775,8 @@
     @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
     FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss();
+
+    @EnforcePermission("QUERY_AUDIO_STATE")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE)")
+    boolean shouldNotificationSoundPlay(in AudioAttributes aa);
 }
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index b7c97208..470a8ac 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -3401,13 +3401,15 @@
         }
 
         /**
-         * Set a harware graphic buffer to this queue request. Exactly one buffer must
+         * Set a hardware graphic buffer to this queue request. Exactly one buffer must
          * be set for a queue request before calling {@link #queue}.
          * <p>
          * Note: buffers should have format {@link HardwareBuffer#YCBCR_420_888},
          * a single layer, and an appropriate usage ({@link HardwareBuffer#USAGE_CPU_READ_OFTEN}
          * for software codecs and {@link HardwareBuffer#USAGE_VIDEO_ENCODE} for hardware)
-         * for codecs to recognize.  Codecs may throw exception if the buffer is not recognizable.
+         * for codecs to recognize. Format {@link ImageFormat#PRIVATE} together with
+         * usage {@link HardwareBuffer#USAGE_VIDEO_ENCODE} will also work for hardware codecs.
+         * Codecs may throw exception if the buffer is not recognizable.
          *
          * @param buffer The hardware graphic buffer object
          * @return this object
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 0eabe66..838630f 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -943,6 +943,10 @@
                         .append(getId())
                         .append(", name=")
                         .append(getName())
+                        .append(", type=")
+                        .append(getDeviceTypeString(getType()))
+                        .append(", isSystem=")
+                        .append(isSystemRoute())
                         .append(", features=")
                         .append(getFeatures())
                         .append(", iconUri=")
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 89792c7..9616b5d 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -48,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -141,7 +142,9 @@
      * dispatch. This is only used to determine what callback a route should be assigned to (added,
      * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}.
      */
-    private volatile ArrayMap<String, MediaRoute2Info> mPreviousRoutes = new ArrayMap<>();
+    private volatile ArrayMap<String, MediaRoute2Info> mPreviousFilteredRoutes = new ArrayMap<>();
+
+    private final Map<String, MediaRoute2Info> mPreviousUnfilteredRoutes = new ArrayMap<>();
 
     /**
      * Stores the latest copy of exposed routes after filtering, sorting, and deduplication. Can be
@@ -282,6 +285,8 @@
             MediaRouter2 instance = sAppToProxyRouterMap.get(key);
             if (instance == null) {
                 instance = new MediaRouter2(context, looper, clientPackageName, user);
+                // Register proxy router after instantiation to avoid race condition.
+                ((ProxyMediaRouter2Impl) instance.mImpl).registerProxyRouter();
                 sAppToProxyRouterMap.put(key, instance);
             }
             return instance;
@@ -368,6 +373,7 @@
                 new SystemRoutingController(
                         ProxyMediaRouter2Impl.getSystemSessionInfoImpl(
                                 mMediaRouterService, clientPackageName));
+
         mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user);
     }
 
@@ -713,7 +719,7 @@
         mImpl.transfer(
                 controller.getRoutingSessionInfo(),
                 route,
-                android.os.Process.myUserHandle(),
+                Process.myUserHandle(),
                 mContext.getPackageName());
     }
 
@@ -883,7 +889,7 @@
                 newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet());
 
         for (MediaRoute2Info route : newRoutes) {
-            MediaRoute2Info prevRoute = mPreviousRoutes.get(route.getId());
+            MediaRoute2Info prevRoute = mPreviousFilteredRoutes.get(route.getId());
             if (prevRoute == null) {
                 addedRoutes.add(route);
             } else if (!prevRoute.equals(route)) {
@@ -891,21 +897,21 @@
             }
         }
 
-        for (int i = 0; i < mPreviousRoutes.size(); i++) {
-            if (!newRouteIds.contains(mPreviousRoutes.keyAt(i))) {
-                removedRoutes.add(mPreviousRoutes.valueAt(i));
+        for (int i = 0; i < mPreviousFilteredRoutes.size(); i++) {
+            if (!newRouteIds.contains(mPreviousFilteredRoutes.keyAt(i))) {
+                removedRoutes.add(mPreviousFilteredRoutes.valueAt(i));
             }
         }
 
         // update previous routes
         for (MediaRoute2Info route : removedRoutes) {
-            mPreviousRoutes.remove(route.getId());
+            mPreviousFilteredRoutes.remove(route.getId());
         }
         for (MediaRoute2Info route : addedRoutes) {
-            mPreviousRoutes.put(route.getId(), route);
+            mPreviousFilteredRoutes.put(route.getId(), route);
         }
         for (MediaRoute2Info route : changedRoutes) {
-            mPreviousRoutes.put(route.getId(), route);
+            mPreviousFilteredRoutes.put(route.getId(), route);
         }
 
         if (!addedRoutes.isEmpty()) {
@@ -924,6 +930,27 @@
         }
     }
 
+    void dispatchControllerUpdatedIfNeededOnHandler(Map<String, MediaRoute2Info> routesMap) {
+        List<RoutingController> controllers = getControllers();
+        for (RoutingController controller : controllers) {
+
+            for (String selectedRoute : controller.getRoutingSessionInfo().getSelectedRoutes()) {
+                if (routesMap.containsKey(selectedRoute)
+                        && mPreviousUnfilteredRoutes.containsKey(selectedRoute)) {
+                    MediaRoute2Info currentRoute = routesMap.get(selectedRoute);
+                    MediaRoute2Info oldRoute = mPreviousUnfilteredRoutes.get(selectedRoute);
+                    if (!currentRoute.equals(oldRoute)) {
+                        notifyControllerUpdated(controller);
+                        break;
+                    }
+                }
+            }
+        }
+
+        mPreviousUnfilteredRoutes.clear();
+        mPreviousUnfilteredRoutes.putAll(routesMap);
+    }
+
     void updateRoutesOnHandler(List<MediaRoute2Info> newRoutes) {
         synchronized (mLock) {
             mRoutes.clear();
@@ -945,6 +972,11 @@
                         MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler,
                         this,
                         mFilteredRoutes));
+        mHandler.sendMessage(
+                obtainMessage(
+                        MediaRouter2::dispatchControllerUpdatedIfNeededOnHandler,
+                        this,
+                        new HashMap<>(mRoutes)));
     }
 
     /**
@@ -1528,7 +1560,7 @@
             UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
             String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
 
-            return Objects.equals(android.os.Process.myUserHandle(), transferInitiatorUserHandle)
+            return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
                     && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
         }
 
@@ -2153,18 +2185,19 @@
             mClientUser = user;
             mClientPackageName = clientPackageName;
             mClient = new Client();
+            mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
+        }
 
+        public void registerProxyRouter() {
             try {
                 mMediaRouterService.registerProxyRouter(
                         mClient,
-                        context.getApplicationContext().getPackageName(),
-                        clientPackageName,
-                        user);
+                        mContext.getApplicationContext().getPackageName(),
+                        mClientPackageName,
+                        mClientUser);
             } catch (RemoteException ex) {
                 throw ex.rethrowFromSystemServer();
             }
-
-            mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
         }
 
         @Override
@@ -2294,11 +2327,7 @@
 
             List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
             RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
-            transfer(
-                    targetSession,
-                    route,
-                    android.os.Process.myUserHandle(),
-                    mContext.getPackageName());
+            transfer(targetSession, route, Process.myUserHandle(), mContext.getPackageName());
         }
 
         @Override
@@ -3165,8 +3194,12 @@
                 return;
             }
 
-            requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE,
-                    android.os.Process.myUserHandle(), mContext.getPackageName());
+            requestCreateController(
+                    controller,
+                    route,
+                    MANAGER_REQUEST_ID_NONE,
+                    Process.myUserHandle(),
+                    mContext.getPackageName());
         }
 
         @Override
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index a7ec6c6..8ce1b6d 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -37,31 +37,41 @@
     const String EXTRA_PACKAGE_REUSING_GRANTED_CONSENT =
             "extra_media_projection_package_reusing_consent";
 
+    /**
+     * Returns whether a combination of process UID and package has the projection permission.
+     *
+     * @param processUid the process UID as returned by {@link android.os.Process.myUid()}.
+     */
     @UnsupportedAppUsage
-    boolean hasProjectionPermission(int uid, String packageName);
+    boolean hasProjectionPermission(int processUid, String packageName);
 
     /**
      * Returns a new {@link IMediaProjection} instance associated with the given package.
+     *
+     * @param processUid the process UID as returned by {@link android.os.Process.myUid()}.
      */
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    IMediaProjection createProjection(int uid, String packageName, int type,
+    IMediaProjection createProjection(int processUid, String packageName, int type,
             boolean permanentGrant);
 
     /**
      * Returns the current {@link IMediaProjection} instance associated with the given
-     * package, or {@code null} if it is not possible to re-use the current projection.
+     * package and process UID, or {@code null} if it is not possible to re-use the current
+     * projection.
      *
      * <p>Should only be invoked when the user has reviewed consent for a re-used projection token.
      * Requires that there is a prior session waiting for the user to review consent, and the given
      * package details match those on the current projection.
      *
      * @see {@link #isCurrentProjection}
+     *
+     * @param processUid the process UID as returned by {@link android.os.Process.myUid()}.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    IMediaProjection getProjection(int uid, String packageName);
+    IMediaProjection getProjection(int processUid, String packageName);
 
     /**
      * Returns {@code true} if the given {@link IMediaProjection} corresponds to the current
@@ -162,8 +172,8 @@
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid               The uid of the process requesting consent to capture, may be an app or
-     *                              SystemUI.
+     * @param hostProcessUid        The uid of the process requesting consent to capture, may be an
+     *                              app or SystemUI.
      * @param sessionCreationSource Only set if the state is MEDIA_PROJECTION_STATE_INITIATED.
      *                              Indicates the entry point for requesting the permission. Must be
      *                              a valid state defined
@@ -172,49 +182,49 @@
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyPermissionRequestInitiated(int hostUid, int sessionCreationSource);
+    oneway void notifyPermissionRequestInitiated(int hostProcessUid, int sessionCreationSource);
 
     /**
      * Notifies system server that the permission request was displayed.
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid The uid of the process requesting consent to capture, may be an app or
-     *                SystemUI.
+     * @param hostProcessUid The uid of the process requesting consent to capture, may be an app or
+     *                       SystemUI.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyPermissionRequestDisplayed(int hostUid);
+    oneway void notifyPermissionRequestDisplayed(int hostProcessUid);
 
     /**
      * Notifies system server that the permission request was cancelled.
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid The uid of the process requesting consent to capture, may be an app or
-     *                SystemUI.
+     * @param hostProcessUid The uid of the process requesting consent to capture, may be an app or
+     *                       SystemUI.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyPermissionRequestCancelled(int hostUid);
+    oneway void notifyPermissionRequestCancelled(int hostProcessUid);
 
     /**
      * Notifies system server that the app selector was displayed.
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid The uid of the process requesting consent to capture, may be an app or
-     *                SystemUI.
+     * @param hostProcessUid The uid of the process requesting consent to capture, may be an app or
+     *                       SystemUI.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyAppSelectorDisplayed(int hostUid);
+    oneway void notifyAppSelectorDisplayed(int hostProcessUid);
 
     @EnforcePermission("MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    void notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode);
+    void notifyWindowingModeChanged(int contentToRecord, int targetProcessUid, int windowingMode);
 }
diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java
index 60497fe..47637b8 100644
--- a/media/java/android/media/session/PlaybackState.java
+++ b/media/java/android/media/session/PlaybackState.java
@@ -15,7 +15,10 @@
  */
 package android.media.session;
 
+import static com.android.media.flags.Flags.FLAG_ENABLE_NOTIFYING_ACTIVITY_MANAGER_WITH_MEDIA_SESSION_STATUS_CHANGE;
+
 import android.annotation.DrawableRes;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.LongDef;
 import android.annotation.Nullable;
@@ -189,7 +192,8 @@
      */
     @IntDef({STATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING,
             STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING,
-            STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM})
+            STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM,
+            STATE_PLAYBACK_SUPPRESSED})
     @Retention(RetentionPolicy.SOURCE)
     public @interface State {}
 
@@ -286,6 +290,19 @@
     public static final int STATE_SKIPPING_TO_QUEUE_ITEM = 11;
 
     /**
+     * State indicating that playback is paused due to an external transient interruption, like a
+     * phone call.
+     *
+     * <p>This state is different from {@link #STATE_PAUSED} in that it is deemed transitory,
+     * possibly allowing the service associated to the session in this state to run in the
+     * foreground.
+     *
+     * @see Builder#setState
+     */
+    @FlaggedApi(FLAG_ENABLE_NOTIFYING_ACTIVITY_MANAGER_WITH_MEDIA_SESSION_STATUS_CHANGE)
+    public static final int STATE_PLAYBACK_SUPPRESSED = 12;
+
+    /**
      * Use this value for the position to indicate the position is not known.
      */
     public static final long PLAYBACK_POSITION_UNKNOWN = -1;
@@ -384,6 +401,7 @@
      * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li>
      * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li>
      * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li>
+     * <li> {@link PlaybackState#STATE_PLAYBACK_SUPPRESSED}</li>
      * </ul>
      */
     @State
@@ -507,6 +525,7 @@
      * <li>{@link #STATE_SKIPPING_TO_NEXT}</li>
      * <li>{@link #STATE_SKIPPING_TO_PREVIOUS}</li>
      * <li>{@link #STATE_SKIPPING_TO_QUEUE_ITEM}</li>
+     * <li>{@link #STATE_PLAYBACK_SUPPRESSED}</li>
      * </ul>
      */
     public boolean isActive() {
@@ -519,33 +538,12 @@
             case PlaybackState.STATE_BUFFERING:
             case PlaybackState.STATE_CONNECTING:
             case PlaybackState.STATE_PLAYING:
+            case PlaybackState.STATE_PLAYBACK_SUPPRESSED:
                 return true;
         }
         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
@@ -586,6 +584,8 @@
                 return "SKIPPING_TO_NEXT";
             case STATE_SKIPPING_TO_QUEUE_ITEM:
                 return "SKIPPING_TO_QUEUE_ITEM";
+            case STATE_PLAYBACK_SUPPRESSED:
+                return "STATE_PLAYBACK_SUPPRESSED";
             default:
                 return "UNKNOWN";
         }
@@ -823,6 +823,7 @@
          * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li>
          * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li>
          * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li>
+         * <li> {@link PlaybackState#STATE_PLAYBACK_SUPPRESSED}</li>
          * </ul>
          *
          * @param state The current state of playback.
@@ -867,6 +868,7 @@
          * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li>
          * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li>
          * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li>
+         * <li> {@link PlaybackState#STATE_PLAYBACK_SUPPRESSED}</li>
          * </ul>
          *
          * @param state The current state of playback.
diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
index 262f5f1..096e8ad 100644
--- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
+++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
@@ -50,8 +50,16 @@
     private static final String TAG = "BluetoothMidiDevice";
     private static final boolean DEBUG = false;
 
-    private static final int DEFAULT_PACKET_SIZE = 20;
-    private static final int MAX_PACKET_SIZE = 512;
+    // Bluetooth services should subtract 5 bytes from the MTU for headers.
+    private static final int HEADER_SIZE = 5;
+    // Min MTU size for BLE
+    private static final int MIN_L2CAP_MTU = 23;
+    // 23 (min L2CAP MTU) - 5 (header size)
+    private static final int DEFAULT_PACKET_SIZE = MIN_L2CAP_MTU - HEADER_SIZE;
+    // Max MTU size on Android
+    private static final int MAX_ANDROID_MTU = 517;
+    // 517 (max Android MTU) - 5 (header size)
+    private static final int MAX_PACKET_SIZE = MAX_ANDROID_MTU - HEADER_SIZE;
 
     //  Bluetooth MIDI Gatt service UUID
     private static final UUID MIDI_SERVICE = UUID.fromString(
@@ -135,8 +143,8 @@
                         // switch to receiving notifications
                         mBluetoothGatt.readCharacteristic(characteristic);
 
-                        // Request higher MTU size
-                        if (!gatt.requestMtu(MAX_PACKET_SIZE)) {
+                        // Request max MTU size
+                        if (!gatt.requestMtu(MAX_ANDROID_MTU)) {
                             Log.e(TAG, "request mtu failed");
                             mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
                             mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
@@ -204,8 +212,15 @@
         public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
             Log.d(TAG, "onMtuChanged callback received. mtu: " + mtu + ", status: " + status);
             if (status == BluetoothGatt.GATT_SUCCESS) {
-                mPacketEncoder.setMaxPacketSize(Math.min(mtu, MAX_PACKET_SIZE));
-                mPacketDecoder.setMaxPacketSize(Math.min(mtu, MAX_PACKET_SIZE));
+                int packetSize = Math.min(mtu - HEADER_SIZE, MAX_PACKET_SIZE);
+                if (packetSize <= 0) {
+                    Log.e(TAG, "onMtuChanged non-positive packet size: " + packetSize);
+                    packetSize = DEFAULT_PACKET_SIZE;
+                } else if (packetSize < DEFAULT_PACKET_SIZE) {
+                    Log.w(TAG, "onMtuChanged small packet size: " + packetSize);
+                }
+                mPacketEncoder.setMaxPacketSize(packetSize);
+                mPacketDecoder.setMaxPacketSize(packetSize);
             } else {
                 mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
                 mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
diff --git a/packages/CredentialManager/res/drawable/autofill_light_selectable_item_background.xml b/packages/CredentialManager/res/drawable/autofill_light_selectable_item_background.xml
deleted file mode 100644
index 9d16f32d..0000000
--- a/packages/CredentialManager/res/drawable/autofill_light_selectable_item_background.xml
+++ /dev/null
@@ -1,25 +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.
-  -->
-
-<!-- Copied from //frameworks/base/core/res/res/drawable/item_background_material.xml -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="@color/autofill_light_colorControlHighlight">
-    <item android:id="@android:id/mask">
-        <color android:color="@android:color/white"/>
-    </item>
-</ripple>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
index 2f0c83b..5becc86 100644
--- a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
+++ b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2023 The Android Open Source Project
+  ~ Copyright (C) 2024 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -23,8 +23,8 @@
         android:shape="rectangle"
         android:top="1dp">
         <shape>
-            <corners android:radius="28dp" />
-            <solid android:color="@android:color/system_surface_container_high_light" />
+            <corners android:radius="16dp" />
+            <solid android:color="@color/dropdown_container" />
         </shape>
     </item>
 </ripple>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/layout/autofill_dataset_left_with_item_tag_hint.xml b/packages/CredentialManager/res/layout/autofill_dataset_left_with_item_tag_hint.xml
deleted file mode 100644
index e4e9f7a..0000000
--- a/packages/CredentialManager/res/layout/autofill_dataset_left_with_item_tag_hint.xml
+++ /dev/null
@@ -1,37 +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.
-  -->
-
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                android:id="@android:id/content"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                style="@style/autofill.Dataset">
-    <ImageView
-        android:id="@android:id/icon1"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerVertical="true"
-        android:layout_alignParentStart="true"
-        android:background="@null"/>
-    <TextView
-        android:id="@android:id/text1"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:layout_toEndOf="@android:id/icon1"
-        style="@style/autofill.TextAppearance"/>
-
-</RelativeLayout>
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
new file mode 100644
index 0000000..cb6c6b4
--- /dev/null
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -0,0 +1,45 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:id="@android:id/content"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxWidth="@dimen/autofill_dropdown_layout_width"
+                android:elevation="3dp">
+
+        <ImageView
+            android:id="@android:id/icon1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:layout_alignParentStart="true"
+            android:background="@null"/>
+        <TextView
+            android:id="@android:id/text1"
+            android:layout_width="@dimen/autofill_dropdown_text_width"
+            android:layout_height="wrap_content"
+            android:layout_alignParentTop="true"
+            android:layout_toEndOf="@android:id/icon1"
+            style="@style/autofill.TextTitle"/>
+        <TextView
+            android:id="@android:id/text2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/text1"
+            android:layout_toEndOf="@android:id/icon1"
+            style="@style/autofill.TextSubtitle"/>
+
+</RelativeLayout>
diff --git a/packages/CredentialManager/res/values/colors.xml b/packages/CredentialManager/res/values/colors.xml
index 63b9f24..dcb7ef9 100644
--- a/packages/CredentialManager/res/values/colors.xml
+++ b/packages/CredentialManager/res/values/colors.xml
@@ -16,23 +16,8 @@
 
 <!-- Color palette -->
 <resources>
-    <color name="autofill_light_colorPrimary">@color/primary_material_light</color>
-    <color name="autofill_light_colorAccent">@color/accent_material_light</color>
-    <color name="autofill_light_colorControlHighlight">@color/ripple_material_light</color>
-    <color name="autofill_light_colorButtonNormal">@color/button_material_light</color>
-
-    <!-- Text colors -->
-    <color name="autofill_light_textColorPrimary">@color/abc_primary_text_material_light</color>
-    <color name="autofill_light_textColorSecondary">@color/abc_secondary_text_material_light</color>
-    <color name="autofill_light_textColorHint">@color/abc_hint_foreground_material_light</color>
-    <color name="autofill_light_textColorHintInverse">@color/abc_hint_foreground_material_dark
-    </color>
-    <color name="autofill_light_textColorHighlight">@color/highlighted_text_material_light</color>
-    <color name="autofill_light_textColorLink">@color/autofill_light_colorAccent</color>
-
     <!-- These colors are used for Remote Views. -->
-    <color name="background_dark_mode">#0E0C0B</color>
-    <color name="background">#F1F3F4</color>
-    <color name="text_primary_dark_mode">#DFDEDB</color>
-    <color name="text_primary">#202124</color>
+    <color name="text_primary">#1A1B20</color>
+    <color name="text_secondary">#44474F</color>
+    <color name="dropdown_container">#F3F3FA</color>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index 67003a3..2a4719d 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -17,6 +17,12 @@
   -->
 
 <resources>
-    <dimen name="autofill_view_padding">16dp</dimen>
-    <dimen name="autofill_icon_size">16dp</dimen>
+    <dimen name="autofill_view_top_padding">12dp</dimen>
+    <dimen name="autofill_view_right_padding">24dp</dimen>
+    <dimen name="autofill_view_bottom_padding">12dp</dimen>
+    <dimen name="autofill_view_left_padding">16dp</dimen>
+    <dimen name="autofill_view_icon_to_text_padding">10dp</dimen>
+    <dimen name="autofill_icon_size">24dp</dimen>
+    <dimen name="autofill_dropdown_layout_width">296dp</dimen>
+    <dimen name="autofill_dropdown_text_width">240dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/styles.xml b/packages/CredentialManager/res/values/styles.xml
index 4a5761a..7de941e 100644
--- a/packages/CredentialManager/res/values/styles.xml
+++ b/packages/CredentialManager/res/values/styles.xml
@@ -15,24 +15,13 @@
   -->
 
 <resources>
-    <style name="autofill.TextAppearance.Small" parent="@style/autofill.TextAppearance">
-        <item name="android:textSize">12sp</item>
-    </style>
-
-
-    <style name="autofill.Dataset" parent="">
-        <item name="android:background">@drawable/autofill_light_selectable_item_background</item>
-    </style>
-
-    <style name="autofill.TextAppearance" parent="">
-        <item name="android:textColor">@color/autofill_light_textColorPrimary</item>
-        <item name="android:textColorHint">@color/autofill_light_textColorHint</item>
-        <item name="android:textColorHighlight">@color/autofill_light_textColorHighlight</item>
-        <item name="android:textColorLink">@color/autofill_light_textColorLink</item>
+    <style name="autofill.TextTitle" parent="">
+        <item name="android:fontFamily">google-sans-medium</item>
         <item name="android:textSize">14sp</item>
     </style>
 
-    <style name="autofill.TextAppearance.Primary">
-        <item name="android:textColor">@color/autofill_light_textColorPrimary</item>
+    <style name="autofill.TextSubtitle" parent="">
+        <item name="android:fontFamily">google-sans-text</item>
+        <item name="android:textSize">12sp</item>
     </style>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 8ac364e7..58467af 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -57,6 +57,7 @@
 import com.android.credentialmanager.getflow.ProviderDisplayInfo
 import com.android.credentialmanager.getflow.toProviderDisplayInfo
 import com.android.credentialmanager.ktx.credentialEntry
+import com.android.credentialmanager.model.CredentialType
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.model.get.ProviderInfo
 import org.json.JSONException
@@ -69,7 +70,8 @@
     companion object {
         private const val TAG = "CredAutofill"
 
-        private const val SESSION_ID_KEY = "session_id"
+        private const val SESSION_ID_KEY = "autofill_session_id"
+        private const val REQUEST_ID_KEY = "autofill_request_id"
         private const val CRED_HINT_PREFIX = "credential="
         private const val REQUEST_DATA_KEY = "requestData"
         private const val CANDIDATE_DATA_KEY = "candidateQueryData"
@@ -97,16 +99,23 @@
         val callingPackage = structure.activityComponent.packageName
         Log.i(TAG, "onFillCredentialRequest called for $callingPackage")
 
-        var sessionId = request.clientState?.getInt(SESSION_ID_KEY)
-
-        Log.i(TAG, "Autofill sessionId: " + sessionId)
-        if (sessionId == null) {
-            Log.i(TAG, "Session Id not found")
-            callback.onFailure("Session Id not found")
+        val clientState = request.clientState
+        if (clientState == null) {
+            Log.i(TAG, "Client state not found")
+            callback.onFailure("Client state not found")
+            return
+        }
+        val sessionId = clientState.getInt(SESSION_ID_KEY)
+        val requestId = clientState.getInt(REQUEST_ID_KEY)
+        Log.i(TAG, "Autofill sessionId: $sessionId, autofill requestId: $requestId")
+        if (sessionId == 0 || requestId == 0) {
+            Log.i(TAG, "Session Id or request Id not found")
+            callback.onFailure("Session Id or request Id not found")
             return
         }
 
-        val getCredRequest: GetCredentialRequest? = getCredManRequest(structure)
+        val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId,
+            requestId)
         if (getCredRequest == null) {
             Log.i(TAG, "No credential manager request found")
             callback.onFailure("No credential manager request found")
@@ -305,12 +314,14 @@
         var i = 0
         var datasetAdded = false
 
-        val duplicateDisplayNames: MutableMap<String, Boolean> = mutableMapOf()
+        val duplicateDisplayNamesForPasskeys: MutableMap<String, Boolean> = mutableMapOf()
         providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach {
             val credentialEntry = it.sortedCredentialEntryList.first()
-            credentialEntry.displayName?.let {displayName ->
-                val duplicateEntry = duplicateDisplayNames.contains(displayName)
-                duplicateDisplayNames[displayName] = duplicateEntry
+            if (credentialEntry.credentialType == CredentialType.PASSKEY) {
+                credentialEntry.displayName?.let { displayName ->
+                    val duplicateEntry = duplicateDisplayNamesForPasskeys.contains(displayName)
+                    duplicateDisplayNamesForPasskeys[displayName] = duplicateEntry
+                }
             }
         }
         providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach usernameLoop@{
@@ -347,12 +358,19 @@
                 } else {
                     spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
                 }
-                val displayName : String = primaryEntry.displayName ?: primaryEntry.userName
+                val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY
+                        && primaryEntry.displayName != null) {
+                    primaryEntry.displayName!!
+                } else {
+                    primaryEntry.userName
+                }
                 val sliceBuilder = InlineSuggestionUi
                         .newContentBuilder(pendingIntent)
                         .setTitle(displayName)
                 sliceBuilder.setStartIcon(icon)
-                if (duplicateDisplayNames[displayName] == true) {
+                if (primaryEntry.credentialType ==
+                        CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[displayName]
+                        == true) {
                     sliceBuilder.setSubtitle(primaryEntry.userName)
                 }
                 inlinePresentation = InlinePresentation(
@@ -515,12 +533,19 @@
         TODO("Not yet implemented")
     }
 
-    private fun getCredManRequest(structure: AssistStructure): GetCredentialRequest? {
+    private fun getCredManRequest(
+        structure: AssistStructure,
+        sessionId: Int,
+        requestId: Int
+    ): GetCredentialRequest? {
         val credentialOptions: MutableList<CredentialOption> = mutableListOf()
         traverseStructure(structure, credentialOptions)
 
         if (credentialOptions.isNotEmpty()) {
-            return GetCredentialRequest.Builder(Bundle.EMPTY)
+            val dataBundle = Bundle()
+            dataBundle.putInt(SESSION_ID_KEY, sessionId)
+            dataBundle.putInt(REQUEST_ID_KEY, requestId)
+            return GetCredentialRequest.Builder(dataBundle)
                     .setCredentialOptions(credentialOptions)
                     .build()
         }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index 4dc7f00..e039dea 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -21,6 +21,7 @@
 import android.widget.RemoteViews
 import androidx.core.content.ContextCompat
 import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.model.CredentialType
 import android.graphics.drawable.Icon
 
 class RemoteViewsFactory {
@@ -29,48 +30,87 @@
         private const val setAdjustViewBoundsMethodName = "setAdjustViewBounds"
         private const val setMaxHeightMethodName = "setMaxHeight"
         private const val setBackgroundResourceMethodName = "setBackgroundResource"
+        private const val bulletPoint = "\u2022"
+        private const val passwordCharacterLength = 15
 
         fun createDropdownPresentation(
                 context: Context,
                 icon: Icon,
                 credentialEntryInfo: CredentialEntryInfo
         ): RemoteViews {
-            val padding = context.resources.getDimensionPixelSize(com.android
-                    .credentialmanager.R.dimen.autofill_view_padding)
             var layoutId: Int = com.android.credentialmanager.R.layout
-                    .autofill_dataset_left_with_item_tag_hint
+                    .credman_dropdown_presentation_layout
             val remoteViews = RemoteViews(context.packageName, layoutId)
-            setRemoteViewsPaddings(remoteViews, padding)
-            val textColorPrimary = getTextColorPrimary(isDarkMode(context), context);
-            remoteViews.setTextColor(android.R.id.text1, textColorPrimary);
-            remoteViews.setTextViewText(android.R.id.text1, credentialEntryInfo.userName)
-
+            if (credentialEntryInfo.credentialType == CredentialType.UNKNOWN) {
+                return remoteViews
+            }
+            setRemoteViewsPaddings(remoteViews, context)
+            if (credentialEntryInfo.credentialType == CredentialType.PASSKEY) {
+                val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName
+                remoteViews.setTextViewText(android.R.id.text1, displayName)
+                val secondaryText = if (credentialEntryInfo.displayName != null)
+                    (credentialEntryInfo.userName + " " + bulletPoint + " "
+                            + credentialEntryInfo.credentialTypeDisplayName
+                            + " " + bulletPoint + " " + credentialEntryInfo.providerDisplayName)
+                else (credentialEntryInfo.credentialTypeDisplayName + " " + bulletPoint + " "
+                        + credentialEntryInfo.providerDisplayName)
+                remoteViews.setTextViewText(android.R.id.text2, secondaryText)
+            } else {
+                remoteViews.setTextViewText(android.R.id.text1, credentialEntryInfo.userName)
+                remoteViews.setTextViewText(android.R.id.text2,
+                        bulletPoint.repeat(passwordCharacterLength))
+            }
+            val textColorPrimary = ContextCompat.getColor(context,
+                    com.android.credentialmanager.R.color.text_primary)
+            remoteViews.setTextColor(android.R.id.text1, textColorPrimary)
+            val textColorSecondary = ContextCompat.getColor(context, com.android
+                    .credentialmanager.R.color.text_secondary)
+            remoteViews.setTextColor(android.R.id.text2, textColorSecondary)
             remoteViews.setImageViewIcon(android.R.id.icon1, icon);
             remoteViews.setBoolean(
                     android.R.id.icon1, setAdjustViewBoundsMethodName, true);
             remoteViews.setInt(
                     android.R.id.icon1,
-                     setMaxHeightMethodName,
+                    setMaxHeightMethodName,
                     context.resources.getDimensionPixelSize(
                             com.android.credentialmanager.R.dimen.autofill_icon_size));
-            val drawableId = if (isDarkMode(context))
-                com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one_dark
-            else com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one
+            val drawableId =
+                    com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one
             remoteViews.setInt(
                     android.R.id.content, setBackgroundResourceMethodName, drawableId);
             return remoteViews
         }
 
         private fun setRemoteViewsPaddings(
-                remoteViews: RemoteViews,
-                padding: Int) {
-            val halfPadding = padding / 2
+                remoteViews: RemoteViews, context: Context) {
+            val leftPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_left_padding)
+            val iconToTextPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_icon_to_text_padding)
+            val rightPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_right_padding)
+            val topPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_top_padding)
+            val bottomPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_bottom_padding)
+            remoteViews.setViewPadding(
+                    android.R.id.icon1,
+                    leftPadding,
+                    /* top=*/0,
+                    /* right=*/0,
+                    /* bottom=*/0)
             remoteViews.setViewPadding(
                     android.R.id.text1,
-                    halfPadding,
-                    halfPadding,
-                    halfPadding,
-                    halfPadding)
+                    iconToTextPadding,
+                    /* top=*/topPadding,
+                    /* right=*/rightPadding,
+                    /* bottom=*/0)
+            remoteViews.setViewPadding(
+                    android.R.id.text2,
+                    iconToTextPadding,
+                    /* top=*/0,
+                    /* right=*/rightPadding,
+                    /* bottom=*/bottomPadding)
         }
 
         private fun isDarkMode(context: Context): Boolean {
@@ -78,11 +118,5 @@
                     Configuration.UI_MODE_NIGHT_MASK
             return currentNightMode == Configuration.UI_MODE_NIGHT_YES
         }
-
-        private fun getTextColorPrimary(darkMode: Boolean, context: Context): Int {
-            return if (darkMode) ContextCompat.getColor(
-                    context, com.android.credentialmanager.R.color.text_primary_dark_mode)
-            else ContextCompat.getColor(context, com.android.credentialmanager.R.color.text_primary)
-        }
     }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
index 170cb45..9ad3e3c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
@@ -91,7 +91,8 @@
         // be stale, if e.g. the app was uninstalled while the activity was destroyed.
         super.onCreate(null);
 
-        if (usePiaV2() && !isTv()) {
+        // TODO(b/318521110) Enable PIA v2 for archive dialog.
+        if (usePiaV2() && !isTv() && !isArchiveDialog(getIntent())) {
             Log.i(TAG, "Using Pia V2");
 
             boolean returnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
@@ -224,6 +225,11 @@
         showConfirmationDialog();
     }
 
+    private boolean isArchiveDialog(Intent intent) {
+        return (intent.getIntExtra(PackageInstaller.EXTRA_DELETE_FLAGS, 0)
+                & PackageManager.DELETE_ARCHIVE) != 0;
+    }
+
     /**
      * Parses specific {@link android.content.pm.PackageManager.DeleteFlags} from {@link Intent}
      * to archive an app if requested.
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index 326e533..aeabbd5 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -46,6 +46,7 @@
 import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_DONE
 import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_INTERNAL_ERROR
 import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_POLICY
+import com.android.packageinstaller.v2.model.InstallAborted.Companion.DLG_NONE
 import com.android.packageinstaller.v2.model.InstallAborted.Companion.DLG_PACKAGE_ERROR
 import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_ANONYMOUS_SOURCE
 import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_INSTALL_CONFIRMATION
@@ -283,14 +284,15 @@
                             createSessionParams(intent, pfd, uri.toString())
                         stagedSessionId = packageInstaller.createSession(params)
                     }
-                } catch (e: IOException) {
+                } catch (e: Exception) {
                     Log.w(LOG_TAG, "Failed to create a staging session", e)
                     _stagingResult.value = InstallAborted(
                         ABORT_REASON_INTERNAL_ERROR,
                         resultIntent = Intent().putExtra(
                             Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK
                         ),
-                        activityResultCode = Activity.RESULT_FIRST_USER
+                        activityResultCode = Activity.RESULT_FIRST_USER,
+                        errorDialogType =  if (e is IOException) DLG_PACKAGE_ERROR else DLG_NONE
                     )
                     return
                 }
@@ -313,6 +315,14 @@
                     )
                 }
             }
+        } else {
+            _stagingResult.value = InstallAborted(
+                ABORT_REASON_INTERNAL_ERROR,
+                resultIntent = Intent().putExtra(
+                    Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_URI
+                ),
+                activityResultCode = Activity.RESULT_FIRST_USER
+            )
         }
     }
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
index be49b39..bbb9bca 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
@@ -122,13 +122,14 @@
      */
     val resultIntent: Intent? = null,
     val activityResultCode: Int = Activity.RESULT_CANCELED,
-    val errorDialogType: Int? = 0,
+    val errorDialogType: Int? = DLG_NONE,
 ) : InstallStage(STAGE_ABORTED) {
 
     companion object {
         const val ABORT_REASON_INTERNAL_ERROR = 0
         const val ABORT_REASON_POLICY = 1
         const val ABORT_REASON_DONE = 2
+        const val DLG_NONE = 0
         const val DLG_PACKAGE_ERROR = 1
     }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt
index c109fc6..1d4d178 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt
@@ -34,6 +34,8 @@
      */
     fun onNegativeResponse(stageCode: Int)
 
+    fun onNegativeResponse(resultCode: Int, data: Intent?)
+
     /**
      * Launch the intent to open the newly installed / updated app.
      */
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
index 2b610d7..6f8eca3 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
@@ -36,10 +36,10 @@
 import androidx.fragment.app.FragmentManager
 import androidx.lifecycle.ViewModelProvider
 import com.android.packageinstaller.R
-import com.android.packageinstaller.v2.model.InstallRepository
 import com.android.packageinstaller.v2.model.InstallAborted
 import com.android.packageinstaller.v2.model.InstallFailed
 import com.android.packageinstaller.v2.model.InstallInstalling
+import com.android.packageinstaller.v2.model.InstallRepository
 import com.android.packageinstaller.v2.model.InstallStage
 import com.android.packageinstaller.v2.model.InstallSuccess
 import com.android.packageinstaller.v2.model.InstallUserActionRequired
@@ -50,6 +50,7 @@
 import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment
 import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment
 import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment
+import com.android.packageinstaller.v2.ui.fragments.ParseErrorFragment
 import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment
 import com.android.packageinstaller.v2.viewmodel.InstallViewModel
 import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory
@@ -124,8 +125,15 @@
             InstallStage.STAGE_ABORTED -> {
                 val aborted = installStage as InstallAborted
                 when (aborted.abortReason) {
-                    InstallAborted.ABORT_REASON_DONE, InstallAborted.ABORT_REASON_INTERNAL_ERROR ->
-                        setResult(aborted.activityResultCode, aborted.resultIntent, true)
+                    InstallAborted.ABORT_REASON_DONE,
+                    InstallAborted.ABORT_REASON_INTERNAL_ERROR -> {
+                        if (aborted.errorDialogType == InstallAborted.DLG_PACKAGE_ERROR) {
+                            val parseErrorDialog = ParseErrorFragment(aborted)
+                            showDialogInner(parseErrorDialog)
+                        } else {
+                            setResult(aborted.activityResultCode, aborted.resultIntent, true)
+                        }
+                    }
 
                     InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted)
                     else -> setResult(Activity.RESULT_CANCELED, null, true)
@@ -204,7 +212,7 @@
             val blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction)
             // Don't finish the package installer app since the next dialog
             // will be shown by this app
-            shouldFinish = blockedByPolicyDialog != null
+            shouldFinish = blockedByPolicyDialog == null
             showDialogInner(blockedByPolicyDialog)
         }
         setResult(Activity.RESULT_CANCELED, null, shouldFinish)
@@ -267,6 +275,10 @@
         setResult(Activity.RESULT_CANCELED, null, true)
     }
 
+    override fun onNegativeResponse(resultCode: Int, data: Intent?) {
+        setResult(resultCode, data, true)
+    }
+
     override fun sendUnknownAppsIntent(sourcePackageName: String) {
         val settingsIntent = Intent()
         settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java
new file mode 100644
index 0000000..68d48d6
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.InstallAborted;
+import com.android.packageinstaller.v2.ui.InstallActionListener;
+
+public class ParseErrorFragment extends DialogFragment {
+
+    private static final String TAG = ParseErrorFragment.class.getSimpleName();
+    private final InstallAborted mDialogData;
+    private InstallActionListener mInstallActionListener;
+
+    public ParseErrorFragment(InstallAborted dialogData) {
+        mDialogData = dialogData;
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        mInstallActionListener = (InstallActionListener) context;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getActivity())
+            .setMessage(R.string.Parse_error_dlg_text)
+            .setPositiveButton(R.string.ok,
+                (dialog, which) ->
+                    mInstallActionListener.onNegativeResponse(
+                        mDialogData.getActivityResultCode(), mDialogData.getResultIntent()))
+            .create();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        super.onCancel(dialog);
+        mInstallActionListener.onNegativeResponse(
+            mDialogData.getActivityResultCode(), mDialogData.getResultIntent());
+    }
+}
diff --git a/packages/SettingsLib/AdaptiveIcon/lint-baseline.xml b/packages/SettingsLib/AdaptiveIcon/lint-baseline.xml
index 7f16517..8127e1a 100644
--- a/packages/SettingsLib/AdaptiveIcon/lint-baseline.xml
+++ b/packages/SettingsLib/AdaptiveIcon/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -8,7 +8,7 @@
         errorLine2="                                 ~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIcon.java"
-            line="78"
+            line="79"
             column="34"/>
     </issue>
 
@@ -19,7 +19,7 @@
         errorLine2="                                   ~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIcon.java"
-            line="90"
+            line="91"
             column="36"/>
     </issue>
 
@@ -30,7 +30,7 @@
         errorLine2="                                             ~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java"
-            line="43"
+            line="45"
             column="46"/>
     </issue>
 
@@ -41,7 +41,7 @@
         errorLine2="        ~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java"
-            line="65"
+            line="67"
             column="9"/>
     </issue>
 
@@ -52,7 +52,7 @@
         errorLine2="        ~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java"
-            line="72"
+            line="74"
             column="9"/>
     </issue>
 
@@ -63,7 +63,7 @@
         errorLine2="        ~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java"
-            line="80"
+            line="82"
             column="9"/>
     </issue>
 
@@ -74,8 +74,8 @@
         errorLine2="                         ~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java"
-            line="105"
+            line="107"
             column="26"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index ffe3d1d..5da4b95 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -61,7 +61,7 @@
         "src/**/*.kt",
     ],
     lint: {
-        baseline_filename: "lint-baseline.xml",
+        extra_check_modules: ["SettingsLibLintChecker"],
     },
 }
 
diff --git a/packages/SettingsLib/EmergencyNumber/lint-baseline.xml b/packages/SettingsLib/EmergencyNumber/lint-baseline.xml
index e9c687f..13bf5f5 100644
--- a/packages/SettingsLib/EmergencyNumber/lint-baseline.xml
+++ b/packages/SettingsLib/EmergencyNumber/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -8,7 +8,7 @@
         errorLine2="                                        ~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="77"
+            line="81"
             column="41"/>
     </issue>
 
@@ -19,7 +19,7 @@
         errorLine2="                                            ~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="78"
+            line="82"
             column="45"/>
     </issue>
 
@@ -30,18 +30,18 @@
         errorLine2="                                                             ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="78"
+            line="82"
             column="62"/>
     </issue>
 
     <issue
         id="NewApi"
         message="Call requires API level 29 (current min is 21): `android.telephony.TelephonyManager#getEmergencyNumberList`"
-        errorLine1="        Map&lt;Integer, List&lt;EmergencyNumber>> allLists = mTelephonyManager.getEmergencyNumberList("
+        errorLine1="        Map&lt;Integer, List&lt;EmergencyNumber&gt;&gt; allLists = mTelephonyManager.getEmergencyNumberList("
         errorLine2="                                                                         ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="173"
+            line="177"
             column="74"/>
     </issue>
 
@@ -52,7 +52,7 @@
         errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="196"
+            line="200"
             column="41"/>
     </issue>
 
@@ -63,7 +63,7 @@
         errorLine2="                                                                    ~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="219"
+            line="223"
             column="69"/>
     </issue>
 
@@ -74,7 +74,7 @@
         errorLine2="                                        ~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="234"
+            line="238"
             column="41"/>
     </issue>
 
@@ -85,8 +85,8 @@
         errorLine2="                                                   ~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java"
-            line="251"
+            line="255"
             column="52"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/LintChecker/Android.bp b/packages/SettingsLib/LintChecker/Android.bp
new file mode 100644
index 0000000..eb489b1
--- /dev/null
+++ b/packages/SettingsLib/LintChecker/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_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+    name: "SettingsLibLintChecker",
+    srcs: ["src/**/*.kt"],
+    plugins: ["auto_service_plugin"],
+    libs: [
+        "auto_service_annotations",
+        "lint_api",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/NullabilityAnnotationsDetector.kt b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/NullabilityAnnotationsDetector.kt
new file mode 100644
index 0000000..1f06261
--- /dev/null
+++ b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/NullabilityAnnotationsDetector.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.tools.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.intellij.psi.PsiModifier
+import com.intellij.psi.PsiPrimitiveType
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UAnnotated
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+
+class NullabilityAnnotationsDetector : Detector(), Detector.UastScanner {
+    override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UMethod::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler? {
+        if (!context.isJavaFile()) return null
+
+        return object : UElementHandler() {
+            override fun visitMethod(node: UMethod) {
+                if (node.isPublic() && node.name != ANONYMOUS_CONSTRUCTOR) {
+                    node.verifyMethod()
+                    node.verifyMethodParameters()
+                }
+            }
+
+            private fun UMethod.isPublic() = modifierList.hasModifierProperty(PsiModifier.PUBLIC)
+
+            private fun UMethod.verifyMethod() {
+                if (isConstructor) return
+                if (returnType.isPrimitive()) return
+                checkAnnotation(METHOD_MSG)
+            }
+
+            private fun UMethod.verifyMethodParameters() {
+                for (parameter in uastParameters) {
+                    if (parameter.type.isPrimitive()) continue
+                    parameter.checkAnnotation(PARAMETER_MSG)
+                }
+            }
+
+            private fun PsiType?.isPrimitive() = this is PsiPrimitiveType
+
+            private fun UAnnotated.checkAnnotation(message: String) {
+                val oldAnnotation = findOldNullabilityAnnotation()
+                val oldAnnotationName = oldAnnotation?.qualifiedName?.substringAfterLast('.')
+
+                if (oldAnnotationName != null) {
+                    val annotation = "androidx.annotation.$oldAnnotationName"
+                    reportIssue(
+                        REQUIRE_NULLABILITY_ISSUE,
+                        "Prefer $annotation",
+                        LintFix.create()
+                                .replace()
+                                .range(context.getLocation(oldAnnotation))
+                                .with("@$annotation")
+                                .autoFix()
+                                .build()
+                    )
+                } else if (!hasNullabilityAnnotation()) {
+                    reportIssue(REQUIRE_NULLABILITY_ISSUE, message)
+                }
+            }
+
+            private fun UElement.reportIssue(
+                issue: Issue,
+                message: String,
+                quickfixData: LintFix? = null,
+            ) {
+                context.report(
+                    issue = issue,
+                    scope = this,
+                    location = context.getNameLocation(this),
+                    message = message,
+                    quickfixData = quickfixData,
+                )
+            }
+
+            private fun UAnnotated.findOldNullabilityAnnotation() =
+                uAnnotations.find { it.qualifiedName in oldAnnotations }
+
+            private fun UAnnotated.hasNullabilityAnnotation() =
+                uAnnotations.any { it.qualifiedName in validAnnotations }
+        }
+    }
+
+    private fun JavaContext.isJavaFile() = psiFile?.fileElementType.toString().startsWith("java")
+
+    companion object {
+        private val validAnnotations = arrayOf("androidx.annotation.NonNull",
+            "androidx.annotation.Nullable")
+
+        private val oldAnnotations = arrayOf("android.annotation.NonNull",
+            "android.annotation.Nullable",
+        )
+
+        private const val ANONYMOUS_CONSTRUCTOR = "<anon-init>"
+
+        private const val METHOD_MSG =
+                "Java public method return with non-primitive type must add androidx annotation. " +
+                        "Example: @NonNull | @Nullable Object functionName() {}"
+
+        private const val PARAMETER_MSG =
+                "Java public method parameter with non-primitive type must add androidx " +
+                        "annotation. Example: functionName(@NonNull Context context, " +
+                        "@Nullable Object obj) {}"
+
+        internal val REQUIRE_NULLABILITY_ISSUE = Issue
+            .create(
+                id = "RequiresNullabilityAnnotation",
+                briefDescription = "Requires nullability annotation for function",
+                explanation = "All public java APIs should specify nullability annotations for " +
+                        "methods and parameters.",
+                category = Category.CUSTOM_LINT_CHECKS,
+                priority = 3,
+                severity = Severity.WARNING,
+                androidSpecific = true,
+                implementation = Implementation(
+                  NullabilityAnnotationsDetector::class.java,
+                  Scope.JAVA_FILE_SCOPE,
+                ),
+            )
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/SettingsLintIssueRegistry.kt b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/SettingsLintIssueRegistry.kt
new file mode 100644
index 0000000..e0ab24a
--- /dev/null
+++ b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/SettingsLintIssueRegistry.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.tools.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+class SettingsLintIssueRegistry : IssueRegistry() {
+    override val issues = listOf(NullabilityAnnotationsDetector.REQUIRE_NULLABILITY_ISSUE)
+
+    override val api: Int = CURRENT_API
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/MainSwitchPreference/Android.bp b/packages/SettingsLib/MainSwitchPreference/Android.bp
index d9f74da..010a6ce 100644
--- a/packages/SettingsLib/MainSwitchPreference/Android.bp
+++ b/packages/SettingsLib/MainSwitchPreference/Android.bp
@@ -28,7 +28,4 @@
         "com.android.extservices",
         "com.android.healthfitness",
     ],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
diff --git a/packages/SettingsLib/MainSwitchPreference/lint-baseline.xml b/packages/SettingsLib/MainSwitchPreference/lint-baseline.xml
deleted file mode 100644
index cfa64a4..0000000
--- a/packages/SettingsLib/MainSwitchPreference/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
-    <issue
-        id="NewApi"
-        message="`@android:dimen/config_restrictedIconSize` requires API level 29 (current min is 28)"
-        errorLine1='    &lt;dimen name="settingslib_restricted_icon_size"&gt;@android:dimen/config_restrictedIconSize&lt;/dimen&gt;'
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml"
-            line="21"
-            column="52"/>
-    </issue>
-
-</issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/RestrictedLockUtils/lint-baseline.xml b/packages/SettingsLib/RestrictedLockUtils/lint-baseline.xml
index 26d05a6..45a07fe 100644
--- a/packages/SettingsLib/RestrictedLockUtils/lint-baseline.xml
+++ b/packages/SettingsLib/RestrictedLockUtils/lint-baseline.xml
@@ -1,38 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 23): `android.os.UserHandle#of`"
-        errorLine1="        context.startActivityAsUser(intent, UserHandle.of(targetUserId));"
-        errorLine2="                                                       ~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/RestrictedLockUtils/src/com/android/settingslib/RestrictedLockUtils.java"
-            line="97"
-            column="56"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 23): `android.os.UserHandle#of`"
-        errorLine1="        return um.getUserProfiles().contains(UserHandle.of(userId));"
-        errorLine2="                                                        ~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/RestrictedLockUtils/src/com/android/settingslib/RestrictedLockUtils.java"
-            line="140"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 26 (current min is 23): `android.app.admin.DevicePolicyManager#getDeviceOwnerComponentOnAnyUser`"
-        errorLine1="            adminComponent = dpm.getDeviceOwnerComponentOnAnyUser();"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/RestrictedLockUtils/src/com/android/settingslib/RestrictedLockUtils.java"
-            line="75"
-            column="34"/>
-    </issue>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -58,6 +25,28 @@
 
     <issue
         id="NewApi"
+        message="Call requires API level 26 (current min is 23): `android.app.admin.DevicePolicyManager#getDeviceOwnerComponentOnAnyUser`"
+        errorLine1="            adminComponent = dpm.getDeviceOwnerComponentOnAnyUser();"
+        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/RestrictedLockUtils/src/com/android/settingslib/RestrictedLockUtils.java"
+            line="75"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 24 (current min is 23): `android.os.UserHandle#of`"
+        errorLine1="        context.startActivityAsUser(intent, UserHandle.of(targetUserId));"
+        errorLine2="                                                       ~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/RestrictedLockUtils/src/com/android/settingslib/RestrictedLockUtils.java"
+            line="97"
+            column="56"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 29 (current min is 23): `android.content.Context#startActivityAsUser`"
         errorLine1="        context.startActivityAsUser(intent, UserHandle.of(targetUserId));"
         errorLine2="                ~~~~~~~~~~~~~~~~~~~">
@@ -67,4 +56,15 @@
             column="17"/>
     </issue>
 
+    <issue
+        id="NewApi"
+        message="Call requires API level 24 (current min is 23): `android.os.UserHandle#of`"
+        errorLine1="        return um.getUserProfiles().contains(UserHandle.of(userId));"
+        errorLine2="                                                        ~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/RestrictedLockUtils/src/com/android/settingslib/RestrictedLockUtils.java"
+            line="120"
+            column="57"/>
+    </issue>
+
 </issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/SchedulesProvider/lint-baseline.xml b/packages/SettingsLib/SchedulesProvider/lint-baseline.xml
index 0744710..db6a882 100644
--- a/packages/SettingsLib/SchedulesProvider/lint-baseline.xml
+++ b/packages/SettingsLib/SchedulesProvider/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
diff --git a/packages/SettingsLib/SearchProvider/lint-baseline.xml b/packages/SettingsLib/SearchProvider/lint-baseline.xml
index 53346e0..3cfca1d 100644
--- a/packages/SettingsLib/SearchProvider/lint-baseline.xml
+++ b/packages/SettingsLib/SearchProvider/lint-baseline.xml
@@ -1,27 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 23 (current min is 21): `new android.provider.SearchIndexableResource`"
-        errorLine1="            super("
-        errorLine2="            ~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
-            line="107"
-            column="13"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 23 (current min is 21): `android.provider.SearchIndexableResource`"
-        errorLine1="    public static final class SearchIndexableIntentResource extends SearchIndexableResource {"
-        errorLine2="                                                                    ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
-            line="97"
-            column="69"/>
-    </issue>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -36,6 +14,39 @@
 
     <issue
         id="NewApi"
+        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexablesContract#INDEXABLES_XML_RES_COLUMNS`"
+        errorLine1="        final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);"
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
+            line="45"
+            column="54"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableData#rank`"
+        errorLine1="                    .add(XmlResource.COLUMN_RANK, indexableResource.rank)"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
+            line="50"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableResource#xmlResId`"
+        errorLine1="                    .add(XmlResource.COLUMN_XML_RESID, indexableResource.xmlResId)"
+        errorLine2="                                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
+            line="51"
+            column="56"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableData#className`"
         errorLine1="                    .add(XmlResource.COLUMN_CLASS_NAME, indexableResource.className)"
         errorLine2="                                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -58,6 +69,39 @@
 
     <issue
         id="NewApi"
+        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableData#intentTargetClass`"
+        errorLine1="                            indexableResource.intentTargetClass);"
+        errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
+            line="56"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 23 (current min is 21): `android.provider.SearchIndexableResource`"
+        errorLine1="    public static final class SearchIndexableIntentResource extends SearchIndexableResource {"
+        errorLine2="                                                                    ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
+            line="97"
+            column="69"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 21): `new android.provider.SearchIndexableResource`"
+        errorLine1="            super("
+        errorLine2="            ~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
+            line="107"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableData#intentAction`"
         errorLine1='                this.intentAction = "android.intent.action.MAIN";'
         errorLine2="                ~~~~~~~~~~~~~~~~~">
@@ -81,17 +125,6 @@
     <issue
         id="NewApi"
         message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableData#intentTargetClass`"
-        errorLine1="                            indexableResource.intentTargetClass);"
-        errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
-            line="56"
-            column="29"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableData#intentTargetClass`"
         errorLine1="            this.intentTargetClass = className;"
         errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -100,37 +133,4 @@
             column="13"/>
     </issue>
 
-    <issue
-        id="NewApi"
-        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableData#rank`"
-        errorLine1="                    .add(XmlResource.COLUMN_RANK, indexableResource.rank)"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
-            line="50"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexableResource#xmlResId`"
-        errorLine1="                    .add(XmlResource.COLUMN_XML_RESID, indexableResource.xmlResId)"
-        errorLine2="                                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
-            line="51"
-            column="56"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 23 (current min is 21): `android.provider.SearchIndexablesContract#INDEXABLES_XML_RES_COLUMNS`"
-        errorLine1="        final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/SearchProvider/src/com/android/settingslib/searchprovider/SettingsXmlIndexProvider.java"
-            line="45"
-            column="54"/>
-    </issue>
-
 </issues>
\ No newline at end of file
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 c143390..b7f2c1e 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
@@ -34,6 +34,15 @@
         end = itemPaddingEnd,
         bottom = itemPaddingVertical,
     )
+    val textFieldPadding = PaddingValues(
+        start = itemPaddingStart,
+        end = itemPaddingEnd,
+    )
+    val menuFieldPadding = PaddingValues(
+        start = itemPaddingStart,
+        end = itemPaddingEnd,
+        bottom = itemPaddingVertical,
+    )
     val itemPaddingAround = 8.dp
     val itemDividerHeight = 32.dp
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
index 0d6c064..f6692a3 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
@@ -51,7 +51,7 @@
         onExpandedChange = { expanded = it },
         modifier = Modifier
             .width(350.dp)
-            .padding(SettingsDimension.itemPadding),
+            .padding(SettingsDimension.menuFieldPadding),
     ) {
         OutlinedTextField(
             // The `menuAnchor` modifier must be passed to the text field for correctness.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
index 5d248e1..ba8e354 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
@@ -63,7 +63,7 @@
         onExpandedChange = { expanded = it },
         modifier = Modifier
             .width(350.dp)
-            .padding(SettingsDimension.itemPadding)
+            .padding(SettingsDimension.menuFieldPadding)
             .onSizeChanged { dropDownWidth = it.width },
     ) {
         OutlinedTextField(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
index e0dd4e1..2ce3c66 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
@@ -42,7 +42,7 @@
     OutlinedTextField(
         modifier = Modifier
             .fillMaxWidth()
-            .padding(SettingsDimension.itemPadding),
+            .padding(SettingsDimension.textFieldPadding),
         value = value,
         onValueChange = onTextChange,
         label = {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt
index 0757df3..3102a00 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt
@@ -52,7 +52,7 @@
     var visibility by remember { mutableStateOf(false) }
     OutlinedTextField(
         modifier = Modifier
-            .padding(SettingsDimension.itemPadding)
+            .padding(SettingsDimension.menuFieldPadding)
             .fillMaxWidth(),
         value = value,
         onValueChange = onTextChange,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
index a9974dc..514ad669 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
@@ -39,6 +39,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -68,6 +71,7 @@
     ) {
         val contentPadding = PaddingValues(horizontal = SettingsDimension.itemPaddingEnd)
         Button(
+            modifier = Modifier.semantics { role = Role.DropdownList },
             onClick = { expanded = true },
             colors = ButtonDefaults.buttonColors(
                 containerColor = SettingsTheme.colorScheme.spinnerHeaderContainer,
diff --git a/packages/SettingsLib/Tile/lint-baseline.xml b/packages/SettingsLib/Tile/lint-baseline.xml
index 326ec0d..56b1bca 100644
--- a/packages/SettingsLib/Tile/lint-baseline.xml
+++ b/packages/SettingsLib/Tile/lint-baseline.xml
@@ -1,55 +1,59 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
-        message="Call requires API level 24 (current min is 21): `java.lang.Iterable#forEach`"
-        errorLine1="        controllers.forEach(controller -&gt; {"
-        errorLine2="                    ~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java"
-            line="79"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 23 (current min is 21): `android.graphics.drawable.Icon#createWithResource`">
+        message="Call requires API level 29 (current min is 21): `android.os.Parcel#writeBoolean`"
+        errorLine1="        dest.writeBoolean(this instanceof ProviderTile);"
+        errorLine2="             ~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
-            line="312"/>
+            line="114"
+            column="14"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 23 (current min is 21): `android.graphics.drawable.Icon#setTint`">
+        message="Call requires API level 23 (current min is 21): `android.graphics.drawable.Icon#createWithResource`"
+        errorLine1="            final Icon icon = Icon.createWithResource(componentInfo.packageName, iconResId);"
+        errorLine2="                                   ~~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
-            line="318"/>
+            line="326"
+            column="36"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 29 (current min is 21): `android.os.Parcel#readBoolean`">
+        message="Call requires API level 23 (current min is 21): `android.graphics.drawable.Icon#setTint`"
+        errorLine1="                icon.setTint(tintColor);"
+        errorLine2="                     ~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
-            line="373"/>
+            line="332"
+            column="22"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 29 (current min is 21): `android.os.Parcel#writeBoolean`">
+        message="Call requires API level 29 (current min is 21): `android.os.Parcel#readBoolean`"
+        errorLine1="            final boolean isProviderTile = source.readBoolean();"
+        errorLine2="                                                  ~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
-            line="108"/>
+            line="387"
+            column="51"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 21): `android.content.Context#getAttributionSource`">
+        message="Call requires API level 31 (current min is 21): `android.content.Context#getAttributionSource`"
+        errorLine1="            return provider.call(context.getAttributionSource(),"
+        errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java"
-            line="565"/>
+            line="601"
+            column="42"/>
     </issue>
 
 </issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/Utils/lint-baseline.xml b/packages/SettingsLib/Utils/lint-baseline.xml
index 3fcd56c..2f6cc3a 100644
--- a/packages/SettingsLib/Utils/lint-baseline.xml
+++ b/packages/SettingsLib/Utils/lint-baseline.xml
@@ -1,26 +1,15 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
-        message="Call requires API level 24 (current min is 21): `android.os.UserHandle#of`"
-        errorLine1="                            mContext.getPackageName(), 0, UserHandle.of(managedUserId)"
-        errorLine2="                                                                     ~~">
+        message="Call requires API level 24 (current min is 23): `android.os.UserManager#isManagedProfile`"
+        errorLine1="        return context.getSystemService(UserManager.class).isManagedProfile(userId)"
+        errorLine2="                                                           ~~~~~~~~~~~~~~~~">
         <location
-            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
-            line="119"
-            column="70"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 21): `android.os.UserHandle#of`"
-        errorLine1="                        intent, 0, UserHandle.of(managedUserId));"
-        errorLine2="                                              ~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
-            line="150"
-            column="47"/>
+            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/applications/AppUtils.java"
+            line="62"
+            column="60"/>
     </issue>
 
     <issue
@@ -36,35 +25,13 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 24 (current min is 21): `android.os.UserManager#isManagedProfile`"
-        errorLine1="            if (mUserManager.isManagedProfile(id)) {"
-        errorLine2="                             ~~~~~~~~~~~~~~~~">
+        message="Call requires API level 29 (current min is 21): `android.content.Context#startActivityAsUser`"
+        errorLine1="            activityContext.startActivityAsUser(intent, UserHandle.of(userId));"
+        errorLine2="                            ~~~~~~~~~~~~~~~~~~~">
         <location
             file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
-            line="173"
-            column="30"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 23): `android.os.UserManager#isManagedProfile`"
-        errorLine1="        return context.getSystemService(UserManager.class).isManagedProfile(userId)"
-        errorLine2="                                                           ~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/applications/AppUtils.java"
-            line="62"
-            column="60"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 26 (current min is 21): `android.app.admin.DevicePolicyManager#getDeviceOwnerComponentOnAnyUser`"
-        errorLine1="        return mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser();"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
-            line="163"
-            column="37"/>
+            line="80"
+            column="29"/>
     </issue>
 
     <issue
@@ -80,13 +47,13 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 29 (current min is 21): `android.content.Context#startActivityAsUser`"
-        errorLine1="            activityContext.startActivityAsUser(intent, UserHandle.of(userId));"
-        errorLine2="                            ~~~~~~~~~~~~~~~~~~~">
+        message="Call requires API level 24 (current min is 21): `android.os.UserHandle#of`"
+        errorLine1="                            mContext.getPackageName(), 0, UserHandle.of(managedUserId)"
+        errorLine2="                                                                     ~~">
         <location
             file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
-            line="80"
-            column="29"/>
+            line="119"
+            column="70"/>
     </issue>
 
     <issue
@@ -102,6 +69,28 @@
 
     <issue
         id="NewApi"
+        message="Call requires API level 24 (current min is 21): `android.os.UserHandle#of`"
+        errorLine1="                        intent, 0, UserHandle.of(managedUserId));"
+        errorLine2="                                              ~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
+            line="150"
+            column="47"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 26 (current min is 21): `android.app.admin.DevicePolicyManager#getDeviceOwnerComponentOnAnyUser`"
+        errorLine1="        return mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser();"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
+            line="163"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 30 (current min is 21): `android.os.UserManager#getAllProfiles`"
         errorLine1="        List&lt;UserHandle&gt; allProfiles = mUserManager.getAllProfiles();"
         errorLine2="                                                    ~~~~~~~~~~~~~~">
@@ -111,4 +100,15 @@
             column="53"/>
     </issue>
 
+    <issue
+        id="NewApi"
+        message="Call requires API level 24 (current min is 21): `android.os.UserManager#isManagedProfile`"
+        errorLine1="            if (mUserManager.isManagedProfile(id)) {"
+        errorLine2="                             ~~~~~~~~~~~~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/Utils/src/com/android/settingslib/utils/WorkPolicyUtils.java"
+            line="173"
+            column="30"/>
+    </issue>
+
 </issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/lint-baseline.xml b/packages/SettingsLib/lint-baseline.xml
deleted file mode 100644
index d6a23fd..0000000
--- a/packages/SettingsLib/lint-baseline.xml
+++ /dev/null
@@ -1,204 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.bluetooth.BluetoothDevice#setAlias`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java"
-            line="584"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.wifi.WifiInfo#getSubscriptionId`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java"
-            line="248"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.wifi.WifiInfo#getSubscriptionId`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java"
-            line="278"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.wifi.WifiManager#registerSubsystemRestartTrackingCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java"
-            line="201"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.net.wifi.WifiManager#unregisterSubsystemRestartTrackingCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java"
-            line="208"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.os.UserManager#isUserForeground`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminController.java"
-            line="78"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#isDataCapable`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/Utils.java"
-            line="498"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#isDataCapable`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java"
-            line="225"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#registerTelephonyCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java"
-            line="215"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#registerTelephonyCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="86"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#unregisterTelephonyCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java"
-            line="222"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#unregisterTelephonyCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="88"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 34 (current min is 30): `android.os.UserManager#isAdminUser`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java"
-            line="66"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 34 (current min is 30): `android.os.UserManager#isAdminUser`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java"
-            line="49"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 34 (current min is 30): `android.os.UserManager#isAdminUser`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSimStatusImeiInfoPreferenceController.java"
-            line="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.net.wifi.WifiManager.SubsystemRestartTrackingCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java"
-            line="64"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="125"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.CarrierNetworkListener`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="124"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.DataActivityListener`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="123"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.DataConnectionStateListener`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="122"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.DisplayInfoListener`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="126"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.ServiceStateListener`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="120"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.SignalStrengthsListener`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="121"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java"
-            line="79"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback`">
-        <location
-            file="frameworks/base/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java"
-            line="119"/>
-    </issue>
-
-</issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/search/Android.bp b/packages/SettingsLib/search/Android.bp
index 3b14712..390c9d2e 100644
--- a/packages/SettingsLib/search/Android.bp
+++ b/packages/SettingsLib/search/Android.bp
@@ -12,9 +12,6 @@
     visibility: ["//visibility:private"],
     srcs: ["interface-src/**/*.java"],
     host_supported: true,
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 android_library {
diff --git a/packages/SettingsLib/search/lint-baseline.xml b/packages/SettingsLib/search/lint-baseline.xml
index 7ec512b..61cdb05 100644
--- a/packages/SettingsLib/search/lint-baseline.xml
+++ b/packages/SettingsLib/search/lint-baseline.xml
@@ -1,16 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 23 (current min is 21): `new android.provider.SearchIndexableData`"
-        errorLine1="        super(context);"
-        errorLine2="        ~~~~~">
-        <location
-            file="frameworks/base/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableRaw.java"
-            line="62"
-            column="9"/>
-    </issue>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
@@ -23,4 +12,15 @@
             column="41"/>
     </issue>
 
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 21): `new android.provider.SearchIndexableData`"
+        errorLine1="        super(context);"
+        errorLine2="        ~~~~~">
+        <location
+            file="frameworks/base/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableRaw.java"
+            line="62"
+            column="9"/>
+    </issue>
+
 </issues>
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
index 8e5396f..d902457 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -19,6 +19,7 @@
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE;
 import static android.app.admin.DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
 import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
+import static android.app.role.RoleManager.ROLE_FINANCED_DEVICE_KIOSK;
 
 import static com.android.settingslib.Utils.getColorAttrDefaultColor;
 
@@ -27,6 +28,7 @@
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.admin.DevicePolicyManager;
+import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -70,6 +72,10 @@
     private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
     private static final Set<String> ECM_KEYS = new ArraySet<>();
 
+    // TODO(b/281701062): reference role name from role manager once its exposed.
+    private static final String ROLE_DEVICE_LOCK_CONTROLLER =
+            "android.app.role.SYSTEM_FINANCED_DEVICE_CONTROLLER";
+
     static {
         if (android.security.Flags.extendEcmToAllSettings()) {
             ECM_KEYS.add(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW);
@@ -476,16 +482,27 @@
     }
 
     /**
-     * Check if {@param packageName} is restricted by the profile or device owner from using
-     * metered data.
+     * Check if user control over metered data usage of {@code packageName} is disabled by the
+     * profile or device owner.
      *
      * @return EnforcedAdmin object containing the enforced admin component and admin user details,
-     * or {@code null} if the {@param packageName} is not restricted.
+     * or {@code null} if the user control is not disabled.
      */
-    public static EnforcedAdmin checkIfMeteredDataRestricted(Context context,
+    public static EnforcedAdmin checkIfMeteredDataUsageUserControlDisabled(Context context,
             String packageName, int userId) {
+        RoleManager roleManager = context.getSystemService(RoleManager.class);
+        UserHandle userHandle = getUserHandleOf(userId);
+        if (roleManager.getRoleHoldersAsUser(ROLE_FINANCED_DEVICE_KIOSK, userHandle)
+                .contains(packageName)
+                || roleManager.getRoleHoldersAsUser(ROLE_DEVICE_LOCK_CONTROLLER, userHandle)
+                .contains(packageName)) {
+            // There is no actual device admin for a financed device, but metered data usage
+            // control should still be disabled for both controller and kiosk apps.
+            return new EnforcedAdmin();
+        }
+
         final EnforcedAdmin enforcedAdmin = getProfileOrDeviceOwner(context,
-                getUserHandleOf(userId));
+                userHandle);
         if (enforcedAdmin == null) {
             return null;
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
index 5bc27195..943e3fc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
@@ -70,10 +70,8 @@
     @Override
     public void displayPreference(PreferenceScreen screen) {
         super.displayPreference(screen);
-        if (isAvailable()) {
-            mWifiMacAddress = screen.findPreference(KEY_WIFI_MAC_ADDRESS);
-            updateConnectivity();
-        }
+        mWifiMacAddress = screen.findPreference(KEY_WIFI_MAC_ADDRESS);
+        updateConnectivity();
     }
 
     @Override
@@ -84,16 +82,16 @@
     @SuppressLint("HardwareIds")
     @Override
     protected void updateConnectivity() {
+        if (mWifiManager == null || mWifiMacAddress == null) {
+            return;
+        }
+
         final String[] macAddresses = mWifiManager.getFactoryMacAddresses();
         String macAddress = null;
         if (macAddresses != null && macAddresses.length > 0) {
             macAddress = macAddresses[0];
         }
 
-        if (mWifiMacAddress == null) {
-            return;
-        }
-
         if (TextUtils.isEmpty(macAddress) || macAddress.equals(WifiInfo.DEFAULT_MAC_ADDRESS)) {
             mWifiMacAddress.setSummary(R.string.status_unavailable);
         } else {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index ebcca42..5925492 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -184,10 +184,6 @@
             return false;
         }
 
-        if (mCurrentConnectedDevice != null) {
-            mCurrentConnectedDevice.disconnect();
-        }
-
         device.setState(MediaDeviceState.STATE_CONNECTING);
         mInfoMediaManager.connectToDevice(device);
         return true;
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index f2d9d14..0c4cf76 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -396,12 +396,6 @@
     }
 
     /**
-     * Stop transfer MediaDevice
-     */
-    public void disconnect() {
-    }
-
-    /**
      * Set current device's state
      */
     public void setState(@LocalMediaManager.MediaDeviceState int state) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 999e8d5..9a7d4f1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -147,7 +147,6 @@
         mLocalMediaManager.registerCallback(mCallback);
         assertThat(mLocalMediaManager.connectDevice(device)).isTrue();
 
-        verify(currentDevice).disconnect();
         verify(mInfoMediaManager).connectToDevice(device);
     }
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index 2e39adc..add3134 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -93,6 +93,7 @@
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME,
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE,
         Settings.Global.Wearable.CLOCKWORK_24HR_TIME,
+        Settings.Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED,
         Settings.Global.Wearable.MUTE_WHEN_OFF_BODY_ENABLED,
         Settings.Global.Wearable.AMBIENT_ENABLED,
         Settings.Global.Wearable.AMBIENT_TILT_TO_WAKE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 5022395..c0a0760 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -450,6 +450,8 @@
         VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Global.FORCE_ENABLE_PSS_PROFILING, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java
index bd99a8b..74fd828 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java
@@ -99,7 +99,6 @@
                 "kiwi",
                 "latency_tracker",
                 "launcher",
-                "launcher_lily",
                 "leaked_animator",
                 "lmkd_native",
                 "location",
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 1481d97..d26c5ff 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -867,6 +867,7 @@
                         Settings.Secure.NEARBY_SHARING_SLICE_URI,
                         Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES,
                         Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT,
+                        Settings.Secure.PRIVATE_SPACE_AUTO_LOCK,
                         Settings.Secure.RELEASE_COMPRESS_BLOCKS_ON_INSTALL,
                         Settings.Secure.SCREENSAVER_COMPLICATIONS_ENABLED,
                         Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 477c42e..507d9c4 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -810,6 +810,9 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT" />
 
     <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
 
     <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cc5dfc6..d61ae7e 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -65,6 +65,7 @@
 
                 "androidx.compose.runtime_runtime",
                 "androidx.compose.material3_material3",
+                "androidx.compose.material_material-icons-extended",
                 "androidx.activity_activity-compose",
                 "androidx.compose.animation_animation-graphics",
             ],
@@ -156,7 +157,7 @@
         "SystemUI-res",
         "WifiTrackerLib",
         "WindowManager-Shell",
-        "SystemUIAnimationLib",
+        "PlatformAnimationLib",
         "SystemUICommon",
         "SystemUICustomizationLib",
         "SystemUILogLib",
@@ -273,7 +274,7 @@
     static_libs: [
         "SystemUI-res",
         "WifiTrackerLib",
-        "SystemUIAnimationLib",
+        "PlatformAnimationLib",
         "SystemUIPluginLib",
         "SystemUISharedLib",
         "SystemUICustomizationLib",
@@ -437,6 +438,7 @@
         "androidx.core_core-animation-testing",
         "androidx.test.ext.junit",
         "inline-mockito-robolectric-prebuilt",
+        "platform-parametric-runner-lib",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a03fa9b..7443e4c 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -84,6 +84,7 @@
     <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/>
     <uses-permission android:name="android.permission.LOCATION_HARDWARE" />
     <uses-permission android:name="android.permission.NETWORK_FACTORY" />
+    <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" />
     <!-- Physical hardware -->
     <uses-permission android:name="android.permission.MANAGE_USB" />
     <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 21263a9..f7b1a26 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -10,6 +10,13 @@
 }
 
 flag {
+    name: "floating_menu_drag_to_hide"
+    namespace: "accessibility"
+    description: "Allows users to hide the FAB then use notification to dismiss or bring it back."
+    bug: "298718415"
+}
+
+flag {
     name: "floating_menu_ime_displacement_animation"
     namespace: "accessibility"
     description: "Adds an animation for when the FAB is displaced by an IME becoming visible."
@@ -28,4 +35,4 @@
     namespace: "accessibility"
     description: "Animates the floating menu's transition between curved and jagged edges."
     bug: "281140482"
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig
index 1ad1666..d0e6b28 100644
--- a/packages/SystemUI/aconfig/predictive_back.aconfig
+++ b/packages/SystemUI/aconfig/predictive_back.aconfig
@@ -19,4 +19,11 @@
     namespace: "systemui"
     description: "Enable Predictive Back Animation in Bouncer"
     bug: "309545085"
+}
+
+flag {
+    name: "predictive_back_animate_dialogs"
+    namespace: "systemui"
+    description: "Enable Predictive Back Animation for SysUI dialogs"
+    bug: "309545085"
 }
\ No newline at end of file
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 5b21854..5fbffbe 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -258,6 +258,15 @@
 }
 
 flag {
+   name: "centralized_status_bar_dimens_refactor"
+   namespace: "systemui"
+   description: "Refactors shade header and keyguard status bar to read status bar dimens from a"
+        " central place, instead of reading resources directly. This is to take into account display"
+        " cutouts and other special cases. "
+   bug: "317199366"
+}
+
+flag {
   name: "enable_layout_tracing"
   namespace: "systemui"
   description: "Enables detailed traversal slices during measure and layout in perfetto traces"
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 8438051..872187a 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -23,7 +23,7 @@
 
 android_library {
 
-    name: "SystemUIAnimationLib",
+    name: "PlatformAnimationLib",
     use_resource_processor: true,
 
     srcs: [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index b738e2b..efdbfdb 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -494,7 +494,7 @@
             }
 
             for (i in 0 until drawable.numberOfLayers) {
-                (drawable.getDrawable(i) as? GradientDrawable)?.cornerRadii = radii
+                applyBackgroundRadii(drawable.getDrawable(i), radii)
             }
         }
 
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index 42d088f..9a4347d 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -30,7 +30,7 @@
     ],
 
     static_libs: [
-        "SystemUIAnimationLib",
+        "PlatformAnimationLib",
 
         "androidx.compose.runtime_runtime",
         "androidx.compose.material3_material3",
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 796abf4b..5ab2235 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -38,6 +38,7 @@
         "androidx.compose.runtime_runtime",
         "androidx.compose.animation_animation-graphics",
         "androidx.compose.material3_material3",
+        "androidx.compose.material_material-icons-extended",
         "androidx.activity_activity-compose",
     ],
 
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 249b3e1..2fea6a5 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
@@ -7,7 +7,6 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Close
@@ -21,10 +20,8 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.pointerInteropFilter
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
@@ -33,15 +30,14 @@
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.SceneTransitionLayout
-import com.android.compose.animation.scene.SceneTransitionLayoutState
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.compose.animation.scene.transitions
+import com.android.compose.animation.scene.updateSceneTransitionLayoutState
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.transform
 
@@ -66,7 +62,6 @@
  * This is a temporary container to allow the communal UI to use [SceneTransitionLayout] for gesture
  * handling and transitions before the full Flexiglass layout is ready.
  */
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalCoroutinesApi::class)
 @Composable
 fun CommunalContainer(
     modifier: Modifier = Modifier,
@@ -76,9 +71,15 @@
         viewModel.currentScene
             .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,
-    // which can be opened from many locations.
+    val sceneTransitionLayoutState =
+        updateSceneTransitionLayoutState(
+            currentScene,
+            onChangeScene = { viewModel.onSceneChanged(it.toCommunalSceneKey()) },
+            transitions = sceneTransitions,
+        )
+
+    // Don't show hub mode UI if keyguard is not present. This is important since we're in the
+    // shade, which can be opened from many locations.
     val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false)
 
     // Failsafe to hide the whole SceneTransitionLayout in case of bugginess.
@@ -96,59 +97,30 @@
         onDispose { viewModel.setTransitionState(null) }
     }
 
-    Box(modifier = modifier.fillMaxSize()) {
-        SceneTransitionLayout(
-            modifier = Modifier.fillMaxSize(),
-            currentScene = currentScene,
-            onChangeScene = { sceneKey -> viewModel.onSceneChanged(sceneKey.toCommunalSceneKey()) },
-            transitions = sceneTransitions,
-            state = sceneTransitionLayoutState,
-            edgeDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize)
+    SceneTransitionLayout(
+        state = sceneTransitionLayoutState,
+        modifier = modifier.fillMaxSize(),
+        edgeDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize),
+    ) {
+        scene(
+            TransitionSceneKey.Blank,
+            userActions =
+                mapOf(
+                    Swipe(SwipeDirection.Left, fromEdge = Edge.Right) to TransitionSceneKey.Communal
+                )
         ) {
-            scene(
-                TransitionSceneKey.Blank,
-                userActions =
-                    mapOf(
-                        Swipe(SwipeDirection.Left, fromEdge = Edge.Right) to
-                            TransitionSceneKey.Communal
-                    )
-            ) {
-                BlankScene { showSceneTransitionLayout = false }
-            }
-
-            scene(
-                TransitionSceneKey.Communal,
-                userActions =
-                    mapOf(
-                        Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to
-                            TransitionSceneKey.Blank
-                    ),
-            ) {
-                CommunalScene(viewModel, modifier = modifier)
-            }
+            BlankScene { showSceneTransitionLayout = false }
         }
 
-        // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't
-        //  block touches anymore.
-        Box(
-            modifier =
-                Modifier.fillMaxSize()
-                    // Offsetting to the left so that edge swipe to open the hub still works. This
-                    // does mean that the very right edge of the hub won't refresh the screen
-                    // timeout, but should be good enough for a temporary solution.
-                    .offset(x = -ContainerDimensions.EdgeSwipeSize)
-                    .pointerInteropFilter {
-                        viewModel.onUserActivity()
-                        if (
-                            sceneTransitionLayoutState.transitionState.currentScene ==
-                                TransitionSceneKey.Blank
-                        ) {
-                            viewModel.onOuterTouch(it)
-                            return@pointerInteropFilter true
-                        }
-                        false
-                    }
-        )
+        scene(
+            TransitionSceneKey.Communal,
+            userActions =
+                mapOf(
+                    Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TransitionSceneKey.Blank
+                ),
+        ) {
+            CommunalScene(viewModel, modifier = modifier)
+        }
     }
 }
 
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 5a4e0a9..d76f0ff 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,13 +20,13 @@
 import android.os.Bundle
 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.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -45,6 +45,8 @@
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.TouchApp
+import androidx.compose.material.icons.outlined.Widgets
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonColors
 import androidx.compose.material3.ButtonDefaults
@@ -52,6 +54,7 @@
 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.Text
 import androidx.compose.runtime.Composable
@@ -72,11 +75,14 @@
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.window.Popup
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalContentSize
@@ -94,13 +100,16 @@
     onEditDone: (() -> Unit)? = null,
 ) {
     val communalContent by viewModel.communalContent.collectAsState(initial = emptyList())
+    val isPopupOnDismissCtaShowing by
+        viewModel.isPopupOnDismissCtaShowing.collectAsState(initial = false)
     var removeButtonCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
     var toolbarSize: IntSize? by remember { mutableStateOf(null) }
     var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
     var isDraggingToRemove by remember { mutableStateOf(false) }
 
     Box(
-        modifier = modifier.fillMaxSize().background(Color.White),
+        modifier =
+            modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant),
     ) {
         CommunalHubLazyGrid(
             communalContent = communalContent,
@@ -111,7 +120,8 @@
                 isDraggingToRemove =
                     checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates)
                 isDraggingToRemove
-            }
+            },
+            onOpenWidgetPicker = onOpenWidgetPicker,
         )
 
         if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
@@ -128,6 +138,10 @@
             }
         }
 
+        if (isPopupOnDismissCtaShowing) {
+            PopupOnDismissCtaTile(viewModel::onHidePopupAfterDismissCta)
+        }
+
         // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving
         // touches, so that the SceneTransitionLayout can intercept the touches and allow an edge
         // swipe back to the blank scene.
@@ -148,13 +162,14 @@
     contentPadding: PaddingValues,
     setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit,
     updateDragPositionForRemove: (offset: Offset) -> Boolean,
+    onOpenWidgetPicker: (() -> Unit)? = null,
 ) {
     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)
+        val contentListState = rememberContentListState(list, viewModel)
         list = contentListState.list
         // for drag & drop operations within the communal hub grid
         dragDropState =
@@ -166,7 +181,7 @@
         gridModifier =
             gridModifier
                 .fillMaxSize()
-                .dragContainer(dragDropState, beforeContentPadding(contentPadding))
+                .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel)
                 .onGloballyPositioned { setGridCoordinates(it) }
         // for widgets dropped from other activities
         val dragAndDropTargetState =
@@ -205,15 +220,18 @@
                     list[index].size.dp().value,
                 )
             if (viewModel.isEditMode && dragDropState != null) {
-                DraggableItem(dragDropState = dragDropState, enabled = true, index = index) {
-                    isDragging ->
-                    val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
+                DraggableItem(
+                    dragDropState = dragDropState,
+                    enabled = list[index] is CommunalContentModel.Widget,
+                    index = index,
+                    size = size
+                ) { _ ->
                     CommunalContent(
                         modifier = cardModifier,
-                        elevation = elevation,
                         model = list[index],
                         viewModel = viewModel,
                         size = size,
+                        onOpenWidgetPicker = onOpenWidgetPicker,
                     )
                 }
             } else {
@@ -254,16 +272,11 @@
         horizontalArrangement = Arrangement.SpaceBetween,
         verticalAlignment = Alignment.CenterVertically
     ) {
-        val buttonContentPadding =
-            PaddingValues(
-                vertical = Dimensions.ToolbarButtonPaddingVertical,
-                horizontal = Dimensions.ToolbarButtonPaddingHorizontal,
-            )
         val spacerModifier = Modifier.width(Dimensions.ToolbarButtonSpaceBetween)
         Button(
             onClick = onOpenWidgetPicker,
-            colors = filledSecondaryButtonColors(),
-            contentPadding = buttonContentPadding
+            colors = filledButtonColors(),
+            contentPadding = Dimensions.ButtonPadding
         ) {
             Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor))
             Spacer(spacerModifier)
@@ -272,25 +285,40 @@
             )
         }
 
-        val buttonColors =
-            if (isDraggingToRemove) filledButtonColors() else ButtonDefaults.outlinedButtonColors()
-        OutlinedButton(
-            onClick = {},
-            colors = buttonColors,
-            contentPadding = buttonContentPadding,
-            modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) },
-        ) {
-            Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_open_widget_editor))
-            Spacer(spacerModifier)
-            Text(
-                text = stringResource(R.string.button_to_remove_widget),
-            )
+        val colors = LocalAndroidColorScheme.current
+        if (isDraggingToRemove) {
+            Button(
+                // Button is disabled to make it non-clickable
+                enabled = false,
+                onClick = {},
+                colors =
+                    ButtonDefaults.buttonColors(
+                        disabledContainerColor = colors.primary,
+                        disabledContentColor = colors.onPrimary,
+                    ),
+                contentPadding = Dimensions.ButtonPadding,
+                modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
+            ) {
+                RemoveButtonContent(spacerModifier)
+            }
+        } else {
+            OutlinedButton(
+                // Button is disabled to make it non-clickable
+                enabled = false,
+                onClick = {},
+                colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary),
+                border = BorderStroke(width = 1.0.dp, color = colors.primary),
+                contentPadding = Dimensions.ButtonPadding,
+                modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
+            ) {
+                RemoveButtonContent(spacerModifier)
+            }
         }
 
         Button(
             onClick = onEditDone,
             colors = filledButtonColors(),
-            contentPadding = buttonContentPadding
+            contentPadding = Dimensions.ButtonPadding
         ) {
             Text(
                 text = stringResource(R.string.hub_mode_editing_exit_button_text),
@@ -300,6 +328,47 @@
 }
 
 @Composable
+private fun PopupOnDismissCtaTile(onHidePopupAfterDismissCta: () -> Unit) {
+    Popup(
+        alignment = Alignment.TopCenter,
+        offset = IntOffset(0, 40),
+        onDismissRequest = onHidePopupAfterDismissCta
+    ) {
+        val colors = LocalAndroidColorScheme.current
+        Row(
+            modifier =
+                Modifier.height(56.dp)
+                    .background(colors.secondary, RoundedCornerShape(50.dp))
+                    .padding(16.dp),
+            horizontalArrangement = Arrangement.Center,
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Icon(
+                imageVector = Icons.Outlined.TouchApp,
+                contentDescription = stringResource(R.string.popup_on_dismiss_cta_tile_text),
+                tint = colors.onSecondary,
+                modifier = Modifier.size(20.dp)
+            )
+            Spacer(modifier = Modifier.size(8.dp))
+            Text(
+                text = stringResource(R.string.popup_on_dismiss_cta_tile_text),
+                style = MaterialTheme.typography.titleSmall,
+                color = colors.onSecondary,
+            )
+        }
+    }
+}
+
+@Composable
+private fun RemoveButtonContent(spacerModifier: Modifier) {
+    Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_remove_widget))
+    Spacer(spacerModifier)
+    Text(
+        text = stringResource(R.string.button_to_remove_widget),
+    )
+}
+
+@Composable
 private fun filledButtonColors(): ButtonColors {
     val colors = LocalAndroidColorScheme.current
     return ButtonDefaults.buttonColors(
@@ -309,25 +378,20 @@
 }
 
 @Composable
-private fun filledSecondaryButtonColors(): ButtonColors {
-    val colors = LocalAndroidColorScheme.current
-    return ButtonDefaults.buttonColors(
-        containerColor = colors.secondary,
-        contentColor = colors.onSecondary,
-    )
-}
-
-@Composable
 private fun CommunalContent(
     model: CommunalContentModel,
     viewModel: BaseCommunalViewModel,
     size: SizeF,
     modifier: Modifier = Modifier,
-    elevation: Dp = 0.dp,
+    onOpenWidgetPicker: (() -> Unit)? = null,
 ) {
     when (model) {
-        is CommunalContentModel.Widget -> WidgetContent(model, size, elevation, modifier)
+        is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier)
         is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size)
+        is CommunalContentModel.CtaTileInViewMode ->
+            CtaTileInViewModeContent(viewModel, size, modifier)
+        is CommunalContentModel.CtaTileInEditMode ->
+            CtaTileInEditModeContent(size, modifier, onOpenWidgetPicker)
         is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
         is CommunalContentModel.Tutorial -> TutorialContent(modifier)
         is CommunalContentModel.Umo -> Umo(viewModel, modifier)
@@ -345,23 +409,141 @@
     ) {}
 }
 
+/** Presents a CTA tile at the end of the grid, to customize the hub. */
 @Composable
-private fun WidgetContent(
-    model: CommunalContentModel.Widget,
+private fun CtaTileInViewModeContent(
+    viewModel: BaseCommunalViewModel,
     size: SizeF,
-    elevation: Dp,
     modifier: Modifier = Modifier,
 ) {
+    val colors = LocalAndroidColorScheme.current
     Card(
         modifier = modifier.height(size.height.dp),
-        elevation = CardDefaults.cardElevation(draggedElevation = elevation),
+        colors =
+            CardDefaults.cardColors(
+                containerColor = colors.primary,
+                contentColor = colors.onPrimary,
+            ),
+        shape = RoundedCornerShape(80.dp, 40.dp, 80.dp, 40.dp)
+    ) {
+        Column(
+            modifier = Modifier.fillMaxSize().padding(horizontal = 82.dp),
+            verticalArrangement =
+                Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically),
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Icon(
+                imageVector = Icons.Outlined.Widgets,
+                contentDescription = stringResource(R.string.cta_label_to_open_widget_picker),
+                modifier = Modifier.size(Dimensions.IconSize),
+            )
+            Text(
+                text = stringResource(R.string.cta_label_to_edit_widget),
+                style = MaterialTheme.typography.titleLarge,
+                textAlign = TextAlign.Center,
+            )
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalArrangement = Arrangement.Center,
+            ) {
+                OutlinedButton(
+                    colors =
+                        ButtonDefaults.buttonColors(
+                            contentColor = colors.onPrimary,
+                        ),
+                    border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
+                    contentPadding = Dimensions.ButtonPadding,
+                    onClick = viewModel::onDismissCtaTile,
+                ) {
+                    Text(
+                        text = stringResource(R.string.cta_tile_button_to_dismiss),
+                    )
+                }
+                Spacer(modifier = Modifier.size(Dimensions.Spacing))
+                Button(
+                    colors =
+                        ButtonDefaults.buttonColors(
+                            containerColor = colors.primaryContainer,
+                            contentColor = colors.onPrimaryContainer,
+                        ),
+                    contentPadding = Dimensions.ButtonPadding,
+                    onClick = viewModel::onOpenWidgetEditor
+                ) {
+                    Text(
+                        text = stringResource(R.string.cta_tile_button_to_open_widget_editor),
+                    )
+                }
+            }
+        }
+    }
+}
+
+/** Presents a CTA tile at the end of the hub in edit mode, to add more widgets. */
+@Composable
+private fun CtaTileInEditModeContent(
+    size: SizeF,
+    modifier: Modifier = Modifier,
+    onOpenWidgetPicker: (() -> Unit)? = null,
+) {
+    if (onOpenWidgetPicker == null) {
+        throw IllegalArgumentException("onOpenWidgetPicker should not be null.")
+    }
+    val colors = LocalAndroidColorScheme.current
+    Card(
+        modifier = modifier.height(size.height.dp),
+        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+        border = BorderStroke(1.dp, colors.primary),
+        shape = RoundedCornerShape(200.dp),
+        onClick = onOpenWidgetPicker,
+    ) {
+        Column(
+            modifier = Modifier.fillMaxSize(),
+            verticalArrangement =
+                Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically),
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Icon(
+                imageVector = Icons.Outlined.Widgets,
+                contentDescription = stringResource(R.string.cta_label_to_open_widget_picker),
+                tint = colors.primary,
+                modifier = Modifier.size(Dimensions.IconSize),
+            )
+            Text(
+                text = stringResource(R.string.cta_label_to_open_widget_picker),
+                style = MaterialTheme.typography.titleLarge,
+                color = colors.primary,
+                textAlign = TextAlign.Center,
+            )
+        }
+    }
+}
+
+@Composable
+private fun WidgetContent(
+    viewModel: BaseCommunalViewModel,
+    model: CommunalContentModel.Widget,
+    size: SizeF,
+    modifier: Modifier = Modifier,
+) {
+    Box(
+        modifier = modifier.height(size.height.dp),
+        contentAlignment = Alignment.Center,
     ) {
         AndroidView(
             modifier = modifier,
             factory = { context ->
-                model.appWidgetHost
-                    .createView(context, model.appWidgetId, model.providerInfo)
-                    .apply { updateAppWidgetSize(Bundle.EMPTY, listOf(size)) }
+                // The AppWidgetHostView will inherit the interaction handler from the
+                // AppWidgetHost. So set the interaction handler here before creating the view, and
+                // then clear it after the view is created. This is a workaround due to the fact
+                // that the interaction handler cannot be specified when creating the view,
+                // and there are race conditions if it is set after the view is created.
+                model.appWidgetHost.setInteractionHandler(viewModel.getInteractionHandler())
+                val view =
+                    model.appWidgetHost
+                        .createView(context, model.appWidgetId, model.providerInfo)
+                        .apply { updateAppWidgetSize(Bundle.EMPTY, listOf(size)) }
+                model.appWidgetHost.setInteractionHandler(null)
+                view
             },
             // For reusing composition in lazy lists.
             onReset = {},
@@ -488,4 +670,10 @@
     val ToolbarButtonPaddingHorizontal = 24.dp
     val ToolbarButtonPaddingVertical = 16.dp
     val ToolbarButtonSpaceBetween = 8.dp
+    val ButtonPadding =
+        PaddingValues(
+            vertical = ToolbarButtonPaddingVertical,
+            horizontal = ToolbarButtonPaddingHorizontal,
+        )
+    val IconSize = 48.dp
 }
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 0d460aa8..1138221 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.ui.compose
 
+import android.util.SizeF
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
 import androidx.compose.foundation.gestures.scrollBy
@@ -39,6 +40,7 @@
 import androidx.compose.ui.unit.toSize
 import androidx.compose.ui.zIndex
 import com.android.systemui.communal.ui.compose.extensions.plus
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.launch
@@ -206,7 +208,8 @@
 
 fun Modifier.dragContainer(
     dragDropState: GridDragDropState,
-    beforeContentPadding: ContentPaddingInPx
+    beforeContentPadding: ContentPaddingInPx,
+    viewModel: BaseCommunalViewModel,
 ): Modifier {
     return pointerInput(dragDropState, beforeContentPadding) {
         detectDragGesturesAfterLongPress(
@@ -219,9 +222,16 @@
                     offset,
                     Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding)
                 )
+                viewModel.onReorderWidgetStart()
             },
-            onDragEnd = { dragDropState.onDragInterrupted() },
-            onDragCancel = { dragDropState.onDragInterrupted() }
+            onDragEnd = {
+                dragDropState.onDragInterrupted()
+                viewModel.onReorderWidgetEnd()
+            },
+            onDragCancel = {
+                dragDropState.onDragInterrupted()
+                viewModel.onReorderWidgetCancel()
+            }
         )
     }
 }
@@ -233,6 +243,7 @@
     dragDropState: GridDragDropState,
     index: Int,
     enabled: Boolean,
+    size: SizeF,
     modifier: Modifier = Modifier,
     content: @Composable (isDragging: Boolean) -> Unit
 ) {
@@ -250,7 +261,11 @@
         } else {
             Modifier.animateItemPlacement()
         }
-    Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
-        content(dragging)
+
+    Box(modifier) {
+        if (dragging) {
+            WidgetPlaceholderContent(size)
+        }
+        Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 4eb9089..c35202c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -25,7 +25,6 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -38,11 +37,11 @@
 import com.android.compose.animation.scene.ObservableTransitionState as SceneTransitionObservableTransitionState
 import com.android.compose.animation.scene.SceneKey as SceneTransitionSceneKey
 import com.android.compose.animation.scene.SceneTransitionLayout
-import com.android.compose.animation.scene.SceneTransitionLayoutState
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction
 import com.android.compose.animation.scene.observableTransitionState
+import com.android.compose.animation.scene.updateSceneTransitionLayoutState
 import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.Edge
@@ -82,7 +81,12 @@
     val currentScene = checkNotNull(sceneByKey[currentSceneKey])
     val currentDestinations: Map<UserAction, SceneModel> by
         currentScene.destinationScenes.collectAsState()
-    val state = remember { SceneTransitionLayoutState(currentSceneKey.toTransitionSceneKey()) }
+    val state =
+        updateSceneTransitionLayoutState(
+            currentSceneKey.toTransitionSceneKey(),
+            onChangeScene = viewModel::onSceneChanged,
+            transitions = SceneContainerTransitions,
+        )
 
     DisposableEffect(viewModel, state) {
         viewModel.setTransitionState(state.observableTransitionState().map { it.toModel() })
@@ -93,9 +97,6 @@
         modifier = Modifier.fillMaxSize(),
     ) {
         SceneTransitionLayout(
-            currentScene = currentSceneKey.toTransitionSceneKey(),
-            onChangeScene = viewModel::onSceneChanged,
-            transitions = SceneContainerTransitions,
             state = state,
             modifier =
                 modifier
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index ba6d00e..7d3b0fb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -28,9 +28,9 @@
  * the currently running transition, if there is one.
  */
 internal fun CoroutineScope.animateToScene(
-    layoutState: SceneTransitionLayoutStateImpl,
+    layoutState: BaseSceneTransitionLayoutState,
     target: SceneKey,
-) {
+): TransitionState.Transition? {
     val transitionState = layoutState.transitionState
     if (transitionState.currentScene == target) {
         // This can happen in 3 different situations, for which there isn't anything else to do:
@@ -41,10 +41,10 @@
         //     a. didn't release their pointer yet.
         //     b. released their pointer such that the swipe gesture was cancelled and the
         //        transition is currently animating back to [target].
-        return
+        return null
     }
 
-    when (transitionState) {
+    return when (transitionState) {
         is TransitionState.Idle -> animate(layoutState, target)
         is TransitionState.Transition -> {
             // A transition is currently running: first check whether `transition.toScene` or
@@ -62,47 +62,43 @@
                     // finish the current transition early to make sure that the current state
                     // change is committed.
                     layoutState.finishTransition(transitionState, transitionState.currentScene)
+                    null
                 } else {
                     // The transition is in progress: start the canned animation at the same
                     // progress as it was in.
                     // TODO(b/290184746): Also take the current velocity into account.
                     animate(layoutState, target, startProgress = progress)
                 }
-
-                return
-            }
-
-            if (transitionState.fromScene == target) {
+            } else if (transitionState.fromScene == target) {
                 // There is a transition from [target] to another scene: simply animate the same
                 // transition progress to `0`.
-
                 check(transitionState.toScene == transitionState.currentScene)
+
                 val progress = transitionState.progress
                 if (progress.absoluteValue < ProgressVisibilityThreshold) {
                     // The transition is at progress ~= 0: no need to animate.We finish the current
                     // transition early to make sure that the current state change is committed.
                     layoutState.finishTransition(transitionState, transitionState.currentScene)
+                    null
                 } else {
                     // TODO(b/290184746): Also take the current velocity into account.
                     animate(layoutState, target, startProgress = progress, reversed = true)
                 }
-
-                return
+            } else {
+                // Generic interruption; the current transition is neither from or to [target].
+                // TODO(b/290930950): Better handle interruptions here.
+                animate(layoutState, target)
             }
-
-            // Generic interruption; the current transition is neither from or to [target].
-            // TODO(b/290930950): Better handle interruptions here.
-            animate(layoutState, target)
         }
     }
 }
 
 private fun CoroutineScope.animate(
-    layoutState: SceneTransitionLayoutStateImpl,
+    layoutState: BaseSceneTransitionLayoutState,
     target: SceneKey,
     startProgress: Float = 0f,
     reversed: Boolean = false,
-) {
+): TransitionState.Transition {
     val fromScene = layoutState.transitionState.currentScene
     val isUserInput =
         (layoutState.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput
@@ -143,10 +139,15 @@
         }
 
     // Animate the progress to its target value.
-    launch {
-        animatable.animateTo(targetProgress, animationSpec)
-        layoutState.finishTransition(transition, target)
-    }
+    launch { animatable.animateTo(targetProgress, animationSpec) }
+        .invokeOnCompletion {
+            // Settle the state to Idle(target). Note that this will do nothing if this transition
+            // was replaced/interrupted by another one, and this also runs if this coroutine is
+            // cancelled, i.e. if [this] coroutine scope is cancelled.
+            layoutState.finishTransition(transition, target)
+        }
+
+    return transition
 }
 
 private class OneOffTransition(
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 280fbfb..a910bca 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
@@ -20,10 +20,10 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateMap
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.isSpecified
 import androidx.compose.ui.geometry.isUnspecified
 import androidx.compose.ui.geometry.lerp
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -46,41 +46,18 @@
 /** An element on screen, that can be composed in one or more scenes. */
 @Stable
 internal class Element(val key: ElementKey) {
-    /**
-     * The last state of this element, coming from any scene. Note that this state will be unstable
-     * if this element is present in multiple scenes but the shared element animation is disabled,
-     * given that multiple instances of the element with different states will write to this state.
-     * You should prefer using [SceneState.lastState] in the current scene when it is defined.
-     */
-    val lastSharedState = State()
-
     /** The mapping between a scene and the state this element has in that scene, if any. */
-    val sceneStates = mutableMapOf<SceneKey, SceneState>()
+    // TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions
+    // are first seen by composition then layout/drawing code. See 316901148#comment2 for details.
+    val sceneStates = SnapshotStateMap<SceneKey, SceneState>()
 
     override fun toString(): String {
         return "Element(key=$key)"
     }
 
-    /** The state of this element, either in a specific scene or in a shared context. */
-    class State {
-        /** The offset of the element, relative to the SceneTransitionLayout containing it. */
-        var offset = Offset.Unspecified
-
-        /** The size of this element. */
-        var size = SizeUnspecified
-
-        /** The draw scale of this element. */
-        var drawScale = Scale.Default
-
-        /** The alpha of this element. */
-        var alpha = AlphaUnspecified
-    }
-
     /** The last and target state of this element in a given scene. */
     @Stable
     class SceneState(val scene: SceneKey) {
-        val lastState = State()
-
         var targetSize by mutableStateOf(SizeUnspecified)
         var targetOffset by mutableStateOf(Offset.Unspecified)
 
@@ -94,7 +71,6 @@
 
     companion object {
         val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-        val AlphaUnspecified = Float.MIN_VALUE
     }
 }
 
@@ -219,7 +195,7 @@
     }
 
     override fun ContentDrawScope.draw() {
-        val drawScale = getDrawScale(layoutImpl, element, scene, sceneState)
+        val drawScale = getDrawScale(layoutImpl, element, scene)
         if (drawScale == Scale.Default) {
             drawContent()
         } else {
@@ -264,7 +240,6 @@
     // Always draw the element if there is no ongoing transition or if the element is not shared.
     if (
         transition == null ||
-            !layoutImpl.isTransitionReady(transition) ||
             transition.fromScene !in element.sceneStates ||
             transition.toScene !in element.sceneStates
     ) {
@@ -304,7 +279,7 @@
 }
 
 private fun isSharedElementEnabled(
-    layoutState: SceneTransitionLayoutStateImpl,
+    layoutState: BaseSceneTransitionLayoutState,
     transition: TransitionState.Transition,
     element: ElementKey,
 ): Boolean {
@@ -312,7 +287,7 @@
 }
 
 internal fun sharedElementTransformation(
-    layoutState: SceneTransitionLayoutStateImpl,
+    layoutState: BaseSceneTransitionLayoutState,
     transition: TransitionState.Transition,
     element: ElementKey,
 ): SharedElementTransformation? {
@@ -342,18 +317,9 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     scene: Scene,
-    sceneState: Element.SceneState,
 ): Boolean {
     val transition = layoutImpl.state.currentTransition ?: return true
 
-    if (!layoutImpl.isTransitionReady(transition)) {
-        val lastValue =
-            sceneState.lastState.alpha.takeIf { it != Element.AlphaUnspecified }
-                ?: element.lastSharedState.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f
-
-        return lastValue == 1f
-    }
-
     val fromScene = transition.fromScene
     val toScene = transition.toScene
     val fromState = element.sceneStates[fromScene]
@@ -383,7 +349,6 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     scene: Scene,
-    sceneState: Element.SceneState,
 ): Float {
     return computeValue(
             layoutImpl,
@@ -393,10 +358,7 @@
             transformation = { it.alpha },
             idleValue = 1f,
             currentValue = { 1f },
-            lastValue = {
-                sceneState.lastState.alpha.takeIf { it != Element.AlphaUnspecified }
-                    ?: element.lastSharedState.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f
-            },
+            isSpecified = { true },
             ::lerp,
         )
         .coerceIn(0f, 1f)
@@ -434,34 +396,23 @@
             transformation = { it.size },
             idleValue = lookaheadSize,
             currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
-            lastValue = {
-                sceneState.lastState.size.takeIf { it != Element.SizeUnspecified }
-                    ?: element.lastSharedState.size.takeIf { it != Element.SizeUnspecified }
-                        ?: measurable.measure(constraints).also { maybePlaceable = it }.size()
-            },
+            isSpecified = { it != Element.SizeUnspecified },
             ::lerp,
         )
 
-    val placeable =
-        maybePlaceable
-            ?: measurable.measure(
-                Constraints.fixed(
-                    targetSize.width.coerceAtLeast(0),
-                    targetSize.height.coerceAtLeast(0),
-                )
+    return maybePlaceable
+        ?: measurable.measure(
+            Constraints.fixed(
+                targetSize.width.coerceAtLeast(0),
+                targetSize.height.coerceAtLeast(0),
             )
-
-    val size = placeable.size()
-    element.lastSharedState.size = size
-    sceneState.lastState.size = size
-    return placeable
+        )
 }
 
 private fun getDrawScale(
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
-    scene: Scene,
-    sceneState: Element.SceneState
+    scene: Scene
 ): Scale {
     return computeValue(
         layoutImpl,
@@ -471,10 +422,7 @@
         transformation = { it.drawScale },
         idleValue = Scale.Default,
         currentValue = { Scale.Default },
-        lastValue = {
-            sceneState.lastState.drawScale.takeIf { it != Scale.Default }
-                ?: element.lastSharedState.drawScale
-        },
+        isSpecified = { true },
         ::lerp,
     )
 }
@@ -498,9 +446,12 @@
             sceneState.targetOffset = targetOffsetInScene
         }
 
+        // No need to place the element in this scene if we don't want to draw it anyways.
+        if (!shouldDrawElement(layoutImpl, scene, element)) {
+            return
+        }
+
         val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
-        val lastSharedState = element.lastSharedState
-        val lastSceneState = sceneState.lastState
         val targetOffset =
             computeValue(
                 layoutImpl,
@@ -510,37 +461,19 @@
                 transformation = { it.offset },
                 idleValue = targetOffsetInScene,
                 currentValue = { currentOffset },
-                lastValue = {
-                    lastSceneState.offset.takeIf { it.isSpecified }
-                        ?: lastSharedState.offset.takeIf { it.isSpecified } ?: currentOffset
-                },
+                isSpecified = { it != Offset.Unspecified },
                 ::lerp,
             )
 
-        lastSharedState.offset = targetOffset
-        lastSceneState.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 last(Shared|Scene)State,
-        // otherwise they will be out of date.
-        if (!shouldDrawElement(layoutImpl, scene, element)) {
-            return
-        }
-
         val offset = (targetOffset - currentOffset).round()
-        if (isElementOpaque(layoutImpl, element, scene, sceneState)) {
+        if (isElementOpaque(layoutImpl, element, scene)) {
             // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is not
             // animated once b/305195729 is fixed. Test that drawing is not invalidated in that
             // case.
             placeable.place(offset)
-            lastSharedState.alpha = 1f
-            lastSceneState.alpha = 1f
         } else {
             placeable.placeWithLayer(offset) {
-                val alpha = elementAlpha(layoutImpl, element, scene, sceneState)
-                this.alpha = alpha
-                lastSharedState.alpha = alpha
-                lastSceneState.alpha = alpha
+                this.alpha = elementAlpha(layoutImpl, element, scene)
             }
         }
     }
@@ -563,8 +496,6 @@
  *   different than [idleValue] even if the value is not transformed directly because it could be
  *   impacted by the transformations on other elements, like a parent that is being translated or
  *   resized.
- * @param lastValue the last value that was used. This should be equal to [currentValue] if this is
- *   the first time the value is set.
  * @param lerp the linear interpolation function used to interpolate between two values of this
  *   value type.
  */
@@ -576,7 +507,7 @@
     transformation: (ElementTransformations) -> PropertyTransformation<T>?,
     idleValue: T,
     currentValue: () -> T,
-    lastValue: () -> T,
+    isSpecified: (T) -> Boolean,
     lerp: (T, T, Float) -> T,
 ): T {
     val transition =
@@ -587,21 +518,16 @@
         // layout phase.
         ?: return currentValue()
 
-    // A transition was started but it's not ready yet (not all elements have been composed/laid
-    // out yet). Use the last value that was set, to make sure elements don't unexpectedly jump.
-    if (!layoutImpl.isTransitionReady(transition)) {
-        return lastValue()
-    }
-
     val fromScene = transition.fromScene
     val toScene = transition.toScene
+
     val fromState = element.sceneStates[fromScene]
     val toState = element.sceneStates[toScene]
 
     if (fromState == null && toState == null) {
         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
         // run anymore.
-        return lastValue()
+        return idleValue
     }
 
     // The element is shared: interpolate between the value in fromScene and the value in toScene.
@@ -612,6 +538,11 @@
         val start = sceneValue(fromState!!)
         val end = sceneValue(toState!!)
 
+        // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all
+        // nodes before the intermediate layout pass.
+        if (!isSpecified(start)) return end
+        if (!isSpecified(end)) return start
+
         // Make sure we don't read progress if values are the same and we don't need to interpolate,
         // so we don't invalidate the phase where this is read.
         return if (start == end) start else lerp(start, end, transition.progress)
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 af3c099..cdc4778 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
@@ -174,22 +174,6 @@
         // If we are idle, there is only one [scene] that is composed so we can compose our
         // movable content here.
         ?: return true
-    val fromScene = transition.fromScene
-    val toScene = transition.toScene
-
-    val fromReady = layoutImpl.isSceneReady(fromScene)
-    val toReady = layoutImpl.isSceneReady(toScene)
-
-    if (!fromReady && !toReady) {
-        // Neither of the scenes will be drawn, so where we compose it doesn't really matter. Note
-        // that we could have slightly more complicated logic here to optimize for this case, but
-        // it's not worth it given that readyScenes should disappear soon (b/316901148).
-        return scene == toScene
-    }
-
-    // If one of the scenes is not ready, compose it in the other one to make sure it is drawn.
-    if (!fromReady) return scene == toScene
-    if (!toReady) return scene == fromScene
 
     // Always compose movable elements in the scene picked by their scene picker.
     return shouldDrawOrComposeSharedElement(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
index 454c0ec..b346a70 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
@@ -16,96 +16,118 @@
 
 package com.android.compose.animation.scene
 
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.geometry.toRect
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Outline
-import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.drawOutline
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.drawscope.translate
-import androidx.compose.ui.graphics.withSaveLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.GlobalPositionAwareModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
 import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.toSize
 
-internal fun Modifier.punchHole(
-    layoutImpl: SceneTransitionLayoutImpl,
-    element: ElementKey,
-    bounds: ElementKey,
-    shape: Shape,
-): Modifier = this.then(PunchHoleElement(layoutImpl, element, bounds, shape))
+/**
+ * Punch a hole in this node with the given [size], [offset] and [shape].
+ *
+ * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
+ * This can be used to make content drawn below an opaque element visible. For example, if we have
+ * [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
+ * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big clock
+ * time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be the
+ * result.
+ */
+@Stable
+fun Modifier.punchHole(
+    size: () -> Size,
+    offset: () -> Offset,
+    shape: Shape = RectangleShape,
+): Modifier = this.then(PunchHoleElement(size, offset, shape))
+
+/**
+ * Punch a hole in this node using the bounds of [coords] and the given [shape].
+ *
+ * You can use [androidx.compose.ui.layout.onGloballyPositioned] to get the last coordinates of a
+ * node.
+ */
+@Stable
+fun Modifier.punchHole(
+    coords: () -> LayoutCoordinates?,
+    shape: Shape = RectangleShape,
+): Modifier = this.then(PunchHoleWithBoundsElement(coords, shape))
 
 private data class PunchHoleElement(
-    private val layoutImpl: SceneTransitionLayoutImpl,
-    private val element: ElementKey,
-    private val bounds: ElementKey,
+    private val size: () -> Size,
+    private val offset: () -> Offset,
     private val shape: Shape,
 ) : ModifierNodeElement<PunchHoleNode>() {
-    override fun create(): PunchHoleNode = PunchHoleNode(layoutImpl, element, bounds, shape)
+    override fun create(): PunchHoleNode = PunchHoleNode(size, offset, { shape })
 
     override fun update(node: PunchHoleNode) {
-        node.layoutImpl = layoutImpl
-        node.element = element
-        node.bounds = bounds
-        node.shape = shape
+        node.size = size
+        node.offset = offset
+        node.shape = { shape }
     }
 }
 
 private class PunchHoleNode(
-    var layoutImpl: SceneTransitionLayoutImpl,
-    var element: ElementKey,
-    var bounds: ElementKey,
-    var shape: Shape,
-) : Modifier.Node(), DrawModifierNode {
+    var size: () -> Size,
+    var offset: () -> Offset,
+    var shape: () -> Shape,
+) : Modifier.Node(), DrawModifierNode, LayoutModifierNode {
     private var lastSize: Size = Size.Unspecified
     private var lastLayoutDirection: LayoutDirection = LayoutDirection.Ltr
     private var lastOutline: Outline? = null
 
-    override fun ContentDrawScope.draw() {
-        val bounds = layoutImpl.elements[bounds]
-
-        if (
-            bounds == null ||
-                bounds.lastSharedState.size == Element.SizeUnspecified ||
-                bounds.lastSharedState.offset == Offset.Unspecified
-        ) {
-            drawContent()
-            return
-        }
-
-        val element = layoutImpl.elements.getValue(element)
-        drawIntoCanvas { canvas ->
-            canvas.withSaveLayer(size.toRect(), Paint()) {
-                drawContent()
-
-                val offset = bounds.lastSharedState.offset - element.lastSharedState.offset
-                translate(offset.x, offset.y) { drawHole(bounds) }
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        return measurable.measure(constraints).run {
+            layout(width, height) {
+                placeWithLayer(0, 0) { compositingStrategy = CompositingStrategy.Offscreen }
             }
         }
     }
 
-    private fun DrawScope.drawHole(bounds: Element) {
-        val boundsSize = bounds.lastSharedState.size.toSize()
+    override fun ContentDrawScope.draw() {
+        drawContent()
+
+        val holeSize = size()
+        if (holeSize != Size.Zero) {
+            val offset = offset()
+            translate(offset.x, offset.y) { drawHole(holeSize) }
+        }
+    }
+
+    private fun DrawScope.drawHole(size: Size) {
         if (shape == RectangleShape) {
-            drawRect(Color.Black, size = boundsSize, blendMode = BlendMode.DstOut)
+            drawRect(Color.Black, size = size, blendMode = BlendMode.DstOut)
             return
         }
 
         val outline =
-            if (boundsSize == lastSize && layoutDirection == lastLayoutDirection) {
+            if (size == lastSize && layoutDirection == lastLayoutDirection) {
                 lastOutline!!
             } else {
-                val newOutline = shape.createOutline(boundsSize, layoutDirection, this)
-                lastSize = boundsSize
+                val newOutline = shape().createOutline(size, layoutDirection, this)
+                lastSize = size
                 lastLayoutDirection = layoutDirection
                 lastOutline = newOutline
                 newOutline
@@ -118,3 +140,39 @@
         )
     }
 }
+
+private data class PunchHoleWithBoundsElement(
+    private val coords: () -> LayoutCoordinates?,
+    private val shape: Shape,
+) : ModifierNodeElement<PunchHoleWithBoundsNode>() {
+    override fun create(): PunchHoleWithBoundsNode = PunchHoleWithBoundsNode(coords, shape)
+
+    override fun update(node: PunchHoleWithBoundsNode) {
+        node.holeCoords = coords
+        node.shape = shape
+    }
+}
+
+private class PunchHoleWithBoundsNode(
+    var holeCoords: () -> LayoutCoordinates?,
+    var shape: Shape,
+) : DelegatingNode(), DrawModifierNode, GlobalPositionAwareModifierNode {
+    private val delegate = delegate(PunchHoleNode(::holeSize, ::holeOffset, ::shape))
+    private var lastCoords: LayoutCoordinates? = null
+
+    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+        this.lastCoords = coordinates
+    }
+
+    override fun ContentDrawScope.draw() = with(delegate) { draw() }
+
+    private fun holeSize(): Size {
+        return holeCoords()?.size?.toSize() ?: Size.Zero
+    }
+
+    private fun holeOffset(): Offset {
+        val holeCoords = holeCoords() ?: return Offset.Zero
+        val lastCoords = lastCoords ?: error("draw() was called before onGloballyPositioned()")
+        return lastCoords.localPositionOf(holeCoords, relativeToSource = Offset.Zero)
+    }
+}
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 3537b79..f67df54 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
@@ -26,7 +26,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.layout.intermediateLayout
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
@@ -139,12 +138,6 @@
             bottomOrRightBehavior = bottomBehavior,
         )
 
-    override fun Modifier.punchHole(
-        element: ElementKey,
-        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/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 338557d..64388b7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -312,13 +312,16 @@
             // immediately go back B => A.
             if (targetScene != swipeTransition._currentScene) {
                 swipeTransition._currentScene = targetScene
-                layoutImpl.onChangeScene(targetScene.key)
+                with(layoutImpl.state) { coroutineScope.onChangeScene(targetScene.key) }
             }
 
-            animateOffset(
+            swipeTransition.animateOffset(
+                coroutineScope = coroutineScope,
                 initialVelocity = velocity,
                 targetOffset = targetOffset,
-                targetScene = targetScene.key
+                onAnimationCompleted = {
+                    layoutState.finishTransition(swipeTransition, idleScene = targetScene.key)
+                }
             )
         }
 
@@ -410,34 +413,6 @@
         }
     }
 
-    private fun animateOffset(
-        initialVelocity: Float,
-        targetOffset: Float,
-        targetScene: SceneKey,
-    ) {
-        swipeTransition.startOffsetAnimation {
-            coroutineScope.launch {
-                if (!swipeTransition.isAnimatingOffset) {
-                    swipeTransition.offsetAnimatable.snapTo(swipeTransition.dragOffset)
-                }
-                swipeTransition.isAnimatingOffset = true
-
-                swipeTransition.offsetAnimatable.animateTo(
-                    targetOffset,
-                    // TODO(b/290184746): Make this spring spec configurable.
-                    spring(
-                        stiffness = Spring.StiffnessMediumLow,
-                        visibilityThreshold = OffsetVisibilityThreshold
-                    ),
-                    initialVelocity = initialVelocity,
-                )
-
-                swipeTransition.finishOffsetAnimation()
-                layoutState.finishTransition(swipeTransition, targetScene)
-            }
-        }
-    }
-
     internal class SwipeTransition(
         val _fromScene: Scene,
         val _toScene: Scene,
@@ -479,12 +454,14 @@
         private var offsetAnimationJob: Job? = null
 
         /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
-        fun startOffsetAnimation(job: () -> Job) {
+        private fun startOffsetAnimation(job: () -> Job) {
             cancelOffsetAnimation()
             offsetAnimationJob = job()
         }
 
         /** Cancel any ongoing offset animation. */
+        // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
+        // the same time.
         fun cancelOffsetAnimation() {
             offsetAnimationJob?.cancel()
             finishOffsetAnimation()
@@ -496,6 +473,43 @@
                 dragOffset = offsetAnimatable.value
             }
         }
+
+        // TODO(b/290184746): Make this spring spec configurable.
+        private val animationSpec =
+            spring(
+                stiffness = Spring.StiffnessMediumLow,
+                visibilityThreshold = OffsetVisibilityThreshold
+            )
+
+        fun animateOffset(
+            // TODO(b/317063114) The CoroutineScope should be removed.
+            coroutineScope: CoroutineScope,
+            initialVelocity: Float,
+            targetOffset: Float,
+            onAnimationCompleted: () -> Unit,
+        ) {
+            startOffsetAnimation {
+                coroutineScope.launch {
+                    animateOffset(targetOffset, initialVelocity)
+                    onAnimationCompleted()
+                }
+            }
+        }
+
+        private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
+            if (!isAnimatingOffset) {
+                offsetAnimatable.snapTo(dragOffset)
+            }
+            isAnimatingOffset = true
+
+            offsetAnimatable.animateTo(
+                targetValue = targetOffset,
+                animationSpec = animationSpec,
+                initialVelocity = initialVelocity,
+            )
+
+            finishOffsetAnimation()
+        }
     }
 
     companion object {
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 84fade89..80f8c1c 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
@@ -19,17 +19,48 @@
 import androidx.annotation.FloatRange
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.platform.LocalDensity
-import kotlinx.coroutines.channels.Channel
+
+/**
+ * [SceneTransitionLayout] is a container that automatically animates its content whenever its state
+ * changes.
+ *
+ * Note: You should use [androidx.compose.animation.AnimatedContent] instead of
+ * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if
+ * you need support for swipe gestures, shared elements or transitions defined declaratively outside
+ * UI code.
+ *
+ * @param state the state of this layout.
+ * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
+ * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
+ *   intercepted, the progress value must be above the threshold, and below (1 - threshold).
+ * @param scenes the configuration of the different scenes of this layout.
+ * @see updateSceneTransitionLayoutState
+ */
+@Composable
+fun SceneTransitionLayout(
+    state: SceneTransitionLayoutState,
+    modifier: Modifier = Modifier,
+    edgeDetector: EdgeDetector = DefaultEdgeDetector,
+    @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
+    scenes: SceneTransitionLayoutScope.() -> Unit,
+) {
+    SceneTransitionLayoutForTesting(
+        state,
+        modifier,
+        edgeDetector,
+        transitionInterceptionThreshold,
+        onLayoutImpl = null,
+        scenes,
+    )
+}
 
 /**
  * [SceneTransitionLayout] is a container that automatically animates its content whenever
@@ -45,7 +76,6 @@
  *   This is called when the user commits a transition to a new scene because of a [UserAction], for
  *   instance by triggering back navigation or by swiping to a new scene.
  * @param transitions the definition of the transitions used to animate a change of scene.
- * @param state the observable state of this layout.
  * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
  * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
  *   intercepted, the progress value must be above the threshold, and below (1 - threshold).
@@ -57,20 +87,16 @@
     onChangeScene: (SceneKey) -> Unit,
     transitions: SceneTransitions,
     modifier: Modifier = Modifier,
-    state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
     edgeDetector: EdgeDetector = DefaultEdgeDetector,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
-    SceneTransitionLayoutForTesting(
-        currentScene,
-        onChangeScene,
-        modifier,
-        transitions,
+    val state = updateSceneTransitionLayoutState(currentScene, onChangeScene, transitions)
+    SceneTransitionLayout(
         state,
+        modifier,
         edgeDetector,
         transitionInterceptionThreshold,
-        onLayoutImpl = null,
         scenes,
     )
 }
@@ -203,18 +229,6 @@
     ): Modifier
 
     /**
-     * Punch a hole in this [element] using the bounds of [bounds] in [scene] and the given [shape].
-     *
-     * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
-     * This can be used to make content drawn below an opaque element visible. For example, if we
-     * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
-     * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big
-     * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be
-     * 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.
      */
@@ -346,11 +360,8 @@
  */
 @Composable
 internal fun SceneTransitionLayoutForTesting(
-    currentScene: SceneKey,
-    onChangeScene: (SceneKey) -> Unit,
+    state: SceneTransitionLayoutState,
     modifier: Modifier = Modifier,
-    transitions: SceneTransitions = transitions {},
-    state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
     edgeDetector: EdgeDetector = DefaultEdgeDetector,
     transitionInterceptionThreshold: Float = 0f,
     onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
@@ -360,8 +371,7 @@
     val coroutineScope = rememberCoroutineScope()
     val layoutImpl = remember {
         SceneTransitionLayoutImpl(
-                state = state as SceneTransitionLayoutStateImpl,
-                onChangeScene = onChangeScene,
+                state = state as BaseSceneTransitionLayoutState,
                 density = density,
                 edgeDetector = edgeDetector,
                 transitionInterceptionThreshold = transitionInterceptionThreshold,
@@ -375,7 +385,6 @@
     // SnapshotStateMap anymore.
     layoutImpl.updateScenes(scenes)
 
-    val targetSceneChannel = remember { Channel<SceneKey>(Channel.CONFLATED) }
     SideEffect {
         if (state != layoutImpl.state) {
             error(
@@ -384,23 +393,8 @@
             )
         }
 
-        layoutImpl.onChangeScene = onChangeScene
-        (state as SceneTransitionLayoutStateImpl).transitions = transitions
         layoutImpl.density = density
         layoutImpl.edgeDetector = edgeDetector
-
-        state.transitions = transitions
-
-        targetSceneChannel.trySend(currentScene)
-    }
-
-    LaunchedEffect(targetSceneChannel) {
-        for (newKey in targetSceneChannel) {
-            // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
-            // late.
-            val newKey = targetSceneChannel.tryReceive().getOrNull() ?: newKey
-            animateToScene(layoutImpl.state, newKey)
-        }
     }
 
     layoutImpl.Content(modifier)
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 0227aba..7cc9d26 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
@@ -20,14 +20,11 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.key
 import androidx.compose.runtime.snapshots.SnapshotStateMap
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.intermediateLayout
 import androidx.compose.ui.unit.Density
@@ -48,13 +45,12 @@
 
 @Stable
 internal class SceneTransitionLayoutImpl(
-    internal val state: SceneTransitionLayoutStateImpl,
-    internal var onChangeScene: (SceneKey) -> Unit,
+    internal val state: BaseSceneTransitionLayoutState,
     internal var density: Density,
     internal var edgeDetector: EdgeDetector,
     internal var transitionInterceptionThreshold: Float,
     builder: SceneTransitionLayoutScope.() -> Unit,
-    coroutineScope: CoroutineScope,
+    private val coroutineScope: CoroutineScope,
 ) {
     /**
      * The map of [Scene]s.
@@ -100,16 +96,6 @@
                 ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>()
                     .also { _sharedValues = it }
 
-    /**
-     * The scenes that are "ready", i.e. they were composed and fully laid-out at least once.
-     *
-     * Note that this map is *read* during composition, so it is a [SnapshotStateMap] to make sure
-     * that we recompose when modifications are made to this map.
-     *
-     * TODO(b/316901148): Remove this map.
-     */
-    private val readyScenes = SnapshotStateMap<SceneKey, Boolean>()
-
     private val horizontalGestureHandler: SceneGestureHandler
     private val verticalGestureHandler: SceneGestureHandler
 
@@ -244,49 +230,19 @@
                 // TODO(b/290184746): Make sure that this works with SystemUI once we use
                 // SceneTransitionLayout in Flexiglass.
                 scene(state.transitionState.currentScene).userActions[Back]?.let { backScene ->
-                    BackHandler { onChangeScene(backScene) }
+                    BackHandler { with(state) { coroutineScope.onChangeScene(backScene) } }
                 }
 
                 Box {
                     scenesToCompose.fastForEach { scene ->
                         val key = scene.key
-                        key(key) {
-                            // Mark this scene as ready once it has been composed, laid out and
-                            // drawn the first time. We have to do this in a LaunchedEffect here
-                            // because DisposableEffect runs between composition and layout.
-                            LaunchedEffect(key) { readyScenes[key] = true }
-                            DisposableEffect(key) { onDispose { readyScenes.remove(key) } }
-
-                            scene.Content(
-                                Modifier.drawWithContent {
-                                    if (state.currentTransition == null) {
-                                        drawContent()
-                                    } else {
-                                        // Don't draw scenes that are not ready yet.
-                                        if (readyScenes.containsKey(key)) {
-                                            drawContent()
-                                        }
-                                    }
-                                }
-                            )
-                        }
+                        key(key) { scene.Content() }
                     }
                 }
             }
         }
     }
 
-    /**
-     * Return whether [transition] is ready, i.e. the elements of both scenes of the transition were
-     * laid out at least once.
-     */
-    internal fun isTransitionReady(transition: TransitionState.Transition): Boolean {
-        return readyScenes.containsKey(transition.fromScene) &&
-            readyScenes.containsKey(transition.toScene)
-    }
-
-    internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene)
-
     internal fun setScenesTargetSizeForTest(size: IntSize) {
         scenes.values.forEach { it.targetSize = size }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 0607aa1..956e326 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -16,12 +16,23 @@
 
 package com.android.compose.animation.scene
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
 
-/** The state of a [SceneTransitionLayout]. */
+/**
+ * The state of a [SceneTransitionLayout].
+ *
+ * @see MutableSceneTransitionLayoutState
+ * @see updateSceneTransitionLayoutState
+ */
 @Stable
 sealed interface SceneTransitionLayoutState {
     /**
@@ -36,6 +47,9 @@
     val currentTransition: TransitionState.Transition?
         get() = transitionState as? TransitionState.Transition
 
+    /** The [SceneTransitions] used when animating this state. */
+    val transitions: SceneTransitions
+
     /**
      * Whether we are transitioning. If [from] or [to] is empty, we will also check that they match
      * the scenes we are animating from and/or to.
@@ -46,9 +60,68 @@
     fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean
 }
 
-/** Create a new [SceneTransitionLayoutState] that is currently idle at scene [currentScene]. */
-fun SceneTransitionLayoutState(currentScene: SceneKey): SceneTransitionLayoutState {
-    return SceneTransitionLayoutStateImpl(currentScene, SceneTransitions.Empty)
+/** A [SceneTransitionLayoutState] whose target scene can be imperatively set. */
+sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState {
+    /** The [SceneTransitions] used when animating this state. */
+    override var transitions: SceneTransitions
+
+    /**
+     * Set the target scene of this state to [targetScene].
+     *
+     * If [targetScene] is the same as the [currentScene][TransitionState.currentScene] of
+     * [transitionState], then nothing will happen and this will return `null`. Note that this means
+     * that this will also do nothing if the user is currently swiping from [targetScene] to another
+     * scene, or if we were already animating to [targetScene].
+     *
+     * If [targetScene] is different than the [currentScene][TransitionState.currentScene] of
+     * [transitionState], then this will animate to [targetScene]. The associated
+     * [TransitionState.Transition] will be returned and will be set as the current
+     * [transitionState] of this [MutableSceneTransitionLayoutState].
+     *
+     * Note that because a non-null [TransitionState.Transition] is returned does not mean that the
+     * transition will finish and that we will settle to [targetScene]. The returned transition
+     * might still be interrupted, for instance by another call to [setTargetScene] or by a user
+     * gesture.
+     *
+     * If [this] [CoroutineScope] is cancelled during the transition and that the transition was
+     * still active, then the [transitionState] of this [MutableSceneTransitionLayoutState] will be
+     * set to `TransitionState.Idle(targetScene)`.
+     *
+     * TODO(b/318794193): Add APIs to await() and cancel() any [TransitionState.Transition].
+     */
+    fun setTargetScene(
+        targetScene: SceneKey,
+        coroutineScope: CoroutineScope,
+    ): TransitionState.Transition?
+}
+
+/** Return a [MutableSceneTransitionLayoutState] initially idle at [initialScene]. */
+fun MutableSceneTransitionLayoutState(
+    initialScene: SceneKey,
+    transitions: SceneTransitions = SceneTransitions.Empty,
+): MutableSceneTransitionLayoutState {
+    return MutableSceneTransitionLayoutStateImpl(initialScene, transitions)
+}
+
+/**
+ * Sets up a [SceneTransitionLayoutState] and keeps it synced with [currentScene], [onChangeScene]
+ * and [transitions]. New transitions will automatically be started whenever [currentScene] is
+ * changed.
+ *
+ * @param currentScene the current scene
+ * @param onChangeScene a mutator that should set [currentScene] to the given scene when called.
+ *   This is called when the user commits a transition to a new scene because of a [UserAction], for
+ *   instance by triggering back navigation or by swiping to a new scene.
+ * @param transitions the definition of the transitions used to animate a change of scene.
+ */
+@Composable
+fun updateSceneTransitionLayoutState(
+    currentScene: SceneKey,
+    onChangeScene: (SceneKey) -> Unit,
+    transitions: SceneTransitions = SceneTransitions.Empty,
+): SceneTransitionLayoutState {
+    return remember { HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene) }
+        .apply { update(currentScene, onChangeScene, transitions) }
 }
 
 @Stable
@@ -109,13 +182,11 @@
     }
 }
 
-internal class SceneTransitionLayoutStateImpl(
-    initialScene: SceneKey,
-    internal var transitions: SceneTransitions,
-) : SceneTransitionLayoutState {
+internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) :
+    SceneTransitionLayoutState {
     override var transitionState: TransitionState by
         mutableStateOf(TransitionState.Idle(initialScene))
-        private set
+        protected set
 
     /**
      * The current [transformationSpec] associated to [transitionState]. Accessing this value makes
@@ -123,6 +194,14 @@
      */
     internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
 
+    /**
+     * Called when the [current scene][TransitionState.currentScene] should be changed to [scene].
+     *
+     * When this is called, the source of truth for the current scene should be changed so that
+     * [transitionState] will animate and settle to [scene].
+     */
+    internal abstract fun CoroutineScope.onChangeScene(scene: SceneKey)
+
     override fun isTransitioning(from: SceneKey?, to: SceneKey?): Boolean {
         val transition = currentTransition ?: return false
         return transition.isTransitioning(from, to)
@@ -154,3 +233,62 @@
         }
     }
 }
+
+/**
+ * A [SceneTransitionLayout] whose current scene/source of truth is hoisted (its current value comes
+ * from outside).
+ */
+internal class HoistedSceneTransitionLayoutScene(
+    initialScene: SceneKey,
+    override var transitions: SceneTransitions,
+    private var changeScene: (SceneKey) -> Unit,
+) : BaseSceneTransitionLayoutState(initialScene) {
+    private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)
+
+    override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene(scene)
+
+    @Composable
+    fun update(
+        currentScene: SceneKey,
+        onChangeScene: (SceneKey) -> Unit,
+        transitions: SceneTransitions,
+    ) {
+        SideEffect {
+            this.changeScene = onChangeScene
+            this.transitions = transitions
+
+            targetSceneChannel.trySend(currentScene)
+        }
+
+        LaunchedEffect(targetSceneChannel) {
+            for (newKey in targetSceneChannel) {
+                // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
+                // late.
+                val newKey = targetSceneChannel.tryReceive().getOrNull() ?: newKey
+                animateToScene(layoutState = this@HoistedSceneTransitionLayoutScene, newKey)
+            }
+        }
+    }
+}
+
+/** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */
+internal class MutableSceneTransitionLayoutStateImpl(
+    initialScene: SceneKey,
+    override var transitions: SceneTransitions,
+) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene) {
+    override fun setTargetScene(
+        targetScene: SceneKey,
+        coroutineScope: CoroutineScope
+    ): TransitionState.Transition? {
+        return with(this) {
+            coroutineScope.animateToScene(
+                layoutState = this@MutableSceneTransitionLayoutStateImpl,
+                target = targetScene,
+            )
+        }
+    }
+
+    override fun CoroutineScope.onChangeScene(scene: SceneKey) {
+        setTargetScene(scene, coroutineScope = this)
+    }
+}
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 54c5de7..c0de87a 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,7 +16,6 @@
 
 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
@@ -35,17 +34,11 @@
 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
@@ -263,8 +256,11 @@
 
         rule.setContent {
             SceneTransitionLayoutForTesting(
-                currentScene = currentScene,
-                onChangeScene = { currentScene = it },
+                state =
+                    updateSceneTransitionLayoutState(
+                        currentScene = currentScene,
+                        onChangeScene = { currentScene = it }
+                    ),
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) { /* Nothing */}
@@ -428,8 +424,11 @@
 
         rule.setContent {
             SceneTransitionLayoutForTesting(
-                currentScene = TestScenes.SceneA,
-                onChangeScene = {},
+                state =
+                    updateSceneTransitionLayoutState(
+                        currentScene = TestScenes.SceneA,
+                        onChangeScene = {}
+                    ),
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) { Box(Modifier.element(key)) }
@@ -478,8 +477,11 @@
             scrollScope = rememberCoroutineScope()
 
             SceneTransitionLayoutForTesting(
-                currentScene = TestScenes.SceneA,
-                onChangeScene = {},
+                state =
+                    updateSceneTransitionLayoutState(
+                        currentScene = TestScenes.SceneA,
+                        onChangeScene = {}
+                    ),
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) {
@@ -487,7 +489,7 @@
                     // page should be composed.
                     HorizontalPager(
                         pagerState,
-                        beyondBoundsPageCount = 0,
+                        outOfBoundsPageCount = 0,
                     ) { page ->
                         when (page) {
                             0 -> Box(Modifier.element(TestElements.Foo).fillMaxSize())
@@ -565,86 +567,4 @@
             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() = lastSharedState.offset.toDpOffset()
-
-        fun Element.lastOffsetIn(scene: SceneKey) =
-            (sceneStates[scene] ?: error("$scene not in sceneValues map"))
-                .lastState
-                .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/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
index 04b3f8a..0f9b024 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
@@ -32,7 +32,7 @@
 
     @Test
     fun testObservableTransitionState() = runTest {
-        val state = SceneTransitionLayoutState(TestScenes.SceneA)
+        lateinit var state: SceneTransitionLayoutState
 
         // Collect the current observable state into [observableState].
         // TODO(b/290184746): Use collectValues {} once it is extracted into a library that can be
@@ -58,12 +58,14 @@
             from = TestScenes.SceneA,
             to = TestScenes.SceneB,
             transitionLayout = { currentScene, onChangeScene ->
-                SceneTransitionLayout(
-                    currentScene,
-                    onChangeScene,
-                    EmptyTestTransitions,
-                    state = state,
-                ) {
+                state =
+                    updateSceneTransitionLayoutState(
+                        currentScene,
+                        onChangeScene,
+                        EmptyTestTransitions
+                    )
+
+                SceneTransitionLayout(state = state) {
                     scene(TestScenes.SceneA) {}
                     scene(TestScenes.SceneB) {}
                 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index d9ce519..066a3e4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -18,9 +18,6 @@
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.material3.Text
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -53,10 +50,8 @@
     private class TestGestureScope(
         val coroutineScope: MonotonicClockTestScope,
     ) {
-        private var internalCurrentScene: SceneKey by mutableStateOf(SceneA)
-
         private val layoutState =
-            SceneTransitionLayoutStateImpl(internalCurrentScene, EmptyTestTransitions)
+            MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
 
         val mutableUserActionsA: MutableMap<UserAction, SceneKey> =
             mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
@@ -94,7 +89,6 @@
         private val layoutImpl =
             SceneTransitionLayoutImpl(
                     state = layoutState,
-                    onChangeScene = { internalCurrentScene = it },
                     density = Density(1f),
                     edgeDetector = DefaultEdgeDetector,
                     transitionInterceptionThreshold = transitionInterceptionThreshold,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 75dee47..48825fb 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -18,7 +18,11 @@
 
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.runMonotonicClockTest
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,7 +33,7 @@
 
     @Test
     fun isTransitioningTo_idle() {
-        val state = SceneTransitionLayoutState(TestScenes.SceneA)
+        val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
 
         assertThat(state.isTransitioning()).isFalse()
         assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse()
@@ -40,7 +44,7 @@
 
     @Test
     fun isTransitioningTo_transition() {
-        val state = SceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
+        val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
         state.startTransition(transition(from = TestScenes.SceneA, to = TestScenes.SceneB))
 
         assertThat(state.isTransitioning()).isTrue()
@@ -50,4 +54,56 @@
         assertThat(state.isTransitioning(to = TestScenes.SceneA)).isFalse()
         assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
     }
+
+    @Test
+    fun setTargetScene_idleToSameScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull()
+    }
+
+    @Test
+    fun setTargetScene_idleToDifferentScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        val transition = state.setTargetScene(TestScenes.SceneB, coroutineScope = this)
+        assertThat(transition).isNotNull()
+        assertThat(state.transitionState).isEqualTo(transition)
+
+        testScheduler.advanceUntilIdle()
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+    }
+
+    @Test
+    fun setTargetScene_transitionToSameScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+        assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNull()
+        testScheduler.advanceUntilIdle()
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+    }
+
+    @Test
+    fun setTargetScene_transitionToDifferentScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+        assertThat(state.setTargetScene(TestScenes.SceneC, coroutineScope = this)).isNotNull()
+        testScheduler.advanceUntilIdle()
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneC))
+    }
+
+    @Test
+    fun setTargetScene_coroutineScopeCancelled() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+
+        lateinit var transition: TransitionState.Transition
+        val job =
+            launch(start = CoroutineStart.UNDISPATCHED) {
+                transition = state.setTargetScene(TestScenes.SceneB, coroutineScope = this)!!
+            }
+        assertThat(state.transitionState).isEqualTo(transition)
+
+        // Cancelling the scope/job still sets the state to Idle(targetScene).
+        job.cancel()
+        testScheduler.advanceUntilIdle()
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+    }
 }
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 649e499..efaea71 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
@@ -63,7 +63,7 @@
     }
 
     private var currentScene by mutableStateOf(TestScenes.SceneA)
-    private val layoutState = SceneTransitionLayoutState(currentScene)
+    private lateinit var layoutState: SceneTransitionLayoutState
 
     // We use createAndroidComposeRule() here and not createComposeRule() because we need an
     // activity for testBack().
@@ -72,10 +72,14 @@
     /** The content under test. */
     @Composable
     private fun TestContent() {
+        layoutState =
+            updateSceneTransitionLayoutState(
+                currentScene,
+                { currentScene = it },
+                EmptyTestTransitions
+            )
+
         SceneTransitionLayout(
-            currentScene,
-            { currentScene = it },
-            EmptyTestTransitions,
             state = layoutState,
             modifier = Modifier.size(LayoutSize),
         ) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 58d853e..1ec3c8b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -20,9 +20,6 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.LocalViewConfiguration
@@ -58,18 +55,15 @@
             get() = Offset(0f, (LayoutHeight / 2).toPx())
     }
 
-    private var currentScene by mutableStateOf(TestScenes.SceneA)
-    private val layoutState = SceneTransitionLayoutState(currentScene)
-
     @get:Rule val rule = createComposeRule()
 
+    private fun layoutState(initialScene: SceneKey = TestScenes.SceneA) =
+        MutableSceneTransitionLayoutState(initialScene, EmptyTestTransitions)
+
     /** The content under test. */
     @Composable
-    private fun TestContent() {
+    private fun TestContent(layoutState: SceneTransitionLayoutState) {
         SceneTransitionLayout(
-            currentScene,
-            { currentScene = it },
-            EmptyTestTransitions,
             state = layoutState,
             modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.debugName),
         ) {
@@ -109,9 +103,11 @@
         // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
         // detected as a drag event.
         var touchSlop = 0f
+
+        val layoutState = layoutState()
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            TestContent()
+            TestContent(layoutState)
         }
 
         assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
@@ -195,9 +191,10 @@
         // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
         // detected as a drag event.
         var touchSlop = 0f
+        val layoutState = layoutState()
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            TestContent()
+            TestContent(layoutState)
         }
 
         assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
@@ -260,14 +257,14 @@
     @Test
     fun multiPointerSwipe() {
         // Start at scene C.
-        currentScene = TestScenes.SceneC
+        val layoutState = layoutState(TestScenes.SceneC)
 
         // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
         // detected as a drag event.
         var touchSlop = 0f
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            TestContent()
+            TestContent(layoutState)
         }
 
         assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
@@ -299,14 +296,14 @@
     @Test
     fun defaultEdgeSwipe() {
         // Start at scene C.
-        currentScene = TestScenes.SceneC
+        val layoutState = layoutState(TestScenes.SceneC)
 
         // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
         // detected as a drag event.
         var touchSlop = 0f
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            TestContent()
+            TestContent(layoutState)
         }
 
         assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt
index cb122dc..fbcd5b2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt
@@ -13,7 +13,7 @@
  *
  * The [TestCoroutineScheduler] is passed to provide the functionality to wait for idle.
  */
-@ExperimentalTestApi
+@OptIn(ExperimentalTestApi::class)
 fun runMonotonicClockTest(block: suspend MonotonicClockTestScope.() -> Unit) = runTest {
     // We need a CoroutineScope (like a TestScope) to create a TestMonotonicFrameClock.
     withContext(TestMonotonicFrameClock(this)) {
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index 927fd8e..1d18496 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -30,8 +30,8 @@
         "src/**/*.aidl",
     ],
     static_libs: [
+        "PlatformAnimationLib",
         "PluginCoreLib",
-        "SystemUIAnimationLib",
         "SystemUIPluginLib",
         "SystemUIUnfoldLib",
         "androidx.dynamicanimation_dynamicanimation",
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 41bde52..3dfe65a 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
@@ -22,6 +22,7 @@
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.annotation.OpenForTesting
+import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.core.LogcatOnlyMessageBuffer
 import com.android.systemui.log.core.Logger
@@ -120,8 +121,9 @@
             override fun onPluginAttached(
                 manager: PluginLifecycleManager<ClockProviderPlugin>
             ): Boolean {
-                manager.isDebug = !keepAllLoaded
-
+                manager.setLogFunc({ tag, msg ->
+                    (clockBuffers?.infraMessageBuffer as LogBuffer?)?.log(tag, LogLevel.DEBUG, msg)
+                })
                 if (keepAllLoaded) {
                     // Always load new plugins if requested
                     return true
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index c8461d2..02d30c5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -197,7 +197,6 @@
         whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(true)
 
         featureFlags = FakeFeatureFlags()
-        featureFlags.set(Flags.BOUNCER_USER_SWITCHER, false)
         featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false)
         featureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false)
         featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index cbcca55..f775175 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -100,9 +100,12 @@
                 mSelectedUserInteractor
             )
         underTest.init()
+        underTest.onViewAttached()
         underTest.onResume(0)
         verify(keyguardUpdateMonitor)
             .registerCallback(updateMonitorCallbackArgumentCaptor.capture())
+        reset(keyguardMessageAreaController)
+        reset(keyguardUpdateMonitor)
     }
 
     @Test
@@ -110,25 +113,24 @@
         underTest.onViewAttached()
         verify(keyguardMessageAreaController)
             .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
-    }
-
-    @Test
-    fun onViewDetached() {
-        underTest.onViewDetached()
-    }
-
-    @Test
-    fun onResume() {
-        reset(keyguardUpdateMonitor)
-        underTest.onResume(KeyguardSecurityView.VIEW_REVEALED)
         verify(keyguardUpdateMonitor)
             .registerCallback(any(KeyguardUpdateMonitorCallback::class.java))
     }
 
     @Test
+    fun onViewDetached() {
+        underTest.onViewDetached()
+        verify(keyguardUpdateMonitor).removeCallback(any(KeyguardUpdateMonitorCallback::class.java))
+    }
+
+    @Test
+    fun onResume() {
+        underTest.onResume(KeyguardSecurityView.VIEW_REVEALED)
+    }
+
+    @Test
     fun onPause() {
         underTest.onPause()
-        verify(keyguardUpdateMonitor).removeCallback(any(KeyguardUpdateMonitorCallback::class.java))
     }
 
     @Test
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 1c1335f..343280d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -801,6 +801,20 @@
     }
 
     @Test
+    public void testOnBiometricPromptDismissedCallback_hideAuthenticationDialog() {
+        // GIVEN a callback is registered
+        AuthController.Callback callback = mock(AuthController.Callback.class);
+        mAuthController.addCallback(callback);
+
+        // WHEN dialog is shown and then dismissed
+        showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */);
+        mAuthController.hideAuthenticationDialog(mAuthController.mCurrentDialog.getRequestId());
+
+        // THEN callback should be received
+        verify(callback).onBiometricPromptDismissed();
+    }
+
+    @Test
     public void testSubscribesToLogContext() {
         mAuthController.setBiometricContextListener(mContextListener);
         verify(mLogContextInteractor).addBiometricContextListener(same(mContextListener));
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 36aa441..cec2d74 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -496,7 +496,8 @@
         final float[] scaleFactor = new float[]{1f, displayHeight[1] / (float) displayHeight[0]};
         final int[] rotation = new int[]{Surface.ROTATION_0, Surface.ROTATION_90};
         final UdfpsOverlayParams oldParams = new UdfpsOverlayParams(sensorBounds[0],
-                sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]);
+                sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0],
+                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
 
         for (int i1 = 0; i1 <= 1; ++i1) {
             for (int i2 = 0; i2 <= 1; ++i2) {
@@ -505,7 +506,8 @@
                         for (int i5 = 0; i5 <= 1; ++i5) {
                             final UdfpsOverlayParams newParams = new UdfpsOverlayParams(
                                     sensorBounds[i1], sensorBounds[i1], displayWidth[i2],
-                                    displayHeight[i3], scaleFactor[i4], rotation[i5]);
+                                    displayHeight[i3], scaleFactor[i4], rotation[i5],
+                                    FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
 
                             if (newParams.equals(oldParams)) {
                                 continue;
@@ -549,7 +551,7 @@
         // Initialize the overlay.
         mUdfpsController.updateOverlayParams(mOpticalProps,
                 new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
-                        scaleFactor, rotation));
+                        scaleFactor, rotation, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL));
 
         // Show the overlay.
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
@@ -560,7 +562,7 @@
         // Update overlay with the same parameters.
         mUdfpsController.updateOverlayParams(mOpticalProps,
                 new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
-                        scaleFactor, rotation));
+                        scaleFactor, rotation, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL));
         mFgExecutor.runAllReady();
 
         // Ensure the overlay was not recreated.
@@ -642,7 +644,8 @@
         // Test ROTATION_0
         mUdfpsController.updateOverlayParams(testParams.sensorProps,
                 new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
-                        scaleFactor, Surface.ROTATION_0));
+                        scaleFactor, Surface.ROTATION_0,
+                        FingerprintSensorProperties.TYPE_UDFPS_OPTICAL));
         MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor,
                 touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
@@ -657,7 +660,8 @@
         reset(mFingerprintManager);
         mUdfpsController.updateOverlayParams(testParams.sensorProps,
                 new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
-                        scaleFactor, Surface.ROTATION_90));
+                        scaleFactor, Surface.ROTATION_90,
+                        FingerprintSensorProperties.TYPE_UDFPS_OPTICAL));
         event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricExecutor.runAllReady();
@@ -671,7 +675,8 @@
         reset(mFingerprintManager);
         mUdfpsController.updateOverlayParams(testParams.sensorProps,
                 new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
-                        scaleFactor, Surface.ROTATION_270));
+                        scaleFactor, Surface.ROTATION_270,
+                        FingerprintSensorProperties.TYPE_UDFPS_OPTICAL));
         event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricExecutor.runAllReady();
@@ -685,7 +690,8 @@
         reset(mFingerprintManager);
         mUdfpsController.updateOverlayParams(testParams.sensorProps,
                 new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
-                        scaleFactor, Surface.ROTATION_180));
+                        scaleFactor, Surface.ROTATION_180,
+                        FingerprintSensorProperties.TYPE_UDFPS_OPTICAL));
         // ROTATION_180 is not supported. It should be treated like ROTATION_0.
         event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
similarity index 69%
rename from packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index 455f986..92b75cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -25,13 +25,13 @@
 import com.android.systemui.util.mockito.KotlinArgumentCaptor
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
@@ -59,37 +59,43 @@
     }
 
     @Test
-    fun mediaPlaying_defaultsToFalse() =
+    fun hasAnyMediaOrRecommendation_defaultsToFalse() =
         testScope.runTest {
             mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
 
-            val isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying)
+            val mediaModel = collectLastValue(mediaRepository.mediaModel)
             runCurrent()
-            assertThat(isMediaPlaying()).isFalse()
+            assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
         }
 
     @Test
-    fun mediaPlaying_emitsInitialValue() =
-        testScope.runTest {
-            // Start with media available.
-            whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true)
-
-            mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
-
-            val isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying)
-            runCurrent()
-            assertThat(isMediaPlaying()).isTrue()
-        }
-
-    @Test
-    fun mediaPlaying_updatesWhenMediaDataLoaded() =
+    fun mediaModel_updatesWhenMediaDataLoaded() =
         testScope.runTest {
             mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
 
+            // Listener is added
+            verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
+
             // Initial value is false.
-            var isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying)
+            val mediaModel = collectLastValue(mediaRepository.mediaModel)
             runCurrent()
-            assertThat(isMediaPlaying()).isFalse()
+            assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
+
+            // Change to media available and notify the listener.
+            whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true)
+            whenever(mediaData.createdTimestampMillis).thenReturn(1234L)
+            mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData)
+            runCurrent()
+
+            // Media active now returns true.
+            assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue()
+            assertThat(mediaModel()?.createdTimestampMillis).isEqualTo(1234L)
+        }
+
+    @Test
+    fun mediaModel_updatesWhenMediaDataRemoved() =
+        testScope.runTest {
+            mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
 
             // Listener is added
             verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
@@ -97,36 +103,18 @@
             // Change to media available and notify the listener.
             whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true)
             mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData)
-
-            // mediaPlaying now returns true.
-            isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying)
             runCurrent()
-            assertThat(isMediaPlaying()).isTrue()
-        }
 
-    @Test
-    fun mediaPlaying_updatesWhenMediaDataRemoved() =
-        testScope.runTest {
-            // Start with media available.
-            whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true)
-
-            mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager)
-
-            // Initial value is true.
-            var isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying)
-            runCurrent()
-            assertThat(isMediaPlaying()).isTrue()
-
-            // Listener is added.
-            verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
+            // Media active now returns true.
+            val mediaModel = collectLastValue(mediaRepository.mediaModel)
+            assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue()
 
             // Change to media unavailable and notify the listener.
             whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(false)
-            mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData)
-
-            // mediaPlaying now returns false.
-            isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying)
+            mediaDataListenerCaptor.value.onMediaDataRemoved("key")
             runCurrent()
-            assertThat(isMediaPlaying()).isFalse()
+
+            // Media active now returns false.
+            assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
         }
 }
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 449ee6f..4079f12 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
@@ -19,14 +19,14 @@
 import android.appwidget.AppWidgetHost
 import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
-import android.content.BroadcastReceiver
 import android.content.ComponentName
+import android.content.Intent
+import android.content.Intent.ACTION_USER_UNLOCKED
 import android.os.UserHandle
 import android.os.UserManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.communal.data.db.CommunalItemRank
 import com.android.systemui.communal.data.db.CommunalWidgetDao
 import com.android.systemui.communal.data.db.CommunalWidgetItem
@@ -38,15 +38,12 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.kotlinArgumentCaptor
-import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -55,8 +52,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito
 import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -70,8 +67,6 @@
 
     @Mock private lateinit var appWidgetHost: AppWidgetHost
 
-    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
-
     @Mock private lateinit var userManager: UserManager
 
     @Mock private lateinit var userHandle: UserHandle
@@ -125,10 +120,10 @@
         testScope.runTest {
             communalEnabled(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.communalWidgets)()
+            repository.communalWidgets.launchIn(backgroundScope)
             runCurrent()
 
-            verify(communalWidgetDao, Mockito.never()).getWidgets()
+            verify(communalWidgetDao, never()).getWidgets()
         }
 
     @Test
@@ -136,10 +131,10 @@
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.communalWidgets)()
+            repository.communalWidgets.launchIn(backgroundScope)
             runCurrent()
 
-            verify(communalWidgetDao, Mockito.never()).getWidgets()
+            verify(communalWidgetDao, never()).getWidgets()
         }
 
     @Test
@@ -147,8 +142,7 @@
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            val communalWidgets = collectLastValue(repository.communalWidgets)
-            communalWidgets()
+            val communalWidgets by collectLastValue(repository.communalWidgets)
             runCurrent()
             val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
             val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
@@ -158,11 +152,14 @@
 
             userUnlocked(true)
             installedProviders(listOf(stopwatchProviderInfo))
-            broadcastReceiverUpdate()
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(ACTION_USER_UNLOCKED)
+            )
             runCurrent()
 
             verify(communalWidgetDao).getWidgets()
-            assertThat(communalWidgets())
+            assertThat(communalWidgets)
                 .containsExactly(
                     CommunalWidgetContentModel(
                         appWidgetId = communalWidgetItemEntry.widgetId,
@@ -182,9 +179,10 @@
             val provider = ComponentName("pkg_name", "cls_name")
             val id = 1
             val priority = 1
+            whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true)
             whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>()))
                 .thenReturn(id)
-            repository.addWidget(provider, priority)
+            repository.addWidget(provider, priority) { true }
             runCurrent()
 
             verify(communalWidgetHost).allocateIdAndBindWidget(provider)
@@ -192,6 +190,71 @@
         }
 
     @Test
+    fun addWidget_configurationFails_doNotAddWidgetToDb() =
+        testScope.runTest {
+            userUnlocked(true)
+            val repository = initCommunalWidgetRepository()
+            runCurrent()
+
+            val provider = ComponentName("pkg_name", "cls_name")
+            val id = 1
+            val priority = 1
+            whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true)
+            whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id)
+            repository.addWidget(provider, priority) { false }
+            runCurrent()
+
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider)
+            verify(communalWidgetDao, never()).addWidget(id, provider, priority)
+            verify(appWidgetHost).deleteAppWidgetId(id)
+        }
+
+    @Test
+    fun addWidget_configurationThrowsError_doNotAddWidgetToDb() =
+        testScope.runTest {
+            userUnlocked(true)
+            val repository = initCommunalWidgetRepository()
+            runCurrent()
+
+            val provider = ComponentName("pkg_name", "cls_name")
+            val id = 1
+            val priority = 1
+            whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true)
+            whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id)
+            repository.addWidget(provider, priority) { throw IllegalStateException("some error") }
+            runCurrent()
+
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider)
+            verify(communalWidgetDao, never()).addWidget(id, provider, priority)
+            verify(appWidgetHost).deleteAppWidgetId(id)
+        }
+
+    @Test
+    fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() =
+        testScope.runTest {
+            userUnlocked(true)
+            val repository = initCommunalWidgetRepository()
+            runCurrent()
+
+            val provider = ComponentName("pkg_name", "cls_name")
+            val id = 1
+            val priority = 1
+            whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(false)
+            whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>()))
+                .thenReturn(id)
+            var configured = false
+            repository.addWidget(provider, priority) {
+                configured = true
+                true
+            }
+            runCurrent()
+
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider)
+            verify(communalWidgetDao).addWidget(id, provider, priority)
+            assertThat(configured).isFalse()
+        }
+
+    @Test
     fun deleteWidget_removeWidgetId_andDeleteFromDb() =
         testScope.runTest {
             userUnlocked(true)
@@ -225,17 +288,9 @@
         testScope.runTest {
             communalEnabled(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.communalWidgets)()
-            verifyBroadcastReceiverNeverRegistered()
-        }
-
-    @Test
-    fun broadcastReceiver_featureEnabledAndUserUnlocked_doNotRegisterBroadcastReceiver() =
-        testScope.runTest {
-            userUnlocked(true)
-            val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.communalWidgets)()
-            verifyBroadcastReceiverNeverRegistered()
+            repository.communalWidgets.launchIn(backgroundScope)
+            runCurrent()
+            assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0)
         }
 
     @Test
@@ -243,24 +298,9 @@
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.communalWidgets)()
-            verifyBroadcastReceiverRegistered()
-        }
-
-    @Test
-    fun broadcastReceiver_whenFlowFinishes_unregisterBroadcastReceiver() =
-        testScope.runTest {
-            userUnlocked(false)
-            val repository = initCommunalWidgetRepository()
-
-            val job = launch { repository.communalWidgets.collect() }
+            repository.communalWidgets.launchIn(backgroundScope)
             runCurrent()
-            val receiver = broadcastReceiverUpdate()
-
-            job.cancel()
-            runCurrent()
-
-            verify(broadcastDispatcher).unregisterReceiver(receiver)
+            assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1)
         }
 
     @Test
@@ -268,12 +308,16 @@
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.communalWidgets)()
-            verify(appWidgetHost, Mockito.never()).startListening()
+            repository.communalWidgets.launchIn(backgroundScope)
+            runCurrent()
+            verify(appWidgetHost, never()).startListening()
 
             userUnlocked(true)
-            broadcastReceiverUpdate()
-            collectLastValue(repository.communalWidgets)()
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(ACTION_USER_UNLOCKED)
+            )
+            runCurrent()
 
             verify(appWidgetHost).startListening()
         }
@@ -283,18 +327,25 @@
         testScope.runTest {
             userUnlocked(false)
             val repository = initCommunalWidgetRepository()
-            collectLastValue(repository.communalWidgets)()
+            repository.communalWidgets.launchIn(backgroundScope)
+            runCurrent()
 
             userUnlocked(true)
-            broadcastReceiverUpdate()
-            collectLastValue(repository.communalWidgets)()
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(ACTION_USER_UNLOCKED)
+            )
+            runCurrent()
 
             verify(appWidgetHost).startListening()
-            verify(appWidgetHost, Mockito.never()).stopListening()
+            verify(appWidgetHost, never()).stopListening()
 
             userUnlocked(false)
-            broadcastReceiverUpdate()
-            collectLastValue(repository.communalWidgets)()
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(ACTION_USER_UNLOCKED)
+            )
+            runCurrent()
 
             verify(appWidgetHost).stopListening()
         }
@@ -305,7 +356,7 @@
             appWidgetHost,
             testScope.backgroundScope,
             testDispatcher,
-            broadcastDispatcher,
+            fakeBroadcastDispatcher,
             communalRepository,
             communalWidgetHost,
             communalWidgetDao,
@@ -315,45 +366,6 @@
         )
     }
 
-    private fun verifyBroadcastReceiverRegistered() {
-        verify(broadcastDispatcher)
-            .registerReceiver(
-                any(),
-                any(),
-                nullable(),
-                nullable(),
-                anyInt(),
-                nullable(),
-            )
-    }
-
-    private fun verifyBroadcastReceiverNeverRegistered() {
-        verify(broadcastDispatcher, Mockito.never())
-            .registerReceiver(
-                any(),
-                any(),
-                nullable(),
-                nullable(),
-                anyInt(),
-                nullable(),
-            )
-    }
-
-    private fun broadcastReceiverUpdate(): BroadcastReceiver {
-        val broadcastReceiverCaptor = kotlinArgumentCaptor<BroadcastReceiver>()
-        verify(broadcastDispatcher)
-            .registerReceiver(
-                broadcastReceiverCaptor.capture(),
-                any(),
-                nullable(),
-                nullable(),
-                anyInt(),
-                nullable(),
-            )
-        broadcastReceiverCaptor.value.onReceive(null, null)
-        return broadcastReceiverCaptor.value
-    }
-
     private fun communalEnabled(enabled: Boolean) {
         communalRepository.setIsCommunalEnabled(enabled)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 62084aa..744b65f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -148,25 +148,29 @@
             whenever(target1.smartspaceTargetId).thenReturn("target1")
             whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_WEATHER)
             whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java))
+            whenever(target1.creationTimeMillis).thenReturn(0L)
 
             // Does not have RemoteViews
             val target2 = mock(SmartspaceTarget::class.java)
-            whenever(target1.smartspaceTargetId).thenReturn("target2")
-            whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target1.remoteViews).thenReturn(null)
+            whenever(target2.smartspaceTargetId).thenReturn("target2")
+            whenever(target2.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+            whenever(target2.remoteViews).thenReturn(null)
+            whenever(target2.creationTimeMillis).thenReturn(0L)
 
             // Timer and has RemoteViews
             val target3 = mock(SmartspaceTarget::class.java)
-            whenever(target1.smartspaceTargetId).thenReturn("target3")
-            whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java))
+            whenever(target3.smartspaceTargetId).thenReturn("target3")
+            whenever(target3.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+            whenever(target3.remoteViews).thenReturn(mock(RemoteViews::class.java))
+            whenever(target3.creationTimeMillis).thenReturn(0L)
 
             val targets = listOf(target1, target2, target3)
             smartspaceRepository.setCommunalSmartspaceTargets(targets)
 
-            val smartspaceContent by collectLastValue(underTest.smartspaceContent)
+            val smartspaceContent by collectLastValue(underTest.ongoingContent)
             assertThat(smartspaceContent?.size).isEqualTo(1)
-            assertThat(smartspaceContent?.get(0)?.key).isEqualTo("smartspace_target3")
+            assertThat(smartspaceContent?.get(0)?.key)
+                .isEqualTo(CommunalContentModel.KEY.smartspace("target3"))
         }
 
     @Test
@@ -256,16 +260,12 @@
 
             val targets = mutableListOf<SmartspaceTarget>()
             for (index in 0 until totalTargets) {
-                val target = mock(SmartspaceTarget::class.java)
-                whenever(target.smartspaceTargetId).thenReturn("target$index")
-                whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-                whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java))
-                targets.add(target)
+                targets.add(smartspaceTimer(index.toString()))
             }
 
             smartspaceRepository.setCommunalSmartspaceTargets(targets)
 
-            val smartspaceContent by collectLastValue(underTest.smartspaceContent)
+            val smartspaceContent by collectLastValue(underTest.ongoingContent)
             assertThat(smartspaceContent?.size).isEqualTo(totalTargets)
             for (index in 0 until totalTargets) {
                 assertThat(smartspaceContent?.get(index)?.size).isEqualTo(expectedSizes[index])
@@ -279,13 +279,77 @@
             tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
 
             // Media is playing.
-            mediaRepository.mediaPlaying.value = true
+            mediaRepository.mediaActive()
 
-            val umoContent by collectLastValue(underTest.umoContent)
+            val umoContent by collectLastValue(underTest.ongoingContent)
 
             assertThat(umoContent?.size).isEqualTo(1)
             assertThat(umoContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java)
-            assertThat(umoContent?.get(0)?.key).isEqualTo(CommunalContentModel.UMO_KEY)
+            assertThat(umoContent?.get(0)?.key).isEqualTo(CommunalContentModel.KEY.umo())
+        }
+
+    @Test
+    fun ongoing_shouldOrderAndSizeByTimestamp() =
+        testScope.runTest {
+            // Keyguard showing, and tutorial completed.
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setKeyguardOccluded(false)
+            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
+
+            // Timer1 started
+            val timer1 = smartspaceTimer("timer1", timestamp = 1L)
+            smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1))
+
+            // Umo started
+            mediaRepository.mediaActive(timestamp = 2L)
+
+            // Timer2 started
+            val timer2 = smartspaceTimer("timer2", timestamp = 3L)
+            smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2))
+
+            // Timer3 started
+            val timer3 = smartspaceTimer("timer3", timestamp = 4L)
+            smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2, timer3))
+
+            val ongoingContent by collectLastValue(underTest.ongoingContent)
+            assertThat(ongoingContent?.size).isEqualTo(4)
+            assertThat(ongoingContent?.get(0)?.key)
+                .isEqualTo(CommunalContentModel.KEY.smartspace("timer3"))
+            assertThat(ongoingContent?.get(0)?.size).isEqualTo(CommunalContentSize.FULL)
+            assertThat(ongoingContent?.get(1)?.key)
+                .isEqualTo(CommunalContentModel.KEY.smartspace("timer2"))
+            assertThat(ongoingContent?.get(1)?.size).isEqualTo(CommunalContentSize.THIRD)
+            assertThat(ongoingContent?.get(2)?.key).isEqualTo(CommunalContentModel.KEY.umo())
+            assertThat(ongoingContent?.get(2)?.size).isEqualTo(CommunalContentSize.THIRD)
+            assertThat(ongoingContent?.get(3)?.key)
+                .isEqualTo(CommunalContentModel.KEY.smartspace("timer1"))
+            assertThat(ongoingContent?.get(3)?.size).isEqualTo(CommunalContentSize.THIRD)
+        }
+
+    @Test
+    fun cta_visibilityTrue_shows() =
+        testScope.runTest {
+            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
+            communalRepository.setCtaTileInViewModeVisibility(true)
+
+            val ctaTileContent by collectLastValue(underTest.ctaTileContent)
+
+            assertThat(ctaTileContent?.size).isEqualTo(1)
+            assertThat(ctaTileContent?.get(0))
+                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
+            assertThat(ctaTileContent?.get(0)?.key)
+                .isEqualTo(CommunalContentModel.KEY.CTA_TILE_IN_VIEW_MODE_KEY)
+        }
+
+    @Test
+    fun ctaTile_visibilityFalse_doesNotShow() =
+        testScope.runTest {
+            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
+            communalRepository.setCtaTileInViewModeVisibility(false)
+
+            val ctaTileContent by collectLastValue(underTest.ctaTileContent)
+
+            assertThat(ctaTileContent).isEmpty()
         }
 
     @Test
@@ -334,4 +398,13 @@
             underTest.showWidgetEditor()
             verify(editWidgetsActivityStarter).startActivity()
         }
+
+    private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget {
+        val timer = mock(SmartspaceTarget::class.java)
+        whenever(timer.smartspaceTargetId).thenReturn(id)
+        whenever(timer.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+        whenever(timer.remoteViews).thenReturn(mock(RemoteViews::class.java))
+        whenever(timer.creationTimeMillis).thenReturn(timestamp)
+        return timer
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt
new file mode 100644
index 0000000..721fc49
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.log
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+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.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class CommunalLoggerStartableTest : SysuiTestCase() {
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+
+    private lateinit var testScope: TestScope
+    private lateinit var communalInteractor: CommunalInteractor
+    private lateinit var underTest: CommunalLoggerStartable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        val withDeps = CommunalInteractorFactory.create()
+        testScope = withDeps.testScope
+        communalInteractor = withDeps.communalInteractor
+
+        underTest =
+            CommunalLoggerStartable(
+                testScope.backgroundScope,
+                communalInteractor,
+                uiEventLogger,
+            )
+        underTest.start()
+    }
+
+    @Test
+    fun transitionStateLogging_enterCommunalHub() =
+        testScope.runTest {
+            // Transition state is default (non-communal)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.DEFAULT))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Verify nothing is logged from the default state
+            verify(uiEventLogger, never()).log(any())
+
+            // Start transition to communal
+            transitionState.value = transition(to = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START)
+
+            // Finish transition to communal
+            transitionState.value = idle(CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH)
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+        }
+
+    @Test
+    fun transitionStateLogging_enterCommunalHub_canceled() =
+        testScope.runTest {
+            // Transition state is default (non-communal)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.DEFAULT))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Verify nothing is logged from the default state
+            verify(uiEventLogger, never()).log(any())
+
+            // Start transition to communal
+            transitionState.value = transition(to = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START)
+
+            // Cancel the transition
+            transitionState.value = idle(CommunalSceneKey.DEFAULT)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL)
+
+            // Verify neither SHOWN nor GONE is logged
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
+        }
+
+    @Test
+    fun transitionStateLogging_exitCommunalHub() =
+        testScope.runTest {
+            // Transition state is communal
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.Communal))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Verify SHOWN is logged when it's the default state
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+
+            // Start transition from communal
+            transitionState.value = transition(from = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START)
+
+            // Finish transition to communal
+            transitionState.value = idle(CommunalSceneKey.DEFAULT)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH)
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
+        }
+
+    @Test
+    fun transitionStateLogging_exitCommunalHub_canceled() =
+        testScope.runTest {
+            // Transition state is communal
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.Communal))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Clear the initial SHOWN event from the logger
+            clearInvocations(uiEventLogger)
+
+            // Start transition from communal
+            transitionState.value = transition(from = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START)
+
+            // Cancel the transition
+            transitionState.value = idle(CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL)
+
+            // Verify neither SHOWN nor GONE is logged
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
+        }
+
+    private fun transition(
+        from: CommunalSceneKey = CommunalSceneKey.DEFAULT,
+        to: CommunalSceneKey = CommunalSceneKey.DEFAULT,
+    ): ObservableCommunalTransitionState.Transition {
+        return ObservableCommunalTransitionState.Transition(
+            fromScene = from,
+            toScene = to,
+            progress = emptyFlow(),
+            isInitiatedByUserInput = true,
+            isUserInputOngoing = emptyFlow(),
+        )
+    }
+
+    private fun idle(sceneKey: CommunalSceneKey): ObservableCommunalTransitionState.Idle {
+        return ObservableCommunalTransitionState.Idle(sceneKey)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 314dfdf..54510a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -16,12 +16,16 @@
 
 package com.android.systemui.communal.view.viewmodel
 
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
 import android.app.smartspace.SmartspaceTarget
-import android.os.PowerManager
+import android.appwidget.AppWidgetHost
+import android.content.ComponentName
 import android.provider.Settings
 import android.widget.RemoteViews
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.FakeCommunalRepository
@@ -29,34 +33,39 @@
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
 import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.ui.MediaHost
-import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import javax.inject.Provider
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class CommunalEditModeViewModelTest : SysuiTestCase() {
     @Mock private lateinit var mediaHost: MediaHost
-    @Mock private lateinit var shadeViewController: ShadeViewController
-    @Mock private lateinit var powerManager: PowerManager
+    @Mock private lateinit var appWidgetHost: AppWidgetHost
+    @Mock private lateinit var uiEventLogger: UiEventLogger
 
-    private lateinit var testScope: TestScope
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var communalRepository: FakeCommunalRepository
@@ -71,9 +80,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        testScope = TestScope()
-
-        val withDeps = CommunalInteractorFactory.create()
+        val withDeps = CommunalInteractorFactory.create(testScope)
         keyguardRepository = withDeps.keyguardRepository
         communalRepository = withDeps.communalRepository
         tutorialRepository = withDeps.tutorialRepository
@@ -84,14 +91,14 @@
         underTest =
             CommunalEditModeViewModel(
                 withDeps.communalInteractor,
-                Provider { shadeViewController },
-                powerManager,
+                appWidgetHost,
                 mediaHost,
+                uiEventLogger,
             )
     }
 
     @Test
-    fun communalContent_onlyWidgetsAreShownInEditMode() =
+    fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() =
         testScope.runTest {
             tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
 
@@ -119,15 +126,97 @@
             smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
 
             // Media playing.
-            mediaRepository.mediaPlaying.value = true
+            mediaRepository.mediaActive()
 
             val communalContent by collectLastValue(underTest.communalContent)
 
-            // Only Widgets are shown.
-            assertThat(communalContent?.size).isEqualTo(2)
+            // Only Widgets and CTA tile are shown.
+            assertThat(communalContent?.size).isEqualTo(3)
             assertThat(communalContent?.get(0))
                 .isInstanceOf(CommunalContentModel.Widget::class.java)
             assertThat(communalContent?.get(1))
                 .isInstanceOf(CommunalContentModel.Widget::class.java)
+            assertThat(communalContent?.get(2))
+                .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java)
         }
+
+    @Test
+    fun interactionHandlerIgnoresClicks() {
+        val interactionHandler = underTest.getInteractionHandler()
+        assertThat(
+                interactionHandler.onInteraction(
+                    /* view = */ mock(),
+                    /* pendingIntent = */ mock(),
+                    /* response = */ mock()
+                )
+            )
+            .isEqualTo(false)
+    }
+
+    @Test
+    fun addingWidgetTriggersConfiguration() =
+        testScope.runTest {
+            val provider = ComponentName("pkg.test", "testWidget")
+            val widgetToConfigure by collectLastValue(underTest.widgetsToConfigure)
+            assertThat(widgetToConfigure).isNull()
+            underTest.onAddWidget(componentName = provider, priority = 0)
+            assertThat(widgetToConfigure).isEqualTo(1)
+        }
+
+    @Test
+    fun settingResultOkAddsWidget() =
+        testScope.runTest {
+            val provider = ComponentName("pkg.test", "testWidget")
+            val widgetAdded by collectLastValue(widgetRepository.widgetAdded)
+            assertThat(widgetAdded).isNull()
+            underTest.onAddWidget(componentName = provider, priority = 0)
+            assertThat(widgetAdded).isNull()
+            underTest.setConfigurationResult(RESULT_OK)
+            assertThat(widgetAdded).isEqualTo(1)
+        }
+
+    @Test
+    fun settingResultCancelledDoesNotAddWidget() =
+        testScope.runTest {
+            val provider = ComponentName("pkg.test", "testWidget")
+            val widgetAdded by collectLastValue(widgetRepository.widgetAdded)
+            assertThat(widgetAdded).isNull()
+            underTest.onAddWidget(componentName = provider, priority = 0)
+            assertThat(widgetAdded).isNull()
+            underTest.setConfigurationResult(RESULT_CANCELED)
+            assertThat(widgetAdded).isNull()
+        }
+
+    @Test(expected = IllegalStateException::class)
+    fun settingResultBeforeWidgetAddedThrowsException() {
+        underTest.setConfigurationResult(RESULT_OK)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun addingWidgetWhileConfigurationActiveFails() =
+        testScope.runTest {
+            val providerOne = ComponentName("pkg.test", "testWidget")
+            underTest.onAddWidget(componentName = providerOne, priority = 0)
+            runCurrent()
+            val providerTwo = ComponentName("pkg.test", "testWidget2")
+            underTest.onAddWidget(componentName = providerTwo, priority = 0)
+        }
+
+    @Test
+    fun reorderWidget_uiEventLogging_start() {
+        underTest.onReorderWidgetStart()
+        verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
+    }
+
+    @Test
+    fun reorderWidget_uiEventLogging_end() {
+        underTest.onReorderWidgetEnd()
+        verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH)
+    }
+
+    @Test
+    fun reorderWidget_uiEventLogging_cancel() {
+        underTest.onReorderWidgetCancel()
+        verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 8a71168..a776062 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.communal.view.viewmodel
 
 import android.app.smartspace.SmartspaceTarget
-import android.os.PowerManager
 import android.provider.Settings
 import android.widget.RemoteViews
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -31,16 +30,18 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS
+import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.media.controls.ui.MediaHost
-import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import javax.inject.Provider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -49,12 +50,11 @@
 import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class CommunalViewModelTest : SysuiTestCase() {
     @Mock private lateinit var mediaHost: MediaHost
-    @Mock private lateinit var shadeViewController: ShadeViewController
-    @Mock private lateinit var powerManager: PowerManager
 
     private lateinit var testScope: TestScope
 
@@ -83,10 +83,10 @@
 
         underTest =
             CommunalViewModel(
+                testScope,
                 withDeps.communalInteractor,
+                WidgetInteractionHandler(mock()),
                 withDeps.tutorialInteractor,
-                Provider { shadeViewController },
-                powerManager,
                 mediaHost,
             )
     }
@@ -110,7 +110,7 @@
         }
 
     @Test
-    fun ordering_smartspaceBeforeUmoBeforeWidgets() =
+    fun ordering_smartspaceBeforeUmoBeforeWidgetsBeforeCtaTile() =
         testScope.runTest {
             tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
 
@@ -138,12 +138,15 @@
             smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
 
             // Media playing.
-            mediaRepository.mediaPlaying.value = true
+            mediaRepository.mediaActive()
+
+            // CTA Tile not dismissed.
+            communalRepository.setCtaTileInViewModeVisibility(true)
 
             val communalContent by collectLastValue(underTest.communalContent)
 
-            // Order is smart space, then UMO, then widget content.
-            assertThat(communalContent?.size).isEqualTo(4)
+            // Order is smart space, then UMO, widget content and cta tile.
+            assertThat(communalContent?.size).isEqualTo(5)
             assertThat(communalContent?.get(0))
                 .isInstanceOf(CommunalContentModel.Smartspace::class.java)
             assertThat(communalContent?.get(1)).isInstanceOf(CommunalContentModel.Umo::class.java)
@@ -151,5 +154,47 @@
                 .isInstanceOf(CommunalContentModel.Widget::class.java)
             assertThat(communalContent?.get(3))
                 .isInstanceOf(CommunalContentModel.Widget::class.java)
+            assertThat(communalContent?.get(4))
+                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
+        }
+
+    @Test
+    fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() =
+        testScope.runTest {
+            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+            communalRepository.setCtaTileInViewModeVisibility(true)
+
+            val communalContent by collectLastValue(underTest.communalContent)
+            val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing)
+
+            assertThat(communalContent?.size).isEqualTo(1)
+            assertThat(communalContent?.get(0))
+                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
+
+            underTest.onDismissCtaTile()
+
+            // hide CTA tile and show the popup
+            assertThat(communalContent).isEmpty()
+            assertThat(isPopupOnDismissCtaShowing).isEqualTo(true)
+
+            // hide popup after time elapsed
+            advanceTimeBy(POPUP_AUTO_HIDE_TIMEOUT_MS)
+            assertThat(isPopupOnDismissCtaShowing).isEqualTo(false)
+        }
+
+    @Test
+    fun popup_onDismiss_hidesImmediately() =
+        testScope.runTest {
+            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+            communalRepository.setCtaTileInViewModeVisibility(true)
+
+            val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing)
+
+            underTest.onDismissCtaTile()
+            assertThat(isPopupOnDismissCtaShowing).isEqualTo(true)
+
+            // dismiss the popup directly
+            underTest.onHidePopupAfterDismissCta()
+            assertThat(isPopupOnDismissCtaShowing).isEqualTo(false)
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
index 3d1efa5..5827671 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
@@ -293,8 +293,8 @@
     }
 
     /**
-     * Verifies that swiping up when the lock pattern is not secure does not consume the scroll
-     * gesture or expand.
+     * Verifies that swiping up when the lock pattern is not secure dismissed dream and consumes
+     * the gesture.
      */
     @Test
     public void testSwipeUp_keyguardNotSecure_doesNotExpand() {
@@ -314,11 +314,44 @@
 
         reset(mScrimController);
 
-        // Scroll gesture is not consumed.
+        // Scroll gesture is consumed.
         assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
-                .isFalse();
+                .isTrue();
         // We should not expand since the keyguard is not secure
         verify(mScrimController, never()).expand(any());
+        // Since we are swiping up, we should wake from dreams.
+        verify(mCentralSurfaces).awakenDreams();
+    }
+
+    /**
+     * Verifies that swiping down when the lock pattern is not secure does not dismiss the dream.
+     */
+    @Test
+    public void testSwipeDown_keyguardNotSecure_doesNotExpand() {
+        when(mLockPatternUtils.isSecure(CURRENT_USER_INFO.id)).thenReturn(false);
+        mTouchHandler.onSessionStart(mTouchSession);
+        ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+        final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+        final float distanceY = SCREEN_HEIGHT_PX * 0.3f;
+        // Swiping down near the bottom of the screen where the touch initiation region is.
+        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, SCREEN_HEIGHT_PX - distanceY, 0);
+        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, SCREEN_HEIGHT_PX, 0);
+
+        reset(mScrimController);
+
+        // Scroll gesture is not consumed.
+        assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
+                .isTrue();
+        // We should not expand since the keyguard is not secure
+        verify(mScrimController, never()).expand(any());
+        // Since we are swiping down, we should not dismiss the dream.
+        verify(mCentralSurfaces, never()).awakenDreams();
     }
 
     private void verifyScroll(float percent, Direction direction,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 6c4bb37..c4ebbdc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -24,6 +24,7 @@
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
 import com.android.systemui.common.shared.model.Position
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.doze.DozeMachine
@@ -71,6 +72,7 @@
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
     private lateinit var systemClock: FakeSystemClock
+    private lateinit var facePropertyRepository: FakeFacePropertyRepository
 
     private lateinit var underTest: KeyguardRepositoryImpl
 
@@ -78,6 +80,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         systemClock = FakeSystemClock()
+        facePropertyRepository = FakeFacePropertyRepository()
         underTest =
             KeyguardRepositoryImpl(
                 statusBarStateController,
@@ -89,6 +92,7 @@
                 mainDispatcher,
                 testScope.backgroundScope,
                 systemClock,
+                facePropertyRepository,
             )
     }
 
@@ -482,10 +486,7 @@
         testScope.runTest {
             val values = mutableListOf<Point?>()
             val job = underTest.faceSensorLocation.onEach(values::add).launchIn(this)
-
-            val captor = argumentCaptor<AuthController.Callback>()
             runCurrent()
-            verify(authController).addCallback(captor.capture())
 
             // An initial, null value should be initially emitted so that flows combined with this
             // one
@@ -500,8 +501,7 @@
                     Point(250, 250),
                 )
                 .onEach {
-                    whenever(authController.faceSensorLocation).thenReturn(it)
-                    captor.value.onFaceSensorLocationChanged()
+                    facePropertyRepository.setSensorLocation(it)
                     runCurrent()
                 }
                 .also { dispatchedSensorLocations ->
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index b483085..9bccf4e2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -82,6 +82,7 @@
                 keyguardTransitionInteractor,
                 fakeLightRevealScrimRepository,
                 testScope.backgroundScope,
+                mock(),
                 mock()
             )
     }
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
index e7037a6..9daf186 100644
--- 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
@@ -94,7 +94,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(5)
+            assertThat(values.size).isEqualTo(6)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index e141c2b..f1690daf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -95,7 +95,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(6)
+            assertThat(values.size).isEqualTo(7)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
         }
 
@@ -117,7 +117,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(3)
+            assertThat(values.size).isEqualTo(4)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
@@ -232,7 +232,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 897ce6d..f763a67 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -59,9 +59,9 @@
                 testScope,
             )
 
-            // Only three values should be present, since the dream overlay runs for a small
+            // Only five values should be present, since the dream overlay runs for a small
             // fraction of the overall animation time
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
@@ -84,7 +84,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 7c3dc97..5b88ebe6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -32,7 +32,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.statusbar.notification.data.repository.fakeNotificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.phone.screenOffAnimationController
 import com.android.systemui.testKosmos
@@ -56,8 +56,7 @@
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val screenOffAnimationController = kosmos.screenOffAnimationController
     private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
-    private val fakeNotificationsKeyguardViewStateRepository =
-        kosmos.fakeNotificationsKeyguardViewStateRepository
+    private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor
     private val dozeParameters = kosmos.dozeParameters
     private val underTest = kosmos.keyguardRootViewModel
 
@@ -118,7 +117,7 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(true)
+            notificationsKeyguardInteractor.setPulseExpanding(true)
             deviceEntryRepository.setBypassEnabled(false)
             runCurrent()
 
@@ -130,9 +129,9 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+            notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(true)
-            fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             runCurrent()
 
             assertThat(isVisible?.value).isTrue()
@@ -144,10 +143,10 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+            notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(false)
-            fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             runCurrent()
 
             assertThat(isVisible?.value).isTrue()
@@ -159,11 +158,11 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+            notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
             whenever(dozeParameters.displayNeedsBlanking).thenReturn(true)
-            fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             runCurrent()
 
             assertThat(isVisible?.value).isTrue()
@@ -175,11 +174,11 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+            notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
             whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
-            fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             runCurrent()
 
             assertThat(isVisible?.value).isTrue()
@@ -191,11 +190,11 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+            notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
             whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
-            fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             runCurrent()
 
             assertThat(isVisible?.isAnimating).isEqualTo(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 4843f8b..74025fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -73,9 +73,9 @@
                 testScope = testScope,
             )
 
-            // Only three values should be present, since the dream overlay runs for a small
+            // Only five values should be present, since the dream overlay runs for a small
             // fraction of the overall animation time
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
@@ -98,10 +98,10 @@
                 testScope = testScope,
             )
 
-            assertThat(values.size).isEqualTo(5)
+            assertThat(values.size).isEqualTo(6)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
             // Validate finished value
-            assertThat(values[4]).isEqualTo(0f)
+            assertThat(values[5]).isEqualTo(0f)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index a1b8aab..6fcb0c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -74,9 +74,9 @@
                     ),
                 testScope = testScope,
             )
-            // Only 3 values should be present, since the dream overlay runs for a small fraction
+            // Only 5 values should be present, since the dream overlay runs for a small fraction
             // of the overall animation time
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index 2111ad5..639114c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -33,6 +33,7 @@
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,6 +55,7 @@
     fun lockscreenFadeIn() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
@@ -83,6 +85,7 @@
                 100
             )
             val values by collectValues(underTest.lockscreenTranslationY)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
@@ -95,7 +98,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
         }
 
@@ -107,6 +110,7 @@
                 100
             )
             val values by collectValues(underTest.lockscreenTranslationY)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
 
@@ -117,6 +121,7 @@
     fun deviceEntryParentViewFadeIn() =
         testScope.runTest {
             val values by collectValues(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 90b8362..30b87bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -65,6 +66,7 @@
     fun bouncerAlpha() =
         testScope.runTest {
             val values by collectValues(underTest.bouncerAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
@@ -83,6 +85,7 @@
     fun bouncerAlpha_runDimissFromKeyguard() =
         testScope.runTest {
             val values by collectValues(underTest.bouncerAlpha)
+            runCurrent()
 
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
 
@@ -95,7 +98,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
@@ -103,11 +106,12 @@
     fun lockscreenAlpha() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             keyguardTransitionRepository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
@@ -115,13 +119,14 @@
     fun lockscreenAlpha_runDimissFromKeyguard() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             keyguardTransitionRepository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(1f) }
         }
 
@@ -129,13 +134,14 @@
     fun lockscreenAlpha_leaveShadeOpen() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             keyguardTransitionRepository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(1f) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt
index bd1c310..c104977 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt
@@ -16,32 +16,45 @@
 
 package com.android.systemui.qs.tiles.base.actions
 
+import android.app.PendingIntent
+import android.content.ComponentName
 import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+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.plugins.ActivityStarter
+import com.android.systemui.util.mockito.argThat
+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.ArgumentMatcher
 import org.mockito.Mock
 import org.mockito.Mockito.any
 import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class QSTileIntentUserInputHandlerTest : SysuiTestCase() {
-
-    @Mock private lateinit var activityStarted: ActivityStarter
+    @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var activityStarter: ActivityStarter
 
     lateinit var underTest: QSTileIntentUserInputHandler
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest = QSTileIntentUserInputHandlerImpl(activityStarted)
+        underTest = QSTileIntentUserInputHandlerImpl(activityStarter, packageManager, user)
     }
 
     @Test
@@ -50,6 +63,103 @@
 
         underTest.handle(null, intent)
 
-        verify(activityStarted).postStartActivityDismissingKeyguard(eq(intent), eq(0), any())
+        verify(activityStarter).postStartActivityDismissingKeyguard(eq(intent), eq(0), any())
+    }
+
+    @Test
+    fun testPassesActivityPendingIntentToStarterAsPendingIntent() {
+        val pendingIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
+
+        underTest.handle(null, pendingIntent, true)
+
+        verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
+    }
+
+    @Test
+    fun testPassesActivityPendingIntentToStarterAsPendingIntentWhenNotRequestingActivityStart() {
+        val pendingIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
+
+        underTest.handle(null, pendingIntent, false)
+
+        verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
+    }
+
+    @Test
+    fun testPassNonActivityPendingIntentAndRequestStartingActivity_findsIntentAndStarts() {
+        val pendingIntent =
+            mock<PendingIntent> {
+                whenever(isActivity).thenReturn(false)
+                whenever(creatorPackage).thenReturn(ORIGINAL_PACKAGE)
+            }
+        setUpQueryResult(listOf(createActivityInfo(testResolvedComponent, exported = true)))
+
+        underTest.handle(null, pendingIntent, true)
+
+        val expectedIntent =
+            Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_LAUNCHER)
+                .setPackage(null)
+                .addFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                )
+                .setComponent(testResolvedComponent)
+
+        verify(activityStarter)
+            .postStartActivityDismissingKeyguard(
+                argThat(IntentMatcher(expectedIntent)),
+                eq(0),
+                any()
+            )
+    }
+
+    @Test
+    fun testPassNonActivityPendingIntentAndDoNotRequestStartingActivity_doesNotStartActivity() {
+        val pendingIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(false) }
+
+        underTest.handle(null, pendingIntent, false)
+
+        verify(activityStarter, never())
+            .postStartActivityDismissingKeyguard(any(Intent::class.java), eq(0), any())
+    }
+
+    private fun createActivityInfo(
+        componentName: ComponentName,
+        exported: Boolean = false,
+    ): ActivityInfo {
+        return ActivityInfo().apply {
+            packageName = componentName.packageName
+            name = componentName.className
+            this.exported = exported
+        }
+    }
+
+    private fun setUpQueryResult(infos: List<ActivityInfo>) {
+        `when`(
+                packageManager.queryIntentActivitiesAsUser(
+                    any(Intent::class.java),
+                    any(ResolveInfoFlags::class.java),
+                    eq(user.identifier)
+                )
+            )
+            .thenReturn(infos.map { ResolveInfo().apply { activityInfo = it } })
+    }
+
+    private class IntentMatcher(intent: Intent) : ArgumentMatcher<Intent> {
+        private val expectedIntent = intent
+        override fun matches(argument: Intent?): Boolean {
+            return argument?.action.equals(expectedIntent.action) &&
+                argument?.`package`.equals(expectedIntent.`package`) &&
+                argument?.component?.equals(expectedIntent.component)!! &&
+                argument?.categories?.equals(expectedIntent.categories)!! &&
+                argument?.flags?.equals(expectedIntent.flags)!!
+        }
+    }
+
+    companion object {
+        private const val ORIGINAL_PACKAGE = "original_pkg"
+        private const val TEST_PACKAGE = "test_pkg"
+        private const val TEST_COMPONENT_CLASS_NAME = "test_component_class_name"
+        private val testResolvedComponent = ComponentName(TEST_PACKAGE, TEST_COMPONENT_CLASS_NAME)
+        private val user = UserHandle.of(0)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
index 00405d0..c2ce392 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
@@ -29,27 +29,37 @@
 import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
+import com.android.systemui.util.time.FakeSystemClock
 import java.time.Instant
 import java.time.LocalDateTime
 import java.util.TimeZone
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class AlarmTileMapperTest : SysuiTestCase() {
+    private val oneMinute = 60000L
     private val kosmos = Kosmos()
     private val alarmTileConfig = kosmos.qsAlarmTileConfig
+    private val fakeClock = FakeSystemClock()
     // Using lazy (versus =) to make sure we override the right context -- see b/311612168
     private val mapper by lazy {
         AlarmTileMapper(
             context.orCreateTestableResources
                 .apply { addOverride(R.drawable.ic_alarm, TestStubDrawable()) }
                 .resources,
-            context.theme
+            context.theme,
+            fakeClock
         )
     }
 
+    @Before
+    fun setup() {
+        fakeClock.setCurrentTimeMillis(0) // same time both in test & map()
+    }
+
     @Test
     fun notAlarmSet() {
         val inputModel = AlarmTileModel.NoAlarmSet
@@ -66,7 +76,7 @@
 
     @Test
     fun nextAlarmSet24HourFormat() {
-        val triggerTime = 1L
+        val triggerTime = fakeClock.currentTimeMillis() + oneMinute
         val inputModel =
             AlarmTileModel.NextAlarmSet(true, AlarmManager.AlarmClockInfo(triggerTime, null))
 
@@ -85,7 +95,7 @@
 
     @Test
     fun nextAlarmSet12HourFormat() {
-        val triggerTime = 1L
+        val triggerTime = fakeClock.currentTimeMillis() + oneMinute
         val inputModel =
             AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null))
 
@@ -102,6 +112,66 @@
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
     }
 
+    @Test
+    fun nextAlarmSetMoreThanAWeekLater_mapsSecondaryLabelToDisplayDateOnly() {
+        val oneWeekAndOneMinute = 7 * 24 * 60 * 60 * 1000L + oneMinute
+        val triggerTime = fakeClock.currentTimeMillis() + oneWeekAndOneMinute
+        val inputModel =
+            AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null))
+
+        val outputState = mapper.map(alarmTileConfig, inputModel)
+
+        val localDateTime =
+            LocalDateTime.ofInstant(
+                Instant.ofEpochMilli(triggerTime),
+                TimeZone.getDefault().toZoneId()
+            )
+        val expectedSecondaryLabel = AlarmTileMapper.formatterDateOnly.format(localDateTime)
+        val expectedState =
+            createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel)
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun nextAlarmSetOneMinuteLessThanAWeekLater_mapsSecondaryLabelToDisplayTime() {
+        val oneWeekMinusOneMinute = 7 * 24 * 60 * 60 * 1000L - oneMinute
+        val triggerTime = fakeClock.currentTimeMillis() + oneWeekMinusOneMinute
+        val inputModel =
+            AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null))
+
+        val outputState = mapper.map(alarmTileConfig, inputModel)
+
+        val localDateTime =
+            LocalDateTime.ofInstant(
+                Instant.ofEpochMilli(triggerTime),
+                TimeZone.getDefault().toZoneId()
+            )
+        val expectedSecondaryLabel = AlarmTileMapper.formatter12Hour.format(localDateTime)
+        val expectedState =
+            createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel)
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun nextAlarmSetExactlyAWeekLater_mapsSecondaryLabelToDisplayDateOnly() {
+        val oneWeek = 7 * 24 * 60 * 60 * 1000L
+        val triggerTime = fakeClock.currentTimeMillis() + oneWeek
+        val inputModel =
+            AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null))
+
+        val outputState = mapper.map(alarmTileConfig, inputModel)
+
+        val localDateTime =
+            LocalDateTime.ofInstant(
+                Instant.ofEpochMilli(triggerTime),
+                TimeZone.getDefault().toZoneId()
+            )
+        val expectedSecondaryLabel = AlarmTileMapper.formatterDateOnly.format(localDateTime)
+        val expectedState =
+            createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel)
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
     private fun createAlarmTileState(
         activationState: QSTileState.ActivationState,
         secondaryLabel: String
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt
index e44c849..be2da17 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt
@@ -18,42 +18,25 @@
 
 import android.app.AlarmManager.AlarmClockInfo
 import android.app.PendingIntent
-import android.content.Intent
 import android.provider.AlarmClock
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.ActivityStarter
+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.click
 import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.verify
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class AlarmTileUserActionInteractorTest : SysuiTestCase() {
-    private lateinit var activityStarter: ActivityStarter
-    private lateinit var intentCaptor: ArgumentCaptor<Intent>
-    private lateinit var pendingIntentCaptor: ArgumentCaptor<PendingIntent>
-
-    lateinit var underTest: AlarmTileUserActionInteractor
-
-    @Before
-    fun setup() {
-        activityStarter = mock<ActivityStarter>()
-        intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
-        pendingIntentCaptor = ArgumentCaptor.forClass(PendingIntent::class.java)
-        underTest = AlarmTileUserActionInteractor(activityStarter)
-    }
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+    private val underTest = AlarmTileUserActionInteractor(inputHandler)
 
     @Test
     fun handleClickWithDefaultIntent() = runTest {
@@ -62,21 +45,21 @@
 
         underTest.handleInput(click(inputModel))
 
-        verify(activityStarter)
-            .postStartActivityDismissingKeyguard(capture(intentCaptor), eq(0), nullable())
-        assertThat(intentCaptor.value.action).isEqualTo(AlarmClock.ACTION_SHOW_ALARMS)
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            assertThat(it.intent.action).isEqualTo(AlarmClock.ACTION_SHOW_ALARMS)
+        }
     }
 
     @Test
     fun handleClickWithPendingIntent() = runTest {
-        val expectedIntent: PendingIntent = mock<PendingIntent>()
+        val expectedIntent = mock<PendingIntent>()
         val alarmInfo = AlarmClockInfo(1L, expectedIntent)
         val inputModel = AlarmTileModel.NextAlarmSet(true, alarmInfo)
 
         underTest.handleInput(click(inputModel))
 
-        verify(activityStarter)
-            .postStartActivityDismissingKeyguard(capture(pendingIntentCaptor), nullable())
-        assertThat(pendingIntentCaptor.value).isEqualTo(expectedIntent)
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOnePendingIntentInput {
+            assertThat(it.pendingIntent).isEqualTo(expectedIntent)
+        }
     }
 }
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 efd4f9b..530d127d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -265,7 +265,8 @@
                 powerInteractor = powerInteractor,
                 bouncerInteractor = bouncerInteractor,
                 simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor },
-                authenticationInteractor = dagger.Lazy { utils.authenticationInteractor() }
+                authenticationInteractor = dagger.Lazy { utils.authenticationInteractor() },
+                windowController = mock(),
             )
         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 2e4986d..dd22976 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
@@ -37,6 +37,7 @@
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -46,18 +47,24 @@
 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.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @EnableFlags(AconfigFlags.FLAG_SCENE_CONTAINER)
 class SceneContainerStartableTest : SysuiTestCase() {
 
+    @Mock private lateinit var windowController: NotificationShadeWindowController
+
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
@@ -77,22 +84,30 @@
     private val falsingCollector: FalsingCollector = mock()
     private val powerInteractor = PowerInteractorFactory.create().powerInteractor
 
-    private val underTest =
-        SceneContainerStartable(
-            applicationScope = testScope.backgroundScope,
-            sceneInteractor = sceneInteractor,
-            deviceEntryInteractor = deviceEntryInteractor,
-            keyguardInteractor = keyguardInteractor,
-            flags = sceneContainerFlags,
-            sysUiState = sysUiState,
-            displayId = Display.DEFAULT_DISPLAY,
-            sceneLogger = mock(),
-            falsingCollector = falsingCollector,
-            powerInteractor = powerInteractor,
-            bouncerInteractor = bouncerInteractor,
-            simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor },
-            authenticationInteractor = dagger.Lazy { authenticationInteractor },
-        )
+    private lateinit var underTest: SceneContainerStartable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            SceneContainerStartable(
+                applicationScope = testScope.backgroundScope,
+                sceneInteractor = sceneInteractor,
+                deviceEntryInteractor = deviceEntryInteractor,
+                keyguardInteractor = keyguardInteractor,
+                flags = sceneContainerFlags,
+                sysUiState = sysUiState,
+                displayId = Display.DEFAULT_DISPLAY,
+                sceneLogger = mock(),
+                falsingCollector = falsingCollector,
+                powerInteractor = powerInteractor,
+                bouncerInteractor = bouncerInteractor,
+                simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor },
+                authenticationInteractor = dagger.Lazy { authenticationInteractor },
+                windowController = windowController,
+            )
+    }
 
     @Test
     fun hydrateVisibility() =
@@ -655,6 +670,58 @@
             assertThat(currentSceneKey).isEqualTo(SceneKey.Gone)
         }
 
+    @Test
+    fun hydrateWindowFocus() =
+        testScope.runTest {
+            val currentDesiredSceneKey by
+                collectLastValue(sceneInteractor.desiredScene.map { it.key })
+            val transitionStateFlow =
+                prepareState(
+                    isDeviceUnlocked = true,
+                    initialSceneKey = SceneKey.Gone,
+                )
+            assertThat(currentDesiredSceneKey).isEqualTo(SceneKey.Gone)
+            verify(windowController, never()).setNotificationShadeFocusable(anyBoolean())
+
+            underTest.start()
+            runCurrent()
+            verify(windowController, times(1)).setNotificationShadeFocusable(false)
+
+            sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason")
+            transitionStateFlow.value =
+                ObservableTransitionState.Transition(
+                    fromScene = SceneKey.Gone,
+                    toScene = SceneKey.Shade,
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            runCurrent()
+            verify(windowController, times(1)).setNotificationShadeFocusable(false)
+
+            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason")
+            transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Shade)
+            runCurrent()
+            verify(windowController, times(1)).setNotificationShadeFocusable(true)
+
+            sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason")
+            transitionStateFlow.value =
+                ObservableTransitionState.Transition(
+                    fromScene = SceneKey.Shade,
+                    toScene = SceneKey.Gone,
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            runCurrent()
+            verify(windowController, times(1)).setNotificationShadeFocusable(true)
+
+            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason")
+            transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone)
+            runCurrent()
+            verify(windowController, times(2)).setNotificationShadeFocusable(false)
+        }
+
     private fun TestScope.prepareState(
         isDeviceUnlocked: Boolean = false,
         isBypassEnabled: Boolean = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java
index 01fe40f..d0e05fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java
@@ -42,19 +42,15 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
 import android.util.SparseArray;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
@@ -86,8 +82,7 @@
 import java.util.concurrent.Executor;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4.class)
 public class NotificationLockscreenUserManagerMainThreadTest extends SysuiTestCase {
     @Mock
     private NotificationPresenter mPresenter;
@@ -128,6 +123,7 @@
     private NotificationEntry mSecondaryUserNotif;
     private NotificationEntry mWorkProfileNotif;
     private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
+    private Executor mMainExecutor = Runnable::run; // Direct executor
     private Executor mBackgroundExecutor = Runnable::run; // Direct executor
 
     @Before
@@ -156,8 +152,6 @@
                 mSecondaryUser));
         when(mUserManager.getProfilesIncludingCommunal(mSecondaryUser.id)).thenReturn(
                 Lists.newArrayList(mSecondaryUser, mCommunalUser));
-        mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
-                Handler.createAsync(Looper.myLooper()));
 
         Notification notifWithPrivateVisibility = new Notification();
         notifWithPrivateVisibility.visibility = Notification.VISIBILITY_PRIVATE;
@@ -378,28 +372,24 @@
 
         // first call explicitly sets user 0 to not public; notifies
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
         assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener).onNotificationStateChanged();
         clearInvocations(listener);
 
         // calling again has no changes; does not notify
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
         assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener, never()).onNotificationStateChanged();
 
         // Calling again with keyguard now showing makes user 0 public; notifies
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
         assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener).onNotificationStateChanged();
         clearInvocations(listener);
 
         // calling again has no changes; does not notify
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
         assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener, never()).onNotificationStateChanged();
     }
@@ -595,8 +585,7 @@
                     (() -> mOverviewProxyService),
                     NotificationLockscreenUserManagerMainThreadTest.this.mKeyguardManager,
                     mStatusBarStateController,
-                    Handler.createAsync(Looper.myLooper()),
-                    Handler.createAsync(Looper.myLooper()),
+                    mMainExecutor,
                     mBackgroundExecutor,
                     mDeviceProvisionedController,
                     mKeyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 757f16c..bcc0710 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -29,10 +29,11 @@
 import static android.os.UserHandle.USER_ALL;
 import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
 import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
-import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -62,13 +63,11 @@
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.provider.Settings;
-import android.testing.TestableLooper;
 import android.util.SparseArray;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
@@ -88,6 +87,7 @@
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.time.FakeSystemClock;
+
 import com.google.android.collect.Lists;
 
 import org.junit.After;
@@ -109,7 +109,6 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
-@TestableLooper.RunWithLooper
 public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
 
     @Parameters(name = "{0}")
@@ -197,8 +196,6 @@
                 mSecondaryUser));
         when(mUserManager.getProfilesIncludingCommunal(mSecondaryUser.id)).thenReturn(
                 Lists.newArrayList(mSecondaryUser, mCommunalUser));
-        mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
-                mockExecutorHandler(mMainExecutor));
 
         Notification notifWithPrivateVisibility = new Notification();
         notifWithPrivateVisibility.visibility = VISIBILITY_PRIVATE;
@@ -949,8 +946,7 @@
                     (() -> mOverviewProxyService),
                     NotificationLockscreenUserManagerTest.this.mKeyguardManager,
                     mStatusBarStateController,
-                    mockExecutorHandler(mMainExecutor),
-                    mockExecutorHandler(mBackgroundExecutor),
+                    mMainExecutor,
                     mBackgroundExecutor,
                     mDeviceProvisionedController,
                     mKeyguardStateController,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
new file mode 100644
index 0000000..8a0400d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.statusbar.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.util.mockito.withArgCaptor
+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.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RemoteInputRepositoryImplTest : SysuiTestCase() {
+    @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
+
+    private lateinit var testScope: TestScope
+    private lateinit var underTest: RemoteInputRepositoryImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        testScope = TestScope()
+        underTest = RemoteInputRepositoryImpl(remoteInputManager)
+    }
+
+    @Test
+    fun isRemoteInputActive_updatesOnChange() =
+        testScope.runTest {
+            val active by collectLastValue(underTest.isRemoteInputActive)
+            runCurrent()
+            assertThat(active).isFalse()
+
+            val callback = withArgCaptor {
+                verify(remoteInputManager).addControllerCallback(capture())
+            }
+
+            callback.onRemoteInputActive(true)
+            runCurrent()
+            assertThat(active).isTrue()
+
+            callback.onRemoteInputActive(false)
+            runCurrent()
+            assertThat(active).isFalse()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
new file mode 100644
index 0000000..12469dd
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RemoteInputInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
+    private val underTest = kosmos.remoteInputInteractor
+
+    @Test
+    fun isRemoteInputActive_true() =
+        testScope.runTest {
+            val active by collectLastValue(underTest.isRemoteInputActive)
+
+            fakeRemoteInputRepository.isRemoteInputActive.value = true
+            runCurrent()
+
+            assertThat(active).isTrue()
+        }
+
+    @Test
+    fun isRemoteInputActive_false() =
+        testScope.runTest {
+            val active by collectLastValue(underTest.isRemoteInputActive)
+
+            fakeRemoteInputRepository.isRemoteInputActive.value = false
+            runCurrent()
+
+            assertThat(active).isFalse()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index de767e3..7274c0c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -21,29 +21,30 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
-import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.content.BroadcastReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.TestableLooper.RunWithLooper;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.Dependency;
+import com.android.systemui.Flags;
 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.Flags;
 import com.android.systemui.model.SysUiState;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -61,17 +62,17 @@
 public class SystemUIDialogTest extends SysuiTestCase {
 
     @Mock
-    private FeatureFlags mFeatureFlags;
-    @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
     private SystemUIDialog.Delegate mDelegate;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        mDependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
         mDependency.injectTestDependency(BroadcastDispatcher.class, mBroadcastDispatcher);
     }
 
@@ -110,16 +111,13 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS)
     public void usePredictiveBackAnimFlag() {
-        when(mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM))
-                .thenReturn(true);
         final SystemUIDialog dialog = new SystemUIDialog(mContext);
 
         dialog.show();
 
         assertTrue(dialog.isShowing());
-        verify(mFeatureFlags, atLeast(1))
-                .isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM);
 
         dialog.dismiss();
         assertFalse(dialog.isShowing());
@@ -174,7 +172,6 @@
     private SystemUIDialog createDialogWithDelegate() {
         SystemUIDialog.Factory factory = new SystemUIDialog.Factory(
                 getContext(),
-                mFeatureFlags,
                 Dependency.get(SystemUIDialogManager.class),
                 Dependency.get(SysUiState.class),
                 Dependency.get(BroadcastDispatcher.class),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 75d1869..a9ee405 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -68,6 +68,8 @@
 
     override val isSingleCarrier = MutableStateFlow(true)
 
+    override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList())
+
     private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
     override val defaultMobileIconMapping = _defaultMobileIconMapping
 
@@ -80,8 +82,12 @@
     override val isForceHidden = MutableStateFlow(false)
 
     /** Always returns a new fake interactor */
-    override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
-        return FakeMobileIconInteractor(tableLogBuffer).also { interactorCache[subId] = it }
+    override fun getMobileConnectionInteractorForSubId(subId: Int): FakeMobileIconInteractor {
+        return FakeMobileIconInteractor(tableLogBuffer).also {
+            interactorCache[subId] = it
+            // Also update the icons
+            icons.value = interactorCache.values.toList()
+        }
     }
 
     /**
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 0537f17..9063a02 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -46,8 +46,8 @@
     static_libs: [
         "androidx.annotation_annotation",
         "androidx-constraintlayout_constraintlayout",
+        "PlatformAnimationLib",
         "PluginCoreLib",
-        "SystemUIAnimationLib",
         "SystemUICommon",
         "SystemUILogLib",
         "androidx.annotation_annotation",
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java
index 3e5e8a0..f0ce460 100644
--- a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java
@@ -18,6 +18,8 @@
 
 import android.content.ComponentName;
 
+import java.util.function.BiConsumer;
+
 /**
  * Provides the ability for consumers to control plugin lifecycle.
  *
@@ -33,11 +35,8 @@
     /** Returns the currently loaded plugin instance (if plugin is loaded) */
     T getPlugin();
 
-    /** Returns true if the lifecycle manager should log debug messages */
-    boolean getIsDebug();
-
-    /** Sets whether or not hte lifecycle manager should log debug messages */
-    void setIsDebug(boolean debug);
+    /** Log tag and messages will be sent to the provided Consumer */
+    void setLogFunc(BiConsumer<String, String> logConsumer);
 
     /** returns true if the plugin is currently loaded */
     default boolean isLoaded() {
diff --git a/packages/SystemUI/res/color/notification_state_color_dark.xml b/packages/SystemUI/res/color/notification_state_color_dark.xml
new file mode 100644
index 0000000..d26cbd5
--- /dev/null
+++ b/packages/SystemUI/res/color/notification_state_color_dark.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <!-- Pressed state's alpha is set to 0.00 temporarily until this bug is resolved permanently
+    b/313920497 Design intended alpha is 0.15-->
+    <item android:state_pressed="true" android:color="#ffffff" android:alpha="0.00" />
+    <item android:state_hovered="true" android:color="#ffffff" android:alpha="0.11" />
+    <item android:color="@color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/color/notification_overlay_color.xml b/packages/SystemUI/res/color/notification_state_color_default.xml
similarity index 100%
rename from packages/SystemUI/res/color/notification_overlay_color.xml
rename to packages/SystemUI/res/color/notification_state_color_default.xml
diff --git a/packages/SystemUI/res/color/notification_state_color_light.xml b/packages/SystemUI/res/color/notification_state_color_light.xml
new file mode 100644
index 0000000..3e8bcf3
--- /dev/null
+++ b/packages/SystemUI/res/color/notification_state_color_light.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <!-- Pressed state's alpha is set to 0.00 temporarily until this bug is resolved permanently
+    b/313920497 Design intended alpha is 0.15-->
+    <item android:state_pressed="true" android:color="#000000" android:alpha="0.00" />
+    <item android:state_hovered="true" android:color="#000000" android:alpha="0.11" />
+    <item android:color="@color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/notification_material_bg.xml b/packages/SystemUI/res/drawable/notification_material_bg.xml
index 355e75d..3f903ae 100644
--- a/packages/SystemUI/res/drawable/notification_material_bg.xml
+++ b/packages/SystemUI/res/drawable/notification_material_bg.xml
@@ -25,7 +25,7 @@
     </item>
     <item>
         <shape>
-            <solid android:color="@color/notification_overlay_color" />
+            <solid android:color="@color/notification_state_color_default" />
         </shape>
     </item>
 </layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/stat_sys_no_internet_branded_vpn.xml b/packages/SystemUI/res/drawable/stat_sys_no_internet_branded_vpn.xml
new file mode 100644
index 0000000..2161a62
--- /dev/null
+++ b/packages/SystemUI/res/drawable/stat_sys_no_internet_branded_vpn.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** 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.
+*/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="17dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M12.09,9C11.11,7.5 9.43,6.5 7.5,6.5C4.46,6.5 2,8.96 2,12c0,3.04 2.46,5.5 5.5,5.5c1.93,0 3.61,-1 4.59,-2.5H14v3h4V9H12.09zM18,13hv3h-2v-3h-5.16c-0.43,1.44 -1.76,2.5 -3.34,2.5C5.57,15.5 4,13.93 4,12c0,-1.93 1.57,-3.5 3.5,-3.5c1.58,0 2.9,1.06 3.34,2.5H18V13z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M7.5,12m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,10h-2v8h2V10z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,20h-2v2h2V20z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/stat_sys_no_internet_vpn_ic.xml b/packages/SystemUI/res/drawable/stat_sys_no_internet_vpn_ic.xml
new file mode 100644
index 0000000..2161a62
--- /dev/null
+++ b/packages/SystemUI/res/drawable/stat_sys_no_internet_vpn_ic.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** 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.
+*/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="17dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M12.09,9C11.11,7.5 9.43,6.5 7.5,6.5C4.46,6.5 2,8.96 2,12c0,3.04 2.46,5.5 5.5,5.5c1.93,0 3.61,-1 4.59,-2.5H14v3h4V9H12.09zM18,13hv3h-2v-3h-5.16c-0.43,1.44 -1.76,2.5 -3.34,2.5C5.57,15.5 4,13.93 4,12c0,-1.93 1.57,-3.5 3.5,-3.5c1.58,0 2.9,1.06 3.34,2.5H18V13z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M7.5,12m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,10h-2v8h2V10z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,20h-2v2h2V20z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 6d7ce06..e602d6c 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -107,4 +107,13 @@
 
     <include layout="@layout/ambient_indication"
              android:id="@id/ambient_indication_container" />
+
+    <FrameLayout
+        android:id="@+id/smartspace_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:layout_marginBottom="@dimen/ambient_indication_margin_bottom"
+        android:visibility="gone">
+    </FrameLayout>
 </com.android.systemui.statusbar.phone.KeyguardBottomAreaView>
diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml
index fc0bf24..4cb7591 100644
--- a/packages/SystemUI/res/layout/keyguard_status_bar.xml
+++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml
@@ -23,7 +23,6 @@
     android:layout_width="match_parent"
     android:layout_height="@dimen/status_bar_header_height_keyguard"
     android:baselineAligned="false"
-    android:gravity="center_vertical"
     >
 
     <LinearLayout
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index 7ab44e7..73874a0 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -44,6 +44,8 @@
                 android:ellipsize="marquee"
                 android:focusable="true"
                 android:gravity="center_vertical"
+                android:textDirection="locale"
+                android:textAlignment="viewStart"
                 android:singleLine="true"
                 android:textAppearance="@style/TextAppearance.QS.Status.Build"
                 android:visibility="gone" />
diff --git a/packages/SystemUI/res/layout/remote_input.xml b/packages/SystemUI/res/layout/remote_input.xml
index f4b0a45..84681d34 100644
--- a/packages/SystemUI/res/layout/remote_input.xml
+++ b/packages/SystemUI/res/layout/remote_input.xml
@@ -89,7 +89,6 @@
                 android:textColorHint="@color/remote_input_hint"
                 android:textSize="16sp"
                 android:background="@null"
-                android:maxLines="4"
                 android:ellipsize="start"
                 android:inputType="textShortMessage|textMultiLine|textAutoCorrect|textCapSentences"
                 android:imeOptions="actionSend|flagNoExtractUi|flagNoFullscreen" />
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index 1f671ac..f1017d8 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -40,6 +40,9 @@
     <!-- Whether to show bottom sheets edge to edge -->
     <bool name="config_edgeToEdgeBottomSheetDialog">false</bool>
 
+    <!-- Flag to activate drag notification to contents feature -->
+    <bool name="config_notificationToContents">true</bool>
+
     <!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a
     string with two parts: the ID of the slot and the comma-delimited list of affordance IDs,
     separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 10f7c4d..17719d1 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -538,15 +538,15 @@
        -->
     <string translatable="false" name="config_frontBuiltInDisplayCutoutProtection"></string>
 
-    <!--  ID for the camera of outer display that needs extra protection -->
+    <!-- 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 -->
+    <!-- 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 -->
+    <!-- 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>
@@ -650,13 +650,20 @@
     <!-- Whether to use window background blur for the volume dialog. -->
     <bool name="config_volumeDialogUseBackgroundBlur">false</bool>
 
-    <!-- The properties of the face auth camera in pixels -->
+    <!-- The properties of the face auth front camera for outer display in pixels -->
     <integer-array name="config_face_auth_props">
         <!-- sensorLocationX -->
         <!-- sensorLocationY -->
         <!--sensorRadius -->
     </integer-array>
 
+    <!-- The properties of the face auth front camera for inner display in pixels -->
+    <integer-array name="config_inner_face_auth_props">
+        <!-- sensorLocationX -->
+        <!-- sensorLocationY -->
+        <!--sensorRadius -->
+    </integer-array>
+
     <!-- Overrides the behavior of the face unlock keyguard bypass setting:
          0 - Don't override the setting (default)
          1 - Override the setting to always bypass keyguard
@@ -995,4 +1002,7 @@
         <item>com.android.switchaccess.SwitchAccessService</item>
         <item>com.google.android.apps.accessibility.voiceaccess.JustSpeakService</item>
     </string-array>
+
+    <!--  Whether to use a machine learning model for back gesture falsing. -->
+    <bool name="config_useBackGestureML">true</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e7eb984..f55a11e2 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -449,11 +449,11 @@
     <!-- Content description after successful auth when confirmation required -->
     <string name="fingerprint_dialog_authenticated_confirmation">Press the unlock icon to continue</string>
     <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->
-    <string name="fingerprint_dialog_use_fingerprint_instead">Can\u2019t recognize face. Use fingerprint instead.</string>
+    <string name="fingerprint_dialog_use_fingerprint_instead">Face not recognized. Use fingerprint instead.</string>
     <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->
     <string name="keyguard_face_failed_use_fp">@string/fingerprint_dialog_use_fingerprint_instead</string>
     <!-- Message shown to inform the user a face cannot be recognized. [CHAR LIMIT=25] -->
-    <string name="keyguard_face_failed">Can\u2019t recognize face</string>
+    <string name="keyguard_face_failed">Face not recognized</string>
     <!-- Message shown to suggest using fingerprint sensor to authenticate after another biometric failed. [CHAR LIMIT=25] -->
     <string name="keyguard_suggest_fingerprint">Use fingerprint instead</string>
     <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=59] -->
@@ -1079,6 +1079,16 @@
 
     <!-- Description for the button that opens the widget editor on click. [CHAR LIMIT=50] -->
     <string name="button_to_open_widget_editor">Open the widget editor</string>
+    <!-- Text for CTA button that launches the hub mode widget editor on click. [CHAR LIMIT=50] -->
+    <string name="cta_tile_button_to_open_widget_editor">Customize</string>
+    <!-- Text for CTA button that dismisses the tile on click. [CHAR LIMIT=50] -->
+    <string name="cta_tile_button_to_dismiss">Dismiss</string>
+    <!-- Label for CTA tile to edit the glanceable hub [CHAR LIMIT=100] -->
+    <string name="cta_label_to_edit_widget">Add, remove, and reorder your widgets in this space</string>
+    <!-- Label for CTA tile that opens widget picker on click in edit mode [CHAR LIMIT=50] -->
+    <string name="cta_label_to_open_widget_picker">Add more widgets</string>
+    <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] -->
+    <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string>
     <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
     <string name="button_to_remove_widget">Remove</string>
     <!-- Text for the button that launches the hub mode widget picker. [CHAR LIMIT=50] -->
@@ -2274,6 +2284,8 @@
     <string name="notification_channel_storage">Storage</string>
     <!-- Title for the notification channel for hints and suggestions. [CHAR LIMIT=NONE] -->
     <string name="notification_channel_hints">Hints</string>
+    <!-- Title for the notification channel for accessibility related (i.e. accessibility floating menu). [CHAR LIMIT=NONE] -->
+    <string name="notification_channel_accessibility">Accessibility</string>
 
     <!-- App label of the instant apps notification [CHAR LIMIT=60] -->
     <string name="instant_apps">Instant Apps</string>
@@ -2544,6 +2556,11 @@
     <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string>
     <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_undo">Undo</string>
+    <!-- Notification title shown when accessibility floating button is in hidden state. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_floating_button_hidden_notification_title">Accessibility button hidden</string>
+    <!-- Notification content text to explain user can tap notification to bring back accessibility floating button. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_floating_button_hidden_notification_text">Tap to show accessibility button</string>
+
     <!-- Text for the message view with undo action of the accessibility floating menu to show which feature shortcut was removed. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_undo_message_label_text"><xliff:g id="feature name" example="Magnification">%s</xliff:g> shortcut removed</string>
     <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]-->
@@ -3262,6 +3279,9 @@
     <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. -->
     <string name="install_app">Install app</string>
 
+    <!-- Instructions informing the user they can swipe up on the lockscreen to dismiss [CHAR LIMIT=48]-->
+    <string name="dismissible_keyguard_swipe">Swipe to continue</string>
+
     <!--- Title of the dialog appearing when an external display is connected, asking whether to start mirroring [CHAR LIMIT=NONE]-->
     <string name="connected_display_dialog_start_mirroring">Mirror to external display?</string>
     <!--- Body of the mirroring dialog, shown when dual display is enabled. This signals that enabling mirroring will stop concurrent displays on a foldable device. [CHAR LIMIT=NONE]-->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 3f026a4..7d7c050 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -921,7 +921,7 @@
     <style name="Theme.ControlsActivity" parent="@android:style/Theme.DeviceDefault.NoActionBar">
         <item name="android:windowActivityTransitions">true</item>
         <item name="android:windowContentTransitions">false</item>
-        <item name="android:windowIsTranslucent">false</item>
+        <item name="android:windowIsTranslucent">true</item>
         <item name="android:windowBackground">@android:color/black</item>
         <item name="android:windowAnimationStyle">@null</item>
         <item name="android:statusBarColor">@android:color/black</item>
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 3a26ebf..326c7ef 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -34,9 +34,6 @@
     srcs: [
         ":statslog-SystemUI-java-gen",
     ],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 android_library {
@@ -51,8 +48,8 @@
     ],
     static_libs: [
         "BiometricsSharedLib",
+        "PlatformAnimationLib",
         "PluginCoreLib",
-        "SystemUIAnimationLib",
         "SystemUIPluginLib",
         "SystemUIUnfoldLib",
         "SystemUISharedLib-Keyguard",
@@ -74,9 +71,6 @@
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
     kotlincflags: ["-Xjvm-default=all"],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 java_library {
@@ -88,9 +82,6 @@
     static_kotlin_stdlib: false,
     java_version: "1.8",
     min_sdk_version: "current",
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 java_library {
@@ -110,7 +101,4 @@
     },
     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/UdfpsOverlayParams.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/UdfpsOverlayParams.kt
index 65c5a49..e31fb89 100644
--- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/UdfpsOverlayParams.kt
+++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/UdfpsOverlayParams.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.biometrics.shared.model
 
 import android.graphics.Rect
+import android.hardware.fingerprint.FingerprintSensorProperties.SensorType
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
 import android.view.Surface
 import android.view.Surface.Rotation
 
@@ -39,6 +41,8 @@
  * the native resolution.
  *
  * [rotation] current rotation of the display.
+ *
+ * [sensorType] fingerprint sensor type
  */
 data class UdfpsOverlayParams(
     val sensorBounds: Rect = Rect(),
@@ -46,7 +50,8 @@
     val naturalDisplayWidth: Int = 0,
     val naturalDisplayHeight: Int = 0,
     val scaleFactor: Float = 1f,
-    @Rotation val rotation: Int = Surface.ROTATION_0
+    @Rotation val rotation: Int = Surface.ROTATION_0,
+    @SensorType val sensorType: Int = TYPE_UDFPS_OPTICAL
 ) {
 
     /** Same as [sensorBounds], but in native resolution. */
diff --git a/packages/SystemUI/shared/lint-baseline.xml b/packages/SystemUI/shared/lint-baseline.xml
deleted file mode 100644
index 4bd6729..0000000
--- a/packages/SystemUI/shared/lint-baseline.xml
+++ /dev/null
@@ -1,708 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-dev" type="baseline" client="" name="" variant="all" version="7.1.0-dev">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.os.RemoteException#rethrowFromSystemServer`"
-        errorLine1="            throw e.rethrowFromSystemServer();"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java"
-            line="90"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.graphics.Bitmap#getHardwareBuffer`"
-        errorLine1="                mBuffer != null ? mBuffer.getHardwareBuffer() : null, mRect);"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/view/AppTransitionAnimationSpecCompat.java"
-            line="39"
-            column="43"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `new android.graphics.ParcelableColorSpace`"
-        errorLine1="                        ? new ParcelableColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))"
-        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/BitmapUtil.java"
-            line="57"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `new android.graphics.ParcelableColorSpace`"
-        errorLine1="                        : new ParcelableColorSpace(bitmap.getColorSpace());"
-        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/BitmapUtil.java"
-            line="58"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.graphics.Bitmap#getHardwareBuffer`"
-        errorLine1="        bundle.putParcelable(KEY_BUFFER, bitmap.getHardwareBuffer());"
-        errorLine2="                                                ~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/BitmapUtil.java"
-            line="61"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Cast from `ParcelableColorSpace` to `Parcelable` requires API level 31 (current min is 26)"
-        errorLine1="        bundle.putParcelable(KEY_COLOR_SPACE, colorSpace);"
-        errorLine2="                                              ~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/BitmapUtil.java"
-            line="62"
-            column="47"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.graphics.Bitmap#wrapHardwareBuffer`"
-        errorLine1="        return Bitmap.wrapHardwareBuffer(Objects.requireNonNull(buffer),"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/BitmapUtil.java"
-            line="84"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.graphics.ParcelableColorSpace#getColorSpace`"
-        errorLine1="                colorSpace.getColorSpace());"
-        errorLine2="                           ~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/BitmapUtil.java"
-            line="85"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `new android.view.SurfaceControl.Transaction`"
-        errorLine1="        final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java"
-            line="122"
-            column="47"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `new android.util.ArraySet`"
-        errorLine1="        mPluginActions = new ArraySet&lt;>(mSharedPrefs.getStringSet(PLUGIN_ACTIONS, null));"
-        errorLine2="                         ~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginPrefs.java"
-            line="41"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `new android.util.ArraySet`"
-        errorLine1="        return new ArraySet&lt;>(mPluginActions);"
-        errorLine2="               ~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginPrefs.java"
-            line="45"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 28 (current min is 26): `android.graphics.Bitmap#createBitmap`"
-        errorLine1="        return Bitmap.createBitmap(picture);"
-        errorLine2="                      ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/view/RecentsTransition.java"
-            line="113"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `new android.view.SurfaceControl.Builder`"
-        errorLine1="            mSurface = new SurfaceControl.Builder()"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="116"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Builder#setName`"
-        errorLine1="                    .setName(&quot;Transition Unrotate&quot;)"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="117"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Builder#setParent`"
-        errorLine1="                    .setParent(parent)"
-        errorLine2="                     ~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="119"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Builder#build`"
-        errorLine1="                    .build();"
-        errorLine2="                     ~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="120"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#reparent`"
-        errorLine1="            t.reparent(child, mSurface);"
-        errorLine2="              ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="137"
-            column="15"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `new android.view.SurfaceControl.Transaction`"
-        errorLine1="            SurfaceControl.Transaction t = new SurfaceControl.Transaction();"
-        errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="143"
-            column="44"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#reparent`"
-        errorLine1="                t.reparent(mRotateChildren.get(i), rootLeash);"
-        errorLine2="                  ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="145"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#apply`"
-        errorLine1="            t.apply();"
-        errorLine2="              ~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="148"
-            column="15"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setLayer`"
-        errorLine1="                        t.setLayer(counterLauncher.mSurface, launcherLayer);"
-        errorLine2="                          ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="200"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setLayer`"
-        errorLine1="                        t.setLayer(counterLauncher.mSurface, info.getChanges().size() * 3);"
-        errorLine2="                          ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="206"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setLayer`"
-        errorLine1="                            t.setLayer(leash, info.getChanges().size() * 3 - i);"
-        errorLine2="                              ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="216"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setAlpha`"
-        errorLine1="                        t.setAlpha(wallpapersCompat[i].leash.getSurfaceControl(), 1.f);"
-        errorLine2="                          ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="223"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setLayer`"
-        errorLine1="                            t.setLayer(counterWallpaper.mSurface, -1);"
-        errorLine2="                              ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="233"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#apply`"
-        errorLine1="                t.apply();"
-        errorLine2="                  ~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java"
-            line="238"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.ActivityManager.RunningTaskInfo#taskId`"
-        errorLine1="        taskId = change.getTaskInfo() != null ? change.getTaskInfo().taskId : -1;"
-        errorLine2="                                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java"
-            line="101"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#taskId`"
-        errorLine1="        taskId = change.getTaskInfo() != null ? change.getTaskInfo().taskId : -1;"
-        errorLine2="                                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java"
-            line="192"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.ActivityManager.RunningTaskInfo#isRunning`"
-        errorLine1="            isNotInRecents = !change.getTaskInfo().isRunning;"
-        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java"
-            line="116"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#isRunning`"
-        errorLine1="            isNotInRecents = !change.getTaskInfo().isRunning;"
-        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java"
-            line="210"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl#release`"
-        errorLine1="        leash.mSurfaceControl.release();"
-        errorLine2="                              ~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java"
-            line="159"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl#release`"
-        errorLine1="            mStartLeash.release();"
-        errorLine2="                        ~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java"
-            line="161"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setLayer`"
-        errorLine1="                        t.setLayer(change.getLeash(), info.getChanges().size() * 3 - i);"
-        errorLine2="                          ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java"
-            line="97"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setAlpha`"
-        errorLine1="                    t.setAlpha(wallpapers[i].leash.mSurfaceControl, 1);"
-        errorLine2="                      ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java"
-            line="105"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#apply`"
-        errorLine1="                t.apply();"
-        errorLine2="                  ~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java"
-            line="107"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl#isValid`"
-        errorLine1="        return mSurfaceControl != null &amp;&amp; mSurfaceControl.isValid();"
-        errorLine2="                                                          ~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceControlCompat.java"
-            line="41"
-            column="59"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.SurfaceControlViewHost#release`"
-        errorLine1="            mSurfaceControlViewHost.release();"
-        errorLine2="                                    ~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestReceiver.java"
-            line="61"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.SurfaceView#getHostToken`"
-        errorLine1="        bundle.putBinder(KEY_HOST_TOKEN, surfaceView.getHostToken());"
-        errorLine2="                                                     ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestUtils.java"
-            line="34"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceView#getSurfaceControl`"
-        errorLine1="        bundle.putParcelable(KEY_SURFACE_CONTROL, surfaceView.getSurfaceControl());"
-        errorLine2="                                                              ~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestUtils.java"
-            line="35"
-            column="63"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Cast from `SurfaceControl` to `Parcelable` requires API level 29 (current min is 26)"
-        errorLine1="        bundle.putParcelable(KEY_SURFACE_CONTROL, surfaceView.getSurfaceControl());"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestUtils.java"
-            line="35"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl#isValid`"
-        errorLine1="                if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {"
-        errorLine2="                                                                              ~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java"
-            line="107"
-            column="79"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `new android.view.SurfaceControl.Transaction`"
-        errorLine1="                Transaction t = new Transaction();"
-        errorLine2="                                ~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java"
-            line="113"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#apply`"
-        errorLine1="                    t.apply();"
-        errorLine2="                      ~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java"
-            line="122"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setAlpha`"
-        errorLine1="                t.setAlpha(surface, alpha);"
-        errorLine2="                  ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java"
-            line="361"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setLayer`"
-        errorLine1="                t.setLayer(surface, layer);"
-        errorLine2="                  ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java"
-            line="364"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#origActivity`"
-        errorLine1="            ComponentName sourceComponent = t.origActivity != null"
-        errorLine2="                                            ~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java"
-            line="73"
-            column="45"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#origActivity`"
-        errorLine1="                    ? t.origActivity"
-        errorLine2="                      ~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java"
-            line="75"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#taskId`"
-        errorLine1="            this.id = t.taskId;"
-        errorLine2="                      ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java"
-            line="78"
-            column="23"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#baseIntent`"
-        errorLine1="            this.baseIntent = t.baseIntent;"
-        errorLine2="                              ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java"
-            line="80"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#taskDescription`"
-        errorLine1="        ActivityManager.TaskDescription td = taskInfo.taskDescription;"
-        errorLine2="                                             ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java"
-            line="242"
-            column="46"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#topActivity`"
-        errorLine1="                taskInfo.supportsSplitScreenMultiWindow, isLocked, td, taskInfo.topActivity);"
-        errorLine2="                                                                       ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java"
-            line="246"
-            column="72"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#topActivity`"
-        errorLine1="        return info.topActivity;"
-        errorLine2="               ~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskInfoCompat.java"
-            line="49"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#taskDescription`"
-        errorLine1="        return info.taskDescription;"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskInfoCompat.java"
-            line="53"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.ActivityManager.RunningTaskInfo#taskId`"
-        errorLine1="        onTaskMovedToFront(taskInfo.taskId);"
-        errorLine2="                           ~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java"
-            line="70"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.app.TaskInfo#taskId`"
-        errorLine1="        onTaskMovedToFront(taskInfo.taskId);"
-        errorLine2="                           ~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java"
-            line="70"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.graphics.Bitmap#wrapHardwareBuffer`"
-        errorLine1="                thumbnail = Bitmap.wrapHardwareBuffer(buffer, snapshot.getColorSpace());"
-        errorLine2="                                   ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/ThumbnailData.java"
-            line="69"
-            column="36"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.app.WallpaperColors#getColorHints`"
-        errorLine1="                    (colors.getColorHints() &amp; WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0;"
-        errorLine2="                            ~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TonalCompat.java"
-            line="42"
-            column="29"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `new android.view.SurfaceControl.Transaction`"
-        errorLine1="        mTransaction = new Transaction();"
-        errorLine2="                       ~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java"
-            line="31"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#apply`"
-        errorLine1="        mTransaction.apply();"
-        errorLine2="                     ~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java"
-            line="35"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setBufferSize`"
-        errorLine1="        mTransaction.setBufferSize(surfaceControl.mSurfaceControl, w, h);"
-        errorLine2="                     ~~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java"
-            line="54"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setLayer`"
-        errorLine1="        mTransaction.setLayer(surfaceControl.mSurfaceControl, z);"
-        errorLine2="                     ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java"
-            line="59"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#setAlpha`"
-        errorLine1="        mTransaction.setAlpha(surfaceControl.mSurfaceControl, alpha);"
-        errorLine2="                     ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java"
-            line="64"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.SurfaceControl.Transaction#apply`"
-        errorLine1="            t.apply();"
-        errorLine2="              ~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/ViewRootImplCompat.java"
-            line="64"
-            column="15"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`"
-        errorLine1="                .getFloat(Resources.getSystem().getIdentifier("
-        errorLine2="                 ~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/WallpaperManagerCompat.java"
-            line="46"
-            column="18"/>
-    </issue>
-
-</issues>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/KeyButtonRipple.java
similarity index 98%
rename from packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
rename to packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/KeyButtonRipple.java
index f005af3..92473e8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/KeyButtonRipple.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.navigationbar.buttons;
+package com.android.systemui.shared.navigationbar;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -125,7 +125,7 @@
     /**
      *  @param onInvisibleRunnable run after we are next drawn invisibly. Only used once.
      */
-    void setOnInvisibleRunnable(Runnable onInvisibleRunnable) {
+    public void setOnInvisibleRunnable(Runnable onInvisibleRunnable) {
         mOnInvisibleRunnable = onInvisibleRunnable;
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
index 387f2e1..87cc86f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
@@ -38,6 +38,7 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
 /**
@@ -57,7 +58,7 @@
     private final PluginFactory<T> mPluginFactory;
     private final String mTag;
 
-    private boolean mIsDebug = false;
+    private BiConsumer<String, String> mLogConsumer = null;
     private Context mPluginContext;
     private T mPlugin;
 
@@ -86,17 +87,13 @@
         return mTag;
     }
 
-    public boolean getIsDebug() {
-        return mIsDebug;
+    public void setLogFunc(BiConsumer logConsumer) {
+        mLogConsumer = logConsumer;
     }
 
-    public void setIsDebug(boolean debug) {
-        mIsDebug = debug;
-    }
-
-    private void logDebug(String message) {
-        if (mIsDebug) {
-            Log.i(mTag, message);
+    private void log(String message) {
+        if (mLogConsumer != null) {
+            mLogConsumer.accept(mTag, message);
         }
     }
 
@@ -105,19 +102,19 @@
         boolean loadPlugin = mListener.onPluginAttached(this);
         if (!loadPlugin) {
             if (mPlugin != null) {
-                logDebug("onCreate: auto-unload");
+                log("onCreate: auto-unload");
                 unloadPlugin();
             }
             return;
         }
 
         if (mPlugin == null) {
-            logDebug("onCreate auto-load");
+            log("onCreate auto-load");
             loadPlugin();
             return;
         }
 
-        logDebug("onCreate: load callbacks");
+        log("onCreate: load callbacks");
         mPluginFactory.checkVersion(mPlugin);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onCreate for plugins that aren't fragments, as fragments
@@ -129,7 +126,7 @@
 
     /** Alerts listener and plugin that the plugin is being shutdown. */
     public synchronized void onDestroy() {
-        logDebug("onDestroy");
+        log("onDestroy");
         unloadPlugin();
         mListener.onPluginDetached(this);
     }
@@ -145,7 +142,7 @@
      */
     public synchronized void loadPlugin() {
         if (mPlugin != null) {
-            logDebug("Load request when already loaded");
+            log("Load request when already loaded");
             return;
         }
 
@@ -157,7 +154,7 @@
             return;
         }
 
-        logDebug("Loaded plugin; running callbacks");
+        log("Loaded plugin; running callbacks");
         mPluginFactory.checkVersion(mPlugin);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onCreate for plugins that aren't fragments, as fragments
@@ -174,11 +171,11 @@
      */
     public synchronized void unloadPlugin() {
         if (mPlugin == null) {
-            logDebug("Unload request when already unloaded");
+            log("Unload request when already unloaded");
             return;
         }
 
-        logDebug("Unloading plugin, running callbacks");
+        log("Unloading plugin, running callbacks");
         mListener.onPluginUnloaded(mPlugin, this);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onDestroy for plugins that aren't fragments, as fragments
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 823bd2d..7088829 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -88,18 +88,23 @@
      */
     void onNavButtonsDarkIntensityChanged(float darkIntensity) = 22;
 
-     /**
-      * Sent when split keyboard shortcut is triggered to enter stage split.
-      */
-     void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
+    /**
+     * Sent when when navigation bar luma sampling is enabled or disabled.
+     */
+    void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) = 23;
 
-     /**
-      * Sent when the surface for navigation bar is created or changed
-      */
-     void onNavigationBarSurface(in SurfaceControl surface) = 26;
+    /**
+     * Sent when split keyboard shortcut is triggered to enter stage split.
+     */
+    void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
 
-     /**
-      * Sent when the task bar stash state is toggled.
-      */
-     void onTaskbarToggled() = 27;
+    /**
+     * Sent when the surface for navigation bar is created or changed
+     */
+    void onNavigationBarSurface(in SurfaceControl surface) = 26;
+
+    /**
+     * Sent when the task bar stash state is toggled.
+     */
+    void onTaskbarToggled() = 27;
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
index a4b6451..2145166 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
@@ -30,7 +30,7 @@
 
 import androidx.annotation.DimenRes;
 
-import com.android.systemui.navigationbar.buttons.KeyButtonRipple;
+import com.android.systemui.shared.navigationbar.KeyButtonRipple;
 
 public class FloatingRotationButtonView extends ImageView {
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java b/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java
index 047ff75..9f82201 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java
@@ -21,6 +21,11 @@
 import androidx.lifecycle.LifecycleEventObserver;
 import androidx.lifecycle.LifecycleOwner;
 
+/**
+ * Implementation of the collection used and thread guarantees are left to the discretion of the
+ * client. Consider using {@link com.android.systemui.util.ListenerSet} to prevent concurrent
+ * modification exceptions.
+ */
 public interface CallbackController<T> {
 
     /** Add a callback */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 74b975c..de7c12d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -22,6 +22,7 @@
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.Flags.migrateClocksToBlueprint;
+import static com.android.systemui.Flags.smartspaceRelocateToBottom;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
@@ -421,6 +422,10 @@
             return;
         }
 
+        if (smartspaceRelocateToBottom()) {
+            return;
+        }
+
         mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
         LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                 MATCH_PARENT, WRAP_CONTENT);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index a8bf229..05eeac6f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -133,7 +133,8 @@
     static final int BOUNCER_DISMISS_EXTENDED_ACCESS = 3;
     // Bouncer is dismissed due to sim card unlock code entered.
     static final int BOUNCER_DISMISS_SIM = 4;
-
+    // Bouncer dismissed after being allowed to dismiss by forceDismissiblekeyguard
+    static final int BOUNCER_DISMISSIBLE_KEYGUARD = 5;
     private static final String TAG = "KeyguardSecurityView";
 
     // Make the view move slower than the finger, as if the spring were applying force.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index b6b8c2a..8e5d0da 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -21,6 +21,7 @@
 
 import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_BIOMETRIC;
 import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_EXTENDED_ACCESS;
+import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISSIBLE_KEYGUARD;
 import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_NONE_SECURITY;
 import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_PASSWORD;
 import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_SIM;
@@ -862,7 +863,11 @@
         boolean finish = false;
         int eventSubtype = -1;
         BouncerUiEvent uiEvent = BouncerUiEvent.UNKNOWN;
-        if (mUpdateMonitor.getUserHasTrust(targetUserId)) {
+        if (mUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked()) {
+            finish = true;
+            eventSubtype = BOUNCER_DISMISSIBLE_KEYGUARD;
+            // TODO: b/308417021 add UI event
+        } else if (mUpdateMonitor.getUserHasTrust(targetUserId)) {
             finish = true;
             eventSubtype = BOUNCER_DISMISS_EXTENDED_ACCESS;
             uiEvent = BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS;
@@ -1106,7 +1111,7 @@
     }
 
     private boolean canDisplayUserSwitcher() {
-        return mFeatureFlags.isEnabled(Flags.BOUNCER_USER_SWITCHER);
+        return getContext().getResources().getBoolean(R.bool.config_enableBouncerUserSwitcher);
     }
 
     private void configureMode() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
index c5e7070..5729119 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
@@ -105,6 +105,13 @@
     @Override
     protected void onViewAttached() {
         super.onViewAttached();
+        mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback);
+    }
+
+    @Override
+    protected void onViewDetached() {
+        super.onViewDetached();
+        mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback);
     }
 
     @Override
@@ -128,14 +135,12 @@
     @Override
     public void onResume(int reason) {
         super.onResume(reason);
-        mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback);
         mView.resetState();
     }
 
     @Override
     public void onPause() {
         super.onPause();
-        mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback);
 
         // dismiss the dialog.
         if (mSimUnlockProgressDialog != null) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 2a54a4e..d372f5a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,7 @@
 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION;
+import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.animation.Animator;
@@ -492,7 +493,12 @@
             boolean splitShadeEnabled,
             boolean shouldBeCentered,
             boolean animate) {
-        mKeyguardClockSwitchController.setSplitShadeCentered(splitShadeEnabled && shouldBeCentered);
+        if (migrateClocksToBlueprint()) {
+            mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered);
+        } else {
+            mKeyguardClockSwitchController.setSplitShadeCentered(
+                    splitShadeEnabled && shouldBeCentered);
+        }
         if (mStatusViewCentered == shouldBeCentered) {
             return;
         }
@@ -548,8 +554,9 @@
         ClockController clock = mKeyguardClockSwitchController.getClock();
         boolean customClockAnimation = clock != null
                 && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation();
-
-        if (customClockAnimation) {
+        // When migrateClocksToBlueprint is on, customized clock animation is conducted in
+        // KeyguardClockViewBinder
+        if (customClockAnimation && !migrateClocksToBlueprint()) {
             // Find the clock, so we can exclude it from this transition.
             FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large);
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index e59868e..fe96099 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -102,6 +102,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEventLogger;
@@ -308,6 +309,10 @@
     private boolean mKeyguardOccluded;
     private boolean mCredentialAttempted;
     private boolean mKeyguardGoingAway;
+    /**
+     * Whether the keyguard is forced into a dismissible state.
+     */
+    private boolean mForceIsDismissible;
     private boolean mGoingToSleep;
     private boolean mPrimaryBouncerFullyShown;
     private boolean mPrimaryBouncerIsOrWillBeShowing;
@@ -362,6 +367,9 @@
     private final BiometricManager mBiometricManager;
     @Nullable
     private DeviceEntryFaceAuthInteractor mFaceAuthInteractor;
+    @VisibleForTesting
+    protected FoldGracePeriodProvider mFoldGracePeriodProvider =
+            new FoldGracePeriodProvider();
     private final DevicePostureController mDevicePostureController;
     private final TaskStackChangeListeners mTaskStackChangeListeners;
     private final IActivityTaskManager mActivityTaskManager;
@@ -728,6 +736,14 @@
     }
 
     /**
+     * Updates KeyguardUpdateMonitor's internal state to know the device should remain unlocked
+     * until the next signal to lock. Does nothing if the keyguard is already showing.
+     */
+    public void tryForceIsDismissibleKeyguard() {
+        setForceIsDismissibleKeyguard(true);
+    }
+
+    /**
      * Updates KeyguardUpdateMonitor's internal state to know if keyguard is going away.
      */
     public void setKeyguardGoingAway(boolean goingAway) {
@@ -1371,7 +1387,18 @@
     }
 
     public boolean getUserCanSkipBouncer(int userId) {
-        return getUserHasTrust(userId) || getUserUnlockedWithBiometric(userId);
+        return getUserHasTrust(userId) || getUserUnlockedWithBiometric(userId)
+                || forceIsDismissibleIsKeepingDeviceUnlocked();
+    }
+
+    /**
+     * Whether the keyguard should be kept unlocked for the folding grace period.
+     */
+    public boolean forceIsDismissibleIsKeepingDeviceUnlocked() {
+        if (mFoldGracePeriodProvider.isEnabled()) {
+            return mForceIsDismissible && isUnlockingWithForceKeyguardDismissibleAllowed();
+        }
+        return false;
     }
 
     public boolean getUserHasTrust(int userId) {
@@ -1475,6 +1502,10 @@
         return isUnlockingWithBiometricAllowed(true);
     }
 
+    private boolean isUnlockingWithForceKeyguardDismissibleAllowed() {
+        return isUnlockingWithBiometricAllowed(false);
+    }
+
     public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) {
         // StrongAuthTracker#isUnlockingWithBiometricAllowed includes
         // STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent;
@@ -1986,6 +2017,7 @@
 
     protected void handleStartedGoingToSleep(int arg1) {
         Assert.isMainThread();
+        setForceIsDismissibleKeyguard(false);
         clearFingerprintRecognized();
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
@@ -3058,6 +3090,7 @@
     void handleUserSwitching(int userId, Runnable resultCallback) {
         mLogger.logUserSwitching(userId, "from UserTracker");
         Assert.isMainThread();
+        setForceIsDismissibleKeyguard(false);
         clearFingerprintRecognized();
         boolean trustUsuallyManaged = mTrustManager.isTrustUsuallyManaged(userId);
         mLogger.logTrustUsuallyManagedUpdated(userId, mUserTrustIsUsuallyManaged.get(userId),
@@ -3636,6 +3669,31 @@
         }
     }
 
+    private void setForceIsDismissibleKeyguard(boolean forceIsDismissible) {
+        Assert.isMainThread();
+        if (!mFoldGracePeriodProvider.isEnabled()) {
+            // never send updates if the feature isn't enabled
+            return;
+        }
+        if (mKeyguardShowing && forceIsDismissible) {
+            // never keep the device unlocked if the keyguard was already showing
+            mLogger.d("Skip setting forceIsDismissibleKeyguard to true. "
+                    + "Keyguard already showing.");
+            return;
+        }
+        if (mForceIsDismissible != forceIsDismissible) {
+            mForceIsDismissible = forceIsDismissible;
+            mLogger.logForceIsDismissibleKeyguard(mForceIsDismissible);
+            for (int i = 0; i < mCallbacks.size(); i++) {
+                KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+                if (cb != null) {
+                    cb.onForceIsDismissibleChanged(forceIsDismissibleIsKeepingDeviceUnlocked());
+                }
+            }
+        }
+
+    }
+
     public boolean isSimPinVoiceSecure() {
         // TODO: only count SIMs that handle voice
         return isSimPinSecure();
@@ -3897,6 +3955,9 @@
     @Override
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("KeyguardUpdateMonitor state:");
+        pw.println("  forceIsDismissible=" + mForceIsDismissible);
+        pw.println("  forceIsDismissibleIsKeepingDeviceUnlocked="
+                + forceIsDismissibleIsKeepingDeviceUnlocked());
         pw.println("  getUserHasTrust()=" + getUserHasTrust(
                 mSelectedUserInteractor.getSelectedUserId()));
         pw.println("  getUserUnlockedWithBiometric()="
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 9d216dce..7ac5ac2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -339,4 +339,9 @@
      * On biometric enrollment state changed
      */
     public void onBiometricEnrollmentStateChanged(BiometricSourceType biometricSourceType) { }
+
+    /**
+     * On force is dismissible state changed.
+     */
+    public void onForceIsDismissibleChanged(boolean forceIsDismissible) { }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 055ca56..1f4e732 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -639,4 +639,12 @@
             { "fingerprint acquire message: $int1" }
         )
     }
+    fun logForceIsDismissibleKeyguard(keepUnlocked: Boolean) {
+        logBuffer.log(
+                TAG,
+                DEBUG,
+                { bool1 = keepUnlocked },
+                { "keepUnlockedOnFold changed to: $bool1" }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index e03c627..d6d5c26 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -68,7 +68,7 @@
 
 import com.android.internal.util.Preconditions;
 import com.android.settingslib.Utils;
-import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.decor.CutoutDecorProviderFactory;
 import com.android.systemui.decor.DebugRoundedCornerDelegate;
@@ -92,6 +92,7 @@
 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.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.SecureSettings;
 
 import dalvik.annotation.optimization.NeverCompile;
@@ -131,8 +132,6 @@
     };
     private final ScreenDecorationsLogger mLogger;
 
-    private final AuthController mAuthController;
-
     private DisplayTracker mDisplayTracker;
     @VisibleForTesting
     protected boolean mIsRegistered;
@@ -183,6 +182,9 @@
     private DisplayCutout mDisplayCutout;
     private boolean mPendingManualConfigUpdate;
 
+    private FacePropertyRepository mFacePropertyRepository;
+    private JavaAdapter mJavaAdapter;
+
     @VisibleForTesting
     protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
         if (mFaceScanningFactory.shouldShowFaceScanningAnim()) {
@@ -330,7 +332,8 @@
             PrivacyDotDecorProviderFactory dotFactory,
             FaceScanningProviderFactory faceScanningFactory,
             ScreenDecorationsLogger logger,
-            AuthController authController) {
+            FacePropertyRepository facePropertyRepository,
+            JavaAdapter javaAdapter) {
         mContext = context;
         mSecureSettings = secureSettings;
         mCommandRegistry = commandRegistry;
@@ -342,22 +345,10 @@
         mFaceScanningFactory = faceScanningFactory;
         mFaceScanningViewId = com.android.systemui.res.R.id.face_scanning_anim;
         mLogger = logger;
-        mAuthController = authController;
+        mFacePropertyRepository = facePropertyRepository;
+        mJavaAdapter = javaAdapter;
     }
 
-
-    private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
-        @Override
-        public void onFaceSensorLocationChanged() {
-            mLogger.onSensorLocationChanged();
-            if (mExecutor != null) {
-                mExecutor.execute(
-                        () -> updateOverlayProviderViews(
-                                new Integer[]{mFaceScanningViewId}));
-            }
-        }
-    };
-
     private final ScreenDecorCommand.Callback mScreenDecorCommandCallback = (cmd, pw) -> {
         // If we are exiting debug mode, we can set it (false) and bail, otherwise we will
         // ensure that debug mode is set
@@ -407,7 +398,8 @@
         mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
         mExecutor.execute(this::startOnScreenDecorationsThread);
         mDotViewController.setUiExecutor(mExecutor);
-        mAuthController.addCallback(mAuthControllerCallback);
+        mJavaAdapter.alwaysCollectFlow(mFacePropertyRepository.getSensorLocation(),
+                this::onFaceSensorLocationChanged);
         mCommandRegistry.registerCommand(ScreenDecorCommand.SCREEN_DECOR_CMD_NAME,
                 () -> new ScreenDecorCommand(mScreenDecorCommandCallback));
     }
@@ -1320,6 +1312,16 @@
         view.setLayoutParams(params);
     }
 
+    @VisibleForTesting
+    void onFaceSensorLocationChanged(Point location) {
+        mLogger.onSensorLocationChanged();
+        if (mExecutor != null) {
+            mExecutor.execute(
+                    () -> updateOverlayProviderViews(
+                            new Integer[]{mFaceScanningViewId}));
+        }
+    }
+
     public static class DisplayCutoutView extends DisplayCutoutBaseView {
         final List<Rect> mBounds = new ArrayList();
         final Rect mBoundingRect = new Rect();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
index 49e0df6..568b24d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.common.bubbles.DismissView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
@@ -116,6 +117,11 @@
         mMagnetizedObject.setMagnetListener(magnetListener);
     }
 
+    @VisibleForTesting
+    MagnetizedObject.MagnetListener getMagnetListener() {
+        return mMagnetizedObject.getMagnetListener();
+    }
+
     void maybeConsumeDownMotionEvent(MotionEvent event) {
         mMagnetizedObject.maybeConsumeMotionEvent(event);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java
new file mode 100644
index 0000000..b5eeaa1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.floatingmenu;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.systemui.res.R;
+import com.android.systemui.util.NotificationChannels;
+
+class MenuNotificationFactory {
+    public static final String ACTION_UNDO =
+            "com.android.systemui.accessibility.floatingmenu.action.UNDO";
+    public static final String ACTION_DELETE =
+            "com.android.systemui.accessibility.floatingmenu.action.DELETE";
+
+    private final Context mContext;
+
+    MenuNotificationFactory(Context context) {
+        mContext = context;
+    }
+
+    public Notification createHiddenNotification() {
+        final CharSequence title = mContext.getText(
+                R.string.accessibility_floating_button_hidden_notification_title);
+        final CharSequence content = mContext.getText(
+                R.string.accessibility_floating_button_hidden_notification_text);
+
+        return new Notification.Builder(mContext, NotificationChannels.ALERTS)
+                .setContentTitle(title)
+                .setContentText(content)
+                .setSmallIcon(R.drawable.ic_settings_24dp)
+                .setContentIntent(buildUndoIntent())
+                .setDeleteIntent(buildDeleteIntent())
+                .setColor(mContext.getResources().getColor(
+                        com.android.internal.R.color.system_notification_accent_color))
+                .setLocalOnly(true)
+                .setCategory(Notification.CATEGORY_SYSTEM)
+                .build();
+    }
+
+    private PendingIntent buildUndoIntent() {
+        final Intent intent = new Intent(ACTION_UNDO);
+
+        return PendingIntent.getBroadcast(mContext, /* requestCode= */ 0, intent,
+                PendingIntent.FLAG_IMMUTABLE);
+
+    }
+
+    private PendingIntent buildDeleteIntent() {
+        final Intent intent = new Intent(ACTION_DELETE);
+
+        return PendingIntent.getBroadcastAsUser(mContext, /* requestCode= */ 0, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
+                        | PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 62d5feb..6869bba 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -26,16 +26,21 @@
 import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
 import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
 import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_DELETE;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_UNDO;
 import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.IntDef;
 import android.annotation.StringDef;
 import android.annotation.SuppressLint;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
 import android.content.ComponentCallbacks;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
@@ -58,6 +63,7 @@
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.util.Preconditions;
 import com.android.systemui.Flags;
 import com.android.systemui.res.R;
@@ -91,6 +97,8 @@
     private final MenuViewAppearance mMenuViewAppearance;
     private final MenuAnimationController mMenuAnimationController;
     private final AccessibilityManager mAccessibilityManager;
+    private final NotificationManager mNotificationManager;
+    private final MenuNotificationFactory mNotificationFactory;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final IAccessibilityFloatingMenu mFloatingMenu;
     private final SecureSettings mSecureSettings;
@@ -103,7 +111,9 @@
     private final Rect mImeInsetsRect = new Rect();
     private boolean mIsMigrationTooltipShowing;
     private boolean mShouldShowDockTooltip;
+    private boolean mIsNotificationShown;
     private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty();
+    private BroadcastReceiver mNotificationActionReceiver;
 
     @IntDef({
             LayerIndex.MENU_VIEW,
@@ -184,10 +194,16 @@
         mMenuViewAppearance = new MenuViewAppearance(context, windowManager);
         mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
-        mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+        if (Flags.floatingMenuDragToHide()) {
+            mMenuAnimationController.setDismissCallback(this::hideMenuAndShowNotification);
+        } else {
+            mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+        }
         mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
         mDismissView = new DismissView(context);
         DismissViewUtils.setup(mDismissView);
+        mNotificationFactory = new MenuNotificationFactory(context);
+        mNotificationManager = context.getSystemService(NotificationManager.class);
         mDragToInteractAnimationController = new DragToInteractAnimationController(
                 mDismissView, mMenuView);
         mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
@@ -204,7 +220,11 @@
 
             @Override
             public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
-                hideMenuAndShowMessage();
+                if (Flags.floatingMenuDragToHide()) {
+                    hideMenuAndShowNotification();
+                } else {
+                    hideMenuAndShowMessage();
+                }
                 mDismissView.hide();
                 mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false);
             }
@@ -218,18 +238,25 @@
         mMessageView = new MenuMessageView(context);
 
         mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> {
-            if (newTargetFeatures.size() < 1) {
-                return;
-            }
-
-            // During the undo action period, the pending action will be canceled and undo back
-            // to the previous state if users did any action related to the accessibility features.
-            if (mMessageView.getVisibility() == VISIBLE) {
+            if (Flags.floatingMenuDragToHide()) {
+                dismissNotification();
                 undo();
-            }
+            } else {
+                if (newTargetFeatures.size() < 1) {
+                    return;
+                }
 
-            final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
-            messageText.setText(getMessageText(newTargetFeatures));
+                // During the undo action period, the pending action will be canceled and undo back
+                // to the previous state if users did any action related to the accessibility
+                // features.
+                if (mMessageView.getVisibility() == VISIBLE) {
+                    undo();
+                }
+
+
+                final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
+                messageText.setText(getMessageText(newTargetFeatures));
+            }
         });
 
         addView(mMenuView, LayerIndex.MENU_VIEW);
@@ -456,6 +483,50 @@
         mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
     }
 
+    private void hideMenuAndShowNotification() {
+        mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
+        showNotification();
+    }
+
+    private void showNotification() {
+        registerReceiverIfNeeded();
+        if (!mIsNotificationShown) {
+            mNotificationManager.notify(
+                    SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN,
+                    mNotificationFactory.createHiddenNotification());
+            mIsNotificationShown = true;
+        }
+    }
+
+    private void dismissNotification() {
+        unregisterReceiverIfNeeded();
+        if (mIsNotificationShown) {
+            mNotificationManager.cancel(
+                    SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+            mIsNotificationShown = false;
+        }
+    }
+
+    private void registerReceiverIfNeeded() {
+        if (mNotificationActionReceiver != null) {
+            return;
+        }
+        mNotificationActionReceiver = new MenuNotificationActionReceiver();
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ACTION_UNDO);
+        intentFilter.addAction(ACTION_DELETE);
+        getContext().registerReceiver(mNotificationActionReceiver, intentFilter,
+                Context.RECEIVER_EXPORTED);
+    }
+
+    private void unregisterReceiverIfNeeded() {
+        if (mNotificationActionReceiver == null) {
+            return;
+        }
+        getContext().unregisterReceiver(mNotificationActionReceiver);
+        mNotificationActionReceiver = null;
+    }
+
     private void undo() {
         mHandler.removeCallbacksAndMessages(/* token= */ null);
         mMessageView.setVisibility(GONE);
@@ -464,4 +535,23 @@
         mMenuView.setVisibility(VISIBLE);
         mMenuAnimationController.startGrowAnimation();
     }
+
+    @VisibleForTesting
+    DragToInteractAnimationController getDragToInteractAnimationController() {
+        return mDragToInteractAnimationController;
+    }
+
+    private class MenuNotificationActionReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (ACTION_UNDO.equals(action)) {
+                dismissNotification();
+                undo();
+            } else if (ACTION_DELETE.equals(action)) {
+                dismissNotification();
+                mDismissMenuAction.run();
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index a4f90eb..26f7646 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -148,10 +148,6 @@
 
     private final Display mDisplay;
     private float mScaleFactor = 1f;
-    // sensor locations without any resolution scaling nor rotation adjustments:
-    @Nullable private final Point mFaceSensorLocationDefault;
-    // cached sensor locations:
-    @Nullable private Point mFaceSensorLocation;
     @Nullable private Point mFingerprintSensorLocation;
     @Nullable private Rect mUdfpsBounds;
     private final Set<Callback> mCallbacks = new HashSet<>();
@@ -622,7 +618,6 @@
         mScaleFactor = mUdfpsUtils.getScaleFactor(mCachedDisplayInfo);
         updateUdfpsLocation();
         updateFingerprintLocation();
-        updateFaceLocation();
     }
     /**
      * @return where the fingerprint sensor exists in pixels in its natural orientation.
@@ -682,31 +677,6 @@
     }
 
     /**
-     * @return where the face sensor exists in pixels in the current device orientation. Returns
-     * null if no face sensor exists.
-     */
-    @Nullable public Point getFaceSensorLocation() {
-        return mFaceSensorLocation;
-    }
-
-    private void updateFaceLocation() {
-        if (mFaceProps == null || mFaceSensorLocationDefault == null) {
-            mFaceSensorLocation = null;
-        } else {
-            mFaceSensorLocation = rotateToCurrentOrientation(
-                    new Point(
-                            (int) (mFaceSensorLocationDefault.x * mScaleFactor),
-                            (int) (mFaceSensorLocationDefault.y * mScaleFactor)),
-                    mCachedDisplayInfo
-            );
-        }
-
-        for (final Callback cb : mCallbacks) {
-            cb.onFaceSensorLocationChanged();
-        }
-    }
-
-    /**
      * @param inOutPoint point on the display in pixels. Going in, represents the point
      *                   in the device's natural orientation. Going out, represents
      *                   the point in the display's current orientation.
@@ -821,17 +791,7 @@
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mPanelInteractionDetector = panelInteractionDetector;
 
-
         mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null;
-        int[] faceAuthLocation = context.getResources().getIntArray(
-                com.android.systemui.res.R.array.config_face_auth_props);
-        if (faceAuthLocation == null || faceAuthLocation.length < 2) {
-            mFaceSensorLocationDefault = null;
-        } else {
-            mFaceSensorLocationDefault = new Point(
-                    faceAuthLocation[0],
-                    faceAuthLocation[1]);
-        }
 
         mDisplay = mContext.getDisplay();
         updateSensorLocations();
@@ -868,7 +828,8 @@
                     mCachedDisplayInfo.getNaturalWidth(),
                     mCachedDisplayInfo.getNaturalHeight(),
                     mScaleFactor,
-                    mCachedDisplayInfo.rotation);
+                    mCachedDisplayInfo.rotation,
+                    udfpsProp.sensorType);
 
             mUdfpsController.updateOverlayParams(udfpsProp, mUdfpsOverlayParams);
             if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds) || !Objects.equals(
@@ -1166,6 +1127,9 @@
         }
 
         mCurrentDialog.dismissFromSystemServer();
+        for (Callback cb : mCallbacks) {
+            cb.onBiometricPromptDismissed();
+        }
 
         // BiometricService will have already sent the callback to the client in this case.
         // This avoids a round trip to SystemUI. So, just dismiss the dialog and we're done.
@@ -1358,8 +1322,6 @@
         final AuthDialog dialog = mCurrentDialog;
         pw.println("  mCachedDisplayInfo=" + mCachedDisplayInfo);
         pw.println("  mScaleFactor=" + mScaleFactor);
-        pw.println("  faceAuthSensorLocationDefault=" + mFaceSensorLocationDefault);
-        pw.println("  faceAuthSensorLocation=" + getFaceSensorLocation());
         pw.println("  fingerprintSensorLocationInNaturalOrientation="
                 + getFingerprintSensorLocationInNaturalOrientation());
         pw.println("  fingerprintSensorLocation=" + getFingerprintSensorLocation());
@@ -1433,11 +1395,5 @@
          * {@link #onFingerprintLocationChanged}.
          */
         default void onUdfpsLocationChanged(UdfpsOverlayParams udfpsOverlayParams) {}
-
-        /**
-         * Called when the location of the face unlock sensor (typically the front facing camera)
-         * changes. The location in pixels can change due to resolution changes.
-         */
-        default void onFaceSensorLocationChanged() {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 45967c6..86f372a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -32,6 +32,7 @@
 import com.android.settingslib.Utils
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.lightRevealMigration
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
@@ -80,6 +81,7 @@
     private val logger: KeyguardLogger,
     private val biometricUnlockController: BiometricUnlockController,
     private val lightRevealScrim: LightRevealScrim,
+    private val facePropertyRepository: FacePropertyRepository,
     rippleView: AuthRippleView?
 ) :
     ViewController<AuthRippleView>(rippleView),
@@ -263,7 +265,7 @@
 
     fun updateSensorLocation() {
         fingerprintSensorLocation = authController.fingerprintSensorLocation
-        faceSensorLocation = authController.faceSensorLocation
+        faceSensorLocation = facePropertyRepository.sensorLocation.value
     }
 
     private fun updateRippleColor() {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
index b0143f5..aaccbc1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
@@ -22,6 +22,7 @@
 import android.hardware.display.DisplayManager.DisplayListener
 import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
 import android.os.Handler
+import android.util.Size
 import android.view.DisplayInfo
 import com.android.internal.util.ArrayUtils
 import com.android.systemui.biometrics.shared.model.DisplayRotation
@@ -40,6 +41,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /** Repository for the current state of the display */
@@ -58,6 +60,9 @@
 
     /** Provides the current display rotation */
     val currentRotation: StateFlow<DisplayRotation>
+
+    /** Provides the current display size */
+    val currentDisplaySize: StateFlow<Size>
 }
 
 // TODO(b/296211844): This class could directly use DeviceStateRepository and DisplayRepository
@@ -110,17 +115,13 @@
                 initialValue = false,
             )
 
-    private fun getDisplayRotation(): DisplayRotation {
+    private fun getDisplayInfo(): DisplayInfo {
         val cachedDisplayInfo = DisplayInfo()
         context.display?.getDisplayInfo(cachedDisplayInfo)
-        var rotation = cachedDisplayInfo.rotation
-        if (isReverseDefaultRotation) {
-            rotation = (rotation + 1) % 4
-        }
-        return rotation.toDisplayRotation()
+        return cachedDisplayInfo
     }
 
-    override val currentRotation: StateFlow<DisplayRotation> =
+    private val currentDisplayInfo: StateFlow<DisplayInfo> =
         conflatedCallbackFlow {
                 val callback =
                     object : DisplayListener {
@@ -129,11 +130,11 @@
                         override fun onDisplayAdded(displayId: Int) {}
 
                         override fun onDisplayChanged(displayId: Int) {
-                            val rotation = getDisplayRotation()
+                            val displayInfo = getDisplayInfo()
                             trySendWithFailureLogging(
-                                rotation,
+                                displayInfo,
                                 TAG,
-                                "Error sending display rotation to $rotation"
+                                "Error sending displayInfo to $displayInfo"
                             )
                         }
                     }
@@ -148,7 +149,37 @@
             .stateIn(
                 applicationScope,
                 started = SharingStarted.Eagerly,
-                initialValue = getDisplayRotation(),
+                initialValue = getDisplayInfo(),
+            )
+
+    private fun rotationToDisplayRotation(rotation: Int): DisplayRotation {
+        var adjustedRotation = rotation
+        if (isReverseDefaultRotation) {
+            adjustedRotation = (rotation + 1) % 4
+        }
+        return adjustedRotation.toDisplayRotation()
+    }
+
+    override val currentRotation: StateFlow<DisplayRotation> =
+        currentDisplayInfo
+            .map { rotationToDisplayRotation(it.rotation) }
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = rotationToDisplayRotation(currentDisplayInfo.value.rotation)
+            )
+
+    override val currentDisplaySize: StateFlow<Size> =
+        currentDisplayInfo
+            .map { Size(it.naturalWidth, it.naturalHeight) }
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue =
+                    Size(
+                        currentDisplayInfo.value.naturalWidth,
+                        currentDisplayInfo.value.naturalHeight
+                    ),
             )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
index 0ae2e16..ae1539e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
@@ -17,25 +17,39 @@
 
 package com.android.systemui.biometrics.data.repository
 
+import android.content.Context
+import android.graphics.Point
+import android.hardware.camera2.CameraManager
 import android.hardware.face.FaceManager
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
 import android.util.Log
+import android.util.RotationUtils
+import android.util.Size
+import com.android.systemui.biometrics.shared.model.DisplayRotation
 import com.android.systemui.biometrics.shared.model.LockoutMode
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.biometrics.shared.model.toLockoutMode
+import com.android.systemui.biometrics.shared.model.toRotation
 import com.android.systemui.biometrics.shared.model.toSensorStrength
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 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.res.R
+import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
@@ -47,20 +61,38 @@
 
     /** Get the current lockout mode for the user. This makes a binder based service call. */
     suspend fun getLockoutMode(userId: Int): LockoutMode
+
+    /** The current face sensor location in current device rotation */
+    val sensorLocation: StateFlow<Point?>
 }
 
 /** Describes a biometric sensor */
 data class FaceSensorInfo(val id: Int, val strength: SensorStrength)
 
+/** Data class for camera info */
+private data class CameraInfo(
+    /** The logical id of the camera */
+    val cameraId: String,
+    /** The physical id of the camera */
+    val cameraPhysicalId: String?,
+    /** The center point of the camera in natural orientation */
+    val cameraLocation: Point?,
+)
+
 private const val TAG = "FaceSensorPropertyRepositoryImpl"
 
 @SysUISingleton
 class FacePropertyRepositoryImpl
 @Inject
 constructor(
+    @Application val applicationContext: Context,
+    @Main mainExecutor: Executor,
     @Application private val applicationScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val faceManager: FaceManager?,
+    private val cameraManager: CameraManager,
+    displayStateRepository: DisplayStateRepository,
+    configurationRepository: ConfigurationRepository,
 ) : FacePropertyRepository {
 
     override val sensorInfo: StateFlow<FaceSensorInfo?> =
@@ -89,10 +121,179 @@
             .onEach { Log.d(TAG, "sensorProps changed: $it") }
             .stateIn(applicationScope, SharingStarted.Eagerly, null)
 
+    private val cameraInfoList: List<CameraInfo> = loadCameraInfoList()
+    private var currentPhysicalCameraId: String? = null
+
+    private val defaultSensorLocation: StateFlow<Point?> =
+        ConflatedCallbackFlow.conflatedCallbackFlow {
+                val callback =
+                    object : CameraManager.AvailabilityCallback() {
+
+                        // This callback will only be called when there is more than one front
+                        // camera on the device (e.g. foldable device with cameras on both outer &
+                        // inner display).
+                        override fun onPhysicalCameraAvailable(
+                            cameraId: String,
+                            physicalCameraId: String
+                        ) {
+                            currentPhysicalCameraId = physicalCameraId
+                            val cameraInfo =
+                                cameraInfoList.firstOrNull {
+                                    physicalCameraId == it.cameraPhysicalId
+                                }
+                            trySendWithFailureLogging(
+                                cameraInfo?.cameraLocation,
+                                TAG,
+                                "Update face sensor location to $cameraInfo."
+                            )
+                        }
+
+                        // This callback will only be called when there is more than one front
+                        // camera on the device (e.g. foldable device with cameras on both outer &
+                        // inner display).
+                        //
+                        // By default, all cameras are available which means there will be no
+                        // onPhysicalCameraAvailable() invoked and depending on the device state
+                        // (Fold or unfold), only the onPhysicalCameraUnavailable() for another
+                        // camera will be invoke. So we need to use this method to decide the
+                        // initial physical ID for foldable devices.
+                        override fun onPhysicalCameraUnavailable(
+                            cameraId: String,
+                            physicalCameraId: String
+                        ) {
+                            if (currentPhysicalCameraId == null) {
+                                val cameraInfo =
+                                    cameraInfoList.firstOrNull {
+                                        physicalCameraId != it.cameraPhysicalId
+                                    }
+                                currentPhysicalCameraId = cameraInfo?.cameraPhysicalId
+                                trySendWithFailureLogging(
+                                    cameraInfo?.cameraLocation,
+                                    TAG,
+                                    "Update face sensor location to $cameraInfo."
+                                )
+                            }
+                        }
+                    }
+                cameraManager.registerAvailabilityCallback(mainExecutor, callback)
+                awaitClose { cameraManager.unregisterAvailabilityCallback(callback) }
+            }
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue =
+                    if (cameraInfoList.isNotEmpty()) cameraInfoList[0].cameraLocation else null
+            )
+
+    override val sensorLocation: StateFlow<Point?> =
+        sensorInfo
+            .flatMapLatest { info ->
+                if (info == null) {
+                    flowOf(null)
+                } else {
+                    combine(
+                        defaultSensorLocation,
+                        displayStateRepository.currentRotation,
+                        displayStateRepository.currentDisplaySize,
+                        configurationRepository.scaleForResolution
+                    ) { defaultLocation, displayRotation, displaySize, scaleForResolution ->
+                        computeCurrentFaceLocation(
+                            defaultLocation,
+                            displayRotation,
+                            displaySize,
+                            scaleForResolution
+                        )
+                    }
+                }
+            }
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = null
+            )
+
+    private fun computeCurrentFaceLocation(
+        defaultLocation: Point?,
+        rotation: DisplayRotation,
+        displaySize: Size,
+        scaleForResolution: Float,
+    ): Point? {
+        if (defaultLocation == null) {
+            return null
+        }
+
+        return rotateToCurrentOrientation(
+            Point(
+                (defaultLocation.x * scaleForResolution).toInt(),
+                (defaultLocation.y * scaleForResolution).toInt()
+            ),
+            rotation,
+            displaySize
+        )
+    }
+
+    private fun rotateToCurrentOrientation(
+        inOutPoint: Point,
+        rotation: DisplayRotation,
+        displaySize: Size
+    ): Point {
+        RotationUtils.rotatePoint(
+            inOutPoint,
+            rotation.toRotation(),
+            displaySize.width,
+            displaySize.height
+        )
+        return inOutPoint
+    }
     override suspend fun getLockoutMode(userId: Int): LockoutMode {
         if (sensorInfo.value == null || faceManager == null) {
             return LockoutMode.NONE
         }
         return faceManager.getLockoutModeForUser(sensorInfo.value!!.id, userId).toLockoutMode()
     }
+
+    private fun loadCameraInfoList(): List<CameraInfo> {
+        val list = mutableListOf<CameraInfo>()
+
+        val outer =
+            loadCameraInfo(
+                R.string.config_protectedCameraId,
+                R.string.config_protectedPhysicalCameraId,
+                R.array.config_face_auth_props
+            )
+        if (outer != null) {
+            list.add(outer)
+        }
+
+        val inner =
+            loadCameraInfo(
+                R.string.config_protectedInnerCameraId,
+                R.string.config_protectedInnerPhysicalCameraId,
+                R.array.config_inner_face_auth_props
+            )
+        if (inner != null) {
+            list.add(inner)
+        }
+        return list
+    }
+
+    private fun loadCameraInfo(
+        cameraIdRes: Int,
+        cameraPhysicalIdRes: Int,
+        cameraLocationRes: Int
+    ): CameraInfo? {
+        val cameraId = applicationContext.getString(cameraIdRes)
+        if (cameraId.isNullOrEmpty()) {
+            return null
+        }
+        val physicalCameraId = applicationContext.getString(cameraPhysicalIdRes)
+        val cameraLocation: IntArray = applicationContext.resources.getIntArray(cameraLocationRes)
+        val location: Point?
+        if (cameraLocation.size < 2) {
+            location = null
+        } else {
+            location = Point(cameraLocation[0], cameraLocation[1])
+        }
+        return CameraInfo(cameraId, physicalCameraId, location)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
new file mode 100644
index 0000000..cf2e33c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.data.model
+
+/** Data model of media on the communal hub. */
+data class CommunalMediaModel(
+    val hasAnyMediaOrRecommendation: Boolean,
+    val createdTimestampMillis: Long = 0L,
+) {
+    companion object {
+        val INACTIVE =
+            CommunalMediaModel(
+                hasAnyMediaOrRecommendation = false,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
index e41c322..e8a561b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
@@ -16,18 +16,17 @@
 
 package com.android.systemui.communal.data.repository
 
+import com.android.systemui.communal.data.model.CommunalMediaModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onStart
 
 /** Encapsulates the state of smartspace in communal. */
 interface CommunalMediaRepository {
-    val mediaPlaying: Flow<Boolean>
+    val mediaModel: Flow<CommunalMediaModel>
 }
 
 @SysUISingleton
@@ -47,27 +46,32 @@
                 receivedSmartspaceCardLatency: Int,
                 isSsReactivated: Boolean
             ) {
-                if (!mediaDataManager.hasAnyMediaOrRecommendation()) {
-                    return
-                }
-                _mediaPlaying.value = true
+                updateMediaModel(data)
             }
 
             override fun onMediaDataRemoved(key: String) {
-                if (mediaDataManager.hasAnyMediaOrRecommendation()) {
-                    return
-                }
-                _mediaPlaying.value = false
+                updateMediaModel()
             }
         }
 
-    private val _mediaPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    init {
+        mediaDataManager.addListener(mediaDataListener)
+    }
 
-    override val mediaPlaying: Flow<Boolean> =
-        _mediaPlaying
-            .onStart {
-                mediaDataManager.addListener(mediaDataListener)
-                _mediaPlaying.value = mediaDataManager.hasAnyMediaOrRecommendation()
-            }
-            .onCompletion { mediaDataManager.removeListener(mediaDataListener) }
+    private val _mediaModel: MutableStateFlow<CommunalMediaModel> =
+        MutableStateFlow(CommunalMediaModel.INACTIVE)
+
+    override val mediaModel: Flow<CommunalMediaModel> = _mediaModel
+
+    private fun updateMediaModel(data: MediaData? = null) {
+        if (mediaDataManager.hasAnyMediaOrRecommendation()) {
+            _mediaModel.value =
+                CommunalMediaModel(
+                    hasAnyMediaOrRecommendation = true,
+                    createdTimestampMillis = data?.createdTimestampMillis ?: 0L,
+                )
+        } else {
+            _mediaModel.value = CommunalMediaModel.INACTIVE
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
index 1f4be40..553b3eb 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
@@ -56,6 +56,9 @@
     /** Exposes the transition state of the communal [SceneTransitionLayout]. */
     val transitionState: StateFlow<ObservableCommunalTransitionState>
 
+    /** Whether the CTA tile is visible in the hub under view mode. */
+    val isCtaTileInViewModeVisible: Flow<Boolean>
+
     /** Updates the requested scene. */
     fun setDesiredScene(desiredScene: CommunalSceneKey)
 
@@ -65,6 +68,9 @@
      * Note that you must call is with `null` when the UI is done or risk a memory leak.
      */
     fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?)
+
+    /** Updates whether to display the CTA tile in the hub under view mode. */
+    fun setCtaTileInViewModeVisibility(isVisible: Boolean)
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -96,6 +102,16 @@
                 initialValue = defaultTransitionState,
             )
 
+    // TODO(b/313462210) - persist the value in local storage, so the tile won't show up again
+    //  once dismissed.
+    private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true)
+    override val isCtaTileInViewModeVisible: Flow<Boolean> =
+        _isCtaTileInViewModeVisible.asStateFlow()
+
+    override fun setCtaTileInViewModeVisibility(isVisible: Boolean) {
+        _isCtaTileInViewModeVisible.value = isVisible
+    }
+
     override fun setDesiredScene(desiredScene: CommunalSceneKey) {
         _desiredScene.value = desiredScene
     }
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 cab8adf..e6816e9 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
@@ -18,14 +18,12 @@
 
 import android.appwidget.AppWidgetHost
 import android.appwidget.AppWidgetManager
-import android.content.BroadcastReceiver
 import android.content.ComponentName
-import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.os.UserManager
+import androidx.annotation.WorkerThread
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.communal.data.db.CommunalItemRank
 import com.android.systemui.communal.data.db.CommunalWidgetDao
 import com.android.systemui.communal.data.db.CommunalWidgetItem
@@ -40,17 +38,21 @@
 import com.android.systemui.settings.UserTracker
 import java.util.Optional
 import javax.inject.Inject
+import kotlin.coroutines.cancellation.CancellationException
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /** Encapsulates the state of widgets for communal mode. */
 interface CommunalWidgetRepository {
@@ -58,7 +60,11 @@
     val communalWidgets: Flow<List<CommunalWidgetContentModel>>
 
     /** Add a widget at the specified position in the app widget service and the database. */
-    fun addWidget(provider: ComponentName, priority: Int) {}
+    fun addWidget(
+        provider: ComponentName,
+        priority: Int,
+        configureWidget: suspend (id: Int) -> Boolean
+    ) {}
 
     /** Delete a widget by id from app widget service and the database. */
     fun deleteWidget(widgetId: Int) {}
@@ -97,37 +103,22 @@
     // Whether the [AppWidgetHost] is listening for updates.
     private var isHostListening = false
 
+    private suspend fun isUserUnlockingOrUnlocked(): Boolean =
+        withContext(bgDispatcher) { userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) }
+
     private val isUserUnlocked: Flow<Boolean> =
-        callbackFlow {
-                if (!communalRepository.isCommunalEnabled) {
-                    awaitClose()
-                }
-
-                fun isUserUnlockingOrUnlocked(): Boolean {
-                    return userManager.isUserUnlockingOrUnlocked(userTracker.userHandle)
-                }
-
-                fun send() {
-                    trySendWithFailureLogging(isUserUnlockingOrUnlocked(), TAG)
-                }
-
-                if (isUserUnlockingOrUnlocked()) {
-                    send()
-                    awaitClose()
+        flowOf(communalRepository.isCommunalEnabled)
+            .flatMapLatest { enabled ->
+                if (enabled) {
+                    broadcastDispatcher
+                        .broadcastFlow(
+                            filter = IntentFilter(Intent.ACTION_USER_UNLOCKED),
+                            user = userTracker.userHandle
+                        )
+                        .onStart { emit(Unit) }
+                        .mapLatest { isUserUnlockingOrUnlocked() }
                 } else {
-                    val receiver =
-                        object : BroadcastReceiver() {
-                            override fun onReceive(context: Context?, intent: Intent?) {
-                                send()
-                            }
-                        }
-
-                    broadcastDispatcher.registerReceiver(
-                        receiver,
-                        IntentFilter(Intent.ACTION_USER_UNLOCKED),
-                    )
-
-                    awaitClose { broadcastDispatcher.unregisterReceiver(receiver) }
+                    emptyFlow()
                 }
             }
             .distinctUntilChanged()
@@ -148,18 +139,52 @@
             if (!isHostActive || !appWidgetManager.isPresent) {
                 return@flatMapLatest flowOf(emptyList())
             }
-            communalWidgetDao.getWidgets().map { it.map(::mapToContentModel) }
+            communalWidgetDao
+                .getWidgets()
+                .map { it.map(::mapToContentModel) }
+                // As this reads from a database and triggers IPCs to AppWidgetManager,
+                // it should be executed in the background.
+                .flowOn(bgDispatcher)
         }
 
-    override fun addWidget(provider: ComponentName, priority: Int) {
+    override fun addWidget(
+        provider: ComponentName,
+        priority: Int,
+        configureWidget: suspend (id: Int) -> Boolean
+    ) {
         applicationScope.launch(bgDispatcher) {
             val id = communalWidgetHost.allocateIdAndBindWidget(provider)
-            id?.let {
-                communalWidgetDao.addWidget(
-                    widgetId = it,
-                    provider = provider,
-                    priority = priority,
-                )
+            if (id != null) {
+                val configured =
+                    if (communalWidgetHost.requiresConfiguration(id)) {
+                        logger.i("Widget ${provider.flattenToString()} requires configuration.")
+                        try {
+                            configureWidget.invoke(id)
+                        } catch (ex: Exception) {
+                            // Cleanup the app widget id if an error happens during configuration.
+                            logger.e("Error during widget configuration, cleaning up id $id", ex)
+                            if (ex is CancellationException) {
+                                appWidgetHost.deleteAppWidgetId(id)
+                                // Re-throw cancellation to ensure the parent coroutine also gets
+                                // cancelled.
+                                throw ex
+                            } else {
+                                false
+                            }
+                        }
+                    } else {
+                        logger.i("Skipping configuration for ${provider.flattenToString()}")
+                        true
+                    }
+                if (configured) {
+                    communalWidgetDao.addWidget(
+                        widgetId = id,
+                        provider = provider,
+                        priority = priority,
+                    )
+                } else {
+                    appWidgetHost.deleteAppWidgetId(id)
+                }
             }
             logger.i("Added widget ${provider.flattenToString()} at position $priority.")
         }
@@ -182,6 +207,7 @@
         }
     }
 
+    @WorkerThread
     private fun mapToContentModel(
         entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
     ): CommunalWidgetContentModel {
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 18fb895..24d4c6c 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
@@ -34,15 +34,13 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.smartspace.data.repository.SmartspaceRepository
 import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 /** Encapsulates business-logic related to communal mode. */
-@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class CommunalInteractor
 @Inject
@@ -98,9 +96,20 @@
         editWidgetsActivityStarter.startActivity()
     }
 
-    /** Add a widget at the specified position. */
-    fun addWidget(componentName: ComponentName, priority: Int) =
-        widgetRepository.addWidget(componentName, priority)
+    /** Dismiss the CTA tile from the hub in view mode. */
+    fun dismissCtaTile() = communalRepository.setCtaTileInViewModeVisibility(isVisible = false)
+
+    /**
+     * Add a widget at the specified position.
+     *
+     * @param configureWidget The callback to trigger if widget configuration is needed. Should
+     *   return whether configuration was successful.
+     */
+    fun addWidget(
+        componentName: ComponentName,
+        priority: Int,
+        configureWidget: suspend (id: Int) -> Boolean
+    ) = widgetRepository.addWidget(componentName, priority, configureWidget)
 
     /** Delete a widget by id. */
     fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
@@ -125,27 +134,25 @@
             }
         }
 
-    /** A flow of available smartspace content. Currently only showing timer targets. */
-    val smartspaceContent: Flow<List<CommunalContentModel.Smartspace>> =
+    /** A flow of available smartspace targets. Currently only showing timers. */
+    private val smartspaceTargets: Flow<List<SmartspaceTarget>> =
         if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) {
             flowOf(emptyList())
         } else {
             smartspaceRepository.communalSmartspaceTargets.map { targets ->
-                targets
-                    .filter { target ->
-                        target.featureType == SmartspaceTarget.FEATURE_TIMER &&
-                            target.remoteViews != null
-                    }
-                    .mapIndexed Target@{ index, target ->
-                        return@Target CommunalContentModel.Smartspace(
-                            smartspaceTargetId = target.smartspaceTargetId,
-                            remoteViews = target.remoteViews!!,
-                            size = dynamicContentSize(targets.size, index),
-                        )
-                    }
+                targets.filter { target ->
+                    target.featureType == SmartspaceTarget.FEATURE_TIMER &&
+                        target.remoteViews != null
+                }
             }
         }
 
+    /** CTA tile to be displayed in the glanceable hub (view mode). */
+    val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> =
+        communalRepository.isCtaTileInViewModeVisible.map { visible ->
+            if (visible) listOf(CommunalContentModel.CtaTileInViewMode()) else emptyList()
+        }
+
     /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */
     val tutorialContent: List<CommunalContentModel.Tutorial> =
         listOf(
@@ -159,14 +166,43 @@
             CommunalContentModel.Tutorial(id = 7, HALF),
         )
 
-    val umoContent: Flow<List<CommunalContentModel.Umo>> =
-        mediaRepository.mediaPlaying.flatMapLatest { mediaPlaying ->
-            if (mediaPlaying) {
-                // TODO(b/310254801): support HALF and FULL layouts
-                flowOf(listOf(CommunalContentModel.Umo(THIRD)))
-            } else {
-                flowOf(emptyList())
+    /**
+     * A flow of ongoing content, including smartspace timers and umo, ordered by creation time and
+     * sized dynamically.
+     */
+    val ongoingContent: Flow<List<CommunalContentModel.Ongoing>> =
+        combine(smartspaceTargets, mediaRepository.mediaModel) { smartspace, media ->
+            val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>()
+
+            // Add smartspace
+            ongoingContent.addAll(
+                smartspace.map { target ->
+                    CommunalContentModel.Smartspace(
+                        smartspaceTargetId = target.smartspaceTargetId,
+                        remoteViews = target.remoteViews!!,
+                        createdTimestampMillis = target.creationTimeMillis,
+                    )
+                }
+            )
+
+            // Add UMO
+            if (media.hasAnyMediaOrRecommendation) {
+                ongoingContent.add(
+                    CommunalContentModel.Umo(
+                        createdTimestampMillis = media.createdTimestampMillis,
+                    )
+                )
             }
+
+            // Order by creation time descending
+            ongoingContent.sortByDescending { it.createdTimestampMillis }
+
+            // Dynamic sizing
+            ongoingContent.forEachIndexed { index, model ->
+                model.size = dynamicContentSize(ongoingContent.size, index)
+            }
+
+            return@combine ongoingContent
         }
 
     companion object {
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 3ae5229..46f957f 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
@@ -30,46 +30,95 @@
     /** Size to be rendered in the grid. */
     val size: CommunalContentSize
 
+    /**
+     * A type of communal content is ongoing / live / ephemeral, and can be sized and ordered
+     * dynamically.
+     */
+    sealed interface Ongoing : CommunalContentModel {
+        override var size: CommunalContentSize
+
+        /** Timestamp in milliseconds of when the content was created. */
+        val createdTimestampMillis: Long
+    }
+
     class Widget(
         val appWidgetId: Int,
         val providerInfo: AppWidgetProviderInfo,
         val appWidgetHost: AppWidgetHost,
     ) : CommunalContentModel {
-        override val key = "widget_$appWidgetId"
+        override val key = KEY.widget(appWidgetId)
         // Widget size is always half.
         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()}"
+        override val key: String = KEY.widgetPlaceholder()
+        // Same as widget size.
+        override val size = CommunalContentSize.HALF
+    }
+
+    /** A CTA tile in the glanceable hub view mode which can be dismissed. */
+    class CtaTileInViewMode : CommunalContentModel {
+        override val key: String = KEY.CTA_TILE_IN_VIEW_MODE_KEY
+        // Same as widget size.
+        override val size = CommunalContentSize.HALF
+    }
+
+    /** A CTA tile in the glanceable hub edit model which remains visible in the grid. */
+    class CtaTileInEditMode : CommunalContentModel {
+        override val key: String = KEY.CTA_TILE_IN_EDIT_MODE_KEY
         // Same as widget size.
         override val size = CommunalContentSize.HALF
     }
 
     class Tutorial(
         id: Int,
-        override val size: CommunalContentSize,
+        override var size: CommunalContentSize,
     ) : CommunalContentModel {
-        override val key = "tutorial_$id"
+        override val key = KEY.tutorial(id)
     }
 
     class Smartspace(
         smartspaceTargetId: String,
         val remoteViews: RemoteViews,
-        override val size: CommunalContentSize,
-    ) : CommunalContentModel {
-        override val key = "smartspace_$smartspaceTargetId"
+        override val createdTimestampMillis: Long,
+        override var size: CommunalContentSize = CommunalContentSize.HALF,
+    ) : Ongoing {
+        override val key = KEY.smartspace(smartspaceTargetId)
     }
 
     class Umo(
-        override val size: CommunalContentSize,
-    ) : CommunalContentModel {
-        override val key = UMO_KEY
+        override val createdTimestampMillis: Long,
+        override var size: CommunalContentSize = CommunalContentSize.HALF,
+    ) : Ongoing {
+        override val key = KEY.umo()
     }
 
-    companion object {
-        /** Key for the [Umo] in CommunalContentModel. There should only ever be one UMO. */
-        const val UMO_KEY = "umo"
+    class KEY {
+        companion object {
+            const val CTA_TILE_IN_VIEW_MODE_KEY = "cta_tile_in_view_mode"
+            const val CTA_TILE_IN_EDIT_MODE_KEY = "cta_tile_in_edit_mode"
+
+            fun widget(id: Int): String {
+                return "widget_$id"
+            }
+
+            fun widgetPlaceholder(): String {
+                return "widget_placeholder_${UUID.randomUUID()}"
+            }
+
+            fun tutorial(id: Int): String {
+                return "tutorial_$id"
+            }
+
+            fun smartspace(id: String): String {
+                return "smartspace_$id"
+            }
+
+            fun umo(): String {
+                return "umo"
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt
new file mode 100644
index 0000000..889023e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.log
+
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.kotlin.pairwise
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/** A [CoreStartable] responsible for logging metrics for the communal hub. */
+@SysUISingleton
+class CommunalLoggerStartable
+@Inject
+constructor(
+    @Background private val backgroundScope: CoroutineScope,
+    private val communalInteractor: CommunalInteractor,
+    private val uiEventLogger: UiEventLogger,
+) : CoreStartable {
+
+    override fun start() {
+        communalInteractor.transitionState
+            .map { state ->
+                when {
+                    state.isOnCommunal() -> CommunalUiEvent.COMMUNAL_HUB_SHOWN
+                    state.isNotOnCommunal() -> CommunalUiEvent.COMMUNAL_HUB_GONE
+                    else -> null
+                }
+            }
+            .filterNotNull()
+            .distinctUntilChanged()
+            // Drop the default value.
+            .drop(1)
+            .onEach { uiEvent -> uiEventLogger.log(uiEvent) }
+            .launchIn(backgroundScope)
+
+        communalInteractor.transitionState
+            .pairwise()
+            .map { (old, new) ->
+                when {
+                    new.isOnCommunal() && old.isSwipingToCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH
+                    new.isOnCommunal() && old.isSwipingFromCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL
+                    new.isNotOnCommunal() && old.isSwipingFromCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH
+                    new.isNotOnCommunal() && old.isSwipingToCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL
+                    new.isSwipingToCommunal() && old.isNotOnCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START
+                    new.isSwipingFromCommunal() && old.isOnCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START
+                    else -> null
+                }
+            }
+            .filterNotNull()
+            .distinctUntilChanged()
+            .onEach { uiEvent -> uiEventLogger.log(uiEvent) }
+            .launchIn(backgroundScope)
+    }
+}
+
+/** Whether currently in communal scene. */
+private fun ObservableCommunalTransitionState.isOnCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Idle && scene == CommunalSceneKey.Communal
+}
+
+/** Whether currently in a scene other than communal. */
+private fun ObservableCommunalTransitionState.isNotOnCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Idle && scene != CommunalSceneKey.Communal
+}
+
+/** Whether currently transitioning from another scene to communal. */
+private fun ObservableCommunalTransitionState.isSwipingToCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Transition &&
+        toScene == CommunalSceneKey.Communal &&
+        isInitiatedByUserInput
+}
+
+/** Whether currently transitioning from communal to another scene. */
+private fun ObservableCommunalTransitionState.isSwipingFromCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Transition &&
+        fromScene == CommunalSceneKey.Communal &&
+        isInitiatedByUserInput
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
index 155de32..41f9cb4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
@@ -18,6 +18,8 @@
 
 import android.appwidget.AppWidgetHost
 import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
+import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
 import android.content.ComponentName
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
@@ -63,4 +65,23 @@
         }
         return false
     }
+
+    /**
+     * Returns whether a particular widget requires configuration when it is first added.
+     *
+     * Must be called after the widget id has been bound.
+     */
+    fun requiresConfiguration(widgetId: Int): Boolean {
+        if (appWidgetManager.isPresent) {
+            val widgetInfo = appWidgetManager.get().getAppWidgetInfo(widgetId)
+            val featureFlags: Int = widgetInfo.widgetFeatures
+            // A widget's configuration is optional only if it's configuration is marked as optional
+            // AND it can be reconfigured later.
+            val configurationOptional =
+                (featureFlags and WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0 &&
+                    featureFlags and WIDGET_FEATURE_RECONFIGURABLE != 0)
+            return widgetInfo.configure != null && !configurationOptional
+        }
+        return false
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
new file mode 100644
index 0000000..b64c195
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.shared.log
+
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger.UiEventEnum
+
+/** UI events for the Communal Hub. */
+enum class CommunalUiEvent(private val id: Int) : UiEventEnum {
+    @UiEvent(doc = "Communal Hub is fully shown") COMMUNAL_HUB_SHOWN(1566),
+    @UiEvent(doc = "Communal Hub is fully gone") COMMUNAL_HUB_GONE(1577),
+    @UiEvent(doc = "Communal Hub times out") COMMUNAL_HUB_TIMEOUT(1578),
+    @UiEvent(doc = "The visible content in the Communal Hub is fully loaded and rendered")
+    COMMUNAL_HUB_LOADED(1579),
+    @UiEvent(doc = "User starts the swipe gesture to enter the Communal Hub")
+    COMMUNAL_HUB_SWIPE_TO_ENTER_START(1580),
+    @UiEvent(doc = "User finishes the swipe gesture to enter the Communal Hub")
+    COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH(1581),
+    @UiEvent(doc = "User cancels the swipe gesture to enter the Communal Hub")
+    COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL(1582),
+    @UiEvent(doc = "User starts the swipe gesture to exit the Communal Hub")
+    COMMUNAL_HUB_SWIPE_TO_EXIT_START(1583),
+    @UiEvent(doc = "User finishes the swipe gesture to exit the Communal Hub")
+    COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH(1584),
+    @UiEvent(doc = "User cancels the swipe gesture to exit the Communal Hub")
+    COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL(1585),
+    @UiEvent(doc = "User starts the drag gesture to reorder a widget")
+    COMMUNAL_HUB_REORDER_WIDGET_START(1586),
+    @UiEvent(doc = "User finishes the drag gesture to reorder a widget")
+    COMMUNAL_HUB_REORDER_WIDGET_FINISH(1587),
+    @UiEvent(doc = "User cancels the drag gesture to reorder a widget")
+    COMMUNAL_HUB_REORDER_WIDGET_CANCEL(1588),
+    @UiEvent(doc = "Edit mode for the Communal Hub is shown") COMMUNAL_HUB_EDIT_MODE_SHOWN(1569),
+    @UiEvent(doc = "Edit mode for the Communal Hub is gone") COMMUNAL_HUB_EDIT_MODE_GONE(1589),
+    @UiEvent(doc = "Widget picker for the Communal Hub is shown")
+    COMMUNAL_HUB_WIDGET_PICKER_SHOWN(1590),
+    @UiEvent(doc = "Widget picker for the Communal Hub is gone")
+    COMMUNAL_HUB_WIDGET_PICKER_GONE(1591),
+    @UiEvent(doc = "User performs a swipe up gesture from bottom to enter bouncer")
+    COMMUNAL_HUB_SWIPE_UP_TO_BOUNCER(1573),
+    @UiEvent(doc = "User performs a swipe down gesture from top to enter shade")
+    COMMUNAL_HUB_SWIPE_DOWN_TO_SHADE(1574);
+
+    override fun getId(): Int {
+        return id
+    }
+}
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 577e404..84708a4 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
@@ -17,24 +17,19 @@
 package com.android.systemui.communal.ui.viewmodel
 
 import android.content.ComponentName
-import android.os.PowerManager
-import android.os.SystemClock
-import android.view.MotionEvent
+import android.widget.RemoteViews
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.media.controls.ui.MediaHost
-import com.android.systemui.shade.ShadeViewController
-import javax.inject.Provider
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
 
 /** The base view model for the communal hub. */
 abstract class BaseCommunalViewModel(
     private val communalInteractor: CommunalInteractor,
-    private val shadeViewController: Provider<ShadeViewController>,
-    private val powerManager: PowerManager,
     val mediaHost: MediaHost,
 ) {
     val isKeyguardVisible: Flow<Boolean> = communalInteractor.isKeyguardVisible
@@ -57,28 +52,16 @@
     /**
      * 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)
+    open fun onAddWidget(componentName: ComponentName, priority: Int) {
+        communalInteractor.addWidget(componentName, priority, ::configureWidget)
     }
 
-    // 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. */
-    fun onOuterTouch(motionEvent: MotionEvent) {
-        // Forward the touch to the shade so that basic gestures like swipe up/down for
-        // shade/bouncer work.
-        shadeViewController.get().handleExternalTouch(motionEvent)
-    }
-
-    // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't block
-    //  touches anymore.
-    /** Called to refresh the screen timeout when a user touch is received. */
-    fun onUserActivity() {
-        powerManager.userActivity(
-            SystemClock.uptimeMillis(),
-            PowerManager.USER_ACTIVITY_EVENT_TOUCH,
-            0
-        )
+    /**
+     * Called when a widget needs to be configured, with the id of the widget. The return value
+     * should represent whether configuring the widget was successful.
+     */
+    protected open suspend fun configureWidget(widgetId: Int): Boolean {
+        return true
     }
 
     /** A list of all the communal content to be displayed in the communal hub. */
@@ -87,6 +70,12 @@
     /** Whether in edit mode for the communal hub. */
     open val isEditMode = false
 
+    /** Whether the popup message triggered by dismissing the CTA tile is showing. */
+    open val isPopupOnDismissCtaShowing: Flow<Boolean> = flowOf(false)
+
+    /** Hide the popup message triggered by dismissing the CTA tile. */
+    open fun onHidePopupAfterDismissCta() {}
+
     /** Called as the UI requests deleting a widget. */
     open fun onDeleteWidget(id: Int) {}
 
@@ -101,4 +90,19 @@
 
     /** Called as the UI requests opening the widget editor. */
     open fun onOpenWidgetEditor() {}
+
+    /** Called as the UI requests to dismiss the CTA tile. */
+    open fun onDismissCtaTile() {}
+
+    /** Gets the interaction handler used to handle taps on a remote view */
+    abstract fun getInteractionHandler(): RemoteViews.InteractionHandler
+
+    /** Called as the user starts dragging a widget to reorder. */
+    open fun onReorderWidgetStart() {}
+
+    /** Called as the user finishes dragging a widget to reorder. */
+    open fun onReorderWidgetEnd() {}
+
+    /** Called as the user cancels dragging a widget to reorder. */
+    open fun onReorderWidgetCancel() {}
 }
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 368df9e..7faf653 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
@@ -16,17 +16,28 @@
 
 package com.android.systemui.communal.ui.viewmodel
 
-import android.os.PowerManager
+import android.app.Activity
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.app.ActivityOptions
+import android.appwidget.AppWidgetHost
+import android.content.ActivityNotFoundException
+import android.content.ComponentName
+import android.widget.RemoteViews
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.media.dagger.MediaModule
-import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.util.nullableAtomicReference
 import javax.inject.Inject
 import javax.inject.Named
-import javax.inject.Provider
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.map
 
 /** The view model for communal hub in edit mode. */
 @SysUISingleton
@@ -34,19 +45,104 @@
 @Inject
 constructor(
     private val communalInteractor: CommunalInteractor,
-    shadeViewController: Provider<ShadeViewController>,
-    powerManager: PowerManager,
+    private val appWidgetHost: AppWidgetHost,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
-) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) {
+    private val uiEventLogger: UiEventLogger,
+) : BaseCommunalViewModel(communalInteractor, mediaHost) {
+
+    private companion object {
+        private const val KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle"
+        private const val SPLASH_SCREEN_STYLE_EMPTY = 0
+    }
+
+    private val _widgetsToConfigure = MutableSharedFlow<Int>()
+
+    /**
+     * Flow emitting ids of widgets which need to be configured. The consumer of this flow should
+     * trigger [startConfigurationActivity] to initiate configuration.
+     */
+    val widgetsToConfigure: Flow<Int> = _widgetsToConfigure
+
+    private var pendingConfiguration: CompletableDeferred<Int>? by nullableAtomicReference()
 
     override val isEditMode = true
 
-    // Only widgets are editable.
+    // Only widgets are editable. The CTA tile comes last in the list and remains visible.
     override val communalContent: Flow<List<CommunalContentModel>> =
-        communalInteractor.widgetContent
+        communalInteractor.widgetContent.map { widgets ->
+            widgets + listOf(CommunalContentModel.CtaTileInEditMode())
+        }
 
     override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
 
     override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) =
         communalInteractor.updateWidgetOrder(widgetIdToPriorityMap)
+
+    override fun getInteractionHandler(): RemoteViews.InteractionHandler {
+        // Ignore all interactions in edit mode.
+        return RemoteViews.InteractionHandler { _, _, _ -> false }
+    }
+
+    override fun onAddWidget(componentName: ComponentName, priority: Int) {
+        if (pendingConfiguration != null) {
+            throw IllegalStateException(
+                "Cannot add $componentName widget while widget configuration is pending"
+            )
+        }
+        super.onAddWidget(componentName, priority)
+    }
+
+    fun startConfigurationActivity(activity: Activity, widgetId: Int, requestCode: Int) {
+        val options =
+            ActivityOptions.makeBasic().apply {
+                setPendingIntentBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+                )
+            }
+        val bundle = options.toBundle()
+        bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY)
+        try {
+            appWidgetHost.startAppWidgetConfigureActivityForResult(
+                activity,
+                widgetId,
+                0,
+                // Use the widget id as the request code.
+                requestCode,
+                bundle
+            )
+        } catch (e: ActivityNotFoundException) {
+            setConfigurationResult(RESULT_CANCELED)
+        }
+    }
+
+    override suspend fun configureWidget(widgetId: Int): Boolean {
+        if (pendingConfiguration != null) {
+            throw IllegalStateException(
+                "Attempting to configure $widgetId while another configuration is already active"
+            )
+        }
+        pendingConfiguration = CompletableDeferred()
+        _widgetsToConfigure.emit(widgetId)
+        val resultCode = pendingConfiguration?.await() ?: RESULT_CANCELED
+        pendingConfiguration = null
+        return resultCode == RESULT_OK
+    }
+
+    /** Sets the result of widget configuration. */
+    fun setConfigurationResult(resultCode: Int) {
+        pendingConfiguration?.complete(resultCode)
+            ?: throw IllegalStateException("No widget pending configuration")
+    }
+
+    override fun onReorderWidgetStart() {
+        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
+    }
+
+    override fun onReorderWidgetEnd() {
+        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH)
+    }
+
+    override fun onReorderWidgetCancel() {
+        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index abf1986..7a96fab 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -16,34 +16,40 @@
 
 package com.android.systemui.communal.ui.viewmodel
 
-import android.os.PowerManager
+import android.widget.RemoteViews
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.media.dagger.MediaModule
-import com.android.systemui.shade.ShadeViewController
 import javax.inject.Inject
 import javax.inject.Named
-import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
 
 /** The default view model used for showing the communal hub. */
 @SysUISingleton
 class CommunalViewModel
 @Inject
 constructor(
+    @Application private val scope: CoroutineScope,
     private val communalInteractor: CommunalInteractor,
+    private val interactionHandler: WidgetInteractionHandler,
     tutorialInteractor: CommunalTutorialInteractor,
-    shadeViewController: Provider<ShadeViewController>,
-    powerManager: PowerManager,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
-) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) {
+) : BaseCommunalViewModel(communalInteractor, mediaHost) {
     @OptIn(ExperimentalCoroutinesApi::class)
     override val communalContent: Flow<List<CommunalContentModel>> =
         tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode ->
@@ -51,13 +57,54 @@
                 return@flatMapLatest flowOf(communalInteractor.tutorialContent)
             }
             combine(
-                communalInteractor.smartspaceContent,
-                communalInteractor.umoContent,
+                communalInteractor.ongoingContent,
                 communalInteractor.widgetContent,
-            ) { smartspace, umo, widgets ->
-                smartspace + umo + widgets
+                communalInteractor.ctaTileContent,
+            ) { ongoing, widgets, ctaTile,
+                ->
+                ongoing + widgets + ctaTile
             }
         }
 
+    private val _isPopupOnDismissCtaShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isPopupOnDismissCtaShowing: Flow<Boolean> =
+        _isPopupOnDismissCtaShowing.asStateFlow()
+
     override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor()
+
+    override fun onDismissCtaTile() {
+        communalInteractor.dismissCtaTile()
+        setPopupOnDismissCtaVisibility(true)
+        schedulePopupHiding()
+    }
+
+    override fun getInteractionHandler(): RemoteViews.InteractionHandler = interactionHandler
+
+    override fun onHidePopupAfterDismissCta() {
+        cancelDelayedPopupHiding()
+        setPopupOnDismissCtaVisibility(false)
+    }
+
+    private fun setPopupOnDismissCtaVisibility(isVisible: Boolean) {
+        _isPopupOnDismissCtaShowing.value = isVisible
+    }
+
+    private var delayedHidePopupJob: Job? = null
+    private fun schedulePopupHiding() {
+        cancelDelayedPopupHiding()
+        delayedHidePopupJob =
+            scope.launch {
+                delay(POPUP_AUTO_HIDE_TIMEOUT_MS)
+                onHidePopupAfterDismissCta()
+            }
+    }
+
+    private fun cancelDelayedPopupHiding() {
+        delayedHidePopupJob?.cancel()
+        delayedHidePopupJob = null
+    }
+
+    companion object {
+        const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L
+    }
 }
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 c936c63..380ed61 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -23,26 +23,33 @@
 import android.os.RemoteException
 import android.util.Log
 import android.view.IWindowManager
+import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.activity.result.ActivityResultLauncher
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
 import javax.inject.Inject
+import kotlinx.coroutines.launch
 
 /** An Activity for editing the widgets that appear in hub mode. */
 class EditWidgetsActivity
 @Inject
 constructor(
     private val communalViewModel: CommunalEditModeViewModel,
-    private val communalInteractor: CommunalInteractor,
     private var windowManagerService: IWindowManager? = null,
+    private val uiEventLogger: UiEventLogger,
 ) : ComponentActivity() {
     companion object {
         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 REQUEST_CODE_CONFIGURE_WIDGET = 1
         private const val TAG = "EditWidgetsActivity"
     }
 
@@ -50,6 +57,8 @@
         registerForActivityResult(StartActivityForResult()) { result ->
             when (result.resultCode) {
                 RESULT_OK -> {
+                    uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
+
                     result.data?.let { intent ->
                         val isPendingWidgetDrag =
                             intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
@@ -62,7 +71,7 @@
                                     Intent.EXTRA_COMPONENT_NAME,
                                     ComponentName::class.java
                                 )
-                                ?.let { communalInteractor.addWidget(it, 0) }
+                                ?.let { communalViewModel.onAddWidget(it, 0) }
                                 ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
                         }
                     }
@@ -79,14 +88,30 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
+        val windowInsetsController = window.decorView.windowInsetsController
+        windowInsetsController?.hide(WindowInsets.Type.systemBars())
+        window.setDecorFitsSystemWindows(false)
+
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Start the configuration activity when new widgets are added.
+                communalViewModel.widgetsToConfigure.collect { widgetId ->
+                    communalViewModel.startConfigurationActivity(
+                        activity = this@EditWidgetsActivity,
+                        widgetId = widgetId,
+                        requestCode = REQUEST_CODE_CONFIGURE_WIDGET
+                    )
+                }
+            }
+        }
+
         setCommunalEditWidgetActivityContent(
             activity = this,
             viewModel = communalViewModel,
             onOpenWidgetPicker = {
-                val localPackageManager: PackageManager = getPackageManager()
                 val intent =
                     Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }
-                localPackageManager
+                packageManager
                     .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
                     ?.activityInfo
                     ?.packageName
@@ -117,4 +142,23 @@
             }
         )
     }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        if (requestCode == REQUEST_CODE_CONFIGURE_WIDGET) {
+            communalViewModel.setConfigurationResult(resultCode)
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN)
+    }
+
+    override fun onStop() {
+        super.onStop()
+
+        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
new file mode 100644
index 0000000..c8db70b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.app.PendingIntent
+import android.view.View
+import android.widget.RemoteViews
+import com.android.systemui.plugins.ActivityStarter
+import javax.inject.Inject
+
+class WidgetInteractionHandler
+@Inject
+constructor(
+    private val activityStarter: ActivityStarter,
+) : RemoteViews.InteractionHandler {
+    override fun onInteraction(
+        view: View,
+        pendingIntent: PendingIntent,
+        response: RemoteViews.RemoteResponse
+    ): Boolean =
+        when {
+            pendingIntent.isActivity -> startActivity(pendingIntent)
+            else ->
+                RemoteViews.startPendingIntent(view, pendingIntent, response.getLaunchOptions(view))
+        }
+
+    private fun startActivity(pendingIntent: PendingIntent): Boolean {
+        activityStarter.startPendingIntentMaybeDismissingKeyguard(
+            /* intent = */ pendingIntent,
+            /* intentSentUiThreadCallback = */ null,
+            // TODO(b/318758390): Properly animate activities started from widgets.
+            /* animationController = */ null
+        )
+        return true
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index e71007b..8831dc6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -484,7 +484,9 @@
                     }
                 },
                 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
-                null,
+                ActivityOptions.makeBasic()
+                    .setPendingIntentCreatorBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(),
                 userTracker.userHandle
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 8b992fc..b2d7052 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -91,6 +91,7 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.telephony.satellite.SatelliteManager;
 import android.view.Choreographer;
 import android.view.CrossWindowBlurListeners;
 import android.view.IWindowManager;
@@ -712,4 +713,10 @@
                 ServiceManager.getService(Context.URI_GRANTS_SERVICE)
         );
     }
+
+    @Provides
+    @Singleton
+    static Optional<SatelliteManager> provideSatelliteManager(Context context) {
+        return Optional.ofNullable(context.getSystemService(SatelliteManager.class));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 87a736d..8d82b55 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.BiometricNotificationService
 import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.communal.log.CommunalLoggerStartable
 import com.android.systemui.controls.dagger.StartControlsStartableModule
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.dreams.AssistantAttentionMonitor
@@ -318,4 +319,9 @@
     @IntoMap
     @ClassKey(KeyguardDismissBinder::class)
     abstract fun bindKeyguardDismissBinder(impl: KeyguardDismissBinder): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalLoggerStartable::class)
+    abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 615b503..3bc4f34 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -32,6 +32,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.FaceScanningOverlay
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.log.ScreenDecorationsLogger
@@ -41,19 +42,20 @@
 
 @SysUISingleton
 class FaceScanningProviderFactory @Inject constructor(
-    private val authController: AuthController,
-    private val context: Context,
-    private val statusBarStateController: StatusBarStateController,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    @Main private val mainExecutor: Executor,
-    private val logger: ScreenDecorationsLogger,
+        private val authController: AuthController,
+        private val context: Context,
+        private val statusBarStateController: StatusBarStateController,
+        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        @Main private val mainExecutor: Executor,
+        private val logger: ScreenDecorationsLogger,
+        private val facePropertyRepository: FacePropertyRepository,
 ) : DecorProviderFactory() {
     private val display = context.display
     private val displayInfo = DisplayInfo()
 
     override val hasProviders: Boolean
         get() {
-            if (authController.faceSensorLocation == null) {
+            if (facePropertyRepository.sensorLocation.value == null) {
                 return false
             }
 
@@ -86,6 +88,7 @@
                                         keyguardUpdateMonitor,
                                         mainExecutor,
                                         logger,
+                                        facePropertyRepository,
                                 )
                         )
                     }
@@ -104,12 +107,13 @@
 }
 
 class FaceScanningOverlayProviderImpl(
-    override val alignedBound: Int,
-    private val authController: AuthController,
-    private val statusBarStateController: StatusBarStateController,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val mainExecutor: Executor,
-    private val logger: ScreenDecorationsLogger,
+        override val alignedBound: Int,
+        private val authController: AuthController,
+        private val statusBarStateController: StatusBarStateController,
+        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        private val mainExecutor: Executor,
+        private val logger: ScreenDecorationsLogger,
+        private val facePropertyRepository: FacePropertyRepository,
 ) : BoundDecorProvider() {
     override val viewId: Int = com.android.systemui.res.R.id.face_scanning_anim
 
@@ -162,8 +166,9 @@
         layoutParams.let { lp ->
             lp.width = ViewGroup.LayoutParams.MATCH_PARENT
             lp.height = ViewGroup.LayoutParams.MATCH_PARENT
-            logger.faceSensorLocation(authController.faceSensorLocation)
-            authController.faceSensorLocation?.y?.let { faceAuthSensorHeight ->
+            logger.faceSensorLocation(facePropertyRepository.sensorLocation.value)
+            facePropertyRepository.sensorLocation.value?.y?.let {
+                faceAuthSensorHeight ->
                 val faceScanningHeight = (faceAuthSensorHeight * 2)
                 when (rotation) {
                     Surface.ROTATION_0, Surface.ROTATION_180 ->
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
index 4e4b79c..7f3b5eb 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
@@ -155,12 +155,12 @@
                         return true;
                     }
 
-                    // Don't set expansion if the user doesn't have a pin/password set so that no
-                    // animations are played we're not transitioning to the bouncer.
-                    if (!mLockPatternUtils.isSecure(mUserTracker.getUserId())) {
-                        // Return false so the gesture is not consumed, allowing the dream to wake
-                        // if it wants instead of doing nothing.
-                        return false;
+                    // If scrolling up and keyguard is not locked, dismiss the dream since there's
+                    // no bouncer to show.
+                    if (e1.getY() > e2.getY()
+                            && !mLockPatternUtils.isSecure(mUserTracker.getUserId())) {
+                        mCentralSurfaces.get().awakenDreams();
+                        return true;
                     }
 
                     // For consistency, we adopt the expansion definition found in the
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 38c7c6a..89a983b 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -122,11 +122,6 @@
     val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag("new_unlock_swipe_animation")
     val CHARGING_RIPPLE = resourceBooleanFlag(R.bool.flag_charging_ripple, "charging_ripple")
 
-    // TODO(b/254512281): Tracking Bug
-    @JvmField
-    val BOUNCER_USER_SWITCHER =
-        resourceBooleanFlag(R.bool.config_enableBouncerUserSwitcher, "bouncer_user_switcher")
-
     // TODO(b/254512676): Tracking Bug
     @JvmField
     val LOCKSCREEN_CUSTOM_CLOCKS =
@@ -377,9 +372,6 @@
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag("simulate_dock_through_charging")
 
-    // TODO(b/254512758): Tracking Bug
-    @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag("rounded_box_ripple")
-
     // TODO(b/273509374): Tracking Bug
     @JvmField
     val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS =
@@ -448,11 +440,6 @@
     // TODO(b/270987164): Tracking Bug
     @JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag("trackpad_gesture_features")
 
-    // TODO(b/265639042): Tracking Bug
-    @JvmField
-    val WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM =
-        unreleasedFlag("persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
-
     // TODO(b/273800936): Tracking Bug
     @JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag("trackpad_gesture_common")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
index aa4c88a..e23ec89 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
@@ -403,6 +403,7 @@
     public static final int INDICATION_TYPE_REVERSE_CHARGING = 10;
     public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE = 11;
     public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP = 12;
+    public static final int INDICATION_IS_DISMISSIBLE = 13;
 
     @IntDef({
             INDICATION_TYPE_NONE,
@@ -417,7 +418,8 @@
             INDICATION_TYPE_USER_LOCKED,
             INDICATION_TYPE_REVERSE_CHARGING,
             INDICATION_TYPE_BIOMETRIC_MESSAGE,
-            INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP
+            INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+            INDICATION_IS_DISMISSIBLE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface IndicationType{}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index edf9648..e2ab20e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -593,6 +593,13 @@
             mKeyguardViewMediator.doKeyguardTimeout(options);
         }
 
+        // Binder interface
+        public void showDismissibleKeyguard() {
+            trace("showDismissibleKeyguard");
+            checkPermission();
+            mKeyguardViewMediator.showDismissibleKeyguard();
+        }
+
         @Override // Binder interface
         public void setSwitchingUser(boolean switching) {
             trace("setSwitchingUser switching=" + switching);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d7a1906..5cebd96 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -107,6 +107,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.app.animation.Interpolators;
+import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.jank.InteractionJankMonitor.Configuration;
 import com.android.internal.logging.UiEventLogger;
@@ -311,6 +312,11 @@
      */
     public static final String OPTION_FORCE_SHOW = "force_show";
     public static final String SYS_BOOT_REASON_PROP = "sys.boot.reason.last";
+    /**
+     * Boolean option for showKeyguard, when set to true, can show the keyguard without immediately
+     * locking.
+     */
+    public static final String OPTION_SHOW_DISMISSIBLE = "show_dismissible";
     public static final String REBOOT_MAINLINE_UPDATE = "reboot,mainline_update";
     private final DreamOverlayStateController mDreamOverlayStateController;
     private final JavaAdapter mJavaAdapter;
@@ -1321,7 +1327,9 @@
     private DozeParameters mDozeParameters;
     private SelectedUserInteractor mSelectedUserInteractor;
     private KeyguardInteractor mKeyguardInteractor;
-
+    @VisibleForTesting
+    protected FoldGracePeriodProvider mFoldGracePeriodProvider =
+            new FoldGracePeriodProvider();
     private final KeyguardStateController mKeyguardStateController;
     private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
             new KeyguardStateController.Callback() {
@@ -1478,6 +1486,9 @@
 
         mOrderUnlockAndWake = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_orderUnlockAndWake);
+
+        mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard");
+        mShowKeyguardWakeLock.setReferenceCounted(false);
     }
 
     public void userActivity() {
@@ -1485,9 +1496,6 @@
     }
 
     private void setupLocked() {
-        mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard");
-        mShowKeyguardWakeLock.setReferenceCounted(false);
-
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_SHUTDOWN);
         mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter);
@@ -1995,7 +2003,7 @@
                 mNeedToReshowWhenReenabled = false;
                 updateInputRestrictedLocked();
 
-                showLocked(null);
+                showKeyguard(null);
 
                 // block until we know the keyguard is done drawing (and post a message
                 // to unblock us after a timeout, so we don't risk blocking too long
@@ -2170,6 +2178,24 @@
     }
 
     /**
+     * Only available if the fold grace period feature is enabled.
+     * Used by PhoneWindowManager to show the keyguard immediately without locking the device.
+     * This method shows the keyguard whether there's a screen lock configured or not (including
+     * screen lock SWIPE or NONE).
+     * This must be safe to call from any thread and with any window manager locks held.
+     */
+    public void showDismissibleKeyguard() {
+        if (mFoldGracePeriodProvider.isEnabled()) {
+            Bundle showKeyguardUnlocked = new Bundle();
+            showKeyguardUnlocked.putBoolean(OPTION_SHOW_DISMISSIBLE, true);
+            showKeyguard(showKeyguardUnlocked);
+        } else {
+            Log.e(TAG, "fold grace period feature isn't enabled, but showKeyguard() method is"
+                    + " being called", new Throwable());
+        }
+    }
+
+    /**
      * Given the state of the keyguard, is the input restricted?
      * Input is restricted when the keyguard is showing, or when the keyguard
      * was suppressed by an app that disabled the keyguard or we haven't been provisioned yet.
@@ -2273,7 +2299,7 @@
         }
 
         if (DEBUG) Log.d(TAG, "doKeyguard: showing the lock screen");
-        showLocked(options);
+        showKeyguard(options);
     }
 
     private void lockProfile(int userId) {
@@ -2335,18 +2361,18 @@
     }
 
     /**
-     * Send message to keyguard telling it to show itself
+     * Send message to keyguard telling it to show itself.
      * @see #handleShow
      */
-    private void showLocked(Bundle options) {
-        Trace.beginSection("KeyguardViewMediator#showLocked acquiring mShowKeyguardWakeLock");
-        if (DEBUG) Log.d(TAG, "showLocked");
+    private void showKeyguard(Bundle options) {
+        Trace.beginSection("KeyguardViewMediator#showKeyguard acquiring mShowKeyguardWakeLock");
+        if (DEBUG) Log.d(TAG, "showKeyguard");
         // ensure we stay awake until we are finished displaying the keyguard
         mShowKeyguardWakeLock.acquire();
         Message msg = mHandler.obtainMessage(SHOW, options);
         // Treat these messages with priority - This call can originate from #doKeyguardTimeout,
-        // meaning the device should lock as soon as possible and not wait for other messages on
-        // the thread to process first.
+        // meaning the device may lock, so it shouldn't wait for other messages on the thread to
+        // process first.
         mHandler.sendMessageAtFrontOfQueue(msg);
         Trace.endSection();
     }
@@ -2724,13 +2750,18 @@
     }
 
     /**
-     * Handle message sent by {@link #showLocked}.
+     * Handle message sent by {@link #showKeyguard}.
      * @see #SHOW
      */
     private void handleShow(Bundle options) {
         Trace.beginSection("KeyguardViewMediator#handleShow");
+        final boolean showUnlocked = options != null
+                && options.getBoolean(OPTION_SHOW_DISMISSIBLE, false);
         final int currentUser = mSelectedUserInteractor.getSelectedUserId();
-        if (mLockPatternUtils.isSecure(currentUser)) {
+        if (showUnlocked) {
+            // tell KeyguardUpdateMonitor to keep the device unlocked until the next lock signal
+            mUpdateMonitor.tryForceIsDismissibleKeyguard();
+        } else if (mLockPatternUtils.isSecure(currentUser)) {
             mLockPatternUtils.getDevicePolicyManager().reportKeyguardSecured(currentUser);
         }
         synchronized (KeyguardViewMediator.this) {
@@ -2755,7 +2786,7 @@
                         + ", which means we're showing in the middle of hiding.");
             }
 
-            // Force if we we're showing in the middle of unlocking, to ensure we end up in the
+            // Force if we're showing in the middle of unlocking, to ensure we end up in the
             // correct state.
             setShowingLocked(true, hidingOrGoingAway /* force */);
             mHiding = false;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 2f937bc..704ebdd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -21,6 +21,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.common.shared.model.Position
@@ -277,6 +278,7 @@
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
     private val systemClock: SystemClock,
+    facePropertyRepository: FacePropertyRepository,
 ) : KeyguardRepository {
     private val _dismissAction: MutableStateFlow<DismissAction> =
         MutableStateFlow(DismissAction.None)
@@ -599,27 +601,7 @@
         awaitClose { authController.removeCallback(callback) }
     }
 
-    override val faceSensorLocation: Flow<Point?> = conflatedCallbackFlow {
-        fun sendSensorLocation() {
-            trySendWithFailureLogging(
-                authController.faceSensorLocation,
-                TAG,
-                "AuthController.Callback#onFingerprintLocationChanged"
-            )
-        }
-
-        val callback =
-            object : AuthController.Callback {
-                override fun onFaceSensorLocationChanged() {
-                    sendSensorLocation()
-                }
-            }
-
-        authController.addCallback(callback)
-        sendSensorLocation()
-
-        awaitClose { authController.removeCallback(callback) }
-    }
+    override val faceSensorLocation: Flow<Point?> = facePropertyRepository.sensorLocation
 
     override val biometricUnlockSource: Flow<BiometricUnlockSource?> = conflatedCallbackFlow {
         val callback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
index cb0f186..42f14f1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
@@ -140,8 +140,9 @@
     override val revealAmount: Flow<Float> = callbackFlow {
         val updateListener =
             Animator.AnimatorUpdateListener {
-                val value = (it as ValueAnimator).animatedValue
-                trySend(value as Float)
+                val value = (it as ValueAnimator).animatedValue as Float
+                trySend(value)
+
                 if (value <= 0.0f || value >= 1.0f) {
                     scrimLogger.d(TAG, "revealAmount", value)
                 }
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 52f2759..d7a2aa0 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
@@ -18,7 +18,8 @@
 
 import android.animation.ValueAnimator
 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.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -28,6 +29,7 @@
 import com.android.wm.shell.animation.Interpolators
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.combine
@@ -39,13 +41,18 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val powerInteractor: PowerInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.ALTERNATE_BOUNCER,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -65,7 +72,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.primaryBouncerShowing,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         powerInteractor.isAwake,
                         keyguardInteractor.isAodAvailable,
                         ::toQuad
@@ -102,20 +109,19 @@
 
     private fun listenForAlternateBouncerToGone() {
         scope.launch {
-            keyguardInteractor.isKeyguardGoingAway
-                .sample(transitionInteractor.finishedKeyguardState, ::Pair)
-                .collect { (isKeyguardGoingAway, keyguardState) ->
-                    if (isKeyguardGoingAway && keyguardState == KeyguardState.ALTERNATE_BOUNCER) {
-                        startTransitionTo(KeyguardState.GONE)
-                    }
+            keyguardInteractor.isKeyguardGoingAway.sample(finishedKeyguardState, ::Pair).collect {
+                (isKeyguardGoingAway, keyguardState) ->
+                if (isKeyguardGoingAway && keyguardState == KeyguardState.ALTERNATE_BOUNCER) {
+                    startTransitionTo(KeyguardState.GONE)
                 }
+            }
         }
     }
 
     private fun listenForAlternateBouncerToPrimaryBouncer() {
         scope.launch {
             keyguardInteractor.primaryBouncerShowing
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (isPrimaryBouncerShowing, startedKeyguardState) ->
                     if (
                         isPrimaryBouncerShowing &&
@@ -139,6 +145,7 @@
     }
 
     companion object {
+        const val TAG = "FromAlternateBouncerTransitionInteractor"
         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/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 8584401..fedd63b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -19,7 +19,8 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 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.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
 import com.android.systemui.keyguard.shared.model.DozeStateModel
@@ -29,6 +30,7 @@
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
@@ -38,12 +40,17 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.AOD,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -58,7 +65,7 @@
                 .dozeTransitionTo(DozeStateModel.FINISH)
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isKeyguardOccluded,
                         ::Pair
                     ),
@@ -77,7 +84,6 @@
                             } else {
                                 TransitionModeOnCanceled.LAST_VALUE
                             }
-
                         startTransitionTo(
                             toState = toState,
                             modeOnCanceled = modeOnCanceled,
@@ -89,16 +95,13 @@
 
     private fun listenForAodToGone() {
         scope.launch {
-            keyguardInteractor.biometricUnlockState
-                .sample(transitionInteractor.finishedKeyguardState, ::Pair)
-                .collect { pair ->
-                    val (biometricUnlockState, keyguardState) = pair
-                    if (
-                        keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
-                    ) {
-                        startTransitionTo(KeyguardState.GONE)
-                    }
+            keyguardInteractor.biometricUnlockState.sample(finishedKeyguardState, ::Pair).collect {
+                pair ->
+                val (biometricUnlockState, keyguardState) = pair
+                if (keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)) {
+                    startTransitionTo(KeyguardState.GONE)
                 }
+            }
         }
     }
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -113,6 +116,7 @@
     }
 
     companion object {
+        const val TAG = "FromAodTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_LOCKSCREEN_DURATION = 500.milliseconds
         val TO_GONE_DURATION = DEFAULT_DURATION
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index eca7088..fcb7698 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -19,7 +19,8 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 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.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -28,6 +29,7 @@
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
@@ -37,13 +39,18 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val powerInteractor: PowerInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DOZING,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -57,7 +64,7 @@
             powerInteractor.isAwake
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isKeyguardOccluded,
                         ::Pair
                     ),
@@ -76,7 +83,7 @@
     private fun listenForDozingToGone() {
         scope.launch {
             keyguardInteractor.biometricUnlockState
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (biometricUnlockState, lastStartedTransition) ->
                     if (
                         lastStartedTransition.to == KeyguardState.DOZING &&
@@ -96,6 +103,7 @@
     }
 
     companion object {
+        const val TAG = "FromDozingTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
index dac6ef5..a6cdaa8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
@@ -19,7 +19,8 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 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.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
@@ -28,6 +29,7 @@
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.combine
@@ -39,12 +41,17 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -64,7 +71,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.dozeTransitionModel,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         ::Pair
                     ),
                     ::toTriple
@@ -88,7 +95,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.isKeyguardOccluded,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         ::Pair,
                     ),
                     ::toTriple
@@ -108,7 +115,7 @@
     private fun listenForDreamingLockscreenHostedToPrimaryBouncer() {
         scope.launch {
             keyguardInteractor.primaryBouncerShowing
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (isBouncerShowing, lastStartedTransitionStep) ->
                     if (
                         isBouncerShowing &&
@@ -123,7 +130,7 @@
     private fun listenForDreamingLockscreenHostedToGone() {
         scope.launch {
             keyguardInteractor.biometricUnlockState
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (biometricUnlockState, lastStartedTransitionStep) ->
                     if (
                         lastStartedTransitionStep.to == KeyguardState.DREAMING_LOCKSCREEN_HOSTED &&
@@ -137,11 +144,7 @@
 
     private fun listenForDreamingLockscreenHostedToDozing() {
         scope.launch {
-            combine(
-                    keyguardInteractor.dozeTransitionModel,
-                    transitionInteractor.startedKeyguardTransitionStep,
-                    ::Pair
-                )
+            combine(keyguardInteractor.dozeTransitionModel, startedKeyguardTransitionStep, ::Pair)
                 .collect { (dozeTransitionModel, lastStartedTransitionStep) ->
                     if (
                         dozeTransitionModel.to == DozeStateModel.DOZE &&
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 7fdcf2f..13ffd63 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -19,7 +19,8 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 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.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
@@ -28,6 +29,7 @@
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
@@ -37,12 +39,17 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DREAMING,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -66,7 +73,7 @@
     private fun listenForDreamingToOccluded() {
         scope.launch {
             combine(keyguardInteractor.isKeyguardOccluded, keyguardInteractor.isDreaming, ::Pair)
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::toTriple)
+                .sample(startedKeyguardTransitionStep, ::toTriple)
                 .collect { (isOccluded, isDreaming, lastStartedTransition) ->
                     if (
                         isOccluded &&
@@ -82,7 +89,7 @@
     private fun listenForDreamingToGone() {
         scope.launch {
             keyguardInteractor.biometricUnlockState
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (biometricUnlockState, lastStartedTransitionStep) ->
                     if (
                         lastStartedTransitionStep.to == KeyguardState.DREAMING &&
@@ -96,11 +103,7 @@
 
     private fun listenForDreamingToAodOrDozing() {
         scope.launch {
-            combine(
-                    keyguardInteractor.dozeTransitionModel,
-                    transitionInteractor.finishedKeyguardState,
-                    ::Pair
-                )
+            combine(keyguardInteractor.dozeTransitionModel, finishedKeyguardState, ::Pair)
                 .collect { (dozeTransitionModel, keyguardState) ->
                     if (keyguardState == KeyguardState.DREAMING) {
                         if (dozeTransitionModel.to == DozeStateModel.DOZE) {
@@ -123,6 +126,7 @@
     }
 
     companion object {
+        const val TAG = "FromDreamingTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_LOCKSCREEN_DURATION = 1167.milliseconds
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 70c2e6d..19fd7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -20,18 +20,29 @@
 import com.android.app.animation.Interpolators
 import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 
 @SysUISingleton
 class FromGlanceableHubTransitionInteractor
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(fromState = KeyguardState.GLANCEABLE_HUB) {
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Main mainDispatcher: CoroutineDispatcher,
+    @Background bgDispatcher: CoroutineDispatcher,
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.GLANCEABLE_HUB,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
+    ) {
     override fun start() {
         if (!Flags.communalHub()) {
             return
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 62a0b0e..742790e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -19,7 +19,8 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 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.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
@@ -28,6 +29,7 @@
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
@@ -37,13 +39,18 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val powerInteractor: PowerInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.GONE,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -57,7 +64,7 @@
     private fun listenForGoneToLockscreen() {
         scope.launch {
             keyguardInteractor.isKeyguardShowing
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (isKeyguardShowing, lastStartedStep) ->
                     if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) {
                         startTransitionTo(KeyguardState.LOCKSCREEN)
@@ -69,7 +76,7 @@
     private fun listenForGoneToDreamingLockscreenHosted() {
         scope.launch {
             keyguardInteractor.isActiveDreamLockscreenHosted
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (isActiveDreamLockscreenHosted, lastStartedStep) ->
                     if (isActiveDreamLockscreenHosted && lastStartedStep.to == KeyguardState.GONE) {
                         startTransitionTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
@@ -83,7 +90,7 @@
             keyguardInteractor.isAbleToDream
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isActiveDreamLockscreenHosted,
                         ::Pair
                     ),
@@ -106,7 +113,7 @@
             powerInteractor.isAsleep
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Pair
                     ),
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 cecc653..2d0baa8 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,9 +18,9 @@
 
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
-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
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
@@ -39,20 +39,25 @@
 import java.util.UUID
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 @SysUISingleton
 class FromLockscreenTransitionInteractor
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
@@ -61,6 +66,9 @@
 ) :
     TransitionInteractor(
         fromState = KeyguardState.LOCKSCREEN,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -147,12 +155,12 @@
 
     private fun listenForLockscreenToDreaming() {
         val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING)
-        scope.launch("$TAG#listenForLockscreenToDreaming") {
+        scope.launch {
             keyguardInteractor.isAbleToDream
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
-                        transitionInteractor.finishedKeyguardState,
+                        startedKeyguardTransitionStep,
+                        finishedKeyguardState,
                         keyguardInteractor.isActiveDreamLockscreenHosted,
                         ::Triple
                     ),
@@ -180,9 +188,9 @@
     }
 
     private fun listenForLockscreenToPrimaryBouncer() {
-        scope.launch("$TAG#listenForLockscreenToPrimaryBouncer") {
+        scope.launch {
             keyguardInteractor.primaryBouncerShowing
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (isBouncerShowing, lastStartedTransitionStep) = pair
                     if (
@@ -195,9 +203,9 @@
     }
 
     private fun listenForLockscreenToAlternateBouncer() {
-        scope.launch("$TAG#listenForLockscreenToAlternateBouncer") {
+        scope.launch {
             keyguardInteractor.alternateBouncerShowing
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (isAlternateBouncerShowing, lastStartedTransitionStep) = pair
                     if (
@@ -213,11 +221,11 @@
     /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
     private fun listenForLockscreenToPrimaryBouncerDragging() {
         var transitionId: UUID? = null
-        scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") {
+        scope.launch {
             shadeRepository.legacyShadeExpansion
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.statusBarState,
                         keyguardInteractor.isKeyguardUnlocked,
                         ::Triple
@@ -225,64 +233,66 @@
                     ::toQuad
                 )
                 .collect { (shadeExpansion, keyguardState, statusBarState, isKeyguardUnlocked) ->
-                    val id = transitionId
-                    if (id != null) {
-                        if (keyguardState.to == KeyguardState.PRIMARY_BOUNCER) {
-                            // An existing `id` means a transition is started, and calls to
-                            // `updateTransition` will control it until FINISHED or CANCELED
-                            var nextState =
-                                if (shadeExpansion == 0f) {
-                                    TransitionState.FINISHED
-                                } else if (shadeExpansion == 1f) {
-                                    TransitionState.CANCELED
-                                } else {
-                                    TransitionState.RUNNING
+                    withContext(mainDispatcher) {
+                        val id = transitionId
+                        if (id != null) {
+                            if (keyguardState.to == KeyguardState.PRIMARY_BOUNCER) {
+                                // An existing `id` means a transition is started, and calls to
+                                // `updateTransition` will control it until FINISHED or CANCELED
+                                var nextState =
+                                    if (shadeExpansion == 0f) {
+                                        TransitionState.FINISHED
+                                    } else if (shadeExpansion == 1f) {
+                                        TransitionState.CANCELED
+                                    } else {
+                                        TransitionState.RUNNING
+                                    }
+                                transitionRepository.updateTransition(
+                                    id,
+                                    1f - shadeExpansion,
+                                    nextState,
+                                )
+
+                                if (
+                                    nextState == TransitionState.CANCELED ||
+                                        nextState == TransitionState.FINISHED
+                                ) {
+                                    transitionId = null
                                 }
-                            transitionRepository.updateTransition(
-                                id,
-                                1f - shadeExpansion,
-                                nextState,
-                            )
 
-                            if (
-                                nextState == TransitionState.CANCELED ||
-                                    nextState == TransitionState.FINISHED
-                            ) {
-                                transitionId = null
-                            }
-
-                            // If canceled, just put the state back
-                            // TODO(b/278086361): This logic should happen in
-                            //  FromPrimaryBouncerInteractor.
-                            if (nextState == TransitionState.CANCELED) {
-                                transitionRepository.startTransition(
-                                    TransitionInfo(
-                                        ownerName = name,
-                                        from = KeyguardState.PRIMARY_BOUNCER,
-                                        to = KeyguardState.LOCKSCREEN,
-                                        animator =
-                                            getDefaultAnimatorForTransitionsToState(
-                                                    KeyguardState.LOCKSCREEN
-                                                )
-                                                .apply { duration = 0 }
+                                // If canceled, just put the state back
+                                // TODO(b/278086361): This logic should happen in
+                                //  FromPrimaryBouncerInteractor.
+                                if (nextState == TransitionState.CANCELED) {
+                                    transitionRepository.startTransition(
+                                        TransitionInfo(
+                                            ownerName = name,
+                                            from = KeyguardState.PRIMARY_BOUNCER,
+                                            to = KeyguardState.LOCKSCREEN,
+                                            animator =
+                                                getDefaultAnimatorForTransitionsToState(
+                                                        KeyguardState.LOCKSCREEN
+                                                    )
+                                                    .apply { duration = 0 }
+                                        )
                                     )
-                                )
+                                }
                             }
-                        }
-                    } else {
-                        // TODO (b/251849525): Remove statusbarstate check when that state is
-                        // integrated into KeyguardTransitionRepository
-                        if (
-                            keyguardState.to == KeyguardState.LOCKSCREEN &&
-                                shadeRepository.legacyShadeTracking.value &&
-                                !isKeyguardUnlocked &&
-                                statusBarState == KEYGUARD
-                        ) {
-                            transitionId =
-                                startTransitionTo(
-                                    toState = KeyguardState.PRIMARY_BOUNCER,
-                                    animator = null, // transition will be manually controlled
-                                )
+                        } else {
+                            // TODO (b/251849525): Remove statusbarstate check when that state is
+                            // integrated into KeyguardTransitionRepository
+                            if (
+                                keyguardState.to == KeyguardState.LOCKSCREEN &&
+                                    shadeRepository.legacyShadeTracking.value &&
+                                    !isKeyguardUnlocked &&
+                                    statusBarState == KEYGUARD
+                            ) {
+                                transitionId =
+                                    startTransitionTo(
+                                        toState = KeyguardState.PRIMARY_BOUNCER,
+                                        animator = null, // transition will be manually controlled
+                                    )
+                            }
                         }
                     }
                 }
@@ -290,7 +300,7 @@
     }
 
     fun dismissKeyguard() {
-        startTransitionTo(KeyguardState.GONE)
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
     }
 
     private fun listenForLockscreenToGone() {
@@ -298,9 +308,9 @@
             return
         }
 
-        scope.launch("$TAG#listenForLockscreenToGone") {
+        scope.launch {
             keyguardInteractor.isKeyguardGoingAway
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (isKeyguardGoingAway, lastStartedStep) = pair
                     if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
@@ -315,9 +325,9 @@
             return
         }
 
-        scope.launch("$TAG#listenForLockscreenToGoneDragging") {
+        scope.launch {
             keyguardInteractor.isKeyguardGoingAway
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (isKeyguardGoingAway, lastStartedStep) = pair
                     if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
@@ -328,23 +338,22 @@
     }
 
     private fun listenForLockscreenToOccluded() {
-        scope.launch("$TAG#listenForLockscreenToOccluded") {
-            keyguardInteractor.isKeyguardOccluded
-                .sample(transitionInteractor.startedKeyguardState, ::Pair)
-                .collect { (isOccluded, keyguardState) ->
-                    if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) {
-                        startTransitionTo(KeyguardState.OCCLUDED)
-                    }
+        scope.launch {
+            keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect {
+                (isOccluded, keyguardState) ->
+                if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) {
+                    startTransitionTo(KeyguardState.OCCLUDED)
                 }
+            }
         }
     }
 
     private fun listenForLockscreenToAodOrDozing() {
-        scope.launch("$TAG#listenForLockscreenToAodOrDozing") {
+        scope.launch {
             powerInteractor.isAsleep
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Pair
                     ),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 6a8555c..40061f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -19,7 +19,8 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 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.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -27,6 +28,7 @@
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
@@ -36,13 +38,18 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val powerInteractor: PowerInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.OCCLUDED,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -57,7 +64,7 @@
     private fun listenForOccludedToPrimaryBouncer() {
         scope.launch {
             keyguardInteractor.primaryBouncerShowing
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (isBouncerShowing, lastStartedTransitionStep) ->
                     if (
                         isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.OCCLUDED
@@ -70,13 +77,12 @@
 
     private fun listenForOccludedToDreaming() {
         scope.launch {
-            keyguardInteractor.isAbleToDream
-                .sample(transitionInteractor.finishedKeyguardState, ::Pair)
-                .collect { (isAbleToDream, keyguardState) ->
-                    if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) {
-                        startTransitionTo(KeyguardState.DREAMING)
-                    }
+            keyguardInteractor.isAbleToDream.sample(finishedKeyguardState, ::Pair).collect {
+                (isAbleToDream, keyguardState) ->
+                if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) {
+                    startTransitionTo(KeyguardState.DREAMING)
                 }
+            }
         }
     }
 
@@ -86,7 +92,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.isKeyguardShowing,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         ::Pair
                     ),
                     ::toTriple
@@ -111,7 +117,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.isKeyguardShowing,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         ::Pair
                     ),
                     ::toTriple
@@ -135,7 +141,7 @@
             powerInteractor.isAsleep
                 .sample(
                     combine(
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Pair
                     ),
@@ -154,7 +160,7 @@
     private fun listenForOccludedToAlternateBouncer() {
         scope.launch {
             keyguardInteractor.alternateBouncerShowing
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (isAlternateBouncerShowing, lastStartedTransitionStep) ->
                     if (
                         isAlternateBouncerShowing &&
@@ -183,6 +189,7 @@
     }
 
     companion object {
+        const val TAG = "FromOccludedTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_LOCKSCREEN_DURATION = 933.milliseconds
         val TO_AOD_DURATION = DEFAULT_DURATION
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index 5f246e1..c62055f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -19,7 +19,8 @@
 import android.animation.ValueAnimator
 import com.android.keyguard.KeyguardSecurityModel
 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.data.repository.KeyguardTransitionRepository
@@ -35,6 +36,7 @@
 import com.android.wm.shell.animation.Interpolators
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -47,8 +49,10 @@
 @Inject
 constructor(
     override val transitionRepository: KeyguardTransitionRepository,
-    override val transitionInteractor: KeyguardTransitionInteractor,
-    @Application private val scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    @Background private val scope: CoroutineScope,
+    @Background bgDispatcher: CoroutineDispatcher,
+    @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val flags: FeatureFlags,
     private val keyguardSecurityModel: KeyguardSecurityModel,
@@ -57,6 +61,9 @@
 ) :
     TransitionInteractor(
         fromState = KeyguardState.PRIMARY_BOUNCER,
+        transitionInteractor = transitionInteractor,
+        mainDispatcher = mainDispatcher,
+        bgDispatcher = bgDispatcher,
     ) {
 
     override fun start() {
@@ -115,7 +122,7 @@
             .distinctUntilChanged()
 
     fun dismissPrimaryBouncer() {
-        startTransitionTo(KeyguardState.GONE)
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
     }
 
     private fun listenForPrimaryBouncerToLockscreenOrOccluded() {
@@ -124,7 +131,7 @@
                 .sample(
                     combine(
                         powerInteractor.isAwake,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isKeyguardOccluded,
                         keyguardInteractor.isActiveDreamLockscreenHosted,
                         ::toQuad
@@ -158,7 +165,7 @@
                 .sample(
                     combine(
                         powerInteractor.isAsleep,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Triple
                     ),
@@ -185,7 +192,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.isActiveDreamLockscreenHosted,
-                        transitionInteractor.startedKeyguardTransitionStep,
+                        startedKeyguardTransitionStep,
                         ::Pair
                     ),
                     ::toTriple
@@ -213,7 +220,7 @@
 
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
-                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(startedKeyguardTransitionStep, ::Pair)
                 .collect { (isKeyguardGoingAway, lastStartedTransitionStep) ->
                     if (
                         isKeyguardGoingAway &&
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index f7d1543..5b0c562 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -154,6 +154,8 @@
     val dozingToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(DOZING, LOCKSCREEN)
 
+    val transitions = repository.transitions
+
     /** Receive all [TransitionStep] matching a filter of [from]->[to] */
     fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
         return repository.transition(from, to)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index fbf6936..19d00cf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -22,12 +22,15 @@
 import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState
 import com.android.systemui.statusbar.LightRevealEffect
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @ExperimentalCoroutinesApi
@@ -39,6 +42,7 @@
     private val lightRevealScrimRepository: LightRevealScrimRepository,
     @Application private val scope: CoroutineScope,
     private val scrimLogger: ScrimLogger,
+    powerInteractor: PowerInteractor,
 ) {
 
     init {
@@ -73,7 +77,16 @@
             lightRevealScrimRepository.revealEffect
         )
 
-    val revealAmount = lightRevealScrimRepository.revealAmount
+    val revealAmount =
+        lightRevealScrimRepository.revealAmount.filter {
+            // When the screen is off we do not want to keep producing frames as this is causing
+            // (invisible) jank. However, we need to still pass through 1f and 0f to ensure that the
+            // correct end states are respected even if the screen turned off (or was still off)
+            // when the animation finished
+            powerInteractor.screenPowerState.value != ScreenPowerState.SCREEN_OFF ||
+                it == 1f ||
+                it == 0f
+        }
 
     companion object {
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index d5ac283..5c2df45 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -24,8 +24,11 @@
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
 import com.android.systemui.util.kotlin.sample
 import java.util.UUID
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /**
  * Each TransitionInteractor is responsible for determining under which conditions to notify
@@ -40,14 +43,25 @@
  */
 sealed class TransitionInteractor(
     val fromState: KeyguardState,
+    val transitionInteractor: KeyguardTransitionInteractor,
+    val mainDispatcher: CoroutineDispatcher,
+    val bgDispatcher: CoroutineDispatcher,
 ) {
     val name = this::class.simpleName ?: "UnknownTransitionInteractor"
-
     abstract val transitionRepository: KeyguardTransitionRepository
-    abstract val transitionInteractor: KeyguardTransitionInteractor
     abstract fun start()
 
-    fun startTransitionTo(
+    /* Use background dispatcher for all [KeyguardTransitionInteractor] flows. Necessary because
+     * the [sample] utility internally runs a collect on the Unconfined dispatcher, resulting
+     * in continuations on the main thread. We don't want that for classes that inherit from this.
+     */
+    val startedKeyguardTransitionStep =
+        transitionInteractor.startedKeyguardTransitionStep.flowOn(bgDispatcher)
+    // The following are MutableSharedFlows, and do not require flowOn
+    val startedKeyguardState = transitionInteractor.startedKeyguardState
+    val finishedKeyguardState = transitionInteractor.finishedKeyguardState
+
+    suspend fun startTransitionTo(
         toState: KeyguardState,
         animator: ValueAnimator? = getDefaultAnimatorForTransitionsToState(toState),
         modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE
@@ -67,16 +81,17 @@
             )
             return null
         }
-
-        return transitionRepository.startTransition(
-            TransitionInfo(
-                name,
-                fromState,
-                toState,
-                animator,
-                modeOnCanceled,
+        return withContext(mainDispatcher) {
+            transitionRepository.startTransition(
+                TransitionInfo(
+                    name,
+                    fromState,
+                    toState,
+                    animator,
+                    modeOnCanceled,
+                )
             )
-        )
+        }
     }
 
     /** This signal may come in before the occlusion signal, and can provide a custom transition */
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 cf1d247..4abda74 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -17,9 +17,13 @@
 
 import android.view.animation.Interpolator
 import com.android.app.animation.Interpolators.LINEAR
+import com.android.app.tracing.coroutines.launch
 import com.android.keyguard.logging.KeyguardTransitionAnimationLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+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.shared.model.TransitionState.CANCELED
 import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
@@ -31,10 +35,12 @@
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
 
 /**
  * Assists in creating sub-flows for a KeyguardTransition. Call [setup] once for a transition, and
@@ -45,21 +51,49 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
+    private val transitionInteractor: KeyguardTransitionInteractor,
     private val logger: KeyguardTransitionAnimationLogger,
 ) {
+    private val transitionMap = mutableMapOf<Edge, MutableSharedFlow<TransitionStep>>()
 
-    /**
-     * Invoke once per transition between FROM->TO states to get access to
-     * [SharedFlowBuilder#sharedFlow].
-     */
+    init {
+        scope.launch("KeyguardTransitionAnimationFlow") {
+            transitionInteractor.transitions.collect {
+                // FROM->TO
+                transitionMap[Edge(it.from, it.to)]?.emit(it)
+                // FROM->(ANY)
+                transitionMap[Edge(it.from, null)]?.emit(it)
+                // (ANY)->TO
+                transitionMap[Edge(null, it.to)]?.emit(it)
+            }
+        }
+    }
+
+    private fun getOrCreateFlow(edge: Edge): MutableSharedFlow<TransitionStep> {
+        return transitionMap.getOrPut(edge) {
+            MutableSharedFlow<TransitionStep>(
+                extraBufferCapacity = 10,
+                onBufferOverflow = BufferOverflow.DROP_OLDEST
+            )
+        }
+    }
+
+    /** Invoke once per transition between FROM->TO states to get access to a shared flow. */
     fun setup(
         duration: Duration,
-        stepFlow: Flow<TransitionStep>,
-    ) = SharedFlowBuilder(duration, stepFlow)
+        from: KeyguardState?,
+        to: KeyguardState?,
+    ): FlowBuilder {
+        if (from == null && to == null) {
+            throw IllegalArgumentException("from and to are both null")
+        }
 
-    inner class SharedFlowBuilder(
+        return FlowBuilder(duration, Edge(from, to))
+    }
+
+    inner class FlowBuilder(
         private val transitionDuration: Duration,
-        private val stepFlow: Flow<TransitionStep>,
+        private val edge: Edge,
     ) {
         /**
          * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted
@@ -115,20 +149,21 @@
                 }?.let { onStep(interpolator.getInterpolation(it)) }
             }
 
-            return stepFlow
+            return getOrCreateFlow(edge)
                 .map { step ->
-                    val value =
-                        when (step.transitionState) {
-                            STARTED -> stepToValue(step)
-                            RUNNING -> stepToValue(step)
-                            CANCELED -> onCancel?.invoke()
-                            FINISHED -> onFinish?.invoke()
-                        }
-                    logger.logTransitionStep(name, step, value)
-                    value
+                    StateToValue(
+                            step.transitionState,
+                            when (step.transitionState) {
+                                STARTED -> stepToValue(step)
+                                RUNNING -> stepToValue(step)
+                                CANCELED -> onCancel?.invoke()
+                                FINISHED -> onFinish?.invoke()
+                            }
+                        )
+                        .also { logger.logTransitionStep(name, step, it.value) }
                 }
-                .filterNotNull()
                 .distinctUntilChanged()
+                .mapNotNull { stateToValue -> stateToValue.value }
         }
 
         /**
@@ -138,4 +173,14 @@
             return sharedFlow(duration = 1.milliseconds, onStep = { value }, onFinish = { value })
         }
     }
+
+    data class Edge(
+        val from: KeyguardState?,
+        val to: KeyguardState?,
+    )
+
+    data class StateToValue(
+        val transitionState: TransitionState,
+        val value: Float?,
+    )
 }
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 05fe0b2..bf763b4 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
@@ -16,13 +16,20 @@
 
 package com.android.systemui.keyguard.ui.binder
 
+import android.animation.Animator
+import android.animation.ValueAnimator
+import android.transition.Transition
 import android.transition.TransitionManager
+import android.transition.TransitionSet
+import android.transition.TransitionValues
+import android.view.ViewGroup
 import androidx.annotation.VisibleForTesting
 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.app.animation.Interpolators
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
@@ -32,6 +39,8 @@
 import com.android.systemui.res.R
 import kotlinx.coroutines.launch
 
+private const val KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS = 1000L
+
 object KeyguardClockViewBinder {
     @JvmStatic
     fun bind(
@@ -69,7 +78,13 @@
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
                     viewModel.clockShouldBeCentered.collect {
-                        applyConstraints(clockSection, keyguardRootView, true)
+                        viewModel.clock?.let {
+                            if (it.largeClock.config.hasCustomPositionUpdatedAnimation) {
+                                playClockCenteringAnimation(clockSection, keyguardRootView, it)
+                            } else {
+                                applyConstraints(clockSection, keyguardRootView, true)
+                            }
+                        }
                     }
                 }
             }
@@ -125,12 +140,84 @@
         clockSection: ClockSection,
         rootView: ConstraintLayout,
         animated: Boolean,
+        set: TransitionSet? = null,
     ) {
         val constraintSet = ConstraintSet().apply { clone(rootView) }
         clockSection.applyConstraints(constraintSet)
         if (animated) {
-            TransitionManager.beginDelayedTransition(rootView)
+            set?.let { TransitionManager.beginDelayedTransition(rootView, it) }
+                ?: run { TransitionManager.beginDelayedTransition(rootView) }
         }
         constraintSet.applyTo(rootView)
     }
+
+    private fun playClockCenteringAnimation(
+        clockSection: ClockSection,
+        keyguardRootView: ConstraintLayout,
+        clock: ClockController,
+    ) {
+        // Find the clock, so we can exclude it from this transition.
+        val clockView = clock.largeClock.view
+        val set = TransitionSet()
+        val adapter = SplitShadeTransitionAdapter(clock)
+        adapter.setInterpolator(Interpolators.LINEAR)
+        adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS)
+        adapter.addTarget(clockView)
+        set.addTransition(adapter)
+        applyConstraints(clockSection, keyguardRootView, true, set)
+    }
+
+    internal class SplitShadeTransitionAdapter
+    @VisibleForTesting
+    constructor(private val clock: ClockController) : Transition() {
+        private fun captureValues(transitionValues: TransitionValues) {
+            transitionValues.values[PROP_BOUNDS_LEFT] = transitionValues.view.left
+            val locationInWindowTmp = IntArray(2)
+            transitionValues.view.getLocationInWindow(locationInWindowTmp)
+            transitionValues.values[PROP_X_IN_WINDOW] = locationInWindowTmp[0]
+        }
+
+        override fun captureEndValues(transitionValues: TransitionValues) {
+            captureValues(transitionValues)
+        }
+
+        override fun captureStartValues(transitionValues: TransitionValues) {
+            captureValues(transitionValues)
+        }
+
+        override fun createAnimator(
+            sceneRoot: ViewGroup,
+            startValues: TransitionValues?,
+            endValues: TransitionValues?
+        ): Animator? {
+            if (startValues == null || endValues == null) {
+                return null
+            }
+            val anim = ValueAnimator.ofFloat(0f, 1f)
+            val fromLeft = startValues.values[PROP_BOUNDS_LEFT] as Int
+            val fromWindowX = startValues.values[PROP_X_IN_WINDOW] as Int
+            val toWindowX = endValues.values[PROP_X_IN_WINDOW] as Int
+            // Using windowX, to determine direction, instead of left, as in RTL the difference of
+            // toLeft - fromLeft is always positive, even when moving left.
+            val direction = if (toWindowX - fromWindowX > 0) 1 else -1
+            anim.addUpdateListener { animation: ValueAnimator ->
+                clock.largeClock.animations.onPositionUpdated(
+                    fromLeft,
+                    direction,
+                    animation.animatedFraction
+                )
+            }
+            return anim
+        }
+
+        override fun getTransitionProperties(): Array<String> {
+            return TRANSITION_PROPERTIES
+        }
+
+        companion object {
+            private const val PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft"
+            private const val PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow"
+            private val TRANSITION_PROPERTIES = arrayOf(PROP_BOUNDS_LEFT, PROP_X_IN_WINDOW)
+        }
+    }
 }
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 92270ad..81ce8f0 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
@@ -23,6 +23,7 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags.migrateClocksToBlueprint
 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
@@ -42,6 +43,7 @@
         keyguardRootView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
+                    if (!migrateClocksToBlueprint()) return@launch
                     clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
                         ->
                         if (hasCustomWeatherDataDisplay) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt
index f2b28d9..f33aed0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt
@@ -29,4 +29,8 @@
     ConstraintLayout(
         context,
         attrs,
-    )
+    ) {
+    init {
+        clipChildren = false
+    }
+}
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 0f8e673..756a4cc 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,11 +23,13 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -35,6 +37,7 @@
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 
@@ -53,6 +56,7 @@
     notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     @Main mainDispatcher: CoroutineDispatcher,
+    private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) :
     NotificationStackScrollLayoutSection(
         context,
@@ -72,7 +76,13 @@
         }
         constraintSet.apply {
             val splitShadeTopMargin =
-                context.resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)
+                if (centralizedStatusBarDimensRefactor()) {
+                    largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+                } else {
+                    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)
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
index a8e3be7..b4b48a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
@@ -19,7 +19,6 @@
 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
@@ -38,14 +37,14 @@
 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),
+            from = KeyguardState.ALTERNATE_BOUNCER,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
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 5d6b0ce..3737e6f 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,7 +18,6 @@
 
 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
@@ -37,14 +36,14 @@
 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),
+            from = ALTERNATE_BOUNCER,
+            to = KeyguardState.GONE,
         )
 
     /** Scrim alpha values */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
index 8e729f7..2526f0a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
@@ -19,7 +19,6 @@
 
 import android.graphics.Color
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TRANSITION_DURATION_MS
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -37,7 +36,6 @@
 @Inject
 constructor(
     private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
-    transitionInteractor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
     // When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be:
@@ -46,7 +44,8 @@
         animationFlow
             .setup(
                 duration = TRANSITION_DURATION_MS,
-                stepFlow = transitionInteractor.anyStateToAlternateBouncerTransition,
+                from = null,
+                to = ALTERNATE_BOUNCER,
             )
             .sharedFlow(
                 duration = TRANSITION_DURATION_MS,
@@ -60,7 +59,8 @@
         animationFlow
             .setup(
                 TRANSITION_DURATION_MS,
-                transitionInteractor.transitionStepsFromState(ALTERNATE_BOUNCER),
+                from = ALTERNATE_BOUNCER,
+                to = null,
             )
             .sharedFlow(
                 duration = TRANSITION_DURATION_MS,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
index 2b14521..b92a9a0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
-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
@@ -31,14 +30,14 @@
 class AodToGoneTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromAodTransitionInteractor.TO_GONE_DURATION,
-            stepFlow = interactor.transition(KeyguardState.AOD, KeyguardState.GONE),
+            from = KeyguardState.AOD,
+            to = KeyguardState.GONE,
         )
 
     override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
index 5e552e1..266fd02 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-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
@@ -37,7 +37,6 @@
 class AodToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
@@ -45,7 +44,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.aodToLockscreenTransition,
+            from = KeyguardState.AOD,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     /** Ensure alpha is set to be visible */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
index d283af3..105a7ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
-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
@@ -29,13 +28,13 @@
 class AodToOccludedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
-            stepFlow = interactor.transition(KeyguardState.AOD, KeyguardState.OCCLUDED),
+            from = KeyguardState.AOD,
+            to = KeyguardState.OCCLUDED,
         )
 
     override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
index 41dc157..924fc5d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
@@ -41,7 +40,6 @@
 class BouncerToGoneFlows
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
     private val keyguardDismissActionInteractor: Lazy<KeyguardDismissActionInteractor>,
@@ -76,7 +74,8 @@
         val transitionAnimation =
             animationFlow.setup(
                 duration = duration,
-                stepFlow = interactor.transition(fromState, GONE)
+                from = fromState,
+                to = GONE,
             )
 
         return shadeInteractor.shadeExpansion.flatMapLatest { shadeExpansion ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
index 0b34326..e4610c1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromDozingTransitionInteractor
-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
@@ -34,13 +34,13 @@
 class DozingToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromDozingTransitionInteractor.TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.dozingToLockscreenTransition,
+            from = KeyguardState.DOZING,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt
index 8bcf3f8..67568e1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromDreamingLockscreenHostedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class DreamingHostedToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.dreamingLockscreenHostedToLockscreenTransition,
+            from = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 5f620af..ead2d48 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 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.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -52,7 +53,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = keyguardTransitionInteractor.dreamingToLockscreenTransition,
+            from = KeyguardState.DREAMING,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val transitionEnded =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
index 3f27eb0..ba04fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_AOD_DURATION
-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
@@ -36,7 +36,6 @@
 class GoneToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
@@ -44,7 +43,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_AOD_DURATION,
-            stepFlow = interactor.goneToAodTransition,
+            from = KeyguardState.GONE,
+            to = KeyguardState.AOD,
         )
 
     /** y-translation from the top of the screen for AOD */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt
index bba790a..b527463 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -32,14 +32,14 @@
 class GoneToDreamingLockscreenHostedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_DURATION,
-            stepFlow = interactor.goneToDreamingLockscreenHostedTransition,
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
         )
 
     /** Lockscreen views alpha - hide immediately */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
index 6762ba6..102242a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -30,14 +30,14 @@
 class GoneToDreamingTransitionViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_DURATION,
-            stepFlow = interactor.goneToDreamingTransition,
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING,
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
index adae8ab..793abb4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class GoneToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.goneToLockscreenTransition
+            from = KeyguardState.GONE,
+            to = KeyguardState.LOCKSCREEN
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
index 65614f4..7bf51a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-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
@@ -36,7 +36,6 @@
 class LockscreenToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
@@ -45,7 +44,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromLockscreenTransitionInteractor.TO_AOD_DURATION,
-            stepFlow = interactor.lockscreenToAodTransition,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
index accb20c..4c0cd2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DOZING_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class LockscreenToDozingTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DOZING_DURATION,
-            stepFlow = interactor.lockscreenToDozingTransition
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.DOZING,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt
index c649b12..19b9cf47 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_HOSTED_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class LockscreenToDreamingHostedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_HOSTED_DURATION,
-            stepFlow = interactor.lockscreenToDreamingLockscreenHostedTransition
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index 7f75b54..13522a6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
-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
@@ -34,14 +34,14 @@
 class LockscreenToDreamingTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_DURATION,
-            stepFlow = interactor.lockscreenToDreamingTransition,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.DREAMING,
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
index 9e19713..a26ef07 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-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
@@ -35,14 +34,14 @@
 class LockscreenToGoneTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromLockscreenTransitionInteractor.TO_GONE_DURATION,
-            stepFlow = interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.GONE,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 9db0b77..dd6652e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
-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 com.android.systemui.res.R
@@ -37,7 +37,6 @@
 class LockscreenToOccludedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     configurationInteractor: ConfigurationInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
@@ -46,7 +45,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_OCCLUDED_DURATION,
-            stepFlow = interactor.lockscreenToOccludedTransition,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.OCCLUDED,
         )
 
     /** Lockscreen views alpha */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 52e3257..ce47f3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-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
@@ -26,7 +25,6 @@
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to
@@ -37,21 +35,21 @@
 class LockscreenToPrimaryBouncerTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
-            stepFlow =
-                interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER),
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.PRIMARY_BOUNCER,
         )
 
     val shortcutsAlpha: Flow<Float> =
-        interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER).map {
-            1 - it.value
-        }
+        transitionAnimation.sharedFlow(
+            duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+            onStep = { 1f - it }
+        )
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         shadeDependentFlows.transitionFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
index ed5e83c..07c1141 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor
-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
@@ -35,14 +34,14 @@
 class OccludedToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromOccludedTransitionInteractor.TO_AOD_DURATION,
-            stepFlow = interactor.transition(KeyguardState.OCCLUDED, KeyguardState.AOD),
+            from = KeyguardState.OCCLUDED,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index 4c24f83..90195bd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-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 com.android.systemui.res.R
@@ -41,7 +41,6 @@
 class OccludedToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     configurationInteractor: ConfigurationInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
@@ -50,7 +49,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.occludedToLockscreenTransition,
+            from = KeyguardState.OCCLUDED,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
index 93482ea..74094be 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -27,14 +27,14 @@
 class OffToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = 250.milliseconds,
-            stepFlow = interactor.offToLockscreenTransition
+            from = KeyguardState.OFF,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
index b0e2aa2..cd8e2f1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
-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
@@ -39,14 +38,14 @@
 class PrimaryBouncerToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromPrimaryBouncerTransitionInteractor.TO_AOD_DURATION,
-            stepFlow = interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.AOD),
+            from = KeyguardState.PRIMARY_BOUNCER,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 9dbe97f..4f28b46 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
@@ -44,7 +43,6 @@
 class PrimaryBouncerToGoneTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
     keyguardDismissActionInteractor: Lazy<KeyguardDismissActionInteractor>,
@@ -55,7 +53,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_GONE_DURATION,
-            stepFlow = interactor.transition(PRIMARY_BOUNCER, GONE)
+            from = PRIMARY_BOUNCER,
+            to = GONE,
         )
 
     private var leaveShadeOpen: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index b2eed60..284a134 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
-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
@@ -28,7 +27,6 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -39,15 +37,14 @@
 class PrimaryBouncerToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
-            stepFlow =
-                interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.LOCKSCREEN),
+            from = KeyguardState.PRIMARY_BOUNCER,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
@@ -60,9 +57,10 @@
         }
 
     val shortcutsAlpha: Flow<Float> =
-        interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.LOCKSCREEN).map {
-            it.value
-        }
+        transitionAnimation.sharedFlow(
+            duration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
+            onStep = { it }
+        )
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(1f)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index b98e9c2..5caa27f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -81,9 +81,12 @@
     /** Set from the notification and used as fallback when PlaybackState cannot be determined */
     val isClearable: Boolean = true,
 
-    /** Timestamp when this player was last active. */
+    /** Milliseconds since boot when this player was last active. */
     var lastActive: Long = 0L,
 
+    /** Timestamp in milliseconds when this player was created. */
+    var createdTimestampMillis: Long = 0L,
+
     /** Instance ID for logging purposes */
     val instanceId: InstanceId,
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 3e8b49d..47df3b7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -400,7 +400,12 @@
             val oldKey = findExistingEntry(key, sbn.packageName)
             if (oldKey == null) {
                 val instanceId = logger.getNewInstanceId()
-                val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId)
+                val temp =
+                    LOADING.copy(
+                        packageName = sbn.packageName,
+                        instanceId = instanceId,
+                        createdTimestampMillis = systemClock.currentTimeMillis(),
+                    )
                 mediaEntries.put(key, temp)
                 isNewlyActiveEntry = true
             } else if (oldKey != key) {
@@ -454,7 +459,8 @@
                     resumeAction = action,
                     hasCheckedForResume = true,
                     instanceId = instanceId,
-                    appUid = appUid
+                    appUid = appUid,
+                    createdTimestampMillis = systemClock.currentTimeMillis(),
                 )
             mediaEntries.put(packageName, resumeData)
             logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
@@ -732,6 +738,7 @@
 
         val mediaAction = getResumeMediaAction(resumeAction)
         val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
         foregroundExecutor.execute {
             onMediaDataLoaded(
                 packageName,
@@ -757,6 +764,7 @@
                     notificationKey = packageName,
                     hasCheckedForResume = true,
                     lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
                     instanceId = instanceId,
                     appUid = appUid,
                     isExplicit = isExplicit,
@@ -907,6 +915,7 @@
         }
 
         val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
         foregroundExecutor.execute {
             val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
             val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
@@ -937,6 +946,7 @@
                     isPlaying = isPlaying,
                     isClearable = !sbn.isOngoing,
                     lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
                     instanceId = instanceId,
                     appUid = appUid,
                     isExplicit = isExplicit,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index 0385aeb..523414c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -1153,7 +1153,7 @@
                 qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
                 onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
                 onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
-                // TODO(b/308813166): revisit logic once interactions between the hub and
+                // TODO(b/311234666): revisit logic once interactions between the hub and
                 //  shade/keyguard state are finalized
                 isCommunalShowing && communalInteractor.isCommunalEnabled -> LOCATION_COMMUNAL_HUB
                 onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index 0320dec..092f1ed 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -302,9 +302,6 @@
             final NavigationBarView navBarView = getNavigationBarView(displayId);
             if (navBarView != null) {
                 navBarView.showPinningEnterExitToast(entering);
-            } else if (displayId == mDisplayTracker.getDefaultDisplayId()
-                    && mTaskbarDelegate.isInitialized()) {
-                mTaskbarDelegate.showPinningEnterExitToast(entering);
             }
         }
 
@@ -314,9 +311,6 @@
             final NavigationBarView navBarView = getNavigationBarView(displayId);
             if (navBarView != null) {
                 navBarView.showPinningEscapeToast();
-            } else if (displayId == mDisplayTracker.getDefaultDisplayId()
-                    && mTaskbarDelegate.isInitialized()) {
-                mTaskbarDelegate.showPinningEscapeToast();
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 62c7343..0167287 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -504,6 +504,11 @@
     }
 
     @Override
+    public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+        mOverviewProxyService.onNavigationBarLumaSamplingEnabled(displayId, enable);
+    }
+
+    @Override
     public void showPinningEnterExitToast(boolean entering) {
         updateSysuiFlags();
         if (mScreenPinningNotify == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
index 6ec46f6..df6843d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
@@ -61,6 +61,7 @@
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.res.R;
+import com.android.systemui.shared.navigationbar.KeyButtonRipple;
 import com.android.systemui.shared.system.QuickStepContract;
 
 public class KeyButtonView extends ImageView implements ButtonInterface {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index e660b97..0d641ac 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -758,7 +758,8 @@
     }
 
     private void updateMLModelState() {
-        boolean newState = mIsGestureHandlingEnabled && DeviceConfig.getBoolean(
+        boolean newState = mIsGestureHandlingEnabled && mContext.getResources().getBoolean(
+                R.bool.config_useBackGestureML) && DeviceConfig.getBoolean(
                 DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false);
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 0644237..a3b9254 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -18,6 +18,8 @@
 
 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
 
+import static com.android.systemui.Flags.centralizedStatusBarDimensRefactor;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Path;
@@ -30,6 +32,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.res.R;
+import com.android.systemui.shade.LargeScreenHeaderHelper;
 import com.android.systemui.shade.TouchLogger;
 import com.android.systemui.util.LargeScreenUtils;
 
@@ -162,8 +165,12 @@
             QuickStatusBarHeaderController quickStatusBarHeaderController) {
         int topPadding = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
         if (!LargeScreenUtils.shouldUseLargeScreenShadeHeader(mContext.getResources())) {
-            topPadding = mContext.getResources()
-                    .getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
+            topPadding =
+                    centralizedStatusBarDimensRefactor()
+                            ? LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext)
+                            : mContext.getResources()
+                                    .getDimensionPixelSize(
+                                            R.dimen.large_screen_shade_header_height);
         }
         mQSPanelContainer.setPaddingRelative(
                 mQSPanelContainer.getPaddingStart(),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index ddd7d67..51b94dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -189,6 +189,7 @@
     public void setBrightnessView(@NonNull View view) {
         if (mBrightnessView != null) {
             removeView(mBrightnessView);
+            mChildrenLayoutTop.remove(mBrightnessView);
             mMovableContentStartIndex--;
         }
         addView(view, 0);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 5eb9620..ef58a60 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -56,14 +56,18 @@
     private final QSCustomizerController mQsCustomizerController;
     private final QSTileRevealController.Factory mQsTileRevealControllerFactory;
     private final FalsingManager mFalsingManager;
-    private final BrightnessController mBrightnessController;
-    private final BrightnessSliderController mBrightnessSliderController;
-    private final BrightnessMirrorHandler mBrightnessMirrorHandler;
+    private BrightnessController mBrightnessController;
+    private BrightnessSliderController mBrightnessSliderController;
+    private BrightnessMirrorHandler mBrightnessMirrorHandler;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private boolean mListening;
 
     private final boolean mSceneContainerEnabled;
 
+    private int mLastDensity;
+    private final BrightnessSliderController.Factory mBrightnessSliderControllerFactory;
+    private final BrightnessController.Factory mBrightnessControllerFactory;
+
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
@@ -93,6 +97,8 @@
         mQsCustomizerController = qsCustomizerController;
         mQsTileRevealControllerFactory = qsTileRevealControllerFactory;
         mFalsingManager = falsingManager;
+        mBrightnessSliderControllerFactory = brightnessSliderFactory;
+        mBrightnessControllerFactory = brightnessControllerFactory;
 
         mBrightnessSliderController = brightnessSliderFactory.create(getContext(), mView);
         mView.setBrightnessView(mBrightnessSliderController.getRootView());
@@ -100,6 +106,7 @@
         mBrightnessController = brightnessControllerFactory.create(mBrightnessSliderController);
         mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController);
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mLastDensity = view.getResources().getConfiguration().densityDpi;
         mSceneContainerEnabled = sceneContainerFlags.isEnabled();
     }
 
@@ -147,11 +154,31 @@
     @Override
     protected void onConfigurationChanged() {
         mView.updateResources();
+        int newDensity = mView.getResources().getConfiguration().densityDpi;
+        if (newDensity != mLastDensity) {
+            mLastDensity = newDensity;
+            reinflateBrightnessSlider();
+        }
+
         if (mView.isListening()) {
             refreshAllTiles();
         }
     }
 
+    private void reinflateBrightnessSlider() {
+        mBrightnessController.unregisterCallbacks();
+        mBrightnessSliderController =
+                mBrightnessSliderControllerFactory.create(getContext(), mView);
+        mView.setBrightnessView(mBrightnessSliderController.getRootView());
+        mBrightnessController = mBrightnessControllerFactory.create(mBrightnessSliderController);
+        mBrightnessMirrorHandler.setBrightnessController(mBrightnessController);
+        mBrightnessSliderController.init();
+        if (mListening) {
+            mBrightnessController.registerCallbacks();
+        }
+    }
+
+
     @Override
     protected void onSplitShadeChanged(boolean shouldUseSplitNotificationShade) {
         ((PagedTileLayout) mView.getOrCreateTileLayout())
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
index 36dc743..a01d658 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
@@ -67,9 +67,7 @@
                 synchronized (mListeners) {
                     if (setting != null && mListeners.size() != 0) {
                         if (setting.equals(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED)) {
-                            for (Listener listener : mListeners) {
-                                listener.onActivated(mManager.isReduceBrightColorsActivated());
-                            }
+                            dispatchOnActivated(mManager.isReduceBrightColorsActivated());
                         }
                     }
                 }
@@ -125,6 +123,13 @@
         mManager.setReduceBrightColorsActivated(activated);
     }
 
+    private void dispatchOnActivated(boolean activated) {
+        ArrayList<Listener> copy = new ArrayList<>(mListeners);
+        for (Listener l : copy) {
+            l.onActivated(activated);
+        }
+    }
+
     /**
      * Listener invoked whenever the Reduce Bright Colors settings are changed.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
index fc06090..fe10eaa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
@@ -18,6 +18,8 @@
 
 import android.app.PendingIntent
 import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
 import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
@@ -32,13 +34,23 @@
 interface QSTileIntentUserInputHandler {
 
     fun handle(view: View?, intent: Intent)
-    fun handle(view: View?, pendingIntent: PendingIntent)
+
+    /** @param requestLaunchingDefaultActivity used in case !pendingIndent.isActivity */
+    fun handle(
+        view: View?,
+        pendingIntent: PendingIntent,
+        requestLaunchingDefaultActivity: Boolean = false
+    )
 }
 
 @SysUISingleton
 class QSTileIntentUserInputHandlerImpl
 @Inject
-constructor(private val activityStarter: ActivityStarter) : QSTileIntentUserInputHandler {
+constructor(
+    private val activityStarter: ActivityStarter,
+    private val packageManager: PackageManager,
+    private val userHandle: UserHandle,
+) : QSTileIntentUserInputHandler {
 
     override fun handle(view: View?, intent: Intent) {
         val animationController: ActivityLaunchAnimator.Controller? =
@@ -52,21 +64,41 @@
     }
 
     // TODO(b/249804373): make sure to allow showing activities over the lockscreen. See b/292112939
-    override fun handle(view: View?, pendingIntent: PendingIntent) {
-        if (!pendingIntent.isActivity) {
-            return
-        }
-        val animationController: ActivityLaunchAnimator.Controller? =
-            view?.let {
-                ActivityLaunchAnimator.Controller.fromView(
-                    it,
-                    InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
+    override fun handle(
+        view: View?,
+        pendingIntent: PendingIntent,
+        requestLaunchingDefaultActivity: Boolean
+    ) {
+        if (pendingIntent.isActivity) {
+            val animationController: ActivityLaunchAnimator.Controller? =
+                view?.let {
+                    ActivityLaunchAnimator.Controller.fromView(
+                        it,
+                        InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
+                    )
+                }
+            activityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController)
+        } else if (requestLaunchingDefaultActivity) {
+            val intent =
+                Intent(Intent.ACTION_MAIN)
+                    .addCategory(Intent.CATEGORY_LAUNCHER)
+                    .setPackage(pendingIntent.creatorPackage)
+                    .addFlags(
+                        Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                    )
+            val intents =
+                packageManager.queryIntentActivitiesAsUser(
+                    intent,
+                    PackageManager.ResolveInfoFlags.of(0L),
+                    userHandle.identifier
                 )
-            }
-        activityStarter.startPendingIntentMaybeDismissingKeyguard(
-            pendingIntent,
-            null,
-            animationController
-        )
+            intents
+                .firstOrNull { it.activityInfo.exported }
+                ?.let { resolved ->
+                    intent.setPackage(null)
+                    intent.setComponent(resolved.activityInfo.componentName)
+                    handle(view, intent)
+                }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
index e075e76..2b8c335 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
+import com.android.systemui.util.time.SystemClock
 import java.time.Instant
 import java.time.LocalDateTime
 import java.time.format.DateTimeFormatter
@@ -36,10 +37,12 @@
 constructor(
     @Main private val resources: Resources,
     private val theme: Theme,
+    private val clock: SystemClock,
 ) : QSTileDataToStateMapper<AlarmTileModel> {
     companion object {
         val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("E hh:mm a")
         val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("E HH:mm")
+        val formatterDateOnly: DateTimeFormatter = DateTimeFormatter.ofPattern("E MMM d")
     }
     override fun map(config: QSTileConfig, data: AlarmTileModel): QSTileState =
         QSTileState.build(resources, theme, config.uiConfig) {
@@ -47,14 +50,32 @@
                 is AlarmTileModel.NextAlarmSet -> {
                     activationState = QSTileState.ActivationState.ACTIVE
 
-                    val localDateTime =
+                    val alarmDateTime =
                         LocalDateTime.ofInstant(
                             Instant.ofEpochMilli(data.alarmClockInfo.triggerTime),
                             TimeZone.getDefault().toZoneId()
                         )
-                    secondaryLabel =
-                        if (data.is24HourFormat) formatter24Hour.format(localDateTime)
-                        else formatter12Hour.format(localDateTime)
+
+                    val nowDateTime =
+                        LocalDateTime.ofInstant(
+                            Instant.ofEpochMilli(clock.currentTimeMillis()),
+                            TimeZone.getDefault().toZoneId()
+                        )
+
+                    // Edge case: If it's 8:00:30 right now and alarm is requested for next week at
+                    // 8:00:29, we still want to show the date. Same at nanosecond level.
+                    val nextWeekThisTime = nowDateTime.plusWeeks(1).withSecond(0).withNano(0)
+
+                    // is the alarm over a week away?
+                    val shouldShowDateAndHideTime = alarmDateTime >= nextWeekThisTime
+
+                    if (shouldShowDateAndHideTime) {
+                        secondaryLabel = formatterDateOnly.format(alarmDateTime)
+                    } else {
+                        secondaryLabel =
+                            if (data.is24HourFormat) formatter24Hour.format(alarmDateTime)
+                            else formatter12Hour.format(alarmDateTime)
+                    }
                 }
                 is AlarmTileModel.NoAlarmSet -> {
                     activationState = QSTileState.ActivationState.INACTIVE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
index afca57c..0ad520b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
@@ -18,9 +18,7 @@
 
 import android.content.Intent
 import android.provider.AlarmClock
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.plugins.ActivityStarter
+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.alarm.domain.model.AlarmTileModel
@@ -31,34 +29,20 @@
 class AlarmTileUserActionInteractor
 @Inject
 constructor(
-    private val activityStarter: ActivityStarter,
+    private val inputHandler: QSTileIntentUserInputHandler,
 ) : QSTileUserActionInteractor<AlarmTileModel> {
     override suspend fun handleInput(input: QSTileInput<AlarmTileModel>): Unit =
         with(input) {
             when (action) {
                 is QSTileUserAction.Click -> {
-                    val animationController =
-                        action.view?.let {
-                            ActivityLaunchAnimator.Controller.fromView(
-                                it,
-                                InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
-                            )
-                        }
                     if (
                         data is AlarmTileModel.NextAlarmSet &&
                             data.alarmClockInfo.showIntent != null
                     ) {
                         val pendingIndent = data.alarmClockInfo.showIntent
-                        activityStarter.postStartActivityDismissingKeyguard(
-                            pendingIndent,
-                            animationController
-                        )
+                        inputHandler.handle(action.view, pendingIndent, true)
                     } else {
-                        activityStarter.postStartActivityDismissingKeyguard(
-                            Intent(AlarmClock.ACTION_SHOW_ALARMS),
-                            0,
-                            animationController
-                        )
+                        inputHandler.handle(action.view, Intent(AlarmClock.ACTION_SHOW_ALARMS))
                     }
                 }
                 is QSTileUserAction.LongClick -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 45917e8..fd53423 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -1037,6 +1037,19 @@
         }
     }
 
+    public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+        try {
+            if (mOverviewProxy != null) {
+                mOverviewProxy.onNavigationBarLumaSamplingEnabled(displayId, enable);
+            } else {
+                Log.e(TAG_OPS, "Failed to get overview proxy to enable/disable nav bar luma"
+                        + "sampling");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG_OPS, "Failed to call onNavigationBarLumaSamplingEnabled()", e);
+        }
+    }
+
     private void updateEnabledState() {
         final int currentUser = mUserTracker.getUserId();
         mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent,
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 5abb4dd..c96651c 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
@@ -44,6 +44,7 @@
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.printSection
@@ -83,6 +84,7 @@
     private val powerInteractor: PowerInteractor,
     private val simBouncerInteractor: Lazy<SimBouncerInteractor>,
     private val authenticationInteractor: Lazy<AuthenticationInteractor>,
+    private val windowController: NotificationShadeWindowController,
 ) : CoreStartable {
 
     override fun start() {
@@ -92,6 +94,7 @@
             automaticallySwitchScenes()
             hydrateSystemUiState()
             collectFalsingSignals()
+            hydrateWindowFocus()
         } else {
             sceneLogger.logFrameworkEnabled(
                 isEnabled = false,
@@ -348,6 +351,20 @@
         }
     }
 
+    /** Keeps the focus state of the window view up-to-date. */
+    private fun hydrateWindowFocus() {
+        applicationScope.launch {
+            sceneInteractor.transitionState
+                .mapNotNull { transitionState ->
+                    (transitionState as? ObservableTransitionState.Idle)?.scene
+                }
+                .distinctUntilChanged()
+                .collect { sceneKey ->
+                    windowController.setNotificationShadeFocusable(sceneKey != SceneKey.Gone)
+                }
+        }
+    }
+
     private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) {
         sceneInteractor.changeScene(
             scene = SceneModel(targetSceneKey),
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index bd43307..7aa0dad 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -32,6 +32,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -143,6 +144,7 @@
         channel.enableVibration(true);
         mNotificationManager.createNotificationChannel(channel);
 
+        int currentUid = Process.myUid();
         int currentUserId = mUserContextTracker.getUserContext().getUserId();
         UserHandle currentUser = new UserHandle(currentUserId);
         switch (action) {
@@ -166,7 +168,7 @@
                 mRecorder = new ScreenMediaRecorder(
                         mUserContextTracker.getUserContext(),
                         mMainHandler,
-                        currentUserId,
+                        currentUid,
                         mAudioSource,
                         captureTarget,
                         this
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index 3aab3bf..a170d0da 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -83,7 +83,7 @@
     private Surface mInputSurface;
     private VirtualDisplay mVirtualDisplay;
     private MediaRecorder mMediaRecorder;
-    private int mUser;
+    private int mUid;
     private ScreenRecordingMuxer mMuxer;
     private ScreenInternalAudioRecorder mAudio;
     private ScreenRecordingAudioSource mAudioSource;
@@ -94,12 +94,12 @@
     ScreenMediaRecorderListener mListener;
 
     public ScreenMediaRecorder(Context context, Handler handler,
-            int user, ScreenRecordingAudioSource audioSource,
+            int uid, ScreenRecordingAudioSource audioSource,
             MediaProjectionCaptureTarget captureRegion,
             ScreenMediaRecorderListener listener) {
         mContext = context;
         mHandler = handler;
-        mUser = user;
+        mUid = uid;
         mCaptureRegion = captureRegion;
         mListener = listener;
         mAudioSource = audioSource;
@@ -111,7 +111,7 @@
         IMediaProjectionManager mediaService =
                 IMediaProjectionManager.Stub.asInterface(b);
         IMediaProjection proj = null;
-        proj = mediaService.createProjection(mUser, mContext.getPackageName(),
+        proj = mediaService.createProjection(mUid, mContext.getPackageName(),
                     MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
         IMediaProjection projection = IMediaProjection.Stub.asInterface(proj.asBinder());
         if (mCaptureRegion != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
index 51aa339..701d814 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
@@ -19,9 +19,16 @@
 import com.android.systemui.statusbar.policy.BrightnessMirrorController
 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener
 
-class BrightnessMirrorHandler(private val brightnessController: MirroredBrightnessController) {
+class BrightnessMirrorHandler(brightnessController: MirroredBrightnessController) {
 
-    private var mirrorController: BrightnessMirrorController? = null
+    var mirrorController: BrightnessMirrorController? = null
+        private set
+
+    var brightnessController: MirroredBrightnessController = brightnessController
+        set(value) {
+            field = value
+            updateBrightnessMirror()
+        }
 
     private val brightnessMirrorListener = BrightnessMirrorListener { updateBrightnessMirror() }
 
@@ -33,7 +40,7 @@
         mirrorController?.removeCallback(brightnessMirrorListener)
     }
 
-    fun setController(controller: BrightnessMirrorController) {
+    fun setController(controller: BrightnessMirrorController?) {
         mirrorController?.removeCallback(brightnessMirrorListener)
         mirrorController = controller
         mirrorController?.addCallback(brightnessMirrorListener)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 3be60b7..782d651 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.shade
 
 import android.content.Context
+import android.os.PowerManager
+import android.os.SystemClock
 import android.view.GestureDetector
 import android.view.MotionEvent
 import android.view.View
@@ -44,6 +46,7 @@
     private val communalViewModel: CommunalViewModel,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val shadeInteractor: ShadeInteractor,
+    private val powerManager: PowerManager,
 ) {
     /** The container view for the hub. This will not be initialized until [initView] is called. */
     private lateinit var communalContainerView: View
@@ -157,7 +160,7 @@
         // If the hub is fully visible, send all touch events to it.
         val communalVisible = hubShowing && !hubOccluded
         if (communalVisible) {
-            communalContainerView.dispatchTouchEvent(ev)
+            dispatchTouchEvent(ev)
             // Return true regardless of dispatch result as some touches at the start of a gesture
             // may return false from dispatchTouchEvent.
             return true
@@ -175,7 +178,7 @@
                 x >= communalContainerView.width - edgeSwipeRegionWidth
             if (inOpeningSwipeRegion && !hubOccluded) {
                 isTrackingOpenGesture = true
-                communalContainerView.dispatchTouchEvent(ev)
+                dispatchTouchEvent(ev)
                 // Return true regardless of dispatch result as some touches at the start of a
                 // gesture may return false from dispatchTouchEvent.
                 return true
@@ -184,7 +187,7 @@
             if (isUp || isCancel) {
                 isTrackingOpenGesture = false
             }
-            communalContainerView.dispatchTouchEvent(ev)
+            dispatchTouchEvent(ev)
             // Return true regardless of dispatch result as some touches at the start of a gesture
             // may return false from dispatchTouchEvent.
             return true
@@ -192,4 +195,17 @@
 
         return false
     }
+
+    /**
+     * Dispatches the touch event to the communal container and sends a user activity event to reset
+     * the screen timeout.
+     */
+    private fun dispatchTouchEvent(ev: MotionEvent) {
+        communalContainerView.dispatchTouchEvent(ev)
+        powerManager.userActivity(
+            SystemClock.uptimeMillis(),
+            PowerManager.USER_ACTIVITY_EVENT_TOUCH,
+            0
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenHeaderHelper.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenHeaderHelper.kt
new file mode 100644
index 0000000..c74f038
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenHeaderHelper.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.systemui.shade
+
+import android.content.Context
+import com.android.internal.policy.SystemBarUtils
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlin.math.max
+
+class LargeScreenHeaderHelper @Inject constructor(private val context: Context) {
+
+    fun getLargeScreenHeaderHeight(): Int = getLargeScreenHeaderHeight(context)
+
+    companion object {
+        @JvmStatic
+        fun getLargeScreenHeaderHeight(context: Context): Int {
+            val defaultHeight =
+                context.resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)
+            val statusBarHeight = SystemBarUtils.getStatusBarHeight(context)
+            // Height has to be at least as tall as the status bar, as the status bar height takes
+            // into account display cutouts.
+            return max(defaultHeight, statusBarHeight)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index a5c1cf1..aeccf00 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -27,6 +27,7 @@
 import static com.android.systemui.Flags.keyguardBottomAreaRefactor;
 import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.Flags.predictiveBackAnimateShade;
+import static com.android.systemui.Flags.smartspaceRelocateToBottom;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
 import static com.android.systemui.classifier.Classifier.GENERIC;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
@@ -1214,7 +1215,7 @@
                 .setMaxLengthSeconds(0.4f).build();
         mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
         mStatusBarHeaderHeightKeyguard = Utils.getStatusBarHeaderHeightKeyguard(mView.getContext());
-        mClockPositionAlgorithm.loadDimens(mResources);
+        mClockPositionAlgorithm.loadDimens(mView.getContext(), mResources);
         mIndicationBottomPadding = mResources.getDimensionPixelSize(
                 R.dimen.keyguard_indication_bottom_padding);
         int statusbarHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
@@ -1428,7 +1429,12 @@
             int index = mView.indexOfChild(mKeyguardBottomArea);
             mView.removeView(mKeyguardBottomArea);
             KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea;
-            setKeyguardBottomArea(mKeyguardBottomAreaViewControllerProvider.get().getView());
+            KeyguardBottomAreaViewController keyguardBottomAreaViewController =
+                    mKeyguardBottomAreaViewControllerProvider.get();
+            if (smartspaceRelocateToBottom()) {
+                keyguardBottomAreaViewController.init();
+            }
+            setKeyguardBottomArea(keyguardBottomAreaViewController.getView());
             mKeyguardBottomArea.initFrom(oldBottomArea);
             mView.addView(mKeyguardBottomArea, index);
 
@@ -1751,14 +1757,9 @@
         } else {
             layout = mNotificationContainerParent;
         }
-
-        if (migrateClocksToBlueprint()) {
-            mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered);
-        } else {
-            mKeyguardStatusViewController.updateAlignment(
-                    layout, mSplitShadeEnabled, shouldBeCentered, animate);
-            mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
-        }
+        mKeyguardStatusViewController.updateAlignment(
+                layout, mSplitShadeEnabled, shouldBeCentered, animate);
+        mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
     }
 
     private boolean shouldKeyguardStatusViewBeCentered() {
@@ -4362,8 +4363,7 @@
         @Override
         public void onHeadsUpPinned(NotificationEntry entry) {
             if (!isKeyguardShowing()) {
-                mNotificationStackScrollLayoutController.generateHeadsUpAnimation(
-                        entry.getHeadsUpAnimationView(), true);
+                mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true);
             }
         }
 
@@ -4375,8 +4375,7 @@
             // notification
             // will stick to the top without any interaction.
             if (isFullyCollapsed() && entry.isRowHeadsUp() && !isKeyguardShowing()) {
-                mNotificationStackScrollLayoutController.generateHeadsUpAnimation(
-                        entry.getHeadsUpAnimationView(), false);
+                mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, false);
                 entry.setHeadsUpIsVisible();
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 9c8a286..84cad1d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -27,6 +27,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.lifecycle.lifecycleScope
+import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
@@ -47,10 +48,11 @@
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.ViewController
 import com.android.systemui.util.concurrency.DelayableExecutor
-import kotlinx.coroutines.launch
+import dagger.Lazy
 import java.util.function.Consumer
 import javax.inject.Inject
 import kotlin.reflect.KMutableProperty0
+import kotlinx.coroutines.launch
 
 @VisibleForTesting
 internal const val INSET_DEBOUNCE_MILLIS = 500L
@@ -67,7 +69,8 @@
         private val featureFlags: FeatureFlags,
         private val
             notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
-        private val splitShadeStateController: SplitShadeStateController
+        private val splitShadeStateController: SplitShadeStateController,
+        private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
 
     private var splitShadeEnabled = false
@@ -186,7 +189,11 @@
     }
 
     private fun calculateLargeShadeHeaderHeight(): Int {
-        return resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)
+        return if (centralizedStatusBarDimensRefactor()) {
+            largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+        } else {
+            resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)
+        }
     }
 
     private fun calculateShadeHeaderHeight(): Int {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 1dff99d..f3e9c75 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -20,6 +20,7 @@
 import static android.view.WindowInsets.Type.ime;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
+import static com.android.systemui.Flags.centralizedStatusBarDimensRefactor;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS;
 import static com.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE;
@@ -126,6 +127,7 @@
     private final Lazy<NotificationPanelViewController> mPanelViewControllerLazy;
 
     private final NotificationPanelView mPanelView;
+    private final Lazy<LargeScreenHeaderHelper> mLargeScreenHeaderHelperLazy;
     private final KeyguardStatusBarView mKeyguardStatusBar;
     private final FrameLayout mQsFrame;
 
@@ -344,10 +346,12 @@
             ActiveNotificationsInteractor activeNotificationsInteractor,
             JavaAdapter javaAdapter,
             CastController castController,
-            SplitShadeStateController splitShadeStateController
+            SplitShadeStateController splitShadeStateController,
+            Lazy<LargeScreenHeaderHelper> largeScreenHeaderHelperLazy
     ) {
         mPanelViewControllerLazy = panelViewControllerLazy;
         mPanelView = panelView;
+        mLargeScreenHeaderHelperLazy = largeScreenHeaderHelperLazy;
         mQsFrame = mPanelView.findViewById(R.id.qs_frame);
         mKeyguardStatusBar = mPanelView.findViewById(R.id.keyguard_header);
         mResources = mPanelView.getResources();
@@ -449,7 +453,10 @@
         mUseLargeScreenShadeHeader =
                 LargeScreenUtils.shouldUseLargeScreenShadeHeader(mPanelView.getResources());
         mLargeScreenShadeHeaderHeight =
-                mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
+                centralizedStatusBarDimensRefactor()
+                        ? mLargeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+                        : mResources.getDimensionPixelSize(
+                                R.dimen.large_screen_shade_header_height);
         int topMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight :
                 mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_top);
         mShadeHeaderController.setLargeScreenActive(mUseLargeScreenShadeHeader);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index 2460a33..a66bacd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -38,6 +38,7 @@
 import com.android.app.animation.Interpolators
 import com.android.settingslib.Utils
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.battery.BatteryMeterViewController
@@ -432,6 +433,9 @@
             changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
         }
 
+        if (centralizedStatusBarDimensRefactor()) {
+            view.setPadding(view.paddingLeft, sbInsets.top, view.paddingRight, view.paddingBottom)
+        }
         view.updateAllConstraints(changes)
         updateBatteryMode()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 08415cb..d6d3e67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -31,6 +31,7 @@
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.IMPORTANT_MSG_MIN_DURATION;
+import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_IS_DISMISSIBLE;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE;
@@ -178,6 +179,7 @@
     private KeyguardInteractor mKeyguardInteractor;
     private String mPersistentUnlockMessage;
     private String mAlignmentIndication;
+    private boolean mForceIsDismissible;
     private CharSequence mTrustGrantedIndication;
     private CharSequence mTransientIndication;
     private CharSequence mBiometricMessage;
@@ -442,6 +444,7 @@
 
         // Update persistent messages. The following methods should only be called if we're on the
         // lock screen:
+        updateForceIsDimissibileChanged();
         updateLockScreenDisclosureMsg();
         updateLockScreenOwnerInfo();
         updateLockScreenBatteryMsg(animate);
@@ -458,6 +461,22 @@
         updateDeviceEntryIndication(false);
     }
 
+    private void updateForceIsDimissibileChanged() {
+        if (mForceIsDismissible) {
+            mRotateTextViewController.updateIndication(
+                    INDICATION_IS_DISMISSIBLE,
+                    new KeyguardIndication.Builder()
+                            .setMessage(mContext.getResources().getString(
+                                    com.android.systemui.res.R.string.dismissible_keyguard_swipe)
+                            )
+                            .setTextColor(mInitialTextColorState)
+                            .build(),
+                    /* updateImmediately */ true);
+        } else {
+            mRotateTextViewController.hideIndication(INDICATION_IS_DISMISSIBLE);
+        }
+    }
+
     private void updateLockScreenDisclosureMsg() {
         if (mOrganizationOwnedDevice) {
             mBackgroundExecutor.execute(() -> {
@@ -1311,6 +1330,12 @@
         }
 
         @Override
+        public void onForceIsDismissibleChanged(boolean forceIsDismissible) {
+            mForceIsDismissible = forceIsDismissible;
+            updateDeviceEntryIndication(false);
+        }
+
+        @Override
         public void onTrustGrantedForCurrentUser(
                 boolean dismissKeyguard,
                 boolean newlyUnlocked,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 39b7930..6e85074 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -253,6 +253,8 @@
     initialHeight: Int? = null
 ) : View(context, attrs) {
 
+    private val logString = this::class.simpleName!! + "@" + hashCode()
+
     /** Listener that is called if the scrim's opaqueness changes */
     var isScrimOpaqueChangedListener: Consumer<Boolean>? = null
 
@@ -267,13 +269,13 @@
             if (field != value) {
                 field = value
                 if (value <= 0.0f || value >= 1.0f) {
-                    scrimLogger?.d(TAG, "revealAmount", "$value on ${logString()}")
+                    scrimLogger?.d(TAG, "revealAmount", "$value on $logString")
                 }
                 revealEffect.setRevealAmountOnScrim(value, this)
                 updateScrimOpaque()
                 Trace.traceCounter(
                     Trace.TRACE_TAG_APP,
-                    "light_reveal_amount",
+                    "light_reveal_amount $logString",
                     (field * 100).toInt()
                 )
                 invalidate()
@@ -290,7 +292,7 @@
                 field = value
 
                 revealEffect.setRevealAmountOnScrim(revealAmount, this)
-                scrimLogger?.d(TAG, "revealEffect", "$value on ${logString()}")
+                scrimLogger?.d(TAG, "revealEffect", "$value on $logString")
                 invalidate()
             }
         }
@@ -350,7 +352,7 @@
             if (field != value) {
                 field = value
                 isScrimOpaqueChangedListener?.accept(field)
-                scrimLogger?.d(TAG, "isScrimOpaque", "$value on ${logString()}")
+                scrimLogger?.d(TAG, "isScrimOpaque", "$value on $logString")
             }
         }
 
@@ -368,13 +370,13 @@
 
     override fun setAlpha(alpha: Float) {
         super.setAlpha(alpha)
-        scrimLogger?.d(TAG, "alpha", "$alpha on ${logString()}")
+        scrimLogger?.d(TAG, "alpha", "$alpha on $logString")
         updateScrimOpaque()
     }
 
     override fun setVisibility(visibility: Int) {
         super.setVisibility(visibility)
-        scrimLogger?.d(TAG, "visibility", "$visibility on ${logString()}")
+        scrimLogger?.d(TAG, "visibility", "$visibility on $logString")
         updateScrimOpaque()
     }
 
@@ -467,8 +469,4 @@
                 PorterDuff.Mode.MULTIPLY
             )
     }
-
-    private fun logString(): String {
-        return this::class.simpleName!! + "@" + hashCode()
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 2438298..7f8be1c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver
+import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll
 import com.android.systemui.plugins.ActivityStarter
@@ -888,6 +889,9 @@
                     isDraggingDown = false
                     isTrackpadReverseScroll = false
                     shadeRepository.setLegacyLockscreenShadeTracking(false)
+                    if (KeyguardShadeMigrationNssl.isEnabled) {
+                        return true
+                    }
                 } else {
                     stopDragging()
                     return false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 9b8dd0b..24ac70e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -15,17 +15,17 @@
  */
 package com.android.systemui.statusbar;
 
+import static android.app.Flags.keyguardPrivateNotifications;
 import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED;
 import static android.app.StatusBarManager.EXTRA_KM_PRIVATE_NOTIFS_ALLOWED;
 import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED;
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS;
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS;
+import static android.os.Flags.allowPrivateProfile;
 import static android.os.UserHandle.USER_ALL;
 import static android.os.UserHandle.USER_NULL;
 import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
 import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
-import static android.app.Flags.keyguardPrivateNotifications;
-import static android.os.Flags.allowPrivateProfile;
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 
@@ -42,9 +42,8 @@
 import android.content.IntentSender;
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
+import android.database.ExecutorContentObserver;
 import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -79,17 +78,17 @@
 import com.android.systemui.util.ListenerSet;
 import com.android.systemui.util.settings.SecureSettings;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.Executor;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 /**
  * Handles keeping track of the current user, profiles, and various things related to hiding
  * contents, redacting notifications, and the lockscreen.
@@ -228,7 +227,7 @@
                 updateCurrentProfilesCache();
                 if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
                     final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
-                    mBackgroundHandler.post(() -> {
+                    mBackgroundExecutor.execute(() -> {
                         initValuesForUser(userId);
                     });
                 }
@@ -289,8 +288,7 @@
             };
 
     protected final Context mContext;
-    private final Handler mMainHandler;
-    private final Handler mBackgroundHandler;
+    private final Executor mMainExecutor;
     private final Executor mBackgroundExecutor;
     /** The current user and its profiles (possibly including a communal profile). */
     protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
@@ -313,8 +311,7 @@
             Lazy<OverviewProxyService> overviewProxyServiceLazy,
             KeyguardManager keyguardManager,
             StatusBarStateController statusBarStateController,
-            @Main Handler mainHandler,
-            @Background Handler backgroundHandler,
+            @Main Executor mainExecutor,
             @Background Executor backgroundExecutor,
             DeviceProvisionedController deviceProvisionedController,
             KeyguardStateController keyguardStateController,
@@ -323,8 +320,7 @@
             LockPatternUtils lockPatternUtils,
             FeatureFlagsClassic featureFlags) {
         mContext = context;
-        mMainHandler = mainHandler;
-        mBackgroundHandler = backgroundHandler;
+        mMainExecutor = mainExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mDevicePolicyManager = devicePolicyManager;
         mUserManager = userManager;
@@ -362,10 +358,10 @@
     }
 
     private void init() {
-        mLockscreenSettingsObserver = new ContentObserver(
+        mLockscreenSettingsObserver = new ExecutorContentObserver(
                 mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)
-                        ? mBackgroundHandler
-                        : mMainHandler) {
+                        ? mBackgroundExecutor
+                        : mMainExecutor) {
 
             @Override
             public void onChange(boolean selfChange, Collection<Uri> uris, int flags) {
@@ -412,7 +408,7 @@
             }
         };
 
-        mSettingsObserver = new ContentObserver(mMainHandler) {
+        mSettingsObserver = new ExecutorContentObserver(mMainExecutor) {
             @Override
             public void onChange(boolean selfChange) {
                 updateLockscreenNotificationSetting();
@@ -468,14 +464,14 @@
         mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null,
                 Context.RECEIVER_EXPORTED_UNAUDITED);
 
-        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler));
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
 
         mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late
         updateCurrentProfilesCache();
 
         if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
             // Set  up
-            mBackgroundHandler.post(() -> {
+            mBackgroundExecutor.execute(() -> {
                 @SuppressLint("MissingPermission") List<UserInfo> users = mUserManager.getUsers();
                 for (int i = users.size() - 1; i >= 0; i--) {
                     initValuesForUser(users.get(i).id);
@@ -796,7 +792,7 @@
                 }
             }
         }
-        mMainHandler.post(() -> {
+        mMainExecutor.execute(() -> {
             for (UserChangedListener listener : mListeners) {
                 listener.onCurrentProfilesChanged(mCurrentProfiles);
             }
@@ -895,7 +891,7 @@
 
     private void notifyNotificationStateChanged() {
         if (!Looper.getMainLooper().isCurrentThread()) {
-            mMainHandler.post(() -> {
+            mMainExecutor.execute(() -> {
                 for (NotificationStateChangedListener listener : mNotifStateChangedListeners) {
                     listener.onNotificationStateChanged();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index a957095..32cd56c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.dagger;
 
+import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
+
 import android.content.Context;
 import android.os.RemoteException;
 import android.service.dreams.IDreamManager;
@@ -31,8 +33,6 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.settings.DisplayTracker;
@@ -230,11 +230,11 @@
     /** */
     @Provides
     @SysUISingleton
-    static AnimationFeatureFlags provideAnimationFeatureFlags(FeatureFlags featureFlags) {
+    static AnimationFeatureFlags provideAnimationFeatureFlags() {
         return new AnimationFeatureFlags() {
             @Override
             public boolean isPredictiveBackQsDialogAnim() {
-                return featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM);
+                return predictiveBackAnimateDialogs();
             }
         };
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
index 29d53fc..9f878b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar.data
 
 import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepositoryModule
+import com.android.systemui.statusbar.data.repository.RemoteInputRepositoryModule
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryModule
 import com.android.systemui.statusbar.phone.data.StatusBarPhoneDataLayerModule
 import dagger.Module
@@ -24,6 +25,7 @@
     includes =
         [
             KeyguardStatusBarRepositoryModule::class,
+            RemoteInputRepositoryModule::class,
             StatusBarModeRepositoryModule::class,
             StatusBarPhoneDataLayerModule::class
         ]
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
new file mode 100644
index 0000000..c0302bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.data.repository
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.RemoteInputController
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Repository used for tracking the state of notification remote input (e.g. when the user presses
+ * "reply" on a notification and the keyboard opens).
+ */
+interface RemoteInputRepository {
+    /** Whether remote input is currently active for any notification. */
+    val isRemoteInputActive: Flow<Boolean>
+}
+
+@SysUISingleton
+class RemoteInputRepositoryImpl
+@Inject
+constructor(
+    private val notificationRemoteInputManager: NotificationRemoteInputManager,
+) : RemoteInputRepository {
+    override val isRemoteInputActive: Flow<Boolean> = conflatedCallbackFlow {
+        trySend(false) // initial value is false
+        val callback =
+            object : RemoteInputController.Callback {
+                override fun onRemoteInputActive(active: Boolean) {
+                    trySend(active)
+                }
+            }
+        notificationRemoteInputManager.addControllerCallback(callback)
+        awaitClose { notificationRemoteInputManager.removeControllerCallback(callback) }
+    }
+}
+
+@Module
+interface RemoteInputRepositoryModule {
+    @Binds fun bindImpl(impl: RemoteInputRepositoryImpl): RemoteInputRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt
new file mode 100644
index 0000000..68f727b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.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.statusbar.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.data.repository.RemoteInputRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Interactor used for business logic pertaining to the notification remote input (e.g. when the
+ * user presses "reply" on a notification and the keyboard opens).
+ */
+@SysUISingleton
+class RemoteInputInteractor @Inject constructor(remoteInputRepository: RemoteInputRepository) {
+    /** Is remote input currently active for a notification? */
+    val isRemoteInputActive: Flow<Boolean> = remoteInputRepository.isRemoteInputActive
+}
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 0c67279..3f2c818 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -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.domain.interactor.NotificationsKeyguardInteractor
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
@@ -58,6 +59,7 @@
     private val dozeParameters: DozeParameters,
     private val screenOffAnimationController: ScreenOffAnimationController,
     private val logger: NotificationWakeUpCoordinatorLogger,
+    private val notifsKeyguardInteractor: NotificationsKeyguardInteractor,
 ) :
     OnHeadsUpChangedListener,
     StatusBarStateController.StateListener,
@@ -144,6 +146,7 @@
                 for (listener in wakeUpListeners) {
                     listener.onFullyHiddenChanged(value)
                 }
+                notifsKeyguardInteractor.setNotificationsFullyHidden(value)
             }
         }
 
@@ -216,6 +219,7 @@
                 for (listener in wakeUpListeners) {
                     listener.onPulseExpandingChanged(pulseExpanding)
                 }
+                notifsKeyguardInteractor.setPulseExpanding(pulseExpanding)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
index 5435fb5..2cac000 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
@@ -15,8 +15,6 @@
  */
 package com.android.systemui.statusbar.notification.data
 
-import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardStateRepositoryModule
 import dagger.Module
 
-@Module(includes = [NotificationsKeyguardStateRepositoryModule::class])
-interface NotificationDataLayerModule
+@Module(includes = []) interface NotificationDataLayerModule
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 2cc1403..bd6ea30 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
@@ -15,59 +15,16 @@
  */
 package com.android.systemui.statusbar.notification.data.repository
 
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
-import dagger.Binds
-import dagger.Module
 import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 
 /** View-states pertaining to notifications on the keyguard. */
-interface NotificationsKeyguardViewStateRepository {
+@SysUISingleton
+class NotificationsKeyguardViewStateRepository @Inject constructor() {
     /** Are notifications fully hidden from view? */
-    val areNotificationsFullyHidden: Flow<Boolean>
+    val areNotificationsFullyHidden = MutableStateFlow(false)
 
     /** Is a pulse expansion occurring? */
-    val isPulseExpanding: Flow<Boolean>
-}
-
-@Module
-interface NotificationsKeyguardStateRepositoryModule {
-    @Binds
-    fun bindImpl(
-        impl: NotificationsKeyguardViewStateRepositoryImpl
-    ): NotificationsKeyguardViewStateRepository
-}
-
-@SysUISingleton
-class NotificationsKeyguardViewStateRepositoryImpl
-@Inject
-constructor(
-    wakeUpCoordinator: NotificationWakeUpCoordinator,
-) : NotificationsKeyguardViewStateRepository {
-    override val areNotificationsFullyHidden: Flow<Boolean> = conflatedCallbackFlow {
-        val listener =
-            object : NotificationWakeUpCoordinator.WakeUpListener {
-                override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
-                    trySend(isFullyHidden)
-                }
-            }
-        trySend(wakeUpCoordinator.notificationsFullyHidden)
-        wakeUpCoordinator.addListener(listener)
-        awaitClose { wakeUpCoordinator.removeListener(listener) }
-    }
-
-    override val isPulseExpanding: Flow<Boolean> = conflatedCallbackFlow {
-        val listener =
-            object : NotificationWakeUpCoordinator.WakeUpListener {
-                override fun onPulseExpandingChanged(isPulseExpanding: Boolean) {
-                    trySend(isPulseExpanding)
-                }
-            }
-        trySend(wakeUpCoordinator.isPulseExpanding())
-        wakeUpCoordinator.addListener(listener)
-        awaitClose { wakeUpCoordinator.removeListener(listener) }
-    }
+    val isPulseExpanding = MutableStateFlow(false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
index 73341db..a6361cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
@@ -15,24 +15,29 @@
  */
 package com.android.systemui.statusbar.notification.domain.interactor
 
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOn
 
 /** Domain logic pertaining to notifications on the keyguard. */
 class NotificationsKeyguardInteractor
 @Inject
 constructor(
-    repository: NotificationsKeyguardViewStateRepository,
-    @Background backgroundDispatcher: CoroutineDispatcher,
+    private val repository: NotificationsKeyguardViewStateRepository,
 ) {
     /** Is a pulse expansion occurring? */
-    val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding.flowOn(backgroundDispatcher)
+    val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding
 
     /** Are notifications fully hidden from view? */
-    val areNotificationsFullyHidden: Flow<Boolean> =
-        repository.areNotificationsFullyHidden.flowOn(backgroundDispatcher)
+    val areNotificationsFullyHidden: Flow<Boolean> = repository.areNotificationsFullyHidden
+
+    /** Updates whether notifications are fully hidden from view. */
+    fun setNotificationsFullyHidden(fullyHidden: Boolean) {
+        repository.areNotificationsFullyHidden.value = fullyHidden
+    }
+
+    /** Updates whether a pulse expansion is occurring. */
+    fun setPulseExpanding(pulseExpanding: Boolean) {
+        repository.isPulseExpanding.value = pulseExpanding
+    }
 }
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 31ca106..e200e65 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
@@ -273,12 +273,15 @@
     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 boolean shouldSimulateSlowMeasure() {
+        return 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;
+    private static final int SLOW_MEASURE_SIMULATE_DELAY_MS =
+            SystemProperties.getInt(SLOW_MEASURE_SIMULATE_DELAY_PROPERTY, 150);
 
     // Listener will be called when receiving a long click event.
     // Use #setLongPressPosition to optionally assign positional data with the long press.
@@ -1886,7 +1889,7 @@
         }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
-        if (Compile.IS_DEBUG && mSimulateSlowMeasure) {
+        if (shouldSimulateSlowMeasure()) {
             simulateExtraMeasureDelay();
         }
         Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index f6431a2..7ea9b14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -32,6 +32,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.util.ContrastColorUtil;
+import com.android.settingslib.Utils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.res.R;
 
@@ -58,11 +60,19 @@
     private int mExpandAnimationWidth = -1;
     private int mExpandAnimationHeight = -1;
     private int mDrawableAlpha = 255;
+    private final ColorStateList mLightColoredStatefulColors;
+    private final ColorStateList mDarkColoredStatefulColors;
+    private final int mNormalColor;
 
     public NotificationBackgroundView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mDontModifyCorners = getResources().getBoolean(
-                R.bool.config_clipNotificationsToOutline);
+        mDontModifyCorners = getResources().getBoolean(R.bool.config_clipNotificationsToOutline);
+        mLightColoredStatefulColors = getResources().getColorStateList(
+                R.color.notification_state_color_light);
+        mDarkColoredStatefulColors = getResources().getColorStateList(
+                R.color.notification_state_color_dark);
+        mNormalColor = Utils.getColorAttrDefaultColor(mContext,
+                com.android.internal.R.attr.materialColorSurfaceContainerHigh);
     }
 
     @Override
@@ -122,6 +132,18 @@
     }
 
     /**
+     * Stateful colors are colors that will overlay on the notification original color when one of
+     * hover states, pressed states or other similar states is activated.
+     */
+    private void setStatefulColors() {
+        if (mTintColor != mNormalColor) {
+            ColorStateList newColor = ContrastColorUtil.isColorDark(mTintColor)
+                    ? mDarkColoredStatefulColors : mLightColoredStatefulColors;
+            ((GradientDrawable) getStatefulBackgroundLayer().mutate()).setColor(newColor);
+        }
+    }
+
+    /**
      * Sets a background drawable. As we need to change our bounds independently of layout, we need
      * the notion of a background independently of the regular View background..
      */
@@ -149,21 +171,20 @@
         setCustomBackground(d);
     }
 
-    public void setTint(int tintColor) {
-        if (tintColor != 0) {
-            ColorStateList stateList = new ColorStateList(new int[][]{
-                    new int[]{com.android.internal.R.attr.state_pressed},
-                    new int[]{com.android.internal.R.attr.state_hovered},
-                    new int[]{}},
+    private Drawable getBaseBackgroundLayer() {
+        return ((LayerDrawable) mBackground).getDrawable(0);
+    }
 
-                    new int[]{tintColor, 0, tintColor}
-            );
-            mBackground.setTintMode(PorterDuff.Mode.SRC_ATOP);
-            mBackground.setTintList(stateList);
-        } else {
-            mBackground.setTintList(null);
-        }
+    private Drawable getStatefulBackgroundLayer() {
+        return ((LayerDrawable) mBackground).getDrawable(1);
+    }
+
+    public void setTint(int tintColor) {
+        Drawable baseLayer = getBaseBackgroundLayer();
+        baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP);
+        baseLayer.setTint(tintColor);
         mTintColor = tintColor;
+        setStatefulColors();
         invalidate();
     }
 
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 805b44c..ea414d2 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
@@ -4869,10 +4869,6 @@
 
     public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) {
         ExpandableNotificationRow row = entry.getHeadsUpAnimationView();
-        generateHeadsUpAnimation(row, isHeadsUp);
-    }
-
-    public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
         final boolean add = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed);
         if (SPEW) {
             Log.v(TAG, "generateHeadsUpAnimation:"
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 6f5058c..a30c294 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
@@ -69,6 +69,7 @@
 import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
+import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -366,8 +367,7 @@
 
                 @Override
                 public void onStatePostChange() {
-                    mView.updateSensitiveness(mStatusBarStateController.goingToFullShade(),
-                            mLockscreenUserManager.isAnyProfilePublicMode());
+                    updateSensitivenessWithAnimation(mStatusBarStateController.goingToFullShade());
                     mView.onStatePostChange(mStatusBarStateController.fromShadeLocked());
                     if (!FooterViewRefactor.isEnabled()) {
                         updateImportantForAccessibility();
@@ -378,7 +378,7 @@
     private final UserChangedListener mLockscreenUserChangeListener = new UserChangedListener() {
         @Override
         public void onUserChanged(int userId) {
-            mView.updateSensitiveness(false, mLockscreenUserManager.isAnyProfilePublicMode());
+            updateSensitivenessWithAnimation(false);
             mHistoryEnabled = null;
             updateFooter();
         }
@@ -388,7 +388,11 @@
      * Recalculate sensitiveness without animation; called when waking up while keyguard occluded.
      */
     public void updateSensitivenessForOccludedWakeup() {
-        mView.updateSensitiveness(false, mLockscreenUserManager.isAnyProfilePublicMode());
+        updateSensitivenessWithAnimation(false);
+    }
+
+    private void updateSensitivenessWithAnimation(boolean animate) {
+        mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode());
     }
 
     /**
@@ -1492,14 +1496,10 @@
         return mView.getFirstChildNotGone();
     }
 
-    private void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) {
+    public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) {
         mView.generateHeadsUpAnimation(entry, isHeadsUp);
     }
 
-    public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
-        mView.generateHeadsUpAnimation(row, isHeadsUp);
-    }
-
     public void setMaxTopPadding(int padding) {
         mView.setMaxTopPadding(padding);
     }
@@ -1958,18 +1958,34 @@
                     mView.dispatchDownEventToScroller(ev);
                 }
             }
-            boolean scrollerWantsIt = false;
-            if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
-                    && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
-                scrollerWantsIt = mView.onScrollTouch(ev);
-            }
             boolean horizontalSwipeWantsIt = false;
-            if (mLongPressedView == null && !mView.isBeingDragged()
-                    && !expandingNotification
-                    && !mView.getExpandedInThisMotion()
-                    && !onlyScrollingInThisMotion
-                    && !mView.getDisallowDismissInThisMotion()) {
-                horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+            boolean scrollerWantsIt = false;
+            if (KeyguardShadeMigrationNssl.isEnabled()) {
+                // Reverse the order relative to the else statement. onScrollTouch will reset on an
+                // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
+                if (mLongPressedView == null && !mView.isBeingDragged()
+                        && !expandingNotification
+                        && !mView.getExpandedInThisMotion()
+                        && !onlyScrollingInThisMotion
+                        && !mView.getDisallowDismissInThisMotion()) {
+                    horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+                }
+                if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
+                        && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
+                    scrollerWantsIt = mView.onScrollTouch(ev);
+                }
+            } else {
+                if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
+                        && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
+                    scrollerWantsIt = mView.onScrollTouch(ev);
+                }
+                if (mLongPressedView == null && !mView.isBeingDragged()
+                        && !expandingNotification
+                        && !mView.getExpandedInThisMotion()
+                        && !onlyScrollingInThisMotion
+                        && !mView.getDisallowDismissInThisMotion()) {
+                    horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+                }
             }
 
             // Check if we need to clear any snooze leavebehinds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 625fdc1..4b8fb1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -18,12 +18,15 @@
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
 import android.content.Context
+import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.res.R
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.statusbar.policy.SplitShadeStateController
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -45,6 +48,7 @@
     private val splitShadeStateController: SplitShadeStateController,
     keyguardInteractor: KeyguardInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) {
 
     private val _topPosition = MutableStateFlow(0f)
@@ -72,7 +76,11 @@
                             getDimensionPixelSize(R.dimen.notification_panel_margin_bottom),
                         marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top),
                         marginTopLargeScreen =
-                            getDimensionPixelSize(R.dimen.large_screen_shade_header_height),
+                            if (centralizedStatusBarDimensRefactor()) {
+                                largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+                            } else {
+                                getDimensionPixelSize(R.dimen.large_screen_shade_header_height)
+                            },
                         keyguardSplitShadeTopMargin =
                             getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin),
                     )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt
index 38a6d39..13d7924 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt
@@ -34,7 +34,6 @@
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.model.SysUiState
 
 /**
@@ -53,7 +52,6 @@
     context: Context,
     theme: Int,
     dismissOnDeviceLock: Boolean,
-    featureFlags: FeatureFlags,
     dialogManager: SystemUIDialogManager,
     sysUiState: SysUiState,
     broadcastDispatcher: BroadcastDispatcher,
@@ -63,7 +61,6 @@
         context,
         theme,
         dismissOnDeviceLock,
-        featureFlags,
         dialogManager,
         sysUiState,
         broadcastDispatcher,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
index 0bad47e..8a45ec1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
@@ -18,12 +18,23 @@
 
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.Flags.smartspaceRelocateToBottom
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
 import com.android.systemui.util.ViewController
 import javax.inject.Inject
 
 class KeyguardBottomAreaViewController
-    @Inject constructor(view: KeyguardBottomAreaView, featureFlags: FeatureFlagsClassic) :
-    ViewController<KeyguardBottomAreaView> (view) {
+    @Inject constructor(
+            view: KeyguardBottomAreaView,
+            private val smartspaceController: LockscreenSmartspaceController,
+            featureFlags: FeatureFlagsClassic
+) : ViewController<KeyguardBottomAreaView> (view) {
+
+    private var smartspaceView: View? = null
 
     init {
         view.setIsLockscreenLandscapeEnabled(
@@ -31,6 +42,14 @@
     }
 
     override fun onViewAttached() {
+        if (!smartspaceRelocateToBottom() || !smartspaceController.isEnabled()) {
+            return
+        }
+
+        val ambientIndicationArea = mView.findViewById<View>(R.id.ambient_indication_container)
+        ambientIndicationArea?.visibility = View.GONE
+
+        addSmartspaceView()
     }
 
     override fun onViewDetached() {
@@ -40,4 +59,24 @@
         // TODO: remove this method.
         return mView
     }
+
+    private fun addSmartspaceView() {
+        if (!smartspaceRelocateToBottom()) {
+            return
+        }
+
+        val smartspaceContainer = mView.findViewById<View>(R.id.smartspace_container)
+        smartspaceContainer!!.visibility = View.VISIBLE
+
+        smartspaceView = smartspaceController.buildAndConnectView(smartspaceContainer as ViewGroup)
+        val lp = LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        (smartspaceContainer as ViewGroup).addView(smartspaceView, 0, lp)
+        val startPadding = context.resources.getDimensionPixelSize(
+                R.dimen.below_clock_padding_start)
+        val endPadding = context.resources.getDimensionPixelSize(
+                R.dimen.below_clock_padding_end)
+        smartspaceView?.setPaddingRelative(startPadding, 0, endPadding, 0)
+//        mKeyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index 0a03af7..ca3e3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -16,10 +16,12 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.Flags.centralizedStatusBarDimensRefactor;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInScale;
 import static com.android.systemui.statusbar.notification.NotificationUtils.interpolate;
 
+import android.content.Context;
 import android.content.res.Resources;
 import android.util.MathUtils;
 
@@ -30,6 +32,7 @@
 import com.android.systemui.log.core.Logger;
 import com.android.systemui.log.dagger.KeyguardClockLog;
 import com.android.systemui.res.R;
+import com.android.systemui.shade.LargeScreenHeaderHelper;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherListView;
 
@@ -160,14 +163,14 @@
         mLogger = new Logger(logBuffer, TAG);
     }
 
-    /**
-     * Refreshes the dimension values.
-     */
-    public void loadDimens(Resources res) {
-        mStatusViewBottomMargin = res.getDimensionPixelSize(
-                R.dimen.keyguard_status_view_bottom_margin);
+    /** Refreshes the dimension values. */
+    public void loadDimens(Context context, Resources res) {
+        mStatusViewBottomMargin =
+                res.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin);
         mSplitShadeTopNotificationsMargin =
-                res.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
+                centralizedStatusBarDimensRefactor()
+                        ? LargeScreenHeaderHelper.getLargeScreenHeaderHeight(context)
+                        : res.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
         mSplitShadeTargetTopMargin =
                 res.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin);
 
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 7cc0888..7691459 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.Flags.centralizedStatusBarDimensRefactor;
 import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection;
 import static com.android.systemui.util.Utils.getStatusBarHeaderHeightKeyguard;
 
@@ -130,6 +131,9 @@
         mUserSwitcherContainer = findViewById(R.id.user_switcher_container);
         mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot);
         loadDimens();
+        if (!centralizedStatusBarDimensRefactor()) {
+            setGravity(Gravity.CENTER_VERTICAL);
+        }
     }
 
     /**
@@ -307,7 +311,8 @@
         final int minRight = (!isLayoutRtl() && mIsPrivacyDotEnabled)
                 ? Math.max(mMinDotWidth, mPadding.right) : mPadding.right;
 
-        setPadding(minLeft, waterfallTop, minRight, 0);
+        int top = centralizedStatusBarDimensRefactor() ? waterfallTop + mPadding.top : waterfallTop;
+        setPadding(minLeft, top, minRight, 0);
     }
 
     private boolean updateLayoutParamsNoCutout() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
index abdf827..56ea00c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -104,7 +104,8 @@
     }
 
     private void notifyManagedProfileRemoved() {
-        for (Callback callback : mCallbacks) {
+        ArrayList<Callback> copy = new ArrayList<>(mCallbacks);
+        for (Callback callback : copy) {
             callback.onManagedProfileRemoved();
         }
     }
@@ -148,7 +149,8 @@
         @Override
         public void onUserChanged(int newUser, @NonNull Context userContext) {
             reloadManagedProfiles();
-            for (Callback callback : mCallbacks) {
+            ArrayList<Callback> copy = new ArrayList<>(mCallbacks);
+            for (Callback callback : copy) {
                 callback.onManagedProfileChanged();
             }
         }
@@ -156,7 +158,8 @@
         @Override
         public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
             reloadManagedProfiles();
-            for (Callback callback : mCallbacks) {
+            ArrayList<Callback> copy = new ArrayList<>(mCallbacks);
+            for (Callback callback : copy) {
                 callback.onManagedProfileChanged();
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 9ae4195..d7cbe5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -14,6 +14,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_BINDABLE;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW;
@@ -40,11 +41,13 @@
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusIconDisplayable;
 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
@@ -432,6 +435,10 @@
 
                 case TYPE_MOBILE_NEW:
                     return addNewMobileIcon(index, slot, holder.getTag());
+
+                case TYPE_BINDABLE:
+                    // Safe cast, since only BindableIconHolders can set this tag on themselves
+                    return addBindableIcon((BindableIconHolder) holder, index);
             }
 
             return null;
@@ -446,6 +453,18 @@
             return view;
         }
 
+        /**
+         * ModernStatusBarViews can be created and bound, and thus do not need to update their
+         *  drawable by sending multiple calls to setIcon. Instead, by using a bindable
+         * icon view, we can simply create the icon when requested and allow the
+         * ViewBinder to control its visual state.
+         */
+        protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder, int index) {
+            ModernStatusBarView view = holder.getInitializer().createAndBind(mContext);
+            mGroup.addView(view, index, onCreateLayoutParams());
+            return view;
+        }
+
         protected StatusIconDisplayable addNewWifiIcon(int index, String slot) {
             ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
             mGroup.addView(view, index, onCreateLayoutParams());
@@ -530,6 +549,7 @@
                     return;
                 case TYPE_MOBILE_NEW:
                 case TYPE_WIFI_NEW:
+                case TYPE_BINDABLE:
                     // Nothing, the new icons update themselves
                     return;
                 default:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 0f4d68c..4f148f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -38,8 +38,11 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusIconDisplayable;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.tuner.TunerService;
@@ -83,7 +86,8 @@
             TunerService tunerService,
             DumpManager dumpManager,
             StatusBarIconList statusBarIconList,
-            StatusBarPipelineFlags statusBarPipelineFlags
+            StatusBarPipelineFlags statusBarPipelineFlags,
+            BindableIconsRegistry modernIconsRegistry
     ) {
         mStatusBarIconList = statusBarIconList;
         mContext = context;
@@ -94,6 +98,28 @@
         tunerService.addTunable(this, ICON_HIDE_LIST);
         demoModeController.addCallback(this);
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
+        addModernBindableIcons(modernIconsRegistry);
+    }
+
+    /**
+     * BindableIcons will always produce ModernStatusBarViews, which will be initialized and bound
+     * upon being added to any icon group. Because their view policy does not require subsequent
+     * calls to setIcon(), we can simply register them all statically here and not have to build
+     * CoreStartables for each modern icon.
+     *
+     * @param registry a statically defined provider of the modern icons
+     */
+    private void addModernBindableIcons(BindableIconsRegistry registry) {
+        List<BindableIcon> icons = registry.getBindableIcons();
+
+        // Initialization point for the bindable (modern) icons. These icons get their own slot
+        // allocated immediately, and are required to control their own display properties
+        for (BindableIcon i : icons) {
+            if (i.getShouldBindIcon()) {
+                addBindableIcon(i);
+            }
+        }
     }
 
     /** */
@@ -182,6 +208,17 @@
         mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, hidden, holder));
     }
 
+    void addBindableIcon(BindableIcon icon) {
+        StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0);
+        // Expected to be null
+        if (existingHolder == null) {
+            BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer());
+            setIcon(icon.getSlot(), bindableIcon);
+        } else {
+            Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring");
+        }
+    }
+
     /** */
     @Override
     public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
index 5b55a1e..bef0b28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
@@ -21,23 +21,24 @@
 import android.os.UserHandle
 import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
 
 /** 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)
+open class StatusBarIconHolder private constructor() {
+    @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW, TYPE_BINDABLE)
     @Retention(AnnotationRetention.SOURCE)
     internal annotation class IconType
 
     var icon: StatusBarIcon? = null
 
     @IconType
-    var type = TYPE_ICON
-        private set
+    open var type = TYPE_ICON
+        internal set
 
     var tag = 0
         private set
 
-    var isVisible: Boolean
+    open var isVisible: Boolean
         get() =
             when (type) {
                 TYPE_ICON -> icon!!.visible
@@ -45,6 +46,7 @@
                 // The new pipeline controls visibilities via the view model and
                 // view binder, so
                 // this is effectively an unused return value.
+                TYPE_BINDABLE,
                 TYPE_MOBILE_NEW,
                 TYPE_WIFI_NEW -> true
                 else -> true
@@ -55,6 +57,7 @@
             }
             when (type) {
                 TYPE_ICON -> icon!!.visible = visible
+                TYPE_BINDABLE,
                 TYPE_MOBILE_NEW,
                 TYPE_WIFI_NEW -> {}
             }
@@ -94,6 +97,9 @@
         )
         const val TYPE_WIFI_NEW = 4
 
+        /** Only applicable to [BindableIconHolder] */
+        const val TYPE_BINDABLE = 5
+
         /** Returns a human-readable string representing the given type. */
         fun getTypeString(@IconType type: Int): String {
             return when (type) {
@@ -154,4 +160,25 @@
             return holder
         }
     }
+
+    /**
+     * Subclass of StatusBarIconHolder that is responsible only for the registration of an icon into
+     * the [StatusBarIconList]. A bindable icon takes care of its own display, including hiding
+     * itself under the correct conditions.
+     *
+     * StatusBarIconController will register all available bindable icons on init (see
+     * [BindableIconsRepository]), and will ignore any call to setIcon for these.
+     *
+     * [initializer] a view creator that can bind the relevant view models to the created view.
+     */
+    class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : StatusBarIconHolder() {
+        override var type: Int = TYPE_BINDABLE
+
+        /** This is unused, as bindable icons use their own view binders to control visibility */
+        override var isVisible: Boolean = true
+
+        override fun toString(): String {
+            return ("StatusBarIconHolder(type=BINDABLE)")
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index 30a445f..703b3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -118,15 +118,25 @@
 
     private void updateVpn() {
         boolean vpnVisible = mSecurityController.isVpnEnabled();
-        int vpnIconId = currentVpnIconId(mSecurityController.isVpnBranded());
+        int vpnIconId = currentVpnIconId(
+                mSecurityController.isVpnBranded(),
+                mSecurityController.isVpnValidated());
 
         mIconController.setIcon(mSlotVpn, vpnIconId,
                 mContext.getResources().getString(R.string.accessibility_vpn_on));
         mIconController.setIconVisibility(mSlotVpn, vpnVisible);
     }
 
-    private int currentVpnIconId(boolean isBranded) {
-        return isBranded ? R.drawable.stat_sys_branded_vpn : R.drawable.stat_sys_vpn_ic;
+    private int currentVpnIconId(boolean isBranded, boolean isValidated) {
+        if (isBranded) {
+            return isValidated
+                    ? R.drawable.stat_sys_branded_vpn
+                    : R.drawable.stat_sys_no_internet_branded_vpn;
+        } else {
+            return isValidated
+                    ? R.drawable.stat_sys_vpn_ic
+                    : R.drawable.stat_sys_no_internet_vpn_ic;
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 3394eac..390d2c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
+
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.BroadcastReceiver;
@@ -45,8 +47,6 @@
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Application;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -78,7 +78,6 @@
     public static final boolean DEFAULT_DISMISS_ON_DEVICE_LOCK = true;
 
     private final Context mContext;
-    private final FeatureFlags mFeatureFlags;
     private final DialogDelegate<SystemUIDialog> mDelegate;
     @Nullable private final DismissReceiver mDismissReceiver;
     private final Handler mHandler = new Handler();
@@ -110,7 +109,6 @@
         // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
         // the content and attach listeners.
         this(context, theme, dismissOnDeviceLock,
-                Dependency.get(FeatureFlags.class),
                 Dependency.get(SystemUIDialogManager.class),
                 Dependency.get(SysUiState.class),
                 Dependency.get(BroadcastDispatcher.class),
@@ -119,7 +117,6 @@
 
     public static class Factory {
         private final Context mContext;
-        private final FeatureFlags mFeatureFlags;
         private final SystemUIDialogManager mSystemUIDialogManager;
         private final SysUiState mSysUiState;
         private final BroadcastDispatcher mBroadcastDispatcher;
@@ -128,13 +125,11 @@
         @Inject
         public Factory(
                 @Application Context context,
-                FeatureFlags featureFlags,
                 SystemUIDialogManager systemUIDialogManager,
                 SysUiState sysUiState,
                 BroadcastDispatcher broadcastDispatcher,
                 DialogLaunchAnimator dialogLaunchAnimator) {
             mContext = context;
-            mFeatureFlags = featureFlags;
             mSystemUIDialogManager = systemUIDialogManager;
             mSysUiState = sysUiState;
             mBroadcastDispatcher = broadcastDispatcher;
@@ -177,7 +172,6 @@
                     context,
                     DEFAULT_THEME,
                     DEFAULT_DISMISS_ON_DEVICE_LOCK,
-                    mFeatureFlags,
                     mSystemUIDialogManager,
                     mSysUiState,
                     mBroadcastDispatcher,
@@ -190,7 +184,6 @@
             Context context,
             int theme,
             boolean dismissOnDeviceLock,
-            FeatureFlags featureFlags,
             SystemUIDialogManager dialogManager,
             SysUiState sysUiState,
             BroadcastDispatcher broadcastDispatcher,
@@ -199,7 +192,6 @@
                 context,
                 theme,
                 dismissOnDeviceLock,
-                featureFlags,
                 dialogManager,
                 sysUiState,
                 broadcastDispatcher,
@@ -211,7 +203,6 @@
             Context context,
             int theme,
             boolean dismissOnDeviceLock,
-            FeatureFlags featureFlags,
             SystemUIDialogManager dialogManager,
             SysUiState sysUiState,
             BroadcastDispatcher broadcastDispatcher,
@@ -221,7 +212,6 @@
                 context,
                 theme,
                 dismissOnDeviceLock,
-                featureFlags,
                 dialogManager,
                 sysUiState,
                 broadcastDispatcher,
@@ -233,7 +223,6 @@
             Context context,
             int theme,
             boolean dismissOnDeviceLock,
-            FeatureFlags featureFlags,
             SystemUIDialogManager dialogManager,
             SysUiState sysUiState,
             BroadcastDispatcher broadcastDispatcher,
@@ -241,7 +230,6 @@
             DialogDelegate<SystemUIDialog> delegate) {
         super(context, theme);
         mContext = context;
-        mFeatureFlags = featureFlags;
         mDelegate = delegate;
 
         applyFlags(this);
@@ -269,7 +257,7 @@
         for (int i = 0; i < mOnCreateRunnables.size(); i++) {
             mOnCreateRunnables.get(i).run();
         }
-        if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM)) {
+        if (predictiveBackAnimateDialogs()) {
             DialogKt.registerAnimationOnBackInvoked(
                     /* dialog = */ this,
                     /* targetView = */ getWindow().getDecorView()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt
index d91ca92..f3e8f62d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt
@@ -20,7 +20,6 @@
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.model.SysUiState
 import com.android.systemui.util.Assert
 import javax.inject.Inject
@@ -30,7 +29,6 @@
 @Inject
 constructor(
     @Application val applicationContext: Context,
-    private val featureFlags: FeatureFlagsClassic,
     private val dialogManager: SystemUIDialogManager,
     private val sysUiState: SysUiState,
     private val broadcastDispatcher: BroadcastDispatcher,
@@ -57,7 +55,6 @@
             context,
             theme,
             dismissOnDeviceLock,
-            featureFlags,
             dialogManager,
             sysUiState,
             broadcastDispatcher,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index e1fd37f..89a2fb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistryImpl
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigCoreStartable
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
@@ -42,6 +44,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
@@ -76,8 +80,16 @@
     abstract fun airplaneModeViewModel(impl: AirplaneModeViewModelImpl): AirplaneModeViewModel
 
     @Binds
+    abstract fun bindableIconsRepository(impl: BindableIconsRegistryImpl): BindableIconsRegistry
+
+    @Binds
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
+    @Binds
+    abstract fun deviceBasedSatelliteRepository(
+        impl: DeviceBasedSatelliteRepositoryImpl
+    ): DeviceBasedSatelliteRepository
+
     @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
 
     @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
new file mode 100644
index 0000000..e3c3139
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.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.statusbar.pipeline.icons.shared
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import javax.inject.Inject
+
+/**
+ * Bindable status bar icons represent icon descriptions which can be registered with
+ * StatusBarIconController and can also create their own bindings. A bound icon is responsible for
+ * its own updates via the [repeatWhenAttached] view lifecycle utility. Thus,
+ * StatusBarIconController can (and will) ignore any call to setIcon.
+ *
+ * In other words, these icons are bound once (at controller init) and they will control their
+ * visibility on their own (while their overall appearance remains at the discretion of
+ * StatusBarIconController, via the ModernStatusBarViewBinding interface).
+ */
+interface BindableIconsRegistry {
+    val bindableIcons: List<BindableIcon>
+}
+
+@SysUISingleton
+class BindableIconsRegistryImpl
+@Inject
+constructor(
+/** Bindables go here */
+) : BindableIconsRegistry {
+    /**
+     * Adding the injected bindables to this list will get them registered with
+     * StatusBarIconController
+     */
+    override val bindableIcons: List<BindableIcon> = listOf()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt
new file mode 100644
index 0000000..9d0d838
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.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.statusbar.pipeline.icons.shared.model
+
+/**
+ * A BindableIcon describes a status bar icon that can be housed in the [ModernStatusBarView]
+ * created by [initializer]. They can be registered statically for [BindableIconsRepositoryImpl].
+ *
+ * Typical usage would be to create an (@SysUISingleton) adapter class that implements the
+ * interface. For example:
+ * ```
+ * @SysuUISingleton
+ * class MyBindableIconAdapter
+ * @Inject constructor(
+ *     // deps
+ *     val viewModel: MyViewModel
+ * ) : BindableIcon {
+ *     override val slot = "icon_slot_name"
+ *
+ *     override val initializer = ModernStatusBarViewCreator() {
+ *         SingleBindableStatusBarIconView.createView(context).also { iconView ->
+ *             MyIconViewBinder.bind(iconView, viewModel)
+ *         }
+ *     }
+ *
+ *     override fun shouldBind() = Flags.myFlag()
+ * }
+ * ```
+ *
+ * By defining this adapter (and injecting it into the repository), we get our icon registered with
+ * the legacy StatusBarIconController while proxying all updates to the view binder that is created
+ * elsewhere.
+ *
+ * Note that the initializer block defines a closure that can pull in the viewModel dependency
+ * without us having to store it directly in the icon controller.
+ */
+interface BindableIcon {
+    val slot: String
+    val initializer: ModernStatusBarViewCreator
+    val shouldBindIcon: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt
new file mode 100644
index 0000000..dbd5c1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.icons.shared.model
+
+import android.content.Context
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
+
+/**
+ * Defined as an interface (as opposed to a typealias) to simplify calling from java.
+ * [ModernStatusBarViewCreator.createAndBind] should return a constructed and bound
+ * [ModernStatusBarView].
+ */
+fun interface ModernStatusBarViewCreator {
+    fun createAndBind(context: Context): ModernStatusBarView
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index dad4093..39135c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -71,6 +71,12 @@
     /** List of subscriptions, potentially filtered for CBRS */
     val filteredSubscriptions: Flow<List<SubscriptionModel>>
 
+    /**
+     * The current list of [MobileIconInteractor]s associated with the current list of
+     * [filteredSubscriptions]
+     */
+    val icons: StateFlow<List<MobileIconInteractor>>
+
     /** True if the active mobile data subscription has data enabled */
     val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
 
@@ -259,6 +265,13 @@
         }
     }
 
+    override val icons =
+        filteredSubscriptions
+            .mapLatest { subs ->
+                subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
     /**
      * Copied from the old pipeline. We maintain a 2s period of time where we will keep the
      * validated bit from the old active network (A) while data is changing to the new one (B).
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
new file mode 100644
index 0000000..ad8b810
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.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.statusbar.pipeline.satellite.data
+
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Device-based satellite refers to the capability of a device to connect directly to a satellite
+ * network. This is in contrast to carrier-based satellite connectivity, which is a property of a
+ * given mobile data subscription.
+ */
+interface DeviceBasedSatelliteRepository {
+    /** See [SatelliteConnectionState] for available states */
+    val connectionState: Flow<SatelliteConnectionState>
+
+    /** 0-4 level (similar to wifi and mobile) */
+    // @IntRange(from = 0, to = 4)
+    val signalStrength: Flow<Int>
+
+    /** Clients must observe this property, as device-based satellite is location-dependent */
+    val isSatelliteAllowedForCurrentLocation: Flow<Boolean>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
new file mode 100644
index 0000000..de46a5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.prod
+
+import android.os.OutcomeReceiver
+import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteModemStateCallback
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.time.SystemClock
+import java.util.Optional
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/**
+ * A SatelliteManager that has responded that it has satellite support. Use [SatelliteSupport] to
+ * get one
+ */
+private typealias SupportedSatelliteManager = SatelliteManager
+
+/**
+ * "Supported" here means supported by the device. The value of this should be stable during the
+ * process lifetime.
+ */
+private sealed interface SatelliteSupport {
+    /** Not yet fetched */
+    data object Unknown : SatelliteSupport
+
+    /**
+     * SatelliteManager says that this mode is supported. Note that satellite manager can never be
+     * null now
+     */
+    data class Supported(val satelliteManager: SupportedSatelliteManager) : SatelliteSupport
+
+    /**
+     * Either we were told that there is no support for this feature, or the manager is null, or
+     * some other exception occurred while querying for support.
+     */
+    data object NotSupported : SatelliteSupport
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    companion object {
+        /** Convenience function to switch to the supported flow */
+        fun <T> Flow<SatelliteSupport>.whenSupported(
+            supported: (SatelliteManager) -> Flow<T>,
+            orElse: Flow<T>,
+        ): Flow<T> = flatMapLatest {
+            when (it) {
+                is Supported -> supported(it.satelliteManager)
+                else -> orElse
+            }
+        }
+    }
+}
+
+/**
+ * Basically your everyday run-of-the-mill system service listener, with three notable exceptions.
+ *
+ * First, there is an availability bit that we are tracking via [SatelliteManager]. See
+ * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about
+ * this bit is that there is no callback that exists. Therefore we implement a simple polling
+ * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see
+ * [POLLING_INTERVAL_MS]) and see what the current state is.
+ *
+ * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See
+ * [SatelliteSupport] for details on how we track the state. What's worth noting here is that
+ * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental
+ * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method
+ * [SatelliteSupport.whenSupported]. By defining flows as simple functions based on a
+ * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told
+ * us that satellite is supported. Therefore, we don't expect exceptions to be thrown.
+ *
+ * Lastly, this class is designed to wait a full minute of process uptime before making any requests
+ * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that
+ * is still booting up or anything like that. We can tune or remove this behavior in the future if
+ * necessary.
+ */
+@SysUISingleton
+class DeviceBasedSatelliteRepositoryImpl
+@Inject
+constructor(
+    satelliteManagerOpt: Optional<SatelliteManager>,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+    private val systemClock: SystemClock,
+) : DeviceBasedSatelliteRepository {
+
+    private val satelliteManager: SatelliteManager?
+
+    override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean>
+
+    // Some calls into satellite manager will throw exceptions if it is not supported.
+    // This is never expected to change after boot, but may need to be retried in some cases
+    private val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
+
+    init {
+        satelliteManager = satelliteManagerOpt.getOrNull()
+
+        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+
+        if (satelliteManager != null) {
+            // First, check that satellite is supported on this device
+            scope.launch {
+                ensureMinUptime(systemClock, MIN_UPTIME)
+                satelliteSupport.value = satelliteManager.checkSatelliteSupported()
+
+                // We only need to check location availability if this mode is supported
+                if (satelliteSupport.value is Supported) {
+                    isSatelliteAllowedForCurrentLocation.subscriptionCount
+                        .map { it > 0 }
+                        .distinctUntilChanged()
+                        .collectLatest { hasSubscribers ->
+                            if (hasSubscribers) {
+                                /*
+                                 * As there is no listener available for checking satellite allowed,
+                                 * we must poll. Defaulting to polling at most once every hour while
+                                 * active. Subsequent OOS events will restart the job, so a flaky
+                                 * connection might cause more frequent checks.
+                                 */
+                                while (true) {
+                                    checkIsSatelliteAllowed()
+                                    delay(POLLING_INTERVAL_MS)
+                                }
+                            }
+                        }
+                }
+            }
+        } else {
+            satelliteSupport.value = NotSupported
+        }
+    }
+
+    override val connectionState =
+        satelliteSupport.whenSupported(
+            supported = ::connectionStateFlow,
+            orElse = flowOf(SatelliteConnectionState.Off)
+        )
+
+    // By using the SupportedSatelliteManager here, we expect registration never to fail
+    private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
+        conflatedCallbackFlow {
+                val cb = SatelliteModemStateCallback { state ->
+                    trySend(SatelliteConnectionState.fromModemState(state))
+                }
+
+                sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+
+                awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) }
+            }
+            .flowOn(bgDispatcher)
+
+    override val signalStrength =
+        satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+
+    // By using the SupportedSatelliteManager here, we expect registration never to fail
+    private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
+        conflatedCallbackFlow {
+                val cb = NtnSignalStrengthCallback { signalStrength ->
+                    trySend(signalStrength.level)
+                }
+
+                sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+
+                awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) }
+            }
+            .flowOn(bgDispatcher)
+
+    /** Fire off a request to check for satellite availability. Always runs on the bg context */
+    private suspend fun checkIsSatelliteAllowed() =
+        withContext(bgDispatcher) {
+            satelliteManager?.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                bgDispatcher.asExecutor(),
+                object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
+                    override fun onError(e: SatelliteManager.SatelliteException) {
+                        android.util.Log.e(TAG, "Found exception when checking for satellite: ", e)
+                        isSatelliteAllowedForCurrentLocation.value = false
+                    }
+
+                    override fun onResult(allowed: Boolean) {
+                        isSatelliteAllowedForCurrentLocation.value = allowed
+                    }
+                }
+            )
+        }
+
+    private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport =
+        suspendCancellableCoroutine { continuation ->
+            val cb =
+                object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
+                    override fun onResult(supported: Boolean) {
+                        continuation.resume(
+                            if (supported) {
+                                Supported(satelliteManager = this@checkSatelliteSupported)
+                            } else {
+                                NotSupported
+                            }
+                        )
+                    }
+
+                    override fun onError(error: SatelliteManager.SatelliteException) {
+                        // Assume that an error means it's not supported
+                        continuation.resume(NotSupported)
+                    }
+                }
+
+            requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb)
+        }
+
+    companion object {
+        // TTL for satellite polling is one hour
+        const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60
+
+        // Let the system boot up and stabilize before we check for system support
+        const val MIN_UPTIME: Long = 1000 * 60
+
+        private const val TAG = "DeviceBasedSatelliteRepo"
+
+        /** If our process hasn't been up for at least MIN_UPTIME, delay until we reach that time */
+        private suspend fun ensureMinUptime(clock: SystemClock, uptime: Long) {
+            val timeTilMinUptime =
+                uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
+            if (timeTilMinUptime > 0) {
+                delay(timeTilMinUptime)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
new file mode 100644
index 0000000..8779577
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.domain.interactor
+
+import com.android.internal.telephony.flags.Flags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class DeviceBasedSatelliteInteractor
+@Inject
+constructor(
+    val repo: DeviceBasedSatelliteRepository,
+    iconsInteractor: MobileIconsInteractor,
+    @Application scope: CoroutineScope,
+) {
+    /** Must be observed by any UI showing Satellite iconography */
+    val isSatelliteAllowed =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                repo.isSatelliteAllowedForCurrentLocation
+            } else {
+                flowOf(false)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    /** See [SatelliteConnectionState] for relevant states */
+    val connectionState =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                repo.connectionState
+            } else {
+
+                flowOf(SatelliteConnectionState.Off)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off)
+
+    /** 0-4 description of the connection strength */
+    val signalStrength =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                repo.signalStrength
+            } else {
+                flowOf(0)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+
+    /** When all connections are considered OOS, satellite connectivity is potentially valid */
+    val areAllConnectionsOutOfService =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                iconsInteractor.icons.aggregateOver(selector = { intr -> intr.isInService }) {
+                    isInServiceList ->
+                    isInServiceList.all { !it }
+                }
+            } else {
+                flowOf(false)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+}
+
+/**
+ * aggregateOver allows us to combine over the leaf-nodes of successive lists emitted from the
+ * top-level flow. Re-emits if the list changes, or any of the intermediate values change.
+ *
+ * Provides a way to connect the reactivity of the top-level flow with the reactivity of an
+ * arbitrarily-defined relationship ([selector]) from R to the flow that R exposes.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+private inline fun <R, reified S, T> Flow<List<R>>.aggregateOver(
+    crossinline selector: (R) -> Flow<S>,
+    crossinline transform: (Array<S>) -> T
+): Flow<T> {
+    return map { list -> list.map { selector(it) } }
+        .flatMapLatest { newFlows -> combine(newFlows) { newVals -> transform(newVals) } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
new file mode 100644
index 0000000..bfe2941
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.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.statusbar.pipeline.satellite.shared.model
+
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+
+enum class SatelliteConnectionState {
+    // State is unknown or undefined
+    Unknown,
+    // Radio is off
+    Off,
+    // Radio is on, but not yet connected
+    On,
+    // Radio is connected, aka satellite is available for use
+    Connected;
+
+    companion object {
+        // TODO(b/316635648): validate these states. We don't need the level of granularity that
+        //  SatelliteManager gives us.
+        fun fromModemState(@SatelliteManager.SatelliteModemState modemState: Int) =
+            when (modemState) {
+                // Transferring data is connected
+                SATELLITE_MODEM_STATE_CONNECTED,
+                SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING,
+                SATELLITE_MODEM_STATE_DATAGRAM_RETRYING -> Connected
+
+                // Modem is on but not connected
+                SATELLITE_MODEM_STATE_IDLE,
+                SATELLITE_MODEM_STATE_LISTENING,
+                SATELLITE_MODEM_STATE_NOT_CONNECTED -> On
+
+                // Consider unavailable equivalent to Off
+                SATELLITE_MODEM_STATE_UNAVAILABLE,
+                SATELLITE_MODEM_STATE_OFF -> Off
+
+                // Else, we don't know what's up
+                SATELLITE_MODEM_STATE_UNKNOWN -> Unknown
+                else -> Unknown
+            }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index 41ed76d..45078e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -20,6 +20,7 @@
 import static android.os.BatteryManager.CHARGING_POLICY_DEFAULT;
 import static android.os.BatteryManager.EXTRA_CHARGING_STATUS;
 import static android.os.BatteryManager.EXTRA_PRESENT;
+
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS;
 import static com.android.systemui.util.DumpUtilsKt.asIndenting;
 
@@ -61,6 +62,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
 
 import javax.annotation.concurrent.GuardedBy;
 
@@ -448,50 +450,38 @@
         firePowerSaveChanged();
     }
 
+    protected final void dispatchSafeChange(Consumer<BatteryStateChangeCallback> action) {
+        ArrayList<BatteryStateChangeCallback> copy;
+        synchronized (mChangeCallbacks) {
+            copy = new ArrayList<>(mChangeCallbacks);
+        }
+        final int n = copy.size();
+        for (int i = 0; i < n; i++) {
+            action.accept(copy.get(i));
+        }
+    }
+
     protected void fireBatteryLevelChanged() {
         mLogger.logBatteryLevelChangedCallback(mLevel, mPluggedIn, mCharging);
-        synchronized (mChangeCallbacks) {
-            final int N = mChangeCallbacks.size();
-            for (int i = 0; i < N; i++) {
-                mChangeCallbacks.get(i).onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
-            }
-        }
+        dispatchSafeChange(
+                (callback) -> callback.onBatteryLevelChanged(mLevel, mPluggedIn, mCharging));
     }
 
     private void fireBatteryUnknownStateChanged() {
-        synchronized (mChangeCallbacks) {
-            final int n = mChangeCallbacks.size();
-            for (int i = 0; i < n; i++) {
-                mChangeCallbacks.get(i).onBatteryUnknownStateChanged(mStateUnknown);
-            }
-        }
+        dispatchSafeChange((callback) -> callback.onBatteryUnknownStateChanged(mStateUnknown));
     }
 
     private void firePowerSaveChanged() {
-        synchronized (mChangeCallbacks) {
-            final int N = mChangeCallbacks.size();
-            for (int i = 0; i < N; i++) {
-                mChangeCallbacks.get(i).onPowerSaveChanged(mPowerSave);
-            }
-        }
+        dispatchSafeChange((callback) -> callback.onPowerSaveChanged(mPowerSave));
     }
 
     private void fireIsBatteryDefenderChanged() {
-        synchronized (mChangeCallbacks) {
-            final int n = mChangeCallbacks.size();
-            for (int i = 0; i < n; i++) {
-                mChangeCallbacks.get(i).onIsBatteryDefenderChanged(mIsBatteryDefender);
-            }
-        }
+        dispatchSafeChange((callback) -> callback.onIsBatteryDefenderChanged(mIsBatteryDefender));
     }
 
     private void fireIsIncompatibleChargingChanged() {
-        synchronized (mChangeCallbacks) {
-            final int n = mChangeCallbacks.size();
-            for (int i = 0; i < n; i++) {
-                mChangeCallbacks.get(i).onIsIncompatibleChargingChanged(mIsIncompatibleCharging);
-            }
-        }
+        dispatchSafeChange(
+                (callback) -> callback.onIsIncompatibleChargingChanged(mIsIncompatibleCharging));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index 53b343c..fc2f6e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -436,6 +436,8 @@
     @Override
     public void onServiceDisconnected() {}
 
+    // IMPORTANT: This handler guarantees that any operations on the list of callbacks is
+    // sequential, so no concurrent exceptions
     private final class H extends Handler {
         private final ArrayList<BluetoothController.Callback> mCallbacks = new ArrayList<>();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
index b06ebe9..149c8fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
@@ -35,9 +35,9 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.res.R;
 import com.android.systemui.util.Utils;
 
 import java.io.PrintWriter;
@@ -291,11 +291,12 @@
 
     @VisibleForTesting
     void fireOnCastDevicesChanged() {
+        final ArrayList<Callback> callbacks;
         synchronized (mCallbacks) {
-            for (Callback callback : mCallbacks) {
-                fireOnCastDevicesChanged(callback);
-            }
-
+            callbacks = new ArrayList<>(mCallbacks);
+        }
+        for (Callback callback : callbacks) {
+            fireOnCastDevicesChanged(callback);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
index 8207012..6319781 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
@@ -36,10 +36,12 @@
     }
 
     private void handleRestrictBackgroundChanged(boolean isDataSaving) {
+        ArrayList<DataSaverController.Listener> copy;
         synchronized (mListeners) {
-            for (int i = 0; i < mListeners.size(); i++) {
-                mListeners.get(i).onDataSaverChanged(isDataSaving);
-            }
+            copy = new ArrayList<>(mListeners);
+        }
+        for (int i = 0; i < copy.size(); i++) {
+            copy.get(i).onDataSaverChanged(isDataSaving);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
index 5dcafb3..b98eff8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -202,10 +202,12 @@
 
     private void dispatchListeners(int message, boolean argument) {
         synchronized (mListeners) {
-            final int N = mListeners.size();
+            final ArrayList<WeakReference<FlashlightController.FlashlightListener>> copy =
+                    new ArrayList<>(mListeners);
+            final int n = copy.size();
             boolean cleanup = false;
-            for (int i = 0; i < N; i++) {
-                FlashlightListener l = mListeners.get(i).get();
+            for (int i = 0; i < n; i++) {
+                FlashlightListener l = copy.get(i).get();
                 if (l != null) {
                     if (message == DISPATCH_ERROR) {
                         l.onFlashlightError();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
index fffd839..87dfc99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
@@ -119,7 +119,8 @@
             mHardwareToggleState.put(sensor, enabled);
         }
 
-        for (Callback callback : mCallbacks) {
+        Set<Callback> copy = new ArraySet<>(mCallbacks);
+        for (Callback callback : copy) {
             callback.onSensorBlockedChanged(sensor, isSensorBlocked(sensor));
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index 16334d3..886010c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -493,5 +493,10 @@
         public void onEnabledTrustAgentsChanged(int userId) {
             update(false /* updateAlways */);
         }
+
+        @Override
+        public void onForceIsDismissibleChanged(boolean keepUnlockedOnFold) {
+            update(false /* updateAlways */);
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
index e5f72eb..9eee5d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -356,6 +356,8 @@
         updateActiveLocationRequests();
     }
 
+    // IMPORTANT: This handler guarantees that any operations on the list of callbacks is
+    // sequential, so no concurrent exceptions
     private final class H extends Handler {
         private static final int MSG_LOCATION_SETTINGS_CHANGED = 1;
         private static final int MSG_LOCATION_ACTIVE_CHANGED = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
index 63b9ff9..b7d8ee3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
@@ -124,9 +124,10 @@
     }
 
     private void fireNextAlarmChanged() {
-        int n = mChangeCallbacks.size();
+        ArrayList<NextAlarmChangeCallback> copy = new ArrayList<>(mChangeCallbacks);
+        int n = copy.size();
         for (int i = 0; i < n; i++) {
-            mChangeCallbacks.get(i).onNextAlarmChanged(mNextAlarm);
+            copy.get(i).onNextAlarmChanged(mNextAlarm);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java
index f3d183c..0176abd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java
@@ -100,10 +100,13 @@
     }
 
     private void handleSafetyCenterEnableChange() {
+        final ArrayList<SafetyController.Listener> copy;
         synchronized (mListeners) {
-            for (int i = 0; i < mListeners.size(); i++) {
-                mListeners.get(i).onSafetyCenterEnableChanged(mSafetyCenterEnabled);
-            }
+            copy = new ArrayList<>(mListeners);
+        }
+        final int n = copy.size();
+        for (int i = 0; i < n; i++) {
+            copy.get(i).onSafetyCenterEnableChanged(mSafetyCenterEnabled);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
index 3be14bc..10bf068 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
@@ -48,6 +48,8 @@
     boolean isNetworkLoggingEnabled();
     boolean isVpnEnabled();
     boolean isVpnRestricted();
+    /** Whether the VPN network is validated. */
+    boolean isVpnValidated();
     /** Whether the VPN app should use branded VPN iconography.  */
     boolean isVpnBranded();
     String getPrimaryVpnName();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 4a4d4e1..9f4a906 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -15,6 +15,9 @@
  */
 package com.android.systemui.statusbar.policy;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+
 import android.annotation.Nullable;
 import android.app.admin.DeviceAdminInfo;
 import android.app.admin.DevicePolicyManager;
@@ -32,7 +35,9 @@
 import android.graphics.drawable.Drawable;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
 import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.VpnManager;
 import android.os.Handler;
@@ -50,12 +55,12 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
-import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 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.res.R;
 import com.android.systemui.settings.UserTracker;
 
 import org.xmlpull.v1.XmlPullParserException;
@@ -76,7 +81,10 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final NetworkRequest REQUEST =
-            new NetworkRequest.Builder().clearCapabilities().build();
+            new NetworkRequest.Builder()
+                    .clearCapabilities()
+                    .addTransportType(TRANSPORT_VPN)
+                    .build();
     private static final int NO_NETWORK = -1;
 
     private static final String VPN_BRANDED_META_DATA = "com.android.systemui.IS_BRANDED";
@@ -99,6 +107,8 @@
     private SparseArray<VpnConfig> mCurrentVpns = new SparseArray<>();
     private int mCurrentUserId;
     private int mVpnUserId;
+    @GuardedBy("mNetworkProperties")
+    private final SparseArray<NetworkProperties> mNetworkProperties = new SparseArray<>();
 
     // Key: userId, Value: whether the user has CACerts installed
     // Needs to be cached here since the query has to be asynchronous
@@ -162,6 +172,21 @@
             pw.print(mCurrentVpns.valueAt(i).user);
         }
         pw.println("}");
+        pw.print("  mNetworkProperties={");
+        synchronized (mNetworkProperties) {
+            for (int i = 0; i < mNetworkProperties.size(); ++i) {
+                if (i > 0) {
+                    pw.print(", ");
+                }
+                pw.print(mNetworkProperties.keyAt(i));
+                pw.print("={");
+                pw.print(mNetworkProperties.valueAt(i).interfaceName);
+                pw.print(", ");
+                pw.print(mNetworkProperties.valueAt(i).validated);
+                pw.print("}");
+            }
+        }
+        pw.println("}");
     }
 
     @Override
@@ -304,6 +329,26 @@
     }
 
     @Override
+    public boolean isVpnValidated() {
+        // Prioritize reporting the network status of the parent user.
+        final VpnConfig primaryVpnConfig = mCurrentVpns.get(mVpnUserId);
+        if (primaryVpnConfig != null) {
+            return getVpnValidationStatus(primaryVpnConfig);
+        }
+        // Identify any Unvalidated status in each active VPN network within other profiles.
+        for (int profileId : mUserManager.getEnabledProfileIds(mVpnUserId)) {
+            final VpnConfig vpnConfig = mCurrentVpns.get(profileId);
+            if (vpnConfig == null) {
+                continue;
+            }
+            if (!getVpnValidationStatus(vpnConfig)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
     public boolean hasCACertInCurrentUser() {
         Boolean hasCACerts = mHasCACerts.get(mCurrentUserId);
         return hasCACerts != null && hasCACerts.booleanValue();
@@ -429,10 +474,12 @@
     }
 
     private void fireCallbacks() {
+        final ArrayList<SecurityControllerCallback> copy;
         synchronized (mCallbacks) {
-            for (SecurityControllerCallback callback : mCallbacks) {
-                callback.onStateChanged();
-            }
+            copy = new ArrayList<>(mCallbacks);
+        }
+        for (SecurityControllerCallback callback : copy) {
+            callback.onStateChanged();
         }
     }
 
@@ -491,11 +538,74 @@
         @Override
         public void onLost(Network network) {
             if (DEBUG) Log.d(TAG, "onLost " + network.getNetId());
+            synchronized (mNetworkProperties) {
+                mNetworkProperties.delete(network.getNetId());
+            }
             updateState();
             fireCallbacks();
         };
+
+
+        @Override
+        public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+            if (DEBUG) Log.d(TAG, "onCapabilitiesChanged " + network.getNetId());
+            final NetworkProperties properties;
+            synchronized (mNetworkProperties) {
+                properties = mNetworkProperties.get(network.getNetId());
+            }
+            // When a new network appears, the system first notifies the application about
+            // its capabilities through onCapabilitiesChanged. This initial notification
+            // will be skipped because the interface information is included in the
+            // subsequent onLinkPropertiesChanged call. After validating the network, the
+            // system might send another onCapabilitiesChanged notification if the network
+            // becomes validated.
+            if (properties == null) {
+                return;
+            }
+            final boolean validated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
+            if (properties.validated != validated) {
+                properties.validated = validated;
+                fireCallbacks();
+            }
+        }
+
+        @Override
+        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
+            if (DEBUG) Log.d(TAG, "onLinkPropertiesChanged " + network.getNetId());
+            final String interfaceName = linkProperties.getInterfaceName();
+            if (interfaceName == null) {
+                Log.w(TAG, "onLinkPropertiesChanged event with null interface");
+                return;
+            }
+            synchronized (mNetworkProperties) {
+                final NetworkProperties properties = mNetworkProperties.get(network.getNetId());
+                if (properties == null) {
+                    mNetworkProperties.put(
+                            network.getNetId(),
+                            new NetworkProperties(interfaceName, false));
+                } else {
+                    properties.interfaceName = interfaceName;
+                }
+            }
+        }
     };
 
+    /**
+     *  Retrieve the validation status of the VPN network associated with the given VpnConfig.
+     */
+    private boolean getVpnValidationStatus(@NonNull VpnConfig vpnConfig) {
+        synchronized (mNetworkProperties) {
+            // Find the network has the same interface as the VpnConfig
+            for (int i = 0; i < mNetworkProperties.size(); ++i) {
+                if (mNetworkProperties.valueAt(i).interfaceName.equals(vpnConfig.interfaze)) {
+                    return mNetworkProperties.valueAt(i).validated;
+                }
+            }
+        }
+        // If no matching network is found, consider it validated.
+        return true;
+    }
+
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override public void onReceive(Context context, Intent intent) {
             if (KeyChain.ACTION_TRUST_STORE_CHANGED.equals(intent.getAction())) {
@@ -506,4 +616,17 @@
             }
         }
     };
+
+    /**
+     *  A data class to hold specific Network properties received through the NetworkCallback.
+     */
+    private static class NetworkProperties {
+        public String interfaceName;
+        public boolean validated;
+
+        NetworkProperties(@NonNull String interfaceName, boolean validated) {
+            this.interfaceName = interfaceName;
+            this.validated = validated;
+        }
+    }
 }
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 66bf527..df210b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -48,12 +48,12 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.util.Utils;
 import com.android.systemui.util.settings.GlobalSettings;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Objects;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
@@ -243,46 +243,43 @@
     }
 
     private void fireNextAlarmChanged() {
-        synchronized (mCallbacksLock) {
-            Utils.safeForeach(mCallbacks, c -> c.onNextAlarmChanged());
-        }
+        fireSafeChange(Callback::onNextAlarmChanged);
     }
 
     private void fireEffectsSuppressorChanged() {
-        synchronized (mCallbacksLock) {
-            Utils.safeForeach(mCallbacks, c -> c.onEffectsSupressorChanged());
-        }
+        fireSafeChange(Callback::onEffectsSupressorChanged);
     }
 
     private void fireZenChanged(int zen) {
-        synchronized (mCallbacksLock) {
-            Utils.safeForeach(mCallbacks, c -> c.onZenChanged(zen));
-        }
+        fireSafeChange(c -> c.onZenChanged(zen));
     }
 
     private void fireZenAvailableChanged(boolean available) {
-        synchronized (mCallbacksLock) {
-            Utils.safeForeach(mCallbacks, c -> c.onZenAvailableChanged(available));
-        }
+        fireSafeChange(c -> c.onZenAvailableChanged(available));
     }
 
     private void fireManualRuleChanged(ZenRule rule) {
-        synchronized (mCallbacksLock) {
-            Utils.safeForeach(mCallbacks, c -> c.onManualRuleChanged(rule));
-        }
+        fireSafeChange(c -> c.onManualRuleChanged(rule));
     }
 
     private void fireConsolidatedPolicyChanged(NotificationManager.Policy policy) {
+        fireSafeChange(c -> c.onConsolidatedPolicyChanged(policy));
+    }
+
+    private void fireSafeChange(Consumer<Callback> action) {
+        final ArrayList<Callback> copy;
         synchronized (mCallbacksLock) {
-            Utils.safeForeach(mCallbacks, c -> c.onConsolidatedPolicyChanged(policy));
+            copy = new ArrayList<>(mCallbacks);
+        }
+        final int n = copy.size();
+        for (int i = 0; i < n; i++) {
+            action.accept(copy.get(i));
         }
     }
 
     @VisibleForTesting
     protected void fireConfigChanged(ZenModeConfig config) {
-        synchronized (mCallbacksLock) {
-            Utils.safeForeach(mCallbacks, c -> c.onConfigChanged(config));
-        }
+        fireSafeChange(c -> c.onConfigChanged(config));
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt b/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt
index ac04d31..4f7dce3 100644
--- a/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt
@@ -2,6 +2,7 @@
 
 import java.lang.ref.SoftReference
 import java.lang.ref.WeakReference
+import java.util.concurrent.atomic.AtomicReference
 import kotlin.properties.ReadWriteProperty
 import kotlin.reflect.KProperty
 
@@ -48,3 +49,25 @@
         }
     }
 }
+
+/**
+ * Creates a nullable Kotlin idiomatic [AtomicReference].
+ *
+ * Usage:
+ * ```
+ * var atomicReferenceObj: Object? by nullableAtomicReference(null)
+ * atomicReferenceObj = Object()
+ * ```
+ */
+fun <T> nullableAtomicReference(obj: T? = null): ReadWriteProperty<Any?, T?> {
+    return object : ReadWriteProperty<Any?, T?> {
+        val t = AtomicReference(obj)
+        override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
+            return t.get()
+        }
+
+        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
+            t.set(value)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java
index df5162a..3d724e1 100644
--- a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java
+++ b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java
@@ -22,12 +22,17 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.os.IBinder;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.util.annotations.WeaklyReferencedCallback;
 
+import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
@@ -244,6 +249,21 @@
         });
     }
 
+    void dump(@NonNull PrintWriter pw) {
+        IndentingPrintWriter ipw = DumpUtilsKt.asIndenting(pw);
+        ipw.println("ObservableServiceConnection state:");
+        DumpUtilsKt.withIncreasedIndent(ipw, () -> {
+            ipw.println("mServiceIntent: " + mServiceIntent);
+            ipw.println("mLastDisconnectReason: " + mLastDisconnectReason.orElse(-1));
+            ipw.println("Callbacks:");
+            DumpUtilsKt.withIncreasedIndent(ipw, () -> {
+                for (WeakReference<Callback<T>> cbRef : mCallbacks) {
+                    ipw.println(cbRef.get());
+                }
+            });
+        });
+    }
+
     private void applyToCallbacksLocked(Consumer<Callback<T>> applicator) {
         final Iterator<WeakReference<Callback<T>>> iterator = mCallbacks.iterator();
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
index 6e19bed..9b72eb7 100644
--- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
@@ -17,6 +17,7 @@
 package com.android.systemui.util.service;
 
 import static com.android.systemui.util.service.dagger.ObservableServiceModule.BASE_RECONNECT_DELAY_MS;
+import static com.android.systemui.util.service.dagger.ObservableServiceModule.DUMPSYS_NAME;
 import static com.android.systemui.util.service.dagger.ObservableServiceModule.MAX_RECONNECT_ATTEMPTS;
 import static com.android.systemui.util.service.dagger.ObservableServiceModule.MIN_CONNECTION_DURATION_MS;
 import static com.android.systemui.util.service.dagger.ObservableServiceModule.OBSERVER;
@@ -24,9 +25,15 @@
 
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.time.SystemClock;
 
+import java.io.PrintWriter;
+
 import javax.inject.Inject;
 import javax.inject.Named;
 
@@ -35,7 +42,7 @@
  * {@link ObservableServiceConnection}.
  * @param <T> The transformed connection type handled by the service.
  */
-public class PersistentConnectionManager<T> {
+public class PersistentConnectionManager<T> implements Dumpable {
     private static final String TAG = "PersistentConnManager";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -45,6 +52,8 @@
     private final int mMaxReconnectAttempts;
     private final int mMinConnectionDuration;
     private final Observer mObserver;
+    private final DumpManager mDumpManager;
+    private final String mDumpsysName;
 
     private int mReconnectAttempts = 0;
     private Runnable mCurrentReconnectCancelable;
@@ -89,6 +98,8 @@
     public PersistentConnectionManager(
             SystemClock clock,
             DelayableExecutor mainExecutor,
+            DumpManager dumpManager,
+            @Named(DUMPSYS_NAME) String dumpsysName,
             @Named(SERVICE_CONNECTION) ObservableServiceConnection<T> serviceConnection,
             @Named(MAX_RECONNECT_ATTEMPTS) int maxReconnectAttempts,
             @Named(BASE_RECONNECT_DELAY_MS) int baseReconnectDelayMs,
@@ -98,6 +109,8 @@
         mMainExecutor = mainExecutor;
         mConnection = serviceConnection;
         mObserver = observer;
+        mDumpManager = dumpManager;
+        mDumpsysName = TAG + "#" + dumpsysName;
 
         mMaxReconnectAttempts = maxReconnectAttempts;
         mBaseReconnectDelayMs = baseReconnectDelayMs;
@@ -108,6 +121,7 @@
      * Begins the {@link PersistentConnectionManager} by connecting to the associated service.
      */
     public void start() {
+        mDumpManager.registerCriticalDumpable(mDumpsysName, this);
         mConnection.addCallback(mConnectionCallback);
         mObserver.addCallback(mObserverCallback);
         initiateConnectionAttempt();
@@ -120,6 +134,32 @@
         mConnection.removeCallback(mConnectionCallback);
         mObserver.removeCallback(mObserverCallback);
         mConnection.unbind();
+        mDumpManager.unregisterDumpable(mDumpsysName);
+    }
+
+    /**
+     * Add a callback to the {@link ObservableServiceConnection}.
+     * @param callback The callback to add.
+     */
+    public void addConnectionCallback(ObservableServiceConnection.Callback<T> callback) {
+        mConnection.addCallback(callback);
+    }
+
+    /**
+     * Remove a callback from the {@link ObservableServiceConnection}.
+     * @param callback The callback to remove.
+     */
+    public void removeConnectionCallback(ObservableServiceConnection.Callback<T> callback) {
+        mConnection.removeCallback(callback);
+    }
+
+    @Override
+    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+        pw.println("mMaxReconnectAttempts: " + mMaxReconnectAttempts);
+        pw.println("mBaseReconnectDelayMs: " + mBaseReconnectDelayMs);
+        pw.println("mMinConnectionDuration: " + mMinConnectionDuration);
+        pw.println("mReconnectAttempts: " + mReconnectAttempts);
+        mConnection.dump(pw);
     }
 
     private void initiateConnectionAttempt() {
diff --git a/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java
index bcf34f8..c52c524 100644
--- a/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java
@@ -19,14 +19,14 @@
 
 import android.content.res.Resources;
 
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.qualifiers.Main;
-
-import javax.inject.Named;
+import com.android.systemui.res.R;
 
 import dagger.Module;
 import dagger.Provides;
 
+import javax.inject.Named;
+
 /**
  * Module containing components and parameters for
  * {@link com.android.systemui.util.service.ObservableServiceConnection}
@@ -41,6 +41,7 @@
     public static final String MIN_CONNECTION_DURATION_MS = "min_connection_duration_ms";
     public static final String SERVICE_CONNECTION = "service_connection";
     public static final String OBSERVER = "observer";
+    public static final String DUMPSYS_NAME = "dumpsys_name";
 
     @Provides
     @Named(MAX_RECONNECT_ATTEMPTS)
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 20fef92..e57c0e7 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -210,7 +210,13 @@
             if (DEBUG) {
                 Log.i(TAG, "onSurfaceDestroyed");
             }
-            mSurfaceHolder = null;
+            mLongExecutor.execute(this::onSurfaceDestroyedSynchronized);
+        }
+
+        private void onSurfaceDestroyedSynchronized() {
+            synchronized (mLock) {
+                mSurfaceHolder = null;
+            }
         }
 
         @Override
@@ -241,7 +247,7 @@
 
         private void drawFrameInternal() {
             if (mSurfaceHolder == null) {
-                Log.e(TAG, "attempt to draw a frame without a valid surface");
+                Log.i(TAG, "attempt to draw a frame without a valid surface");
                 return;
             }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index bb98595..d8eb05a 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -110,6 +110,7 @@
 import android.text.TextUtils;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEventLogger;
@@ -222,6 +223,8 @@
     @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
+    private FoldGracePeriodProvider mFoldGracePeriodProvider;
+    @Mock
     private TelephonyManager mTelephonyManager;
     @Mock
     private SensorPrivacyManager mSensorPrivacyManager;
@@ -338,6 +341,7 @@
                         anyInt());
 
         mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
+        mKeyguardUpdateMonitor.mFoldGracePeriodProvider = mFoldGracePeriodProvider;
         setupBiometrics(mKeyguardUpdateMonitor);
         mKeyguardUpdateMonitor.setFaceAuthInteractor(mFaceAuthInteractor);
         verify(mFaceAuthInteractor).registerListener(mFaceAuthenticationListener.capture());
@@ -2139,6 +2143,30 @@
                 mSelectedUserInteractor.getSelectedUserId(), 0, null);
     }
 
+    @Test
+    public void forceIsDismissibleKeyguard_foldingGracePeriodNotEnabled() {
+        when(mFoldGracePeriodProvider.isEnabled()).thenReturn(false);
+        primaryAuthNotRequiredByStrongAuthTracker();
+        mKeyguardUpdateMonitor.tryForceIsDismissibleKeyguard();
+        Assert.assertFalse(mKeyguardUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked());
+    }
+
+    @Test
+    public void forceIsDismissibleKeyguard() {
+        when(mFoldGracePeriodProvider.isEnabled()).thenReturn(true);
+        primaryAuthNotRequiredByStrongAuthTracker();
+        mKeyguardUpdateMonitor.tryForceIsDismissibleKeyguard();
+        Assert.assertTrue(mKeyguardUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked());
+    }
+
+    @Test
+    public void forceIsDismissibleKeyguard_respectsLockdown() {
+        when(mFoldGracePeriodProvider.isEnabled()).thenReturn(true);
+        userDeviceLockDown();
+        mKeyguardUpdateMonitor.tryForceIsDismissibleKeyguard();
+        Assert.assertFalse(mKeyguardUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked());
+    }
+
     private void verifyFingerprintAuthenticateNeverCalled() {
         verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), any());
         verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
index 342494d..46936d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
@@ -25,6 +25,7 @@
 import com.android.internal.R
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
 import com.android.systemui.decor.FaceScanningProviderFactory
 import com.android.systemui.log.ScreenDecorationsLogger
 import com.android.systemui.log.logcatLogBuffer
@@ -53,6 +54,8 @@
 
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
+    private val facePropertyRepository = FakeFacePropertyRepository()
+
     private val displayId = 2
 
     @Before
@@ -86,9 +89,10 @@
                 keyguardUpdateMonitor,
                 mock(Executor::class.java),
                 ScreenDecorationsLogger(logcatLogBuffer("FaceScanningProviderFactoryTest")),
+                facePropertyRepository,
             )
 
-        whenever(authController.faceSensorLocation).thenReturn(Point(10, 10))
+        facePropertyRepository.setSensorLocation(Point(10, 10))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index c094df5..c07148b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -54,6 +54,7 @@
 import android.content.res.TypedArray;
 import android.graphics.Path;
 import android.graphics.PixelFormat;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.hardware.display.DisplayManager;
@@ -80,6 +81,7 @@
 
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository;
 import com.android.systemui.decor.CornerDecorProvider;
 import com.android.systemui.decor.CutoutDecorProviderFactory;
 import com.android.systemui.decor.CutoutDecorProviderImpl;
@@ -101,6 +103,7 @@
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.concurrency.FakeThreadFactory;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -108,8 +111,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
@@ -169,8 +170,11 @@
     private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener;
     @Mock
     private CutoutDecorProviderFactory mCutoutFactory;
-    @Captor
-    private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback;
+    @Mock
+    private JavaAdapter mJavaAdapter;
+
+    private FakeFacePropertyRepository mFakeFacePropertyRepository =
+            new FakeFacePropertyRepository();
     private List<DecorProvider> mMockCutoutList;
 
     @Before
@@ -227,20 +231,23 @@
         doAnswer(it -> !(mMockCutoutList.isEmpty())).when(mCutoutFactory).getHasProviders();
         doReturn(mMockCutoutList).when(mCutoutFactory).getProviders();
 
+        mFakeFacePropertyRepository.setSensorLocation(new Point(10, 10));
+
         mFaceScanningDecorProvider = spy(new FaceScanningOverlayProviderImpl(
                 BOUNDS_POSITION_TOP,
                 mAuthController,
                 mStatusBarStateController,
                 mKeyguardUpdateMonitor,
                 mExecutor,
-                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
+                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
+                mFakeFacePropertyRepository));
 
         mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings,
                 mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController,
                 mThreadFactory,
                 mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
                 new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
-                mAuthController) {
+                mFakeFacePropertyRepository, mJavaAdapter) {
             @Override
             public void start() {
                 super.start();
@@ -1235,9 +1242,9 @@
                 mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker,
                 mDotViewController,
                 mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
-                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
+                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
+                mFakeFacePropertyRepository, mJavaAdapter);
         screenDecorations.start();
-        verify(mAuthController).addCallback(mAuthControllerCallback.capture());
         when(mContext.getDisplay()).thenReturn(mDisplay);
         when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() {
             @Override
@@ -1252,9 +1259,9 @@
         });
         mExecutor.runAllReady();
         clearInvocations(mFaceScanningDecorProvider);
-
-        AuthController.Callback callback = mAuthControllerCallback.getValue();
-        callback.onFaceSensorLocationChanged();
+        final Point location = new Point();
+        mFakeFacePropertyRepository.setSensorLocation(location);
+        screenDecorations.onFaceSensorLocationChanged(location);
         mExecutor.runAllReady();
 
         verify(mFaceScanningDecorProvider).onReloadResAndMeasure(any(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 837a130..2afb3a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -43,7 +43,6 @@
 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
 import android.view.animation.AccelerateInterpolator;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -68,7 +67,6 @@
 
 @LargeTest
 @RunWith(AndroidTestingRunner.class)
-@FlakyTest(bugId = 308501761)
 public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
 
     @Rule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java
new file mode 100644
index 0000000..9dd337e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java
@@ -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.accessibility.floatingmenu;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class MenuNotificationFactoryTest extends SysuiTestCase {
+    private MenuNotificationFactory mMenuNotificationFactory;
+
+    @Before
+    public void setUp() {
+        mMenuNotificationFactory = new MenuNotificationFactory(mContext);
+    }
+
+    @Test
+    public void createHiddenNotification_hasUndoAndDeleteAction() {
+        Notification notification = mMenuNotificationFactory.createHiddenNotification();
+
+        assertThat(notification.contentIntent.getIntent().getAction()).isEqualTo(
+                MenuNotificationFactory.ACTION_UNDO);
+        assertThat(notification.deleteIntent.getIntent().getAction()).isEqualTo(
+                MenuNotificationFactory.ACTION_DELETE);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index be6f3ff..68879a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -21,15 +21,30 @@
 import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.systemBars;
+
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_DELETE;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_UNDO;
 import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex;
+
 import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
@@ -40,6 +55,8 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -53,16 +70,21 @@
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.SysuiTestableContext;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 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;
 import org.mockito.Mock;
+import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -98,6 +120,12 @@
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Spy
+    private SysuiTestableContext mSpyContext = getContext();
     @Mock
     private IAccessibilityFloatingMenu mFloatingMenu;
 
@@ -110,8 +138,12 @@
     @Mock
     private AccessibilityManager mStubAccessibilityManager;
 
+    private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
+
     @Before
     public void setUp() throws Exception {
+        mSpyContext.addMockSystemService(Context.NOTIFICATION_SERVICE, mMockNotificationManager);
+
         final Rect mDisplayBounds = new Rect();
         mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH,
                 DISPLAY_WINDOW_HEIGHT);
@@ -119,31 +151,31 @@
                 new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f));
         doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics();
 
-        mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager,
-                mFloatingMenu, mSecureSettings);
+        mMenuViewLayer = new MenuViewLayer(mSpyContext, mStubWindowManager,
+                mStubAccessibilityManager, mFloatingMenu, mSecureSettings);
         mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
 
         mLastAccessibilityButtonTargets =
-                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.getStringForUser(mSpyContext.getContentResolver(),
                         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
         mLastEnabledAccessibilityServices =
-                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.getStringForUser(mSpyContext.getContentResolver(),
                         Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT);
 
         mMenuViewLayer.onAttachedToWindow();
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "", UserHandle.USER_CURRENT);
     }
 
     @After
     public void tearDown() throws Exception {
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastAccessibilityButtonTargets,
                 UserHandle.USER_CURRENT);
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices,
                 UserHandle.USER_CURRENT);
 
@@ -188,7 +220,7 @@
         setupEnabledAccessibilityServiceList();
 
         mMenuViewLayer.mDismissMenuAction.run();
-        final String value = Settings.Secure.getString(mContext.getContentResolver(),
+        final String value = Settings.Secure.getString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         assertThat(value).isEqualTo("");
@@ -203,7 +235,7 @@
                 AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY)).thenReturn(stubShortcutTargets);
 
         mMenuViewLayer.mDismissMenuAction.run();
-        final String value = Settings.Secure.getString(mContext.getContentResolver(),
+        final String value = Settings.Secure.getString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         assertThat(value).isEqualTo(TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
@@ -278,9 +310,60 @@
         assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
         assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y);
     }
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void onReleasedInTarget_hideMenuAndShowNotificationWithExpectedActions() {
+        dragMenuThenReleasedInTarget();
+
+        verify(mMockNotificationManager).notify(
+                eq(SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN),
+                any(Notification.class));
+        ArgumentCaptor<IntentFilter> intentFilterCaptor = ArgumentCaptor.forClass(
+                IntentFilter.class);
+        verify(mSpyContext).registerReceiver(
+                any(BroadcastReceiver.class),
+                intentFilterCaptor.capture(),
+                anyInt());
+        assertThat(intentFilterCaptor.getValue().matchAction(ACTION_UNDO)).isTrue();
+        assertThat(intentFilterCaptor.getValue().matchAction(ACTION_DELETE)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void receiveActionUndo_dismissNotificationAndMenuVisible() {
+        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        dragMenuThenReleasedInTarget();
+
+        verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(),
+                any(IntentFilter.class), anyInt());
+        broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_UNDO));
+
+        verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue());
+        verify(mMockNotificationManager).cancel(
+                SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+        assertThat(mMenuView.getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void receiveActionDelete_dismissNotificationAndHideMenu() {
+        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        dragMenuThenReleasedInTarget();
+
+        verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(),
+                any(IntentFilter.class), anyInt());
+        broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_DELETE));
+
+        verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue());
+        verify(mMockNotificationManager).cancel(
+                SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+        verify(mFloatingMenu).hide();
+    }
 
     private void setupEnabledAccessibilityServiceList() {
-        Settings.Secure.putString(mContext.getContentResolver(),
+        Settings.Secure.putString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
 
@@ -344,6 +427,12 @@
                     springAnimation.skipToEnd();
                     springAnimation.doAnimationFrame(500);
                 });
+    }
 
+    private void dragMenuThenReleasedInTarget() {
+        MagnetizedObject.MagnetListener magnetListener =
+                mMenuViewLayer.getDragToInteractAnimationController().getMagnetListener();
+        magnetListener.onReleasedInTarget(
+                new MagnetizedObject.MagneticTarget(mock(View.class), 200));
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
index c525711..9b6c8cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
@@ -25,14 +25,12 @@
 import android.widget.Button
 import android.widget.SeekBar
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.model.SysUiState
+import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK
@@ -78,7 +76,6 @@
     @Mock private lateinit var dialogManager: SystemUIDialogManager
     @Mock private lateinit var dialogFactory: SystemUIDialog.Factory
     @Mock private lateinit var userTracker: UserTracker
-    private val featureFlags = FakeFeatureFlags()
     @Mock private lateinit var sysuiState: SysUiState
     @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
 
@@ -88,7 +85,6 @@
         testableLooper = TestableLooper.get(this)
         val mainHandler = Handler(testableLooper.looper)
         systemSettings = FakeSettings()
-        featureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true)
         // Guarantee that the systemSettings always starts with the default font scale.
         systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId)
         secureSettings = FakeSettings()
@@ -96,29 +92,32 @@
         backgroundDelayableExecutor = FakeExecutor(systemClock)
         whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState)
 
-        fontScalingDialogDelegate = spy(FontScalingDialogDelegate(
-                mContext,
-                dialogFactory,
-                LayoutInflater.from(mContext),
-                systemSettings,
-                secureSettings,
-                systemClock,
-                userTracker,
-                mainHandler,
-                backgroundDelayableExecutor
-            ))
+        fontScalingDialogDelegate =
+            spy(
+                FontScalingDialogDelegate(
+                    mContext,
+                    dialogFactory,
+                    LayoutInflater.from(mContext),
+                    systemSettings,
+                    secureSettings,
+                    systemClock,
+                    userTracker,
+                    mainHandler,
+                    backgroundDelayableExecutor
+                )
+            )
 
-        dialog = SystemUIDialog(
-            mContext,
-            0,
-            DEFAULT_DISMISS_ON_DEVICE_LOCK,
-            featureFlags,
-            dialogManager,
-            sysuiState,
-            fakeBroadcastDispatcher,
-            dialogLaunchAnimator,
-            fontScalingDialogDelegate
-        )
+        dialog =
+            SystemUIDialog(
+                mContext,
+                0,
+                DEFAULT_DISMISS_ON_DEVICE_LOCK,
+                dialogManager,
+                sysuiState,
+                fakeBroadcastDispatcher,
+                dialogLaunchAnimator,
+                fontScalingDialogDelegate
+            )
 
         whenever(dialogFactory.create(any(), any())).thenReturn(dialog)
     }
@@ -299,11 +298,7 @@
         // Default seekbar progress for font size is 1, simulate dragging to 0 without
         // releasing the finger
         changeListener.onStartTrackingTouch(seekBar)
-        changeListener.onProgressChanged(
-            seekBar,
-            /* progress= */ 0,
-            /* fromUser= */ false
-        )
+        changeListener.onProgressChanged(seekBar, /* progress= */ 0, /* fromUser= */ false)
         backgroundDelayableExecutor.advanceClockToNext()
         backgroundDelayableExecutor.runAllReady()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
index c143bc0..a47e288 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
@@ -29,6 +29,7 @@
 import com.android.keyguard.logging.KeyguardLogger
 import com.android.systemui.Flags.FLAG_LIGHT_REVEAL_MIGRATION
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.keyguard.WakefulnessLifecycle
@@ -93,6 +94,7 @@
     @Mock
     private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
 
+    private val facePropertyRepository = FakeFacePropertyRepository()
     private val displayMetrics = DisplayMetrics()
 
     @Captor
@@ -126,6 +128,7 @@
             KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
             biometricUnlockController,
             lightRevealScrim,
+            facePropertyRepository,
             rippleView,
         )
         controller.init()
@@ -202,7 +205,7 @@
 
     @Test
     fun testNullFaceSensorLocationDoesNothing() {
-        `when`(authController.faceSensorLocation).thenReturn(null)
+        facePropertyRepository.setSensorLocation(null)
         controller.onViewAttached()
 
         val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
@@ -270,7 +273,7 @@
     fun testAnimatorRunWhenWakeAndUnlock_faceUdfpsFingerDown() {
         mSetFlagsRule.disableFlags(FLAG_LIGHT_REVEAL_MIGRATION)
         val faceLocation = Point(5, 5)
-        `when`(authController.faceSensorLocation).thenReturn(faceLocation)
+        facePropertyRepository.setSensorLocation(faceLocation)
         controller.onViewAttached()
         `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java
index 2aeba9a..3dcb3f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java
@@ -20,6 +20,7 @@
 
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.view.Surface;
 
 import androidx.test.filters.SmallTest;
@@ -63,28 +64,32 @@
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, 0/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[0]);
         // touch at 90 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, -1/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[1]);
         // touch at 180 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         -1 /* touchX */, 0/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[2]);
         // touch at 270 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, 1/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[3]);
     }
@@ -97,28 +102,32 @@
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, 0 /* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[1]);
         // touch at 90 degrees -> 180 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, -1 /* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[2]);
         // touch at 180 degrees -> 270 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         -1 /* touchX */, 0 /* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[3]);
         // touch at 270 degrees -> 0 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, 1/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[0]);
     }
@@ -131,28 +140,32 @@
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, 0/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[3]);
         // touch at 90 degrees -> 0 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, -1/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[0]);
         // touch at 180 degrees -> 90 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         -1 /* touchX */, 0/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[1]);
         // touch at 270 degrees -> 180 degrees
         assertThat(
                 mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                         0 /* touchX */, 1/* touchY */,
-                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation,
+                                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)
                 )
         ).isEqualTo(mTouchHints[2]);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt
index 834179bf..a84778a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt
@@ -19,6 +19,7 @@
 import android.hardware.devicestate.DeviceStateManager
 import android.hardware.display.DisplayManager
 import android.os.Handler
+import android.util.Size
 import android.view.Display
 import android.view.DisplayInfo
 import android.view.Surface
@@ -147,6 +148,40 @@
             displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_180)
             assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_180)
         }
+
+    @Test
+    fun updatesCurrentSize_whenDisplayStateChanges() =
+        testScope.runTest {
+            val currentSize by collectLastValue(underTest.currentDisplaySize)
+            runCurrent()
+
+            verify(displayManager)
+                .registerDisplayListener(
+                    displayListenerCaptor.capture(),
+                    same(handler),
+                    eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED)
+                )
+
+            whenever(display.getDisplayInfo(any())).then {
+                val info = it.getArgument<DisplayInfo>(0)
+                info.rotation = Surface.ROTATION_0
+                info.logicalWidth = 100
+                info.logicalHeight = 200
+                return@then true
+            }
+            displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_0)
+            assertThat(currentSize).isEqualTo(Size(100, 200))
+
+            whenever(display.getDisplayInfo(any())).then {
+                val info = it.getArgument<DisplayInfo>(0)
+                info.rotation = Surface.ROTATION_90
+                info.logicalWidth = 100
+                info.logicalHeight = 200
+                return@then true
+            }
+            displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_180)
+            assertThat(currentSize).isEqualTo(Size(200, 100))
+        }
 }
 
 private fun DeviceStateManager.captureCallback() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
index c14ad6a..9f24d5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
@@ -17,10 +17,12 @@
 
 package com.android.systemui.biometrics.data.repository
 
+import android.graphics.Point
 import android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_NONE
 import android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_PERMANENT
 import android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED
 import android.hardware.biometrics.SensorProperties
+import android.hardware.camera2.CameraManager
 import android.hardware.face.FaceManager
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
@@ -28,9 +30,12 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.shared.model.LockoutMode
 import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestDispatcher
@@ -45,6 +50,7 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.any
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
@@ -53,23 +59,56 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class FacePropertyRepositoryImplTest : SysuiTestCase() {
+    companion object {
+        private const val LOGICAL_CAMERA_ID_OUTER_FRONT = "0"
+        private const val LOGICAL_CAMERA_ID_INNER_FRONT = "1"
+        private const val PHYSICAL_CAMERA_ID_OUTER_FRONT = "5"
+        private const val PHYSICAL_CAMERA_ID_INNER_FRONT = "6"
+        private val OUTER_FRONT_SENSOR_LOCATION = intArrayOf(100, 10, 20)
+        private val INNER_FRONT_SENSOR_LOCATION = intArrayOf(200, 20, 30)
+    }
+
     @JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
 
     private lateinit var underTest: FacePropertyRepository
     private lateinit var dispatcher: TestDispatcher
     private lateinit var testScope: TestScope
 
+    private val displayStateRepository = FakeDisplayStateRepository()
+    private val configurationRepository = FakeConfigurationRepository()
+
     @Captor private lateinit var callback: ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback>
     @Mock private lateinit var faceManager: FaceManager
+    @Captor private lateinit var cameraCallback: ArgumentCaptor<CameraManager.AvailabilityCallback>
+    @Mock private lateinit var cameraManager: CameraManager
     @Before
     fun setup() {
+        overrideResource(R.string.config_protectedCameraId, LOGICAL_CAMERA_ID_OUTER_FRONT)
+        overrideResource(R.string.config_protectedPhysicalCameraId, PHYSICAL_CAMERA_ID_OUTER_FRONT)
+        overrideResource(R.string.config_protectedInnerCameraId, LOGICAL_CAMERA_ID_INNER_FRONT)
+        overrideResource(
+            R.string.config_protectedInnerPhysicalCameraId,
+            PHYSICAL_CAMERA_ID_INNER_FRONT
+        )
+        overrideResource(R.array.config_face_auth_props, OUTER_FRONT_SENSOR_LOCATION)
+        overrideResource(R.array.config_inner_face_auth_props, INNER_FRONT_SENSOR_LOCATION)
+
         dispatcher = StandardTestDispatcher()
         testScope = TestScope(dispatcher)
         underTest = createRepository(faceManager)
     }
 
     private fun createRepository(manager: FaceManager? = faceManager) =
-        FacePropertyRepositoryImpl(testScope.backgroundScope, dispatcher, manager)
+        FacePropertyRepositoryImpl(
+            context,
+            context.mainExecutor,
+            testScope.backgroundScope,
+            dispatcher,
+            manager,
+            cameraManager,
+            displayStateRepository,
+            configurationRepository,
+        )
 
     @Test
     fun whenFaceManagerIsNotPresentIsNull() =
@@ -129,6 +168,75 @@
             assertThat(underTest.getLockoutMode(userId)).isEqualTo(LockoutMode.NONE)
         }
 
+    @Test
+    fun providesTheSensorLocationOfOuterCameraFromOnPhysicalCameraAvailable() {
+        testScope.runTest {
+            runCurrent()
+            collectLastValue(underTest.sensorLocation)
+
+            verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture())
+            callback.value.onAllAuthenticatorsRegistered(
+                listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG))
+            )
+            runCurrent()
+            verify(cameraManager)
+                .registerAvailabilityCallback(any(Executor::class.java), cameraCallback.capture())
+
+            cameraCallback.value.onPhysicalCameraAvailable("1", PHYSICAL_CAMERA_ID_OUTER_FRONT)
+            runCurrent()
+
+            val sensorLocation by collectLastValue(underTest.sensorLocation)
+            assertThat(sensorLocation)
+                .isEqualTo(Point(OUTER_FRONT_SENSOR_LOCATION[0], OUTER_FRONT_SENSOR_LOCATION[1]))
+        }
+    }
+
+    @Test
+    fun providesTheSensorLocationOfInnerCameraFromOnPhysicalCameraAvailable() {
+        testScope.runTest {
+            runCurrent()
+            collectLastValue(underTest.sensorLocation)
+
+            verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture())
+            callback.value.onAllAuthenticatorsRegistered(
+                listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG))
+            )
+            runCurrent()
+            verify(cameraManager)
+                .registerAvailabilityCallback(any(Executor::class.java), cameraCallback.capture())
+
+            cameraCallback.value.onPhysicalCameraAvailable("1", PHYSICAL_CAMERA_ID_INNER_FRONT)
+            runCurrent()
+
+            val sensorLocation by collectLastValue(underTest.sensorLocation)
+            assertThat(sensorLocation)
+                .isEqualTo(Point(INNER_FRONT_SENSOR_LOCATION[0], INNER_FRONT_SENSOR_LOCATION[1]))
+        }
+    }
+
+    @Test
+    fun providesTheSensorLocationOfCameraFromOnPhysicalCameraUnavailable() {
+        testScope.runTest {
+            runCurrent()
+            collectLastValue(underTest.sensorLocation)
+
+            verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture())
+            callback.value.onAllAuthenticatorsRegistered(
+                listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG))
+            )
+            runCurrent()
+            verify(cameraManager)
+                .registerAvailabilityCallback(any(Executor::class.java), cameraCallback.capture())
+
+            cameraCallback.value.onPhysicalCameraUnavailable("1", PHYSICAL_CAMERA_ID_INNER_FRONT)
+            runCurrent()
+
+            val sensorLocation by collectLastValue(underTest.sensorLocation)
+            assertThat(sensorLocation)
+                .isEqualTo(Point(OUTER_FRONT_SENSOR_LOCATION[0], OUTER_FRONT_SENSOR_LOCATION[1]))
+        }
+    }
+
     private fun createSensorProperties(id: Int, strength: Int) =
         FaceSensorPropertiesInternal(id, strength, 0, emptyList(), 1, false, false, false)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
index 3ff43c6..7d5aec6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
@@ -17,7 +17,9 @@
 package com.android.systemui.bluetooth;
 
 import static com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.any;
@@ -40,8 +42,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.res.R;
@@ -72,7 +72,6 @@
             LocalBluetoothLeBroadcast.class);
     private final BroadcastSender mBroadcastSender = mock(BroadcastSender.class);
     private BroadcastDialogDelegate mBroadcastDialogDelegate;
-    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     @Mock SystemUIDialog.Factory mSystemUIDialogFactory;
     @Mock SystemUIDialogManager mDialogManager;
     @Mock SysUiState mSysUiState;
@@ -91,7 +90,6 @@
         when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
         when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(null);
 
-        mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
         when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
         when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog);
 
@@ -110,7 +108,6 @@
                 mContext,
                 0,
                 DEFAULT_DISMISS_ON_DEVICE_LOCK,
-                mFeatureFlags,
                 mDialogManager,
                 mSysUiState,
                 getFakeBroadcastDispatcher(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index b38c9ec..b57cf53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -72,6 +72,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEventLogger;
@@ -324,6 +325,54 @@
 
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void showKeyguardAfterKeyguardNotEnabled() {
+        // GIVEN feature is enabled
+        final FoldGracePeriodProvider mockedFoldGracePeriodProvider =
+                mock(FoldGracePeriodProvider.class);
+        mViewMediator.mFoldGracePeriodProvider = mockedFoldGracePeriodProvider;
+        when(mockedFoldGracePeriodProvider.isEnabled()).thenReturn(true);
+
+        // GIVEN keyguard is not enabled and isn't showing
+        mViewMediator.onSystemReady();
+        mViewMediator.setKeyguardEnabled(false);
+        TestableLooper.get(this).processAllMessages();
+        captureKeyguardUpdateMonitorCallback();
+        assertFalse(mViewMediator.isShowingAndNotOccluded());
+
+        // WHEN showKeyguard is requested
+        mViewMediator.showDismissibleKeyguard();
+
+        // THEN keyguard is shown
+        TestableLooper.get(this).processAllMessages();
+        assertTrue(mViewMediator.isShowingAndNotOccluded());
+    }
+
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void showKeyguardAfterKeyguardNotEnabled_featureNotEnabled() {
+        // GIVEN feature is NOT enabled
+        final FoldGracePeriodProvider mockedFoldGracePeriodProvider =
+                mock(FoldGracePeriodProvider.class);
+        mViewMediator.mFoldGracePeriodProvider = mockedFoldGracePeriodProvider;
+        when(mockedFoldGracePeriodProvider.isEnabled()).thenReturn(false);
+
+        // GIVEN keyguard is not enabled and isn't showing
+        mViewMediator.onSystemReady();
+        mViewMediator.setKeyguardEnabled(false);
+        TestableLooper.get(this).processAllMessages();
+        captureKeyguardUpdateMonitorCallback();
+        assertFalse(mViewMediator.isShowingAndNotOccluded());
+
+        // WHEN showKeyguard is requested
+        mViewMediator.showDismissibleKeyguard();
+
+        // THEN keyguard is still NOT shown
+        TestableLooper.get(this).processAllMessages();
+        assertFalse(mViewMediator.isShowingAndNotOccluded());
+    }
+
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
     public void doNotHideKeyguard_whenLockdown_onKeyguardNotEnabledExternally() {
         // GIVEN keyguard is enabled and lockdown occurred so the keyguard is showing
         mViewMediator.onSystemReady();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
index 8e81185..809947d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
@@ -63,6 +63,8 @@
                 transitionRepository = super.transitionRepository,
                 transitionInteractor = super.transitionInteractor,
                 scope = super.testScope.backgroundScope,
+                bgDispatcher = super.testDispatcher,
+                mainDispatcher = super.testDispatcher,
                 keyguardInteractor = super.keyguardInteractor,
                 flags = FakeFeatureFlags(),
                 keyguardSecurityModel = mock(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index b8a8bdf..e531d44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -20,7 +20,6 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
-import com.android.keyguard.TestScopeProvider
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.flags.FakeFeatureFlags
@@ -51,6 +50,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runCurrent
@@ -109,7 +109,8 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        testScope = TestScopeProvider.getTestScope()
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
 
         keyguardRepository = FakeKeyguardRepository()
         bouncerRepository = FakeKeyguardBouncerRepository()
@@ -139,6 +140,8 @@
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -160,6 +163,8 @@
         fromPrimaryBouncerTransitionInteractor =
             FromPrimaryBouncerTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -173,6 +178,8 @@
         fromDreamingTransitionInteractor =
             FromDreamingTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -182,6 +189,8 @@
         fromDreamingLockscreenHostedTransitionInteractor =
             FromDreamingLockscreenHostedTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -191,6 +200,8 @@
         fromAodTransitionInteractor =
             FromAodTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -200,6 +211,8 @@
         fromGoneTransitionInteractor =
             FromGoneTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -210,6 +223,8 @@
         fromDozingTransitionInteractor =
             FromDozingTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -220,6 +235,8 @@
         fromOccludedTransitionInteractor =
             FromOccludedTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -230,6 +247,8 @@
         fromAlternateBouncerTransitionInteractor =
             FromAlternateBouncerTransitionInteractor(
                     scope = testScope,
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index edd781d..2d9d5ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -20,14 +20,16 @@
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.coroutines.collectValues
+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.util.mockito.mock
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -37,23 +39,21 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
-    private lateinit var underTest: KeyguardTransitionAnimationFlow.SharedFlowBuilder
-    private lateinit var repository: FakeKeyguardTransitionRepository
-    private lateinit var testScope: TestScope
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+    val animationFlow = kosmos.keyguardTransitionAnimationFlow
+    val repository = kosmos.fakeKeyguardTransitionRepository
+
+    private lateinit var underTest: KeyguardTransitionAnimationFlow.FlowBuilder
 
     @Before
     fun setUp() {
-        testScope = TestScope()
-        repository = FakeKeyguardTransitionRepository()
         underTest =
-            KeyguardTransitionAnimationFlow(
-                    testScope.backgroundScope,
-                    mock(),
-                )
-                .setup(
-                    duration = 1000.milliseconds,
-                    stepFlow = repository.transitions,
-                )
+            animationFlow.setup(
+                duration = 1000.milliseconds,
+                from = KeyguardState.GONE,
+                to = KeyguardState.DREAMING,
+            )
     }
 
     @Test(expected = IllegalArgumentException::class)
@@ -83,6 +83,8 @@
                     onFinish = { 10f },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(1f, TransitionState.FINISHED), validateStep = false)
             assertThat(animationValues()).isEqualTo(10f)
         }
@@ -97,6 +99,8 @@
                     onCancel = { 100f },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
             assertThat(animationValues()).isEqualTo(100f)
         }
@@ -111,6 +115,8 @@
                     onStep = { it },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertThat(animationValues()).isEqualTo(0f)
 
@@ -137,6 +143,8 @@
                     onStep = { it },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f))
             repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
@@ -157,17 +165,56 @@
                     duration = 1000.milliseconds,
                     onStep = { it * 2 },
                 )
-            var animationValues = collectLastValue(flow)
+            val animationValues by collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            assertFloat(animationValues(), 0f)
+            assertFloat(animationValues, 0f)
             repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 0.6f)
+            assertFloat(animationValues, 0.6f)
             repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 1.2f)
+            assertFloat(animationValues, 1.2f)
             repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 1.6f)
+            assertFloat(animationValues, 1.6f)
             repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 2f)
+            assertFloat(animationValues, 2f)
+        }
+
+    @Test
+    fun sameFloatValueWithTheSameTransitionStateDoesNotEmitTwice() =
+        testScope.runTest {
+            val flow =
+                underTest.sharedFlow(
+                    duration = 1000.milliseconds,
+                    onStep = { it },
+                )
+            val values by collectValues(flow)
+            runCurrent()
+
+            repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+            repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+
+            assertThat(values.size).isEqualTo(1)
+            assertThat(values[0]).isEqualTo(0.3f)
+        }
+
+    @Test
+    fun sameFloatValueWithADifferentTransitionStateDoesEmitTwice() =
+        testScope.runTest {
+            val flow =
+                underTest.sharedFlow(
+                    duration = 1000.milliseconds,
+                    onStep = { it },
+                )
+            val values by collectValues(flow)
+            runCurrent()
+
+            repository.sendTransitionStep(step(0.3f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+
+            assertThat(values.size).isEqualTo(2)
+            assertThat(values[0]).isEqualTo(0.3f)
+            assertThat(values[0]).isEqualTo(0.3f)
         }
 
     private fun assertFloat(actual: Float?, expected: Float) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
index d959872..87391cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
@@ -31,6 +31,7 @@
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -50,6 +51,7 @@
     fun transitionToAlternateBouncer_scrimAlphaUpdate() =
         testScope.runTest {
             val scrimAlphas by collectValues(underTest.scrimAlpha)
+            runCurrent()
 
             transitionRepository.sendTransitionSteps(
                 listOf(
@@ -69,17 +71,17 @@
     fun transitionFromAlternateBouncer_scrimAlphaUpdate() =
         testScope.runTest {
             val scrimAlphas by collectValues(underTest.scrimAlpha)
+            runCurrent()
 
             transitionRepository.sendTransitionSteps(
                 listOf(
-                    stepToAlternateBouncer(0f, TransitionState.STARTED),
-                    stepToAlternateBouncer(.4f),
-                    stepToAlternateBouncer(.6f),
-                    stepToAlternateBouncer(1f),
+                    stepFromAlternateBouncer(0f, TransitionState.STARTED),
+                    stepFromAlternateBouncer(.4f),
+                    stepFromAlternateBouncer(.6f),
+                    stepFromAlternateBouncer(1f),
                 ),
                 testScope,
             )
-
             assertThat(scrimAlphas.size).isEqualTo(4)
             scrimAlphas.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
index af8d8a8..795e68d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -63,6 +64,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // fade in
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index daafe12..75994da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -39,6 +39,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -78,6 +79,8 @@
     fun scrimAlpha_runDimissFromKeyguard_shadeExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
+
             shadeRepository.setLockscreenShadeExpansion(1f)
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
 
@@ -101,6 +104,8 @@
     fun scrimAlpha_runDimissFromKeyguard_shadeNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
+
             shadeRepository.setLockscreenShadeExpansion(0f)
 
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
@@ -123,6 +128,7 @@
     fun scrimBehindAlpha_leaveShadeOpen() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
 
             sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
 
@@ -146,6 +152,8 @@
     fun scrimBehindAlpha_doNotLeaveShadeOpen() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
+
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
                     step(0f, TransitionState.STARTED),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
index dd542d4..471029b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
@@ -20,18 +20,15 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+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.KeyguardTransitionAnimationFlow
-import com.android.systemui.util.mockito.mock
+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.TestScope
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,29 +36,10 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DozingToLockscreenTransitionViewModelTest : SysuiTestCase() {
-    private lateinit var testScope: TestScope
-    private lateinit var underTest: DozingToLockscreenTransitionViewModel
-    private lateinit var repository: FakeKeyguardTransitionRepository
-
-    @Before
-    fun setUp() {
-        testScope = TestScope()
-        repository = FakeKeyguardTransitionRepository()
-        underTest =
-            DozingToLockscreenTransitionViewModel(
-                interactor =
-                    KeyguardTransitionInteractorFactory.create(
-                            scope = testScope.backgroundScope,
-                            repository = repository,
-                        )
-                        .keyguardTransitionInteractor,
-                animationFlow =
-                    KeyguardTransitionAnimationFlow(
-                        scope = testScope.backgroundScope,
-                        logger = mock()
-                    ),
-            )
-    }
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+    val repository = kosmos.fakeKeyguardTransitionRepository
+    val underTest = kosmos.dozingToLockscreenTransitionViewModel
 
     @Test
     fun deviceEntryParentViewShows() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
index a105008..1c9c942 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
@@ -31,6 +31,7 @@
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -52,6 +53,7 @@
             val pixels = -100f
             val enterFromTopTranslationY by
                 collectLastValue(underTest.enterFromTopTranslationY(pixels.toInt()))
+            runCurrent()
 
             // The animation should only start > .4f way through
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -72,6 +74,7 @@
     fun enterFromTopAnimationAlpha() =
         testScope.runTest {
             val enterFromTopAnimationAlpha by collectLastValue(underTest.enterFromTopAnimationAlpha)
+            runCurrent()
 
             // The animation should only start > .4f way through
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -92,6 +95,7 @@
         testScope.runTest {
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 0f
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -113,6 +117,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -137,6 +142,7 @@
             fingerprintPropertyRepository.supportsRearFps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -161,6 +167,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
index 5e62317..1912987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -51,6 +52,7 @@
         testScope.runTest {
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 0f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -72,6 +74,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // immediately 1f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -96,6 +99,7 @@
             fingerprintPropertyRepository.supportsRearFps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // no updates
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -120,6 +124,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // no updates
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
index 9729022..c55c27c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,6 +55,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 0f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -75,6 +77,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
 
@@ -92,6 +95,7 @@
             fingerprintPropertyRepository.supportsRearFps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -116,6 +120,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertThat(deviceEntryParentViewAlpha).isNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
index 2c6436e..0796af0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -71,6 +72,7 @@
         testScope.runTest {
             fingerprintPropertyRepository.supportsUdfps()
             val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 1f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index 0a5b124..fb101dd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -78,7 +78,7 @@
         mMediaData = new MediaData(
                 USER_ID, true, APP, null, ARTIST, TITLE, null,
                 new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
-                MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
+                MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, 0L,
                 InstanceId.fakeInstanceId(-1), -1, false, null);
         mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
     }
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 35bf775..dc211303 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -83,7 +83,6 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
         when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
         when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
         when(mSystemUIDialog.getContext()).thenReturn(mContext);
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 7ce51ae..86ab01c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -105,7 +105,6 @@
             spy(
                 SystemUIDialog.Factory(
                     context,
-                    flags,
                     systemUIDialogManager,
                     sysuiState,
                     broadcastDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index cb90cc5..0ba99f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -44,7 +44,6 @@
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger;
 import com.android.systemui.mediaprojection.SessionCreationSource;
@@ -113,7 +112,6 @@
 
         mDialogFactory = new TestSystemUIDialogFactory(
                 mContext,
-                mFeatureFlags,
                 Dependency.get(SystemUIDialogManager.class),
                 Dependency.get(SysUiState.class),
                 Dependency.get(BroadcastDispatcher.class),
@@ -313,14 +311,12 @@
 
         TestSystemUIDialogFactory(
                 Context context,
-                FeatureFlags featureFlags,
                 SystemUIDialogManager systemUIDialogManager,
                 SysUiState sysUiState,
                 BroadcastDispatcher broadcastDispatcher,
                 DialogLaunchAnimator dialogLaunchAnimator) {
             super(
                     context,
-                    featureFlags,
                     systemUIDialogManager,
                     sysUiState,
                     broadcastDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
index 8f696e7..2399536 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
@@ -74,7 +74,6 @@
         val systemUIDialogFactory =
             SystemUIDialog.Factory(
                 context,
-                Dependency.get(FeatureFlags::class.java),
                 Dependency.get(SystemUIDialogManager::class.java),
                 Dependency.get(SysUiState::class.java),
                 Dependency.get(BroadcastDispatcher::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 5569ca9..b7a9ea7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade
 
+import android.os.PowerManager
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.ViewUtils
@@ -43,6 +44,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -52,6 +55,7 @@
     @Mock private lateinit var communalViewModel: CommunalViewModel
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock private lateinit var shadeInteractor: ShadeInteractor
+    @Mock private lateinit var powerManager: PowerManager
 
     private lateinit var containerView: View
     private lateinit var testableLooper: TestableLooper
@@ -76,7 +80,8 @@
                 communalInteractor,
                 communalViewModel,
                 keyguardTransitionInteractor,
-                shadeInteractor
+                shadeInteractor,
+                powerManager
             )
         testableLooper = TestableLooper.get(this)
 
@@ -90,14 +95,14 @@
     }
 
     @Test
-    fun isEnabled_interactorEnabled_returnsTrue() {
+    fun isEnabled_interactorEnabled_interceptsTouches() {
         communalRepository.setIsCommunalEnabled(true)
 
         assertThat(underTest.isEnabled()).isTrue()
     }
 
     @Test
-    fun isEnabled_interactorDisabled_returnsFalse() {
+    fun isEnabled_interactorDisabled_doesNotIntercept() {
         communalRepository.setIsCommunalEnabled(false)
 
         assertThat(underTest.isEnabled()).isFalse()
@@ -120,7 +125,7 @@
     }
 
     @Test
-    fun onTouchEvent_touchInsideGestureRegion_returnsTrue() {
+    fun onTouchEvent_touchInsideGestureRegion_interceptsTouches() {
         // Communal is open.
         communalRepository.setDesiredScene(CommunalSceneKey.Communal)
 
@@ -131,7 +136,7 @@
     }
 
     @Test
-    fun onTouchEvent_subsequentTouchesAfterGestureStart_returnsTrue() {
+    fun onTouchEvent_subsequentTouchesAfterGestureStart_interceptsTouches() {
         // Communal is open.
         communalRepository.setDesiredScene(CommunalSceneKey.Communal)
 
@@ -146,7 +151,7 @@
     }
 
     @Test
-    fun onTouchEvent_communalOpen_returnsTrue() {
+    fun onTouchEvent_communalOpen_interceptsTouches() {
         // Communal is open.
         communalRepository.setDesiredScene(CommunalSceneKey.Communal)
 
@@ -155,10 +160,12 @@
 
         // Touch events are intercepted.
         assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+        // User activity sent to PowerManager.
+        verify(powerManager).userActivity(any(), any(), any())
     }
 
     @Test
-    fun onTouchEvent_communalAndBouncerShowing_returnsFalse() {
+    fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() {
         // Communal is open.
         communalRepository.setDesiredScene(CommunalSceneKey.Communal)
 
@@ -170,10 +177,12 @@
 
         // Touch events are not intercepted.
         assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+        // User activity is not sent to PowerManager.
+        verify(powerManager, times(0)).userActivity(any(), any(), any())
     }
 
     @Test
-    fun onTouchEvent_communalAndShadeShowing_returnsFalse() {
+    fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() {
         // Communal is open.
         communalRepository.setDesiredScene(CommunalSceneKey.Communal)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 3132767..b239482 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -152,7 +152,9 @@
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger;
+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.NotificationsKeyguardInteractor;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -344,6 +346,7 @@
     @Mock protected ActiveNotificationsInteractor mActiveNotificationsInteractor;
     @Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm;
     @Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
+    @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper;
 
     protected final int mMaxUdfpsBurnInOffsetY = 5;
     protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
@@ -424,7 +427,8 @@
                                 mContext,
                                 new ResourcesSplitShadeStateController(),
                                 mKeyguardInteractor,
-                                deviceEntryUdfpsInteractor
+                                deviceEntryUdfpsInteractor,
+                                () -> mLargeScreenHeaderHelper
                         ),
                         mShadeRepository
                 )
@@ -586,6 +590,10 @@
         when(mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha())
                 .thenReturn(emptyFlow());
 
+        NotificationsKeyguardViewStateRepository notifsKeyguardViewStateRepository =
+                new NotificationsKeyguardViewStateRepository();
+        NotificationsKeyguardInteractor notifsKeyguardInteractor =
+                new NotificationsKeyguardInteractor(notifsKeyguardViewStateRepository);
         NotificationWakeUpCoordinator coordinator =
                 new NotificationWakeUpCoordinator(
                         mDumpManager,
@@ -596,7 +604,8 @@
                         mKeyguardBypassController,
                         mDozeParameters,
                         mScreenOffAnimationController,
-                        new NotificationWakeUpCoordinatorLogger(logcatLogBuffer()));
+                        new NotificationWakeUpCoordinatorLogger(logcatLogBuffer()),
+                        notifsKeyguardInteractor);
         mConfigurationController = new ConfigurationControllerImpl(mContext);
         PulseExpansionHandler expansionHandler = new PulseExpansionHandler(
                 mContext,
@@ -805,7 +814,8 @@
                 mActiveNotificationsInteractor,
                 mJavaAdapter,
                 mCastController,
-                new ResourcesSplitShadeStateController()
+                new ResourcesSplitShadeStateController(),
+                () -> mLargeScreenHeaderHelper
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 9d8b214..5d663d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -143,6 +143,7 @@
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
     @Mock private UserTracker mUserTracker;
     @Mock private SceneContainerFlags mSceneContainerFlags;
+    @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
 
@@ -215,6 +216,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 mTestScope.getBackgroundScope(),
+                mUtils.getTestDispatcher(),
+                mUtils.getTestDispatcher(),
                 keyguardInteractor,
                 featureFlags,
                 shadeRepository,
@@ -233,6 +236,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 mTestScope.getBackgroundScope(),
+                mUtils.getTestDispatcher(),
+                mUtils.getTestDispatcher(),
                 keyguardInteractor,
                 featureFlags,
                 mKeyguardSecurityModel,
@@ -261,7 +266,8 @@
                                 mContext,
                                 new ResourcesSplitShadeStateController(),
                                 keyguardInteractor,
-                                deviceEntryUdfpsInteractor),
+                                deviceEntryUdfpsInteractor,
+                                () -> mLargeScreenHeaderHelper),
                         shadeRepository
                 )
         );
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
index 88a47eb..ea3caa3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.capture
+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
@@ -52,7 +53,6 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
-import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
 import org.mockito.Mockito.any
@@ -74,15 +74,16 @@
 @SmallTest
 class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() {
 
-    @Mock lateinit var view: NotificationsQuickSettingsContainer
-    @Mock lateinit var navigationModeController: NavigationModeController
-    @Mock lateinit var overviewProxyService: OverviewProxyService
-    @Mock lateinit var shadeHeaderController: ShadeHeaderController
-    @Mock lateinit var shadeInteractor: ShadeInteractor
-    @Mock lateinit var fragmentService: FragmentService
-    @Mock lateinit var fragmentHostManager: FragmentHostManager
-    @Mock
-    lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController
+    private val view = mock<NotificationsQuickSettingsContainer>()
+    private val navigationModeController = mock<NavigationModeController>()
+    private val overviewProxyService = mock<OverviewProxyService>()
+    private val shadeHeaderController = mock<ShadeHeaderController>()
+    private val shadeInteractor = mock<ShadeInteractor>()
+    private val fragmentService = mock<FragmentService>()
+    private val fragmentHostManager = mock<FragmentHostManager>()
+    private val notificationStackScrollLayoutController =
+        mock<NotificationStackScrollLayoutController>()
+    private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>()
 
     @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
     @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
@@ -123,7 +124,8 @@
                 delayableExecutor,
                 featureFlags,
                 notificationStackScrollLayoutController,
-                ResourcesSplitShadeStateController()
+                ResourcesSplitShadeStateController(),
+                largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
             )
 
         overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
@@ -480,7 +482,8 @@
                 delayableExecutor,
                 featureFlags,
                 notificationStackScrollLayoutController,
-                ResourcesSplitShadeStateController()
+                ResourcesSplitShadeStateController(),
+                largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
             )
         controller.updateConstraints()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
index 1f37ca0..c1bc303 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.capture
+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
@@ -53,7 +54,6 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
-import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
 import org.mockito.Mockito.any
@@ -71,15 +71,16 @@
 @SmallTest
 class NotificationsQSContainerControllerTest : SysuiTestCase() {
 
-    @Mock lateinit var view: NotificationsQuickSettingsContainer
-    @Mock lateinit var navigationModeController: NavigationModeController
-    @Mock lateinit var overviewProxyService: OverviewProxyService
-    @Mock lateinit var shadeHeaderController: ShadeHeaderController
-    @Mock lateinit var shadeInteractor: ShadeInteractor
-    @Mock lateinit var fragmentService: FragmentService
-    @Mock lateinit var fragmentHostManager: FragmentHostManager
-    @Mock
-    lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController
+    private val view = mock<NotificationsQuickSettingsContainer>()
+    private val navigationModeController = mock<NavigationModeController>()
+    private val overviewProxyService = mock<OverviewProxyService>()
+    private val shadeHeaderController = mock<ShadeHeaderController>()
+    private val shadeInteractor = mock<ShadeInteractor>()
+    private val fragmentService = mock<FragmentService>()
+    private val fragmentHostManager = mock<FragmentHostManager>()
+    private val notificationStackScrollLayoutController =
+        mock<NotificationStackScrollLayoutController>()
+    private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>()
 
     @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
     @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
@@ -122,7 +123,8 @@
                 delayableExecutor,
                 featureFlags,
                 notificationStackScrollLayoutController,
-                ResourcesSplitShadeStateController()
+                ResourcesSplitShadeStateController(),
+                largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
             )
 
         overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
@@ -463,7 +465,8 @@
                 delayableExecutor,
                 featureFlags,
                 notificationStackScrollLayoutController,
-                ResourcesSplitShadeStateController()
+                ResourcesSplitShadeStateController(),
+                largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
             )
         controller.updateConstraints()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 727a6c3..982787b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -176,6 +176,7 @@
     @Mock protected CastController mCastController;
     @Mock protected UserSwitcherInteractor mUserSwitcherInteractor;
     @Mock protected SelectedUserInteractor mSelectedUserInteractor;
+    @Mock protected LargeScreenHeaderHelper mLargeScreenHeaderHelper;
 
     protected FakeDisableFlagsRepository mDisableFlagsRepository =
             new FakeDisableFlagsRepository();
@@ -250,6 +251,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 mTestScope.getBackgroundScope(),
+                mUtils.getTestDispatcher(),
+                mUtils.getTestDispatcher(),
                 keyguardInteractor,
                 featureFlags,
                 mShadeRepository,
@@ -268,6 +271,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 mTestScope.getBackgroundScope(),
+                mUtils.getTestDispatcher(),
+                mUtils.getTestDispatcher(),
                 keyguardInteractor,
                 featureFlags,
                 mock(KeyguardSecurityModel.class),
@@ -299,7 +304,8 @@
                                 mContext,
                                 splitShadeStateController,
                                 keyguardInteractor,
-                                deviceEntryUdfpsInteractor),
+                                deviceEntryUdfpsInteractor,
+                                () -> mLargeScreenHeaderHelper),
                         mShadeRepository
                 )
         );
@@ -384,7 +390,8 @@
                 mActiveNotificationsInteractor,
                 new JavaAdapter(mTestScope.getBackgroundScope()),
                 mCastController,
-                splitShadeStateController
+                splitShadeStateController,
+                () -> mLargeScreenHeaderHelper
         );
         mQsController.init();
 
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 ee27c5c..64fd80d 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
@@ -35,6 +35,7 @@
 import com.android.systemui.plugins.PluginManager
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
+import java.util.function.BiConsumer
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.fail
 import kotlinx.coroutines.CoroutineDispatcher
@@ -100,10 +101,7 @@
         override fun toString() = "Manager[$tag]"
         override fun getPackage(): String = mComponentName.getPackageName()
         override fun getComponentName(): ComponentName = mComponentName
-
-        private var isDebug: Boolean = false
-        override fun getIsDebug(): Boolean = isDebug
-        override fun setIsDebug(value: Boolean) { isDebug = value }
+        override fun setLogFunc(func: BiConsumer<String, String>) { }
 
         override fun loadPlugin() {
             if (!mIsLoaded) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index bc50c25..3defee9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -112,7 +112,7 @@
         mPluginInstance = mPluginInstanceFactory.create(
                 mContext, mAppInfo, TEST_PLUGIN_COMPONENT_NAME,
                 TestPlugin.class, mPluginListener);
-        mPluginInstance.setIsDebug(true);
+        mPluginInstance.setLogFunc((tag, msg) -> Log.d((String) tag, (String) msg));
         mPluginContext = new WeakReference<>(mPluginInstance.getPluginContext());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index f25ce0a..83590ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -27,7 +27,7 @@
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -44,6 +44,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
@@ -65,7 +66,6 @@
 import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
@@ -79,13 +79,15 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
+    private val testDispatcher = utils.testDispatcher
     private lateinit var shadeInteractor: ShadeInteractor
     private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
     private lateinit var fromPrimaryBouncerTransitionInteractor:
         FromPrimaryBouncerTransitionInteractor
-    @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock lateinit var mockDarkAnimator: ObjectAnimator
-    @Mock lateinit var deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor
+    private val interactionJankMonitor = mock<InteractionJankMonitor>()
+    private val mockDarkAnimator = mock<ObjectAnimator>()
+    private val deviceEntryUdfpsInteractor = mock<DeviceEntryUdfpsInteractor>()
+    private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>()
 
     private lateinit var controller: StatusBarStateControllerImpl
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -142,6 +144,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 testScope.backgroundScope,
+                testDispatcher,
+                testDispatcher,
                 keyguardInteractor,
                 featureFlags,
                 shadeRepository,
@@ -161,6 +165,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 testScope.backgroundScope,
+                testDispatcher,
+                testDispatcher,
                 keyguardInteractor,
                 featureFlags,
                 mock(),
@@ -189,6 +195,7 @@
                         ResourcesSplitShadeStateController(),
                         keyguardInteractor,
                         deviceEntryUdfpsInteractor,
+                        largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
                     ),
                     shadeRepository,
                 )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
index 438b33d..039fef9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
@@ -22,12 +22,14 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.AnimatorTestRule
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeViewController.Companion.WAKEUP_ANIMATION_DELAY_MS
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_WAKEUP
+import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -54,6 +56,8 @@
 
     @get:Rule val animatorTestRule = AnimatorTestRule()
 
+    private val kosmos = Kosmos()
+
     private val dumpManager: DumpManager = mock()
     private val headsUpManager: HeadsUpManager = mock()
     private val statusBarStateController: StatusBarStateController = mock()
@@ -100,6 +104,7 @@
                 dozeParameters,
                 screenOffAnimationController,
                 logger,
+                kosmos.notificationsKeyguardInteractor,
             )
         statusBarStateCallback = withArgCaptor {
             verify(statusBarStateController).addCallback(capture())
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
deleted file mode 100644
index 170f651..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.data.repository
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.mockito.withArgCaptor
-import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-@SmallTest
-class NotificationsKeyguardViewStateRepositoryTest : SysuiTestCase() {
-
-    @SysUISingleton
-    @Component(modules = [SysUITestModule::class])
-    interface TestComponent : SysUITestComponent<NotificationsKeyguardViewStateRepositoryImpl> {
-
-        val mockWakeUpCoordinator: NotificationWakeUpCoordinator
-
-        @Component.Factory
-        interface Factory {
-            fun create(
-                @BindsInstance test: SysuiTestCase,
-            ): TestComponent
-        }
-    }
-
-    private val testComponent: TestComponent =
-        DaggerNotificationsKeyguardViewStateRepositoryTest_TestComponent.factory()
-            .create(test = this)
-
-    @Test
-    fun areNotifsFullyHidden_reflectsWakeUpCoordinator() =
-        testComponent.runTest {
-            whenever(mockWakeUpCoordinator.notificationsFullyHidden).thenReturn(false)
-            val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden)
-            runCurrent()
-
-            assertThat(notifsFullyHidden).isFalse()
-
-            withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) }
-                .onFullyHiddenChanged(true)
-            runCurrent()
-
-            assertThat(notifsFullyHidden).isTrue()
-        }
-
-    @Test
-    fun isPulseExpanding_reflectsWakeUpCoordinator() =
-        testComponent.runTest {
-            whenever(mockWakeUpCoordinator.isPulseExpanding()).thenReturn(false)
-            val isPulseExpanding by collectLastValue(underTest.isPulseExpanding)
-            runCurrent()
-
-            assertThat(isPulseExpanding).isFalse()
-
-            withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) }
-                .onPulseExpandingChanged(true)
-            runCurrent()
-
-            assertThat(isPulseExpanding).isTrue()
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
index bb3113a..3593f5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.runCurrent
 import com.android.systemui.runTest
-import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
@@ -33,9 +32,6 @@
     @SysUISingleton
     @Component(modules = [SysUITestModule::class])
     interface TestComponent : SysUITestComponent<NotificationsKeyguardInteractor> {
-
-        val repository: FakeNotificationsKeyguardViewStateRepository
-
         @Component.Factory
         interface Factory {
             fun create(@BindsInstance test: SysuiTestCase): TestComponent
@@ -48,13 +44,13 @@
     @Test
     fun areNotifsFullyHidden_reflectsRepository() =
         testComponent.runTest {
-            repository.setNotificationsFullyHidden(false)
+            underTest.setNotificationsFullyHidden(false)
             val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden)
             runCurrent()
 
             assertThat(notifsFullyHidden).isFalse()
 
-            repository.setNotificationsFullyHidden(true)
+            underTest.setNotificationsFullyHidden(true)
             runCurrent()
 
             assertThat(notifsFullyHidden).isTrue()
@@ -63,13 +59,13 @@
     @Test
     fun isPulseExpanding_reflectsRepository() =
         testComponent.runTest {
-            repository.setPulseExpanding(false)
+            underTest.setPulseExpanding(false)
             val isPulseExpanding by collectLastValue(underTest.isPulseExpanding)
             runCurrent()
 
             assertThat(isPulseExpanding).isFalse()
 
-            repository.setPulseExpanding(true)
+            underTest.setPulseExpanding(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 47feccf..7faf562 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
@@ -29,8 +29,8 @@
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 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.domain.interactor.NotificationsKeyguardInteractor
 import com.android.systemui.statusbar.notification.shared.byIsAmbient
 import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
 import com.android.systemui.statusbar.notification.shared.byIsPulsing
@@ -61,7 +61,7 @@
     interface TestComponent : SysUITestComponent<NotificationIconsInteractor> {
 
         val activeNotificationListRepository: ActiveNotificationListRepository
-        val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+        val notificationsKeyguardInteractor: NotificationsKeyguardInteractor
 
         @Component.Factory
         interface Factory {
@@ -136,7 +136,7 @@
     fun filteredEntrySet_noPulsing_notifsNotFullyHidden() =
         testComponent.runTest {
             val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false))
-            keyguardViewStateRepository.setNotificationsFullyHidden(false)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(false)
             assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true)
         }
 
@@ -144,7 +144,7 @@
     fun filteredEntrySet_noPulsing_notifsFullyHidden() =
         testComponent.runTest {
             val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false))
-            keyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
         }
 }
@@ -161,7 +161,7 @@
 
         val activeNotificationListRepository: ActiveNotificationListRepository
         val deviceEntryRepository: FakeDeviceEntryRepository
-        val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+        val notificationsKeyguardInteractor: NotificationsKeyguardInteractor
 
         @Component.Factory
         interface Factory {
@@ -222,7 +222,7 @@
         testComponent.runTest {
             val filteredSet by collectLastValue(underTest.aodNotifs)
             deviceEntryRepository.setBypassEnabled(false)
-            keyguardViewStateRepository.setNotificationsFullyHidden(false)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(false)
             assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
         }
 
@@ -231,7 +231,7 @@
         testComponent.runTest {
             val filteredSet by collectLastValue(underTest.aodNotifs)
             deviceEntryRepository.setBypassEnabled(false)
-            keyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
         }
 
@@ -240,7 +240,7 @@
         testComponent.runTest {
             val filteredSet by collectLastValue(underTest.aodNotifs)
             deviceEntryRepository.setBypassEnabled(true)
-            keyguardViewStateRepository.setNotificationsFullyHidden(false)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(false)
             assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true)
         }
 
@@ -249,7 +249,7 @@
         testComponent.runTest {
             val filteredSet by collectLastValue(underTest.aodNotifs)
             deviceEntryRepository.setBypassEnabled(true)
-            keyguardViewStateRepository.setNotificationsFullyHidden(true)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
         }
 }
@@ -266,7 +266,7 @@
 
         val activeNotificationListRepository: ActiveNotificationListRepository
         val headsUpIconsInteractor: HeadsUpNotificationIconInteractor
-        val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
+        val notificationsKeyguardInteractor: NotificationsKeyguardInteractor
         val notificationListenerSettingsRepository: NotificationListenerSettingsRepository
 
         @Component.Factory
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 83ba684..a172120 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
@@ -626,8 +626,6 @@
 
     @Test
     public void testClearNotifications_clearAllInProgress() {
-        mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
-
         ExpandableNotificationRow row = createClearableRow();
         when(row.getEntry().hasFinishedInitialization()).thenReturn(true);
         doReturn(true).when(mStackScroller).isVisible(row);
@@ -672,8 +670,6 @@
 
     @Test
     public void testAddNotificationUpdatesSpeedBumpIndex() {
-        mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
-
         // initial state calculated == 0
         assertEquals(0, mStackScroller.getSpeedBumpIndex());
 
@@ -690,8 +686,6 @@
 
     @Test
     public void testAddAmbientNotificationNoSpeedBumpUpdate() {
-        mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
-
         // initial state calculated  == 0
         assertEquals(0, mStackScroller.getSpeedBumpIndex());
 
@@ -708,8 +702,6 @@
 
     @Test
     public void testRemoveNotificationUpdatesSpeedBump() {
-        mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
-
         // initial state calculated == 0
         assertEquals(0, mStackScroller.getSpeedBumpIndex());
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index 3556703..4dc4798 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -84,7 +84,7 @@
         LogBuffer logBuffer = FakeLogBuffer.Factory.Companion.create();
         mClockPositionAlgorithm = new KeyguardClockPositionAlgorithm(logBuffer);
         when(mResources.getDimensionPixelSize(anyInt())).thenReturn(0);
-        mClockPositionAlgorithm.loadDimens(mResources);
+        mClockPositionAlgorithm.loadDimens(mContext, mResources);
 
         mClockPosition = new KeyguardClockPositionAlgorithm.Result();
     }
@@ -297,7 +297,7 @@
                 .thenReturn(100);
         when(mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height))
                 .thenReturn(70);
-        mClockPositionAlgorithm.loadDimens(mResources);
+        mClockPositionAlgorithm.loadDimens(mContext, mResources);
         givenLockScreen();
         mIsSplitShade = true;
         // WHEN the position algorithm is run
@@ -589,7 +589,7 @@
     private void setSplitShadeTopMargin(int value) {
         when(mResources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin))
                 .thenReturn(value);
-        mClockPositionAlgorithm.loadDimens(mResources);
+        mClockPositionAlgorithm.loadDimens(mContext, mResources);
     }
 
     private void givenHighestBurnInOffset() {
@@ -603,7 +603,7 @@
     private void givenMaxBurnInOffset(int offset) {
         when(mResources.getDimensionPixelSize(R.dimen.burn_in_prevention_offset_y_clock))
                 .thenReturn(offset);
-        mClockPositionAlgorithm.loadDimens(mResources);
+        mClockPositionAlgorithm.loadDimens(mContext, mResources);
     }
 
     private void givenAOD() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt
index 7eba3b46..c44c178 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt
@@ -22,11 +22,15 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.time.FakeSystemClock
 import junit.framework.Assert
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
@@ -72,6 +76,34 @@
         Assert.assertEquals(true, controller.hasActiveProfile())
     }
 
+    @Test
+    fun callbackRemovedWhileDispatching_doesntCrash() {
+        var remove = false
+        val callback =
+            object : ManagedProfileController.Callback {
+                override fun onManagedProfileChanged() {
+                    if (remove) {
+                        controller.removeCallback(this)
+                    }
+                }
+
+                override fun onManagedProfileRemoved() {
+                    if (remove) {
+                        controller.removeCallback(this)
+                    }
+                }
+            }
+        controller.addCallback(callback)
+        controller.addCallback(TestCallback)
+
+        remove = true
+        setupWorkingProfile(1)
+
+        val captor = argumentCaptor<UserTracker.Callback>()
+        verify(userTracker).addCallback(capture(captor), any())
+        captor.value.onProfilesChanged(userManager.getEnabledProfiles(1))
+    }
+
     private fun setupWorkingProfile(userId: Int) {
         `when`(userManager.getEnabledProfiles(userId))
             .thenReturn(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
index 6f04f36..f6a8243 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
@@ -23,6 +23,9 @@
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl.EXTERNAL_SLOT_SUFFIX
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
 import com.android.systemui.util.mockito.kotlinArgumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
@@ -49,14 +52,15 @@
         iconList = StatusBarIconList(arrayOf())
         underTest =
             StatusBarIconControllerImpl(
-                context,
-                commandQueue,
-                mock(),
-                mock(),
-                mock(),
-                mock(),
-                iconList,
-                mock(),
+                /* context = */ context,
+                /* commandQueue = */ commandQueue,
+                /* demoModeController = */ mock(),
+                /* configurationController = */ mock(),
+                /* tunerService = */ mock(),
+                /* dumpManager = */ mock(),
+                /* statusBarIconList = */ iconList,
+                /* statusBarPipelineFlags = */ mock(),
+                /* modernIconsRegistry = */ mock(),
             )
         underTest.addIconGroup(iconGroup)
         val commandQueueCallbacksCaptor = kotlinArgumentCaptor<CommandQueue.Callbacks>()
@@ -366,6 +370,31 @@
         assertThat(iconList.slots[0].name).isEqualTo("myslot$EXTERNAL_SLOT_SUFFIX")
     }
 
+    @Test
+    fun bindableIcons_addedOnInit() {
+        val fakeIcon = FakeBindableIcon("test_slot")
+
+        iconList = StatusBarIconList(arrayOf())
+
+        // WHEN there are registered icons
+        underTest =
+            StatusBarIconControllerImpl(
+                /* context = */ context,
+                /* commandQueue = */ commandQueue,
+                /* demoModeController = */ mock(),
+                /* configurationController = */ mock(),
+                /* tunerService = */ mock(),
+                /* dumpManager = */ mock(),
+                /* statusBarIconList = */ iconList,
+                /* statusBarPipelineFlags = */ mock(),
+                /* modernIconsRegistry = */ FakeBindableIconsRegistry(listOf(fakeIcon)),
+            )
+
+        // THEN they are properly added to the list on init
+        assertThat(iconList.getIconHolder("test_slot", 0))
+            .isInstanceOf(StatusBarIconHolder.BindableIconHolder::class.java)
+    }
+
     private fun createExternalIcon(): StatusBarIcon {
         return StatusBarIcon(
             "external.package",
@@ -377,3 +406,20 @@
         )
     }
 }
+
+class FakeBindableIconsRegistry(
+    override val bindableIcons: List<BindableIcon>,
+) : BindableIconsRegistry
+
+class FakeBindableIcon(
+    override val slot: String,
+    override val shouldBindIcon: Boolean = true,
+) : BindableIcon {
+    // Track initialized so we can know that our icon was properly bound
+    var hasInitialized = false
+
+    override val initializer = ModernStatusBarViewCreator { _ ->
+        hasInitialized = true
+        mock()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 0ff6f20..ca31623 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
 import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
@@ -108,7 +109,8 @@
                 mock(TunerService.class),
                 mock(DumpManager.class),
                 mock(StatusBarIconList.class),
-                flags
+                flags,
+                mock(BindableIconsRegistry.class)
         );
 
         iconController.addIconGroup(manager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
new file mode 100644
index 0000000..02e6fd5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.prod
+
+import android.os.OutcomeReceiver
+import android.os.Process
+import android.telephony.satellite.NtnSignalStrength
+import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+import android.telephony.satellite.SatelliteManager.SatelliteException
+import android.telephony.satellite.SatelliteModemStateCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.StandardTestDispatcher
+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.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
+    private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl
+
+    @Mock private lateinit var satelliteManager: SatelliteManager
+
+    private val systemClock = FakeSystemClock()
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun nullSatelliteManager_usesDefaultValues() =
+        testScope.runTest {
+            setupDefaultRepo()
+            underTest =
+                DeviceBasedSatelliteRepositoryImpl(
+                    Optional.empty(),
+                    dispatcher,
+                    testScope.backgroundScope,
+                    systemClock,
+                )
+
+            val connectionState by collectLastValue(underTest.connectionState)
+            val strength by collectLastValue(underTest.signalStrength)
+            val allowed by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(connectionState).isEqualTo(SatelliteConnectionState.Off)
+            assertThat(strength).isEqualTo(0)
+            assertThat(allowed).isFalse()
+        }
+
+    @Test
+    fun connectionState_mapsFromSatelliteModemState() =
+        testScope.runTest {
+            setupDefaultRepo()
+            val latest by collectLastValue(underTest.connectionState)
+            runCurrent()
+            val callback =
+                withArgCaptor<SatelliteModemStateCallback> {
+                    verify(satelliteManager).registerForSatelliteModemStateChanged(any(), capture())
+                }
+
+            // Mapping from modem state to SatelliteConnectionState is rote, just run all of the
+            // possibilities here
+
+            // Off states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNAVAILABLE)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // On states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_IDLE)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_LISTENING)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_NOT_CONNECTED)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+
+            // Connected states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_CONNECTED)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_RETRYING)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+
+            // Unknown states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNKNOWN)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+            // Garbage value (for completeness' sake)
+            callback.onSatelliteModemStateChanged(123456)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+        }
+
+    @Test
+    fun signalStrength_readsSatelliteManagerState() =
+        testScope.runTest {
+            setupDefaultRepo()
+            val latest by collectLastValue(underTest.signalStrength)
+            runCurrent()
+            val callback =
+                withArgCaptor<NtnSignalStrengthCallback> {
+                    verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture())
+                }
+
+            assertThat(latest).isNull()
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(1))
+            assertThat(latest).isEqualTo(1)
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(2))
+            assertThat(latest).isEqualTo(2)
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(3))
+            assertThat(latest).isEqualTo(3)
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(4))
+            assertThat(latest).isEqualTo(4)
+        }
+
+    @Test
+    fun isSatelliteAllowed_readsSatelliteManagerState_enabled() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is allowed in this location
+            val allowed = true
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isSatelliteAllowed_readsSatelliteManagerState_disabled() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is not allowed in this location
+            val allowed = false
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isSatelliteAllowed_pollsOnTimeout() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is not allowed in this location
+            var allowed = false
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isFalse()
+
+            // WHEN satellite becomes enabled
+            allowed = true
+
+            // WHEN the timeout has not yet been reached
+            advanceTimeBy(POLLING_INTERVAL_MS / 2)
+
+            // THEN the value is still false
+            assertThat(latest).isFalse()
+
+            // WHEN time advances beyond the polling interval
+            advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1)
+
+            // THEN then new value is emitted
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // Use the old school launch/cancel so we can simulate subscribers arriving and leaving
+
+            var latest: Boolean? = false
+            var job =
+                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
+
+            // GIVEN satellite is not allowed in this location
+            var allowed = false
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            assertThat(latest).isFalse()
+
+            // WHEN satellite becomes enabled
+            allowed = true
+
+            // WHEN the job is restarted
+            advanceTimeBy(POLLING_INTERVAL_MS / 2)
+
+            job.cancel()
+            job =
+                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
+
+            // THEN the value is re-fetched
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isSatelliteAllowed_falseWhenErrorOccurs() =
+        testScope.runTest {
+            setupDefaultRepo()
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onError(SatelliteException(1 /* unused */))
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun satelliteNotSupported_listenersAreNotRegistered() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is not supported
+            setUpRepo(
+                uptime = MIN_UPTIME,
+                satMan = satelliteManager,
+                satelliteSupported = false,
+            )
+
+            // WHEN data is requested from the repo
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN the manager is not asked for the information, and default values are returned
+            verify(satelliteManager, never()).registerForSatelliteModemStateChanged(any(), any())
+            verify(satelliteManager, never()).registerForNtnSignalStrengthChanged(any(), any())
+        }
+
+    @Test
+    fun repoDoesNotCheckForSupportUntilMinUptime() =
+        testScope.runTest {
+            // GIVEN we init 100ms after sysui starts up
+            setUpRepo(
+                uptime = 100,
+                satMan = satelliteManager,
+                satelliteSupported = true,
+            )
+
+            // WHEN data is requested
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN we have not yet talked to satellite manager, since we are well before MIN_UPTIME
+            Mockito.verifyZeroInteractions(satelliteManager)
+
+            // WHEN enough time has passed
+            systemClock.advanceTime(MIN_UPTIME)
+            runCurrent()
+
+            // THEN we finally register with the satellite manager
+            verify(satelliteManager).registerForSatelliteModemStateChanged(any(), any())
+        }
+
+    private fun setUpRepo(
+        uptime: Long = MIN_UPTIME,
+        satMan: SatelliteManager? = satelliteManager,
+        satelliteSupported: Boolean = true,
+    ) {
+        doAnswer {
+                val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                callback.onResult(satelliteSupported)
+            }
+            .whenever(satelliteManager)
+            .requestIsSatelliteSupported(any(), any())
+
+        systemClock.setUptimeMillis(Process.getStartUptimeMillis() + uptime)
+
+        underTest =
+            DeviceBasedSatelliteRepositoryImpl(
+                if (satMan != null) Optional.of(satMan) else Optional.empty(),
+                dispatcher,
+                testScope.backgroundScope,
+                systemClock,
+            )
+    }
+
+    // Set system time to MIN_UPTIME and create a repo with satellite supported
+    private fun setupDefaultRepo() {
+        setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = true)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
new file mode 100644
index 0000000..5fa2d33
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.prod
+
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState.Off
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeDeviceBasedSatelliteRepository() : DeviceBasedSatelliteRepository {
+    override val connectionState = MutableStateFlow(Off)
+
+    override val signalStrength = MutableStateFlow(0)
+
+    override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
new file mode 100644
index 0000000..e010b86
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+
+@SmallTest
+class DeviceBasedSatelliteInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: DeviceBasedSatelliteInteractor
+
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private val iconsInteractor =
+        FakeMobileIconsInteractor(
+            FakeMobileMappingsProxy(),
+            mock(),
+        )
+
+    private val repo = FakeDeviceBasedSatelliteRepository()
+
+    @Before
+    fun setUp() {
+        mSetFlagsRule.enableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+        underTest =
+            DeviceBasedSatelliteInteractor(
+                repo,
+                iconsInteractor,
+                testScope.backgroundScope,
+            )
+    }
+
+    @Test
+    fun isSatelliteAllowed_falseWhenNotAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+            // WHEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = false
+
+            // THEN the interactor returns false due to the flag value
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isSatelliteAllowed_trueWhenAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+            // WHEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // THEN the interactor returns false due to the flag value
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isSatelliteAllowed_offWhenFlagIsOff() =
+        testScope.runTest {
+            // GIVEN feature is disabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+            // WHEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // THEN the interactor returns false due to the flag value
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun connectionState_matchesRepositoryValue() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.connectionState)
+
+            // Off
+            repo.connectionState.value = SatelliteConnectionState.Off
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // On
+            repo.connectionState.value = SatelliteConnectionState.On
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+
+            // Connected
+            repo.connectionState.value = SatelliteConnectionState.Connected
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+
+            // Unknown
+            repo.connectionState.value = SatelliteConnectionState.Unknown
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+        }
+
+    @Test
+    fun connectionState_offWhenFeatureIsDisabled() =
+        testScope.runTest {
+            // GIVEN the flag is disabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.connectionState)
+
+            // THEN the state is always Off, regardless of status in system_server
+
+            // Off
+            repo.connectionState.value = SatelliteConnectionState.Off
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // On
+            repo.connectionState.value = SatelliteConnectionState.On
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // Connected
+            repo.connectionState.value = SatelliteConnectionState.Connected
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // Unknown
+            repo.connectionState.value = SatelliteConnectionState.Unknown
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+        }
+
+    @Test
+    fun signalStrength_matchesRepo() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalStrength)
+
+            repo.signalStrength.value = 1
+            assertThat(latest).isEqualTo(1)
+
+            repo.signalStrength.value = 2
+            assertThat(latest).isEqualTo(2)
+
+            repo.signalStrength.value = 3
+            assertThat(latest).isEqualTo(3)
+
+            repo.signalStrength.value = 4
+            assertThat(latest).isEqualTo(4)
+        }
+
+    @Test
+    fun signalStrength_zeroWhenDisabled() =
+        testScope.runTest {
+            // GIVEN the flag is enabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.signalStrength)
+
+            // THEN the value is always 0, regardless of what the system says
+            repo.signalStrength.value = 1
+            assertThat(latest).isEqualTo(0)
+
+            repo.signalStrength.value = 2
+            assertThat(latest).isEqualTo(0)
+
+            repo.signalStrength.value = 3
+            assertThat(latest).isEqualTo(0)
+
+            repo.signalStrength.value = 4
+            assertThat(latest).isEqualTo(0)
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_twoConnectionsOos_yes() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 2 connections
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+            // WHEN all of the connections are OOS
+            i1.isInService.value = false
+            i2.isInService.value = false
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_oneConnectionOos_yes() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 1 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            // WHEN all of the connections are OOS
+            i1.isInService.value = false
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_oneConnectionInService_no() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 1 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            // WHEN all of the connections are NOT OOS
+            i1.isInService.value = true
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_twoConnectionsOneInService_no() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 2 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+            // WHEN at least 1 connection is NOT OOS.
+            i1.isInService.value = false
+            i2.isInService.value = true
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_twoConnectionsInService_no() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 2 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            // WHEN all connections are NOT OOS.
+            i1.isInService.value = true
+            i2.isInService.value = true
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_falseWhenFlagIsOff() =
+        testScope.runTest {
+            // GIVEN the flag is disabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN a condition that should return true (all conections OOS)
+
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            i1.isInService.value = true
+            i2.isInService.value = true
+
+            // THEN the value is still false, because the flag is off
+            assertThat(latest).isFalse()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index 2f79955..a5c766d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -17,10 +17,12 @@
 package com.android.systemui.statusbar.policy;
 
 import static android.os.BatteryManager.EXTRA_PRESENT;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS;
+
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -59,6 +61,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -286,6 +289,24 @@
         Assert.assertFalse(mBatteryController.isIncompatibleCharging());
     }
 
+    @Test
+    public void callbackRemovedWhileDispatching_doesntCrash() {
+        final AtomicBoolean remove = new AtomicBoolean(false);
+        BatteryStateChangeCallback callback = new BatteryStateChangeCallback() {
+            @Override
+            public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+                if (remove.get()) {
+                    mBatteryController.removeCallback(this);
+                }
+            }
+        };
+        mBatteryController.addCallback(callback);
+        // Add another callback so the iteration continues
+        mBatteryController.addCallback(new BatteryStateChangeCallback() {});
+        remove.set(true);
+        mBatteryController.fireBatteryLevelChanged();
+    }
+
     private void setupIncompatibleCharging() {
         final List<UsbPort> usbPorts = new ArrayList<>();
         usbPorts.add(mUsbPort);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
index b8e4306..68c1b8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
@@ -127,6 +127,25 @@
         }
     }
 
+    /** Regression test for b/317700495 */
+    @Test
+    public void removeCallbackWhileIterating_doesntCrash() {
+        final AtomicBoolean remove = new AtomicBoolean(false);
+        Callback callback = new Callback() {
+            @Override
+            public void onCastDevicesChanged() {
+                if (remove.get()) {
+                    mController.removeCallback(this);
+                }
+            }
+        };
+        mController.addCallback(callback);
+        // Add another callback so the iteration continues
+        mController.addCallback(() -> {});
+        remove.set(true);
+        mController.fireOnCastDevicesChanged();
+    }
+
     @Test
     public void hasConnectedCastDevice_connected() {
         CastController.CastDevice castDevice = new CastController.CastDevice();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt
index db0029a..777fa28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt
@@ -26,6 +26,8 @@
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.time.FakeSystemClock
 import java.util.concurrent.Executor
@@ -128,11 +130,42 @@
         verify(cameraManager).setTorchMode(id, enable)
     }
 
+    @Test
+    fun testCallbackRemovedWhileDispatching_doesntCrash() {
+        injectCamera()
+        var remove = false
+        val callback = object : FlashlightController.FlashlightListener {
+            override fun onFlashlightChanged(enabled: Boolean) {
+                if (remove) {
+                    controller.removeCallback(this)
+                }
+            }
+
+            override fun onFlashlightError() {}
+
+            override fun onFlashlightAvailabilityChanged(available: Boolean) {}
+        }
+        controller.addCallback(callback)
+        controller.addCallback(object : FlashlightController.FlashlightListener {
+            override fun onFlashlightChanged(enabled: Boolean) {}
+
+            override fun onFlashlightError() {}
+
+            override fun onFlashlightAvailabilityChanged(available: Boolean) {}
+        })
+        backgroundExecutor.runAllReady()
+
+        val captor = argumentCaptor<CameraManager.TorchCallback>()
+        verify(cameraManager).registerTorchCallback(any(), capture(captor))
+        remove = true
+        captor.value.onTorchModeChanged(ID, true)
+    }
+
     private fun injectCamera(
         flash: Boolean = true,
         facing: Int = CameraCharacteristics.LENS_FACING_BACK
     ): String {
-        val cameraID = "ID"
+        val cameraID = ID
         val camera = CameraCharacteristics(CameraMetadataNative().apply {
             set(CameraCharacteristics.FLASH_INFO_AVAILABLE, flash)
             set(CameraCharacteristics.LENS_FACING, facing)
@@ -141,4 +174,8 @@
         `when`(cameraManager.getCameraCharacteristics(cameraID)).thenReturn(camera)
         return cameraID
     }
+
+    companion object {
+        private const val ID = "ID"
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt
index 89989ce..b03edaf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt
@@ -27,6 +27,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -130,4 +131,61 @@
         controller.mPermControllerChangeReceiver.onReceive(context, testIntent)
         verify(listener, never()).onSafetyCenterEnableChanged(true)
     }
+
+    @Test
+    fun listenerRemovedWhileDispatching_doesNotCrash() {
+        var remove = false
+        val callback = object : SafetyController.Listener {
+            override fun onSafetyCenterEnableChanged(isSafetyCenterEnabled: Boolean) {
+                if (remove) {
+                    controller.removeCallback(this)
+                }
+            }
+        }
+
+        controller.addCallback(callback)
+        controller.addCallback {}
+
+        remove = true
+
+        `when`(scm.isSafetyCenterEnabled).thenReturn(true)
+        val testIntent = Intent(Intent.ACTION_PACKAGE_CHANGED)
+        testIntent.data = Uri.parse("package:$TEST_PC_PKG")
+        controller.mPermControllerChangeReceiver.onReceive(context, testIntent)
+    }
+
+    @Test
+    fun listenerRemovedWhileDispatching_otherCallbacksCalled() {
+        var remove = false
+        var called = false
+
+        val callback1 = object : SafetyController.Listener {
+            override fun onSafetyCenterEnableChanged(isSafetyCenterEnabled: Boolean) {
+                if (remove) {
+                    controller.removeCallback(this)
+                }
+            }
+        }
+
+        val callback2 = object : SafetyController.Listener {
+            override fun onSafetyCenterEnableChanged(isSafetyCenterEnabled: Boolean) {
+                // When the first callback is removed, we track if this is called
+                if (remove) {
+                    called = true
+                }
+            }
+        }
+
+        controller.addCallback(callback1)
+        controller.addCallback(callback2)
+
+        remove = true
+
+        `when`(scm.isSafetyCenterEnabled).thenReturn(true)
+        val testIntent = Intent(Intent.ACTION_PACKAGE_CHANGED)
+        testIntent.data = Uri.parse("package:$TEST_PC_PKG")
+        controller.mPermControllerChangeReceiver.onReceive(context, testIntent)
+
+        assertThat(called).isTrue()
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
index c35bc69..cb6ce68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.policy;
 
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -63,6 +64,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -213,10 +215,33 @@
     public void testNetworkRequest() {
         verify(mConnectivityManager, times(1)).registerNetworkCallback(argThat(
                 (NetworkRequest request) ->
-                        request.equals(new NetworkRequest.Builder().clearCapabilities().build())
+                        request.equals(new NetworkRequest.Builder()
+                                .clearCapabilities().addTransportType(TRANSPORT_VPN).build())
                 ), any(NetworkCallback.class));
     }
 
+    @Test
+    public void testRemoveCallbackWhileDispatch_doesntCrash() {
+        final AtomicBoolean remove = new AtomicBoolean(false);
+        SecurityController.SecurityControllerCallback callback =
+                new SecurityController.SecurityControllerCallback() {
+                    @Override
+                    public void onStateChanged() {
+                        if (remove.get()) {
+                            mSecurityController.removeCallback(this);
+                        }
+                    }
+                };
+        mSecurityController.addCallback(callback);
+        // Add another callback so the iteration continues
+        mSecurityController.addCallback(() -> {});
+        mBgExecutor.runAllReady();
+        remove.set(true);
+
+        mSecurityController.onUserSwitched(10);
+        mBgExecutor.runAllReady();
+    }
+
     /**
      * refresh CA certs by sending a user unlocked broadcast for the desired user
      */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
index 6825f65..f1a2c28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
@@ -47,6 +47,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 @SmallTest
@@ -174,4 +175,26 @@
         }
 
     }
+
+    @Test
+    public void testCallbackRemovedWhileDispatching_doesntCrash() {
+        final AtomicBoolean remove = new AtomicBoolean(false);
+        mGlobalSettings.putInt(Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF);
+        TestableLooper.get(this).processAllMessages();
+        final ZenModeController.Callback callback = new ZenModeController.Callback() {
+            @Override
+            public void onZenChanged(int zen) {
+                if (remove.get()) {
+                    mController.removeCallback(this);
+                }
+            }
+        };
+        mController.addCallback(callback);
+        mController.addCallback(new ZenModeController.Callback() {});
+
+        remove.set(true);
+
+        mGlobalSettings.putInt(Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_NO_INTERRUPTIONS);
+        TestableLooper.get(this).processAllMessages();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt
index 4e61b89..2bc05fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.unfold.progress
 
+import android.os.Handler
+import android.os.HandlerThread
 import android.os.Looper
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
@@ -24,6 +26,7 @@
 import com.android.systemui.unfold.TestUnfoldTransitionProvider
 import com.android.systemui.utils.os.FakeHandler
 import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
 import org.junit.runner.RunWith
 
 @RunWith(AndroidTestingRunner::class)
@@ -93,4 +96,13 @@
 
         listener.assertNotStarted()
     }
+
+    @Test
+    fun addCallback_fromBackgroundThread_succeeds() = runTest {
+        val bgHandler = Handler(HandlerThread("TestBgThread").apply { start() }.looper)
+        bgHandler.runWithScissors({ progressProvider.addCallback(listener) }, 5000L)
+
+        wrappedProgressProvider.onTransitionStarted()
+        listener.assertStarted()
+    }
 }
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 6714c94..fb5375a 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
@@ -32,6 +32,7 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.GuestResetOrExitSessionReceiver
 import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.SysuiTestCase
@@ -121,6 +122,7 @@
         )
 
         utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_SWITCH_USER_ON_BG)
         spyContext = spy(context)
         keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.featureFlags)
         keyguardRepository = keyguardReply.repository
@@ -172,6 +174,7 @@
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
             underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower)
+            runCurrent()
 
             verify(uiEventLogger, times(1))
                 .log(MultiUserActionsEvent.SWITCH_TO_USER_FROM_USER_SWITCHER)
@@ -191,6 +194,7 @@
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
             underTest.onRecordSelected(UserRecord(info = userInfos.last()))
+            runCurrent()
 
             verify(uiEventLogger, times(1))
                 .log(MultiUserActionsEvent.SWITCH_TO_GUEST_FROM_USER_SWITCHER)
@@ -218,6 +222,7 @@
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
             underTest.onRecordSelected(UserRecord(info = userInfos.last()))
+            runCurrent()
 
             verify(uiEventLogger, times(1))
                 .log(MultiUserActionsEvent.SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java
index db0139c..55c49ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java
@@ -24,6 +24,7 @@
 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.time.FakeSystemClock;
 
@@ -41,6 +42,7 @@
     private static final int MAX_RETRIES = 5;
     private static final int RETRY_DELAY_MS = 1000;
     private static final int CONNECTION_MIN_DURATION_MS = 5000;
+    private static final String DUMPSYS_NAME = "dumpsys_name";
 
     private FakeSystemClock mFakeClock = new FakeSystemClock();
     private FakeExecutor mFakeExecutor = new FakeExecutor(mFakeClock);
@@ -49,8 +51,14 @@
     private ObservableServiceConnection<Proxy> mConnection;
 
     @Mock
+    private ObservableServiceConnection.Callback<Proxy> mConnectionCallback;
+
+    @Mock
     private Observer mObserver;
 
+    @Mock
+    private DumpManager mDumpManager;
+
     private static class Proxy {
     }
 
@@ -63,6 +71,8 @@
         mConnectionManager = new PersistentConnectionManager<>(
                 mFakeClock,
                 mFakeExecutor,
+                mDumpManager,
+                DUMPSYS_NAME,
                 mConnection,
                 MAX_RETRIES,
                 RETRY_DELAY_MS,
@@ -154,4 +164,16 @@
         callbackCaptor.getValue().onSourceChanged();
         verify(mConnection).bind();
     }
+
+    @Test
+    public void testAddConnectionCallback() {
+        mConnectionManager.addConnectionCallback(mConnectionCallback);
+        verify(mConnection).addCallback(mConnectionCallback);
+    }
+
+    @Test
+    public void testRemoveConnectionCallback() {
+        mConnectionManager.removeConnectionCallback(mConnectionCallback);
+        verify(mConnection).removeCallback(mConnectionCallback);
+    }
 }
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 814ea19..048120a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -126,6 +126,7 @@
 import com.android.systemui.scene.shared.logger.SceneLogger;
 import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.LargeScreenHeaderHelper;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
@@ -349,6 +350,8 @@
     private Display mDefaultDisplay;
     @Mock
     private SceneContainerFlags mSceneContainerFlags;
+    @Mock
+    private LargeScreenHeaderHelper mLargeScreenHeaderHelper;
 
     private final SceneTestUtils mUtils = new SceneTestUtils(this);
     private final TestScope mTestScope = mUtils.getTestScope();
@@ -441,6 +444,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 mTestScope.getBackgroundScope(),
+                mUtils.getTestDispatcher(),
+                mUtils.getTestDispatcher(),
                 keyguardInteractor,
                 featureFlags,
                 shadeRepository,
@@ -459,6 +464,8 @@
                 keyguardTransitionRepository,
                 keyguardTransitionInteractor,
                 mTestScope.getBackgroundScope(),
+                mUtils.getTestDispatcher(),
+                mUtils.getTestDispatcher(),
                 keyguardInteractor,
                 featureFlags,
                 mock(KeyguardSecurityModel.class),
@@ -490,7 +497,8 @@
                                         mContext,
                                         splitShadeStateController,
                                         keyguardInteractor,
-                                        deviceEntryUdfpsInteractor),
+                                        deviceEntryUdfpsInteractor,
+                                        () -> mLargeScreenHeaderHelper),
                                 shadeRepository
                         )
                 );
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
index e24ba26..c2dc673 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
@@ -48,6 +48,9 @@
     @get:[Provides Application]
     val appScope: CoroutineScope = scope.backgroundScope
 
+    @get:[Provides Background]
+    val bgScope: CoroutineScope = scope.backgroundScope
+
     @Module
     interface Bindings {
         @Binds @Main fun bindMainContext(dispatcher: TestDispatcher): CoroutineContext
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt
index 3fdeb30..3b5ff38 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.biometrics.data.repository
 
+import android.util.Size
 import com.android.systemui.biometrics.shared.model.DisplayRotation
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -29,6 +30,9 @@
     private val _currentRotation = MutableStateFlow<DisplayRotation>(DisplayRotation.ROTATION_0)
     override val currentRotation: StateFlow<DisplayRotation> = _currentRotation.asStateFlow()
 
+    private val _currentDisplaySize = MutableStateFlow<Size>(Size(0, 0))
+    override val currentDisplaySize: StateFlow<Size> = _currentDisplaySize.asStateFlow()
+
     override val isReverseDefaultRotation = false
 
     fun setIsInRearDisplayMode(isInRearDisplayMode: Boolean) {
@@ -38,4 +42,8 @@
     fun setCurrentRotation(currentRotation: DisplayRotation) {
         _currentRotation.value = currentRotation
     }
+
+    fun setCurrentDisplaySize(size: Size) {
+        _currentDisplaySize.value = size
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
index 51ce9f0..77f501f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.biometrics.data.repository
 
+import android.graphics.Point
 import com.android.systemui.biometrics.shared.model.LockoutMode
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -28,6 +29,10 @@
 
     private val lockoutModesForUser = mutableMapOf<Int, LockoutMode>()
 
+    private val faceSensorLocation = MutableStateFlow<Point?>(null)
+    override val sensorLocation: StateFlow<Point?>
+        get() = faceSensorLocation
+
     fun setLockoutMode(userId: Int, mode: LockoutMode) {
         lockoutModesForUser[userId] = mode
     }
@@ -38,4 +43,8 @@
     fun setSensorInfo(value: FaceSensorInfo?) {
         faceSensorInfo.value = value
     }
+
+    fun setSensorLocation(value: Point?) {
+        faceSensorLocation.value = value
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
index 3ab1b6c..3ea3ccf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
@@ -16,8 +16,24 @@
 
 package com.android.systemui.communal.data.repository
 
+import com.android.systemui.communal.data.model.CommunalMediaModel
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
-class FakeCommunalMediaRepository(
-    override val mediaPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false)
-) : CommunalMediaRepository
+class FakeCommunalMediaRepository : CommunalMediaRepository {
+    private val _mediaModel = MutableStateFlow(CommunalMediaModel.INACTIVE)
+
+    override val mediaModel: Flow<CommunalMediaModel> = _mediaModel
+
+    fun mediaActive(timestamp: Long = 0L) {
+        _mediaModel.value =
+            CommunalMediaModel(
+                hasAnyMediaOrRecommendation = true,
+                createdTimestampMillis = timestamp,
+            )
+    }
+
+    fun mediaInactive() {
+        _mediaModel.value = CommunalMediaModel.INACTIVE
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
index c85c27e..e82cae4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
@@ -9,6 +9,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
@@ -52,4 +53,12 @@
     fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) {
         _isCommunalHubShowing.value = isCommunalHubShowing
     }
+
+    private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true)
+    override val isCtaTileInViewModeVisible: Flow<Boolean> =
+        _isCtaTileInViewModeVisible.asStateFlow()
+
+    override fun setCtaTileInViewModeVisibility(isVisible: Boolean) {
+        _isCtaTileInViewModeVisible.value = isVisible
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index c6f12e2..397dc1a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -1,15 +1,38 @@
 package com.android.systemui.communal.data.repository
 
+import android.content.ComponentName
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
 
 /** Fake implementation of [CommunalWidgetRepository] */
-class FakeCommunalWidgetRepository : CommunalWidgetRepository {
+class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) :
+    CommunalWidgetRepository {
     private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList())
     override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets
+    private val _widgetAdded = MutableSharedFlow<Int>()
+    val widgetAdded: Flow<Int> = _widgetAdded
+
+    private var nextWidgetId = 1
 
     fun setCommunalWidgets(inventory: List<CommunalWidgetContentModel>) {
         _communalWidgets.value = inventory
     }
+
+    override fun addWidget(
+        provider: ComponentName,
+        priority: Int,
+        configureWidget: suspend (id: Int) -> Boolean
+    ) {
+        coroutineScope.launch {
+            val id = nextWidgetId++
+            if (configureWidget.invoke(id)) {
+                _widgetAdded.emit(id)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
index faacce6..95ff889 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
@@ -35,8 +35,10 @@
     @JvmStatic
     fun create(
         testScope: TestScope = TestScope(),
-        communalRepository: FakeCommunalRepository = FakeCommunalRepository(),
-        widgetRepository: FakeCommunalWidgetRepository = FakeCommunalWidgetRepository(),
+        communalRepository: FakeCommunalRepository =
+            FakeCommunalRepository(testScope.backgroundScope),
+        widgetRepository: FakeCommunalWidgetRepository =
+            FakeCommunalWidgetRepository(testScope.backgroundScope),
         mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(),
         smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(),
         tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(),
@@ -50,6 +52,7 @@
                 communalRepository = communalRepository,
             )
         return WithDependencies(
+            testScope,
             communalRepository,
             widgetRepository,
             mediaRepository,
@@ -73,6 +76,7 @@
     }
 
     data class WithDependencies(
+        val testScope: TestScope,
         val communalRepository: FakeCommunalRepository,
         val widgetRepository: FakeCommunalWidgetRepository,
         val mediaRepository: FakeCommunalMediaRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a94ca29..0c1dbfe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -147,7 +147,6 @@
                 )
             }
         }
-
         _transitions.emit(step)
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index b03d0b8..b1a0b67 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.shade.data.repository.shadeRepository
 import dagger.Lazy
@@ -30,6 +31,8 @@
             transitionRepository = keyguardTransitionRepository,
             transitionInteractor = keyguardTransitionInteractor,
             scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
             flags = featureFlagsClassic,
             shadeRepository = shadeRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
index ade3e1a..97536e2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
 
@@ -30,6 +31,8 @@
             transitionRepository = keyguardTransitionRepository,
             transitionInteractor = keyguardTransitionInteractor,
             scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
             flags = featureFlagsClassic,
             keyguardSecurityModel = keyguardSecurityModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
index 8d6529a..dad1887 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
@@ -19,6 +19,7 @@
 package com.android.systemui.keyguard.ui
 
 import com.android.keyguard.logging.keyguardTransitionAnimationLogger
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -27,6 +28,7 @@
 val Kosmos.keyguardTransitionAnimationFlow by Fixture {
     KeyguardTransitionAnimationFlow(
         scope = applicationCoroutineScope,
+        transitionInteractor = keyguardTransitionInteractor,
         logger = keyguardTransitionAnimationLogger,
     )
 }
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
index d9c6e4f..3ed9392 100644
--- 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
@@ -19,7 +19,6 @@
 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
@@ -27,7 +26,6 @@
 
 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
index e4821b0..c909dd6 100644
--- 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
@@ -18,7 +18,6 @@
 
 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
@@ -26,7 +25,6 @@
 
 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/AlternateBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
index 9f0466d..b4f1218 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 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
@@ -28,7 +27,6 @@
 val Kosmos.alternateBouncerViewModel by Fixture {
     AlternateBouncerViewModel(
         statusBarKeyguardViewManager = statusBarKeyguardViewManager,
-        transitionInteractor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt
index 44e5426..b6f278c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 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
@@ -26,7 +25,6 @@
 
 val Kosmos.aodToGoneTransitionViewModel by Fixture {
     AodToGoneTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt
index b5a5f03..733340c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 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
@@ -27,7 +26,6 @@
 
 val Kosmos.aodToLockscreenTransitionViewModel by Fixture {
     AodToLockscreenTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt
index 27ad0f0..8d066fc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 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
@@ -26,7 +25,6 @@
 
 val Kosmos.aodToOccludedTransitionViewModel by Fixture {
     AodToOccludedTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
index 6ffcc9a..c71c1c3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
@@ -20,7 +20,6 @@
 
 import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
-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
@@ -31,7 +30,6 @@
 
 val Kosmos.bouncerToGoneFlows by Fixture {
     BouncerToGoneFlows(
-        interactor = keyguardTransitionInteractor,
         statusBarStateController = sysuiStatusBarStateController,
         primaryBouncerInteractor = primaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..400a0d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.dozingToLockscreenTransitionViewModel by Fixture {
+    DozingToLockscreenTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
index 00ece14..19e4241 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 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
@@ -27,7 +26,6 @@
 
 var Kosmos.goneToAodTransitionViewModel by Fixture {
     GoneToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt
index 073b34b..b267a96 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 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
@@ -26,7 +25,6 @@
 
 val Kosmos.goneToDreamingTransitionViewModel by Fixture {
     GoneToDreamingTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt
index 7865f71..07b4cd4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 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
@@ -27,7 +26,6 @@
 
 val Kosmos.lockscreenToAodTransitionViewModel by Fixture {
     LockscreenToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         shadeDependentFlows = shadeDependentFlows,
         animationFlow = keyguardTransitionAnimationFlow,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt
index b9f4b71..56d5ff6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 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
@@ -26,7 +25,6 @@
 
 val Kosmos.lockscreenToDreamingTransitionViewModel by Fixture {
     LockscreenToDreamingTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         shadeDependentFlows = shadeDependentFlows,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
index 475aa2d..1b2337f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 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
@@ -26,7 +25,6 @@
 
 val Kosmos.lockscreenToGoneTransitionViewModel by Fixture {
     LockscreenToGoneTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt
index 8541a4f..9953d39 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
-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
@@ -27,7 +26,6 @@
 
 val Kosmos.lockscreenToOccludedTransitionViewModel by Fixture {
     LockscreenToOccludedTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         shadeDependentFlows = shadeDependentFlows,
         configurationInteractor = configurationInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt
index 65c47fc..f094f22 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 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
@@ -26,7 +25,6 @@
 
 val Kosmos.lockscreenToPrimaryBouncerTransitionViewModel by Fixture {
     LockscreenToPrimaryBouncerTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         shadeDependentFlows = shadeDependentFlows,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt
index ddde549..b7867b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 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
@@ -27,7 +26,6 @@
 
 val Kosmos.occludedToAodTransitionViewModel by Fixture {
     OccludedToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
index 93ecb79..e6651a4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
@@ -20,7 +20,6 @@
 
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 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
@@ -28,7 +27,6 @@
 
 var Kosmos.occludedToLockscreenTransitionViewModel by Fixture {
     OccludedToLockscreenTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         configurationInteractor = configurationInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt
index a7f29d6..8d88730 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 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
@@ -27,7 +26,6 @@
 
 val Kosmos.primaryBouncerToAodTransitionViewModel by Fixture {
     PrimaryBouncerToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
index ace6ae3..ab28d0d6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
@@ -20,7 +20,6 @@
 
 import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
-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
@@ -30,7 +29,6 @@
 
 val Kosmos.primaryBouncerToGoneTransitionViewModel by Fixture {
     PrimaryBouncerToGoneTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         statusBarStateController = sysuiStatusBarStateController,
         primaryBouncerInteractor = primaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
index 3bbabf7..8566251 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 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
@@ -27,7 +26,6 @@
 
 val Kosmos.primaryBouncerToLockscreenTransitionViewModel by Fixture {
     PrimaryBouncerToLockscreenTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
index 1185f2e..0307c41 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
@@ -35,14 +35,21 @@
         mutableInputs.add(Input.Intent(view, intent))
     }
 
-    override fun handle(view: View?, pendingIntent: PendingIntent) {
-        mutableInputs.add(Input.PendingIntent(view, pendingIntent))
+    override fun handle(
+        view: View?,
+        pendingIntent: PendingIntent,
+        requestLaunchingDefaultActivity: Boolean
+    ) {
+        mutableInputs.add(Input.PendingIntent(view, pendingIntent, requestLaunchingDefaultActivity))
     }
 
     sealed interface Input {
         data class Intent(val view: View?, val intent: android.content.Intent) : Input
-        data class PendingIntent(val view: View?, val pendingIntent: android.app.PendingIntent) :
-            Input
+        data class PendingIntent(
+            val view: View?,
+            val pendingIntent: android.app.PendingIntent,
+            val requestLaunchingDefaultActivity: Boolean
+        ) : Input
     }
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/LargeScreenHeaderHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/LargeScreenHeaderHelperKosmos.kt
new file mode 100644
index 0000000..d3e7574
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/LargeScreenHeaderHelperKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.largeScreenHeaderHelper by Fixture { mockLargeScreenHeaderHelper }
+val Kosmos.mockLargeScreenHeaderHelper by Fixture { mock<LargeScreenHeaderHelper>() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
new file mode 100644
index 0000000..c416ea1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeRemoteInputRepository : RemoteInputRepository {
+    override val isRemoteInputActive = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryKosmos.kt
new file mode 100644
index 0000000..1684efb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.remoteInputRepository: RemoteInputRepository by
+    Kosmos.Fixture { fakeRemoteInputRepository }
+val Kosmos.fakeRemoteInputRepository by Kosmos.Fixture { FakeRemoteInputRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorKosmos.kt
new file mode 100644
index 0000000..07b39dc
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.data.repository.remoteInputRepository
+
+val Kosmos.remoteInputInteractor by Kosmos.Fixture { RemoteInputInteractor(remoteInputRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt
index 788e3aa..1ffc9f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt
@@ -15,8 +15,6 @@
  */
 package com.android.systemui.statusbar.notification.data
 
-import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardStateRepositoryModule
 import dagger.Module
 
-@Module(includes = [FakeNotificationsKeyguardStateRepositoryModule::class])
-object FakeStatusBarNotificationsDataLayerModule
+@Module(includes = []) object FakeStatusBarNotificationsDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt
deleted file mode 100644
index 5d3cb4d..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.statusbar.notification.data.repository
-
-import com.android.systemui.dagger.SysUISingleton
-import dagger.Binds
-import dagger.Module
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-
-@SysUISingleton
-class FakeNotificationsKeyguardViewStateRepository @Inject constructor() :
-    NotificationsKeyguardViewStateRepository {
-    private val _notificationsFullyHidden = MutableStateFlow(false)
-    override val areNotificationsFullyHidden: Flow<Boolean> = _notificationsFullyHidden
-
-    private val _isPulseExpanding = MutableStateFlow(false)
-    override val isPulseExpanding: Flow<Boolean> = _isPulseExpanding
-
-    fun setNotificationsFullyHidden(fullyHidden: Boolean) {
-        _notificationsFullyHidden.value = fullyHidden
-    }
-
-    fun setPulseExpanding(expanding: Boolean) {
-        _isPulseExpanding.value = expanding
-    }
-}
-
-@Module
-interface FakeNotificationsKeyguardStateRepositoryModule {
-    @Binds
-    fun bindFake(
-        fake: FakeNotificationsKeyguardViewStateRepository
-    ): NotificationsKeyguardViewStateRepository
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt
index f2b9da4..df7fd94 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt
@@ -18,7 +18,5 @@
 
 import com.android.systemui.kosmos.Kosmos
 
-var Kosmos.notificationsKeyguardViewStateRepository: NotificationsKeyguardViewStateRepository by
-    Kosmos.Fixture { fakeNotificationsKeyguardViewStateRepository }
-val Kosmos.fakeNotificationsKeyguardViewStateRepository by
-    Kosmos.Fixture { FakeNotificationsKeyguardViewStateRepository() }
+val Kosmos.notificationsKeyguardViewStateRepository: NotificationsKeyguardViewStateRepository by
+    Kosmos.Fixture { NotificationsKeyguardViewStateRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
index 432464e..61a38b8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
@@ -18,13 +18,11 @@
 
 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.data.repository.notificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
 
 val Kosmos.notificationsKeyguardInteractor by Fixture {
     NotificationsKeyguardInteractor(
         repository = notificationsKeyguardViewStateRepository,
-        backgroundDispatcher = testDispatcher,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
index 13d577b..8909d75 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.statusbar.policy.splitShadeStateController
 
 val Kosmos.sharedNotificationContainerInteractor by
@@ -31,5 +32,6 @@
             splitShadeStateController = splitShadeStateController,
             keyguardInteractor = keyguardInteractor,
             deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
+            largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt
index 021e7df..ac90a45 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt
@@ -77,6 +77,8 @@
 
     override fun isVpnBranded(): Boolean = fakeState.isVpnBranded
 
+    override fun isVpnValidated(): Boolean = fakeState.isVpnValidated
+
     override fun getPrimaryVpnName(): String? = fakeState.primaryVpnName
 
     override fun getWorkProfileVpnName(): String? = fakeState.workProfileVpnName
@@ -110,6 +112,7 @@
         var isVpnEnabled: Boolean = false,
         var isVpnRestricted: Boolean = false,
         var isVpnBranded: Boolean = false,
+        var isVpnValidated: Boolean = false,
         var primaryVpnName: String? = null,
         var workProfileVpnName: String? = null,
         var hasCACertInCurrentUser: Boolean = false,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
index 76199e3..791165d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
@@ -109,6 +109,11 @@
     }
 
     @Override
+    public boolean isVpnValidated() {
+        return false;
+    }
+
+    @Override
     public String getPrimaryVpnName() {
         return null;
     }
diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp
index e52cefb..81fd8ce 100644
--- a/packages/SystemUI/unfold/Android.bp
+++ b/packages/SystemUI/unfold/Android.bp
@@ -39,7 +39,4 @@
     sdk_version: "current",
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
diff --git a/packages/SystemUI/unfold/lint-baseline.xml b/packages/SystemUI/unfold/lint-baseline.xml
deleted file mode 100644
index 449ed2e..0000000
--- a/packages/SystemUI/unfold/lint-baseline.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-dev" type="baseline" client="" name="" variant="all" version="7.1.0-dev">
-</issues>
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt
index 9bdf3d5..fdce147 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt
@@ -24,12 +24,16 @@
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import java.util.Collections.synchronizedMap
 
 /**
  * [UnfoldTransitionProgressProvider] that forwards all progress to the main thread handler.
  *
  * This is needed when progress are calculated in the background, but some listeners need the
  * callbacks in the main thread.
+ *
+ * Note that this class assumes that the root provider has thread safe callback registration, as
+ * they might be called from any thread.
  */
 class MainThreadUnfoldTransitionProgressProvider
 @AssistedInject
@@ -38,27 +42,20 @@
     @Assisted private val rootProvider: UnfoldTransitionProgressProvider
 ) : UnfoldTransitionProgressProvider {
 
-    private val listenerMap = mutableMapOf<TransitionProgressListener, TransitionProgressListener>()
+    private val listenerMap: MutableMap<TransitionProgressListener, TransitionProgressListener> =
+        synchronizedMap(mutableMapOf())
 
     override fun addCallback(listener: TransitionProgressListener) {
-        assertMainThread()
         val proxy = TransitionProgressListerProxy(listener)
         rootProvider.addCallback(proxy)
         listenerMap[listener] = proxy
     }
 
     override fun removeCallback(listener: TransitionProgressListener) {
-        assertMainThread()
         val proxy = listenerMap.remove(listener) ?: return
         rootProvider.removeCallback(proxy)
     }
 
-    private fun assertMainThread() {
-        check(mainHandler.looper.isCurrentThread) {
-            "Should be called from the main thread, but this is ${Thread.currentThread()}"
-        }
-    }
-
     override fun destroy() {
         rootProvider.destroy()
     }
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
index 1fc74f7..67a7d12 100644
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
+++ b/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
@@ -16,6 +16,8 @@
 
 package com.android.vpndialogs;
 
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
 import android.content.DialogInterface;
 import android.net.VpnManager;
 import android.os.Bundle;
@@ -87,6 +89,7 @@
             mAlertParams.mNegativeButtonListener = this;
             mAlertParams.mView = view;
             setupAlert();
+            getWindow().addPrivateFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
 
             if (mHandler == null) {
                 mHandler = new Handler(this);
diff --git a/packages/overlays/NoCutoutOverlay/res/values/config.xml b/packages/overlays/NoCutoutOverlay/res/values/config.xml
index ed0340b..b44a153a 100644
--- a/packages/overlays/NoCutoutOverlay/res/values/config.xml
+++ b/packages/overlays/NoCutoutOverlay/res/values/config.xml
@@ -20,10 +20,17 @@
          black in software (to avoid aliasing or emulate a cutout that is not physically existent).
      -->
     <bool name="config_fillMainBuiltInDisplayCutout">false</bool>
+    <!-- Whether the display cutout region of the secondary built-in display should be forced to
+         black in software (to avoid aliasing or emulate a cutout that is not physically existent).
+     -->
+    <bool name="config_fillSecondaryBuiltInDisplayCutout">false</bool>
 
     <!-- If true, and there is a cutout on the main built in display, the cutout will be masked
          by shrinking the display such that it does not overlap the cutout area. -->
     <bool name="config_maskMainBuiltInDisplayCutout">true</bool>
+    <!-- If true, and there is a cutout on the secondary built in display, the cutout will be masked
+         by shrinking the display such that it does not overlap the cutout area. -->
+    <bool name="config_maskSecondaryBuiltInDisplayCutout">true</bool>
 
     <!-- Height of the status bar -->
     <dimen name="status_bar_height_portrait">28dp</dimen>
diff --git a/proto/src/criticalevents/critical_event_log.proto b/proto/src/criticalevents/critical_event_log.proto
index 9cda267..cffcd09 100644
--- a/proto/src/criticalevents/critical_event_log.proto
+++ b/proto/src/criticalevents/critical_event_log.proto
@@ -60,8 +60,11 @@
     JavaCrash java_crash = 5;
     NativeCrash native_crash = 6;
     SystemServerStarted system_server_started = 7;
+    InstallPackages install_packages = 8;
   }
 
+  message InstallPackages {}
+
   message SystemServerStarted {}
 
   message Watchdog {
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index b403a7f..7f542d1 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -408,5 +408,9 @@
     // Notify the user about external display events related to screenshot.
     // Package: com.android.systemui
     NOTE_GLOBAL_SCREENSHOT_EXTERNAL_DISPLAY = 1008;
+
+    // Notify the user that accessibility floating menu is hidden.
+    // Package: com.android.systemui
+    NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009;
   }
 }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 513c095..d967874 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -142,6 +142,11 @@
                     Assume.assumeFalse(IS_UNDER_RAVENWOOD);
                 }
 
+                // Stopgap for http://g/ravenwood/EPAD-N5ntxM
+                if (description.getMethodName().endsWith("$noRavenwood")) {
+                    Assume.assumeFalse(IS_UNDER_RAVENWOOD);
+                }
+
                 RavenwoodRuleImpl.init(RavenwoodRule.this);
                 try {
                     base.evaluate();
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
index 085c186..7abfecf 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
@@ -42,4 +42,9 @@
     public void testIgnored() {
         throw new RuntimeException("Shouldn't be executed under ravenwood");
     }
+
+    @Test
+    public void testIgnored$noRavenwood() {
+        throw new RuntimeException("Shouldn't be executed under ravenwood");
+    }
 }
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 491ed22..ab2546b 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -9,8 +9,10 @@
 com.android.internal.os.LongArrayMultiStateCounter
 com.android.internal.os.LongArrayMultiStateCounter$LongArrayContainer
 com.android.internal.os.MonotonicClock
+com.android.internal.os.PowerProfile
 com.android.internal.os.PowerStats
 com.android.internal.os.PowerStats$Descriptor
+com.android.internal.power.ModemPowerProfile
 
 android.util.AtomicFile
 android.util.DataUnit
@@ -32,11 +34,16 @@
 android.util.TimeUtils
 android.util.Xml
 
+android.os.AggregateBatteryConsumer
 android.os.BatteryConsumer
+android.os.BatteryStats
 android.os.BatteryStats$HistoryItem
 android.os.BatteryStats$HistoryStepDetails
 android.os.BatteryStats$HistoryTag
+android.os.BatteryStats$LongCounter
 android.os.BatteryStats$ProcessStateChange
+android.os.BatteryUsageStats
+android.os.BatteryUsageStatsQuery
 android.os.Binder
 android.os.Binder$IdentitySupplier
 android.os.Broadcaster
@@ -54,11 +61,14 @@
 android.os.PackageTagsList
 android.os.Parcel
 android.os.Parcelable
+android.os.PowerComponents
 android.os.Process
 android.os.ServiceSpecificException
 android.os.SystemClock
 android.os.ThreadLocalWorkSource
 android.os.TimestampedValue
+android.os.UidBatteryConsumer
+android.os.UidBatteryConsumer$Builder
 android.os.UserHandle
 android.os.WorkSource
 
@@ -117,6 +127,7 @@
 android.content.ContentProvider
 
 com.android.server.LocalServices
+com.android.server.power.stats.BatteryStatsImpl
 
 com.android.internal.util.BitUtils
 com.android.internal.util.BitwiseInputStream
diff --git a/services/Android.bp b/services/Android.bp
index 0b484f4..7e8333c 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -148,9 +148,6 @@
 java_library {
     name: "Slogf",
     srcs: ["core/java/com/android/server/utils/Slogf.java"],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 // merge all required services into one jar
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index a354671..69cc68a 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -22,6 +22,7 @@
     lint: {
         error_checks: ["MissingPermissionAnnotation"],
         baseline_filename: "lint-baseline.xml",
+
     },
     srcs: [
         ":services.accessibility-sources",
@@ -50,9 +51,6 @@
     libs: [
         "androidx.annotation_annotation",
     ],
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-    },
 }
 
 aconfig_declarations {
diff --git a/services/accessibility/lint-baseline.xml b/services/accessibility/lint-baseline.xml
index 6bec8cf..b808219 100644
--- a/services/accessibility/lint-baseline.xml
+++ b/services/accessibility/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="8.1.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="SimpleManualPermissionEnforcement"
@@ -23,4 +23,4 @@
             column="9"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
index 553ba12..7fc1738 100644
--- a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
+++ b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
@@ -16,6 +16,7 @@
 
 package com.android.server.autofill;
 
+import static com.android.server.autofill.Session.REQUEST_ID_KEY;
 import static com.android.server.autofill.Session.SESSION_ID_KEY;
 
 import android.annotation.NonNull;
@@ -107,15 +108,16 @@
         mLastFlag = flag;
         if (mRemoteFillService != null && mRemoteFillService.isCredentialAutofillService()) {
             Slog.v(TAG, "About to call CredAutofill service as secondary provider");
-            addSessionIdToClientState(pendingFillRequest, pendingInlineSuggestionsRequest, id);
+            addSessionIdAndRequestIdToClientState(pendingFillRequest,
+                    pendingInlineSuggestionsRequest, id);
             mRemoteFillService.onFillCredentialRequest(pendingFillRequest, client);
         } else {
             mRemoteFillService.onFillRequest(pendingFillRequest);
         }
     }
 
-    private FillRequest addSessionIdToClientState(FillRequest pendingFillRequest,
-            InlineSuggestionsRequest pendingInlineSuggestionsRequest, int id) {
+    private FillRequest addSessionIdAndRequestIdToClientState(FillRequest pendingFillRequest,
+            InlineSuggestionsRequest pendingInlineSuggestionsRequest, int sessionId) {
         if (pendingFillRequest.getClientState() == null) {
             pendingFillRequest = new FillRequest(pendingFillRequest.getId(),
                     pendingFillRequest.getFillContexts(),
@@ -125,7 +127,8 @@
                     pendingInlineSuggestionsRequest,
                     pendingFillRequest.getDelayedFillIntentSender());
         }
-        pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, id);
+        pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, sessionId);
+        pendingFillRequest.getClientState().putInt(REQUEST_ID_KEY, pendingFillRequest.getId());
         return pendingFillRequest;
     }
 
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 007be05..6a81425 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -234,7 +234,8 @@
             new ComponentName("com.android.credentialmanager",
                     "com.android.credentialmanager.autofill.CredentialAutofillService");
 
-    static final String SESSION_ID_KEY = "session_id";
+    static final String SESSION_ID_KEY = "autofill_session_id";
+    static final String REQUEST_ID_KEY = "autofill_request_id";
 
     final Object mLock;
 
@@ -729,7 +730,7 @@
                         mPendingFillRequest.getFlags(), id, mClient);
             } else if (mRemoteFillService != null) {
                 if (mIsPrimaryCredential) {
-                    mPendingFillRequest = addSessionIdToClientState(mPendingFillRequest,
+                    mPendingFillRequest = addSessionIdAndRequestIdToClientState(mPendingFillRequest,
                             mPendingInlineSuggestionsRequest, id);
                     mRemoteFillService.onFillCredentialRequest(mPendingFillRequest, mClient);
                 } else {
@@ -877,8 +878,8 @@
         }
     }
 
-    private FillRequest addSessionIdToClientState(FillRequest pendingFillRequest,
-            InlineSuggestionsRequest pendingInlineSuggestionsRequest, int id) {
+    private FillRequest addSessionIdAndRequestIdToClientState(FillRequest pendingFillRequest,
+            InlineSuggestionsRequest pendingInlineSuggestionsRequest, int sessionId) {
         if (pendingFillRequest.getClientState() == null) {
             pendingFillRequest = new FillRequest(pendingFillRequest.getId(),
                     pendingFillRequest.getFillContexts(),
@@ -888,7 +889,8 @@
                     pendingInlineSuggestionsRequest,
                     pendingFillRequest.getDelayedFillIntentSender());
         }
-        pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, id);
+        pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, sessionId);
+        pendingFillRequest.getClientState().putInt(REQUEST_ID_KEY, pendingFillRequest.getId());
         return pendingFillRequest;
     }
 
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index d695d36..549fa36 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -7,4 +7,12 @@
             "restore for apps that have been launched."
     bug: "308401499"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_max_size_writes_to_pipes"
+    namespace: "onboarding"
+    description: "Enables the write buffer to pipes to be of maximum size."
+    bug: "265976737"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index 6aed9aa..cca166b 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -40,8 +40,8 @@
 
 import com.android.server.EventLogTags;
 import com.android.server.backup.BackupAgentTimeoutParameters;
-import com.android.server.backup.BackupAndRestoreFeatureFlags;
 import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.Flags;
 import com.android.server.backup.FullBackupJob;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.OperationStorage.OpState;
@@ -390,8 +390,11 @@
 
             // Set up to send data to the transport
             final int N = mPackages.size();
-            final int chunkSizeInBytes =
-                    BackupAndRestoreFeatureFlags.getFullBackupWriteToTransportBufferSizeBytes();
+            int chunkSizeInBytes = 8 * 1024; // 8KB
+            if (Flags.enableMaxSizeWritesToPipes()) {
+                // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+                chunkSizeInBytes = 64 * 1024; // 64KB
+            }
             final byte[] buffer = new byte[chunkSizeInBytes];
             for (int i = 0; i < N; i++) {
                 mBackupRunner = null;
diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
index ff72476..2c9eb51 100644
--- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
@@ -29,7 +29,6 @@
 import android.app.IBackupAgent;
 import android.app.backup.BackupAgent;
 import android.app.backup.BackupAnnotations;
-import android.app.backup.BackupManager;
 import android.app.backup.FullBackup;
 import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.IFullBackupRestoreObserver;
@@ -51,6 +50,7 @@
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.FileMetadata;
+import com.android.server.backup.Flags;
 import com.android.server.backup.KeyValueAdbRestoreEngine;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.OperationStorage.OpType;
@@ -157,13 +157,19 @@
         mMonitor = monitor;
         mOnlyPackage = onlyPackage;
         mAllowApks = allowApks;
-        mBuffer = new byte[32 * 1024];
         mAgentTimeoutParameters = Objects.requireNonNull(
                 backupManagerService.getAgentTimeoutParameters(),
                 "Timeout parameters cannot be null");
         mIsAdbRestore = isAdbRestore;
         mUserId = backupManagerService.getUserId();
         mBackupEligibilityRules = backupEligibilityRules;
+
+        if (Flags.enableMaxSizeWritesToPipes()) {
+            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+            mBuffer = new byte[64 * 1024]; // 64KB
+        } else {
+            mBuffer = new byte[32 * 1024];
+        }
     }
 
     @VisibleForTesting
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 316a16d..2fbc3cd 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -968,7 +968,12 @@
             throws Exception {
         Set<String> excludedKeysForPackage = getExcludedKeysForPackage(packageName);
 
-        byte[] buffer = new byte[8192]; // will grow when needed
+        int bufferSize = 8192; // 8KB
+        if (Flags.enableMaxSizeWritesToPipes()) {
+            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+            bufferSize = 64 * 1024; // 64KB
+        }
+        byte[] buffer = new byte[bufferSize]; // will grow when needed
         while (in.readNextHeader()) {
             final String key = in.getKey();
             final int size = in.getDataSize();
@@ -1116,7 +1121,11 @@
             ParcelFileDescriptor tReadEnd = mTransportPipes[0];
             ParcelFileDescriptor tWriteEnd = mTransportPipes[1];
 
-            int bufferSize = 32 * 1024;
+            int bufferSize = 32 * 1024; // 32KB
+            if (Flags.enableMaxSizeWritesToPipes()) {
+                // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+                bufferSize = 64 * 1024; // 64KB
+            }
             byte[] buffer = new byte[bufferSize];
             FileOutputStream engineOut = new FileOutputStream(eWriteEnd.getFileDescriptor());
             FileInputStream transportIn = new FileInputStream(tReadEnd.getFileDescriptor());
diff --git a/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java b/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
index 1c0cd87..843354e 100644
--- a/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
@@ -21,7 +21,7 @@
 import android.os.ParcelFileDescriptor;
 import android.util.Slog;
 
-import com.android.server.backup.BackupAndRestoreFeatureFlags;
+import com.android.server.backup.Flags;
 
 import java.io.DataInputStream;
 import java.io.EOFException;
@@ -46,8 +46,11 @@
         // We do not take close() responsibility for the pipe FD
         FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor());
         DataInputStream in = new DataInputStream(raw);
-        final int chunkSizeInBytes =
-                BackupAndRestoreFeatureFlags.getFullBackupUtilsRouteBufferSizeBytes();
+        int chunkSizeInBytes = 32 * 1024; // 32KB
+        if (Flags.enableMaxSizeWritesToPipes()) {
+            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+            chunkSizeInBytes = 64 * 1024; // 64KB
+        }
         byte[] buffer = new byte[chunkSizeInBytes];
         int chunkTotal;
         while ((chunkTotal = in.readInt()) > 0) {
diff --git a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
index 0accb9f..5a8533a 100644
--- a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
@@ -40,6 +40,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.LocalServices;
 import com.android.server.backup.FileMetadata;
+import com.android.server.backup.Flags;
 import com.android.server.backup.restore.RestoreDeleteObserver;
 import com.android.server.backup.restore.RestorePolicy;
 
@@ -93,7 +94,12 @@
                 try (Session session = installer.openSession(sessionId)) {
                     try (OutputStream apkStream = session.openWrite(info.packageName, 0,
                             info.size)) {
-                        byte[] buffer = new byte[32 * 1024];
+                        int bufferSize = 32 * 1024; // 32KB
+                        if (Flags.enableMaxSizeWritesToPipes()) {
+                            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+                            bufferSize = 64 * 1024; // 64KB
+                        }
+                        byte[] buffer = new byte[bufferSize];
                         long size = info.size;
                         while (size > 0) {
                             long toRead = (buffer.length < size) ? buffer.length : size;
diff --git a/services/backup/lint-baseline.xml b/services/backup/lint-baseline.xml
index 93c9390..46de2cdd 100644
--- a/services/backup/lint-baseline.xml
+++ b/services/backup/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="7.1.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NonUserGetterCalled"
@@ -36,4 +36,4 @@
             line="207"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index a6ed846..4b3772a 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -284,7 +284,7 @@
         final AssociationInfo association = new AssociationInfo(id, userId, packageName,
                 /* tag */ null, macAddress, displayName, deviceProfile, associatedDevice,
                 selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false,
-                timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0);
+                /* pending */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0);
 
         // Add role holder for association (if specified) and add new association to store.
         maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver);
diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
new file mode 100644
index 0000000..e4cc1f8
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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;
+
+import static android.os.UserHandle.getCallingUserId;
+
+import static com.android.server.companion.CompanionDeviceManagerService.PerUserAssociationSet;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.UserIdInt;
+import android.companion.AssociationInfo;
+import android.companion.Flags;
+import android.companion.datatransfer.SystemDataTransferRequest;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.CollectionUtils;
+import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+
+@SuppressLint("LongLogTag")
+class BackupRestoreProcessor {
+    static final String TAG = "CDM_BackupRestoreProcessor";
+    private static final int BACKUP_AND_RESTORE_VERSION = 0;
+
+    @NonNull
+    private final CompanionDeviceManagerService mService;
+    @NonNull
+    private final PackageManagerInternal mPackageManager;
+    @NonNull
+    private final AssociationStoreImpl mAssociationStore;
+    @NonNull
+    private final PersistentDataStore mPersistentStore;
+    @NonNull
+    private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
+    @NonNull
+    private final AssociationRequestsProcessor mAssociationRequestsProcessor;
+
+    /**
+     * A structure that consists of a set of restored associations that are pending corresponding
+     * companion app to be installed.
+     */
+    @GuardedBy("mAssociationsPendingAppInstall")
+    private final PerUserAssociationSet mAssociationsPendingAppInstall =
+            new PerUserAssociationSet();
+
+    BackupRestoreProcessor(@NonNull CompanionDeviceManagerService service,
+                           @NonNull AssociationStoreImpl associationStore,
+                           @NonNull PersistentDataStore persistentStore,
+                           @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore,
+                           @NonNull AssociationRequestsProcessor associationRequestsProcessor) {
+        mService = service;
+        mPackageManager = service.mPackageManagerInternal;
+        mAssociationStore = associationStore;
+        mPersistentStore = persistentStore;
+        mSystemDataTransferRequestStore = systemDataTransferRequestStore;
+        mAssociationRequestsProcessor = associationRequestsProcessor;
+    }
+
+    /**
+     * Generate CDM state payload to be backed up.
+     * Backup payload is formatted as following:
+     * | (4) payload version | (4) AssociationInfo length | AssociationInfo XML
+     * | (4) SystemDataTransferRequest length | SystemDataTransferRequest XML (without userId)|
+     */
+    byte[] getBackupPayload(int userId) {
+        // Persist state first to generate an up-to-date XML file
+        mService.persistStateForUser(userId);
+        byte[] associationsPayload = mPersistentStore.getBackupPayload(userId);
+        int associationsPayloadLength = associationsPayload.length;
+
+        // System data transfer requests are persisted up-to-date already
+        byte[] requestsPayload = mSystemDataTransferRequestStore.getBackupPayload(userId);
+        int requestsPayloadLength = requestsPayload.length;
+
+        int payloadSize = /* 3 integers */ 12
+                + associationsPayloadLength
+                + requestsPayloadLength;
+
+        return ByteBuffer.allocate(payloadSize)
+                .putInt(BACKUP_AND_RESTORE_VERSION)
+                .putInt(associationsPayloadLength)
+                .put(associationsPayload)
+                .putInt(requestsPayloadLength)
+                .put(requestsPayload)
+                .array();
+    }
+
+    /**
+     * Create new associations and system data transfer request consents using backed up payload.
+     */
+    void applyRestoredPayload(byte[] payload, int userId) {
+        ByteBuffer buffer = ByteBuffer.wrap(payload);
+
+        // Make sure that payload version matches current version to ensure proper deserialization
+        int version = buffer.getInt();
+        if (version != BACKUP_AND_RESTORE_VERSION) {
+            Slog.e(TAG, "Unsupported backup payload version");
+            return;
+        }
+
+        // Read the bytes containing backed-up associations
+        byte[] associationsPayload = new byte[buffer.getInt()];
+        buffer.get(associationsPayload);
+        final Set<AssociationInfo> restoredAssociations = new HashSet<>();
+        mPersistentStore.readStateFromPayload(associationsPayload, userId,
+                restoredAssociations, new HashMap<>());
+
+        // Read the bytes containing backed-up system data transfer requests user consent
+        byte[] requestsPayload = new byte[buffer.getInt()];
+        buffer.get(requestsPayload);
+        List<SystemDataTransferRequest> restoredRequestsForUser =
+                mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload, userId);
+
+        // Get a list of installed packages ahead of time.
+        List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications(
+                0, userId, getCallingUserId());
+
+        // Restored device may have a different user ID than the backed-up user's user-ID. Since
+        // association ID is dependent on the user ID, restored associations must account for
+        // this potential difference on their association IDs.
+        for (AssociationInfo restored : restoredAssociations) {
+            // Don't restore a revoked association. Since they weren't added to the device being
+            // restored in the first place, there is no need to worry about revoking a role that
+            // was never granted either.
+            if (restored.isRevoked()) {
+                continue;
+            }
+
+            // Filter restored requests for those that belong to the restored association.
+            List<SystemDataTransferRequest> restoredRequests = CollectionUtils.filter(
+                    restoredRequestsForUser, it -> it.getAssociationId() == restored.getId());
+
+            // Handle collision: If a local association belonging to the same package already exists
+            // and their tags match, then keep the local one in favor of creating a new association.
+            if (handleCollision(userId, restored, restoredRequests)) {
+                continue;
+            }
+
+            // Create a new association reassigned to this user and a valid association ID
+            final String packageName = restored.getPackageName();
+            final int newId = mService.getNewAssociationIdForPackage(userId, packageName);
+            AssociationInfo newAssociation =
+                    new AssociationInfo.Builder(newId, userId, packageName, restored)
+                            .build();
+
+            // Check if the companion app for this association is already installed, then do one
+            // of the following:
+            // (1) If the app is already installed, then go ahead and add this association and grant
+            // the role attached to this association to the app.
+            // (2) If the app isn't yet installed, then add this association to the list of pending
+            // associations to be added when the package is installed in the future.
+            boolean isPackageInstalled = installedApps.stream()
+                    .anyMatch(app -> packageName.equals(app.packageName));
+            if (isPackageInstalled) {
+                mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation,
+                        null, null);
+            } else {
+                addToPendingAppInstall(newAssociation);
+            }
+
+            // Re-map restored system data transfer requests to newly created associations
+            for (SystemDataTransferRequest restoredRequest : restoredRequests) {
+                SystemDataTransferRequest newRequest = restoredRequest.copyWithNewId(newId);
+                newRequest.setUserId(userId);
+                mSystemDataTransferRequestStore.writeRequest(userId, newRequest);
+            }
+        }
+
+        // Persist restored state.
+        mService.persistStateForUser(userId);
+    }
+
+    void addToPendingAppInstall(@NonNull AssociationInfo association) {
+        association = (new AssociationInfo.Builder(association))
+                .setPending(true)
+                .build();
+
+        synchronized (mAssociationsPendingAppInstall) {
+            mAssociationsPendingAppInstall.forUser(association.getUserId()).add(association);
+        }
+    }
+
+    void removeFromPendingAppInstall(@NonNull AssociationInfo association) {
+        synchronized (mAssociationsPendingAppInstall) {
+            mAssociationsPendingAppInstall.forUser(association.getUserId()).remove(association);
+        }
+    }
+
+    @NonNull
+    Set<AssociationInfo> getAssociationsPendingAppInstallForUser(@UserIdInt int userId) {
+        synchronized (mAssociationsPendingAppInstall) {
+            // Return a copy.
+            return new ArraySet<>(mAssociationsPendingAppInstall.forUser(userId));
+        }
+    }
+
+    /**
+     * Detects and handles collision between restored association and local association. Returns
+     * true if there has been a collision and false otherwise.
+     */
+    private boolean handleCollision(@UserIdInt int userId,
+            AssociationInfo restored,
+            List<SystemDataTransferRequest> restoredRequests) {
+        List<AssociationInfo> localAssociations = mAssociationStore.getAssociationsForPackage(
+                restored.getUserId(), restored.getPackageName());
+        Predicate<AssociationInfo> isSameDevice = associationInfo -> {
+            boolean matchesMacAddress = Objects.equals(
+                    associationInfo.getDeviceMacAddress(),
+                    restored.getDeviceMacAddress());
+            boolean matchesTag = !Flags.associationTag()
+                    || Objects.equals(associationInfo.getTag(), restored.getTag());
+            return matchesMacAddress && matchesTag;
+        };
+        AssociationInfo local = CollectionUtils.find(localAssociations, isSameDevice);
+
+        // No collision detected
+        if (local == null) {
+            return false;
+        }
+
+        Log.d(TAG, "Conflict detected with association id=" + local.getId()
+                + " while restoring CDM backup. Keeping local association.");
+
+        List<SystemDataTransferRequest> localRequests = mSystemDataTransferRequestStore
+                .readRequestsByAssociationId(userId, local.getId());
+
+        // If local association doesn't have any existing system data transfer request of same type
+        // attached, then restore corresponding request onto the local association. Otherwise, keep
+        // the locally stored request.
+        for (SystemDataTransferRequest restoredRequest : restoredRequests) {
+            boolean requestTypeExists = CollectionUtils.any(localRequests, request ->
+                    request.getDataType() == restoredRequest.getDataType());
+
+            // This type of request consent already exists for the association.
+            if (requestTypeExists) {
+                continue;
+            }
+
+            Log.d(TAG, "Restoring " + restoredRequest.getClass().getSimpleName()
+                    + " to an existing association id=" + local.getId() + ".");
+
+            SystemDataTransferRequest newRequest =
+                    restoredRequest.copyWithNewId(local.getId());
+            newRequest.setUserId(userId);
+            mSystemDataTransferRequestStore.writeRequest(userId, newRequest);
+        }
+
+        return true;
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 487b66c..056ec89 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -164,6 +164,7 @@
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
     private AssociationRequestsProcessor mAssociationRequestsProcessor;
     private SystemDataTransferProcessor mSystemDataTransferProcessor;
+    private BackupRestoreProcessor mBackupRestoreProcessor;
     private CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private CompanionApplicationController mCompanionAppController;
     private CompanionTransportManager mTransportManager;
@@ -256,6 +257,9 @@
         mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
                 mPackageManagerInternal, mAssociationStore,
                 mSystemDataTransferRequestStore, mTransportManager);
+        mBackupRestoreProcessor = new BackupRestoreProcessor(
+                /* cdmService */ this, mAssociationStore, mPersistentStore,
+                mSystemDataTransferRequestStore, mAssociationRequestsProcessor);
         // TODO(b/279663946): move context sync to a dedicated system service
         mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager);
 
@@ -283,7 +287,9 @@
         final Set<Integer> usersToPersistStateFor = new ArraySet<>();
 
         for (AssociationInfo association : allAssociations) {
-            if (!association.isRevoked()) {
+            if (association.isPending()) {
+                mBackupRestoreProcessor.addToPendingAppInstall(association);
+            } else if (!association.isRevoked()) {
                 activeAssociations.add(association);
             } else if (maybeRemoveRoleHolderForAssociation(association)) {
                 // Nothing more to do here, but we'll need to persist all the associations to the
@@ -501,7 +507,7 @@
         updateAtm(userId, updatedAssociations);
     }
 
-    private void persistStateForUser(@UserIdInt int userId) {
+    void persistStateForUser(@UserIdInt int userId) {
         // We want to store both active associations and the revoked (removed) association that we
         // are keeping around for the final clean-up (delayed role holder removal).
         final List<AssociationInfo> allAssociations;
@@ -510,6 +516,9 @@
                 mAssociationStore.getAssociationsForUser(userId));
         // ... and add the revoked (removed) association, that are yet to be permanently removed.
         allAssociations.addAll(getPendingRoleHolderRemovalAssociationsForUser(userId));
+        // ... and add the restored associations that are pending missing package installation.
+        allAssociations.addAll(mBackupRestoreProcessor
+                .getAssociationsPendingAppInstallForUser(userId));
 
         final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId);
 
@@ -577,6 +586,23 @@
         mCompanionAppController.onPackagesChanged(userId);
     }
 
+    private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) {
+        if (DEBUG) Log.i(TAG, "onPackageAddedInternal() u" + userId + "/" + packageName);
+
+        Set<AssociationInfo> associationsPendingAppInstall = mBackupRestoreProcessor
+                .getAssociationsPendingAppInstallForUser(userId);
+        for (AssociationInfo association : associationsPendingAppInstall) {
+            if (!packageName.equals(association.getPackageName())) continue;
+
+            AssociationInfo newAssociation = new AssociationInfo.Builder(association)
+                    .setPending(false)
+                    .build();
+            mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation,
+                    null, null);
+            mBackupRestoreProcessor.removeFromPendingAppInstall(association);
+        }
+    }
+
     // Revoke associations if the selfManaged companion device does not connect for 3 months.
     void removeInactiveSelfManagedAssociations() {
         final long currentTime = System.currentTimeMillis();
@@ -1052,13 +1078,14 @@
 
         @Override
         public byte[] getBackupPayload(int userId) {
-            // TODO(b/286124853): back up CDM data
-            return new byte[0];
+            Log.i(TAG, "getBackupPayload() userId=" + userId);
+            return mBackupRestoreProcessor.getBackupPayload(userId);
         }
 
         @Override
         public void applyRestoredPayload(byte[] payload, int userId) {
-            // TODO(b/286124853): restore CDM data
+            Log.i(TAG, "applyRestoredPayload() userId=" + userId);
+            mBackupRestoreProcessor.applyRestoredPayload(payload, userId);
         }
 
         @Override
@@ -1067,7 +1094,8 @@
                 @NonNull String[] args) {
             return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this,
                     mAssociationStore, mDevicePresenceMonitor, mTransportManager,
-                    mSystemDataTransferProcessor, mAssociationRequestsProcessor)
+                    mSystemDataTransferProcessor, mAssociationRequestsProcessor,
+                    mBackupRestoreProcessor)
                     .exec(this, in.getFileDescriptor(), out.getFileDescriptor(),
                             err.getFileDescriptor(), args);
         }
@@ -1141,6 +1169,15 @@
                 usedIds.put(it.getId(), true);
             }
 
+            // Some IDs may be reserved by associations that aren't stored yet due to missing
+            // package after a backup restoration. We don't want the ID to have been taken by
+            // another association by the time when it is activated from the package installation.
+            final Set<AssociationInfo> pendingAssociations = mBackupRestoreProcessor
+                    .getAssociationsPendingAppInstallForUser(userId);
+            for (AssociationInfo it: pendingAssociations) {
+                usedIds.put(it.getId(), true);
+            }
+
             // Second: collect all IDs that have been previously used for this package (and user).
             final Set<Integer> previouslyUsedIds =
                     getPreviouslyUsedIdsForPackageLocked(userId, packageName);
@@ -1499,6 +1536,11 @@
         public void onPackageModified(String packageName) {
             onPackageModifiedInternal(getChangingUserId(), packageName);
         }
+
+        @Override
+        public void onPackageAdded(String packageName, int uid) {
+            onPackageAddedInternal(getChangingUserId(), packageName);
+        }
     };
 
     static int getFirstAssociationIdForUser(@UserIdInt int userId) {
@@ -1702,7 +1744,7 @@
         }
     }
 
-    private static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> {
+    static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> {
         @Override
         protected @NonNull Set<AssociationInfo> create(int userId) {
             return new ArraySet<>();
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 9fdf5c2..53c0184c 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -18,6 +18,8 @@
 
 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC;
 
+import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks;
+
 import android.companion.AssociationInfo;
 import android.companion.ContextSyncMessage;
 import android.companion.Flags;
@@ -26,6 +28,7 @@
 import android.net.MacAddress;
 import android.os.Binder;
 import android.os.ShellCommand;
+import android.util.Base64;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.server.companion.datatransfer.SystemDataTransferProcessor;
@@ -47,19 +50,22 @@
 
     private final SystemDataTransferProcessor mSystemDataTransferProcessor;
     private final AssociationRequestsProcessor mAssociationRequestsProcessor;
+    private final BackupRestoreProcessor mBackupRestoreProcessor;
 
     CompanionDeviceShellCommand(CompanionDeviceManagerService service,
             AssociationStoreImpl associationStore,
             CompanionDevicePresenceMonitor devicePresenceMonitor,
             CompanionTransportManager transportManager,
             SystemDataTransferProcessor systemDataTransferProcessor,
-            AssociationRequestsProcessor associationRequestsProcessor) {
+            AssociationRequestsProcessor associationRequestsProcessor,
+            BackupRestoreProcessor backupRestoreProcessor) {
         mService = service;
         mAssociationStore = associationStore;
         mDevicePresenceMonitor = devicePresenceMonitor;
         mTransportManager = transportManager;
         mSystemDataTransferProcessor = systemDataTransferProcessor;
         mAssociationRequestsProcessor = associationRequestsProcessor;
+        mBackupRestoreProcessor = backupRestoreProcessor;
     }
 
     @Override
@@ -111,6 +117,19 @@
                 }
                 break;
 
+                case "disassociate-all": {
+                    final int userId = getNextIntArgRequired();
+                    final String packageName = getNextArgRequired();
+                    final List<AssociationInfo> userAssociations =
+                            mAssociationStore.getAssociationsForPackage(userId, packageName);
+                    for (AssociationInfo association : userAssociations) {
+                        if (sanitizeWithCallerChecks(mService.getContext(), association) != null) {
+                            mService.disassociateInternal(association.getId());
+                        }
+                    }
+                }
+                break;
+
                 case "clear-association-memory-cache":
                     mService.persistState();
                     mService.loadAssociationsFromDisk();
@@ -126,6 +145,20 @@
                     mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1);
                     break;
 
+                case "get-backup-payload": {
+                    final int userId = getNextIntArgRequired();
+                    byte[] payload = mBackupRestoreProcessor.getBackupPayload(userId);
+                    out.println(Base64.encodeToString(payload, Base64.NO_WRAP));
+                }
+                break;
+
+                case "apply-restored-payload": {
+                    final int userId = getNextIntArgRequired();
+                    byte[] payload = Base64.decode(getNextArgRequired(), Base64.NO_WRAP);
+                    mBackupRestoreProcessor.applyRestoredPayload(payload, userId);
+                }
+                break;
+
                 case "remove-inactive-associations": {
                     // This command should trigger the same "clean-up" job as performed by the
                     // InactiveAssociationsRemovalService JobService. However, since the
@@ -355,6 +388,8 @@
         pw.println("      Create a new Association.");
         pw.println("  disassociate USER_ID PACKAGE MAC_ADDRESS");
         pw.println("      Remove an existing Association.");
+        pw.println("  disassociate-all USER_ID");
+        pw.println("      Remove all Associations for a user.");
         pw.println("  clear-association-memory-cache");
         pw.println("      Clear the in-memory association cache and reload all association ");
         pw.println("      information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY.");
@@ -378,6 +413,14 @@
         pw.println("      invoked for the same device (same ASSOCIATION_ID) no longer than");
         pw.println("      60 seconds ago.");
 
+        pw.println("  get-backup-payload USER_ID");
+        pw.println("      Generate backup payload for the given user and print its content");
+        pw.println("      encoded to a Base64 string.");
+        pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+        pw.println("  apply-restored-payload USER_ID PAYLOAD");
+        pw.println("      Apply restored backup payload for the given user.");
+        pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
         if (Flags.devicePresence()) {
             pw.println("  simulate-device-event ASSOCIATION_ID EVENT");
             pw.println("  Simulate the companion device event changes:");
diff --git a/services/companion/java/com/android/server/companion/DataStoreUtils.java b/services/companion/java/com/android/server/companion/DataStoreUtils.java
index c182529..04ce1f6 100644
--- a/services/companion/java/com/android/server/companion/DataStoreUtils.java
+++ b/services/companion/java/com/android/server/companion/DataStoreUtils.java
@@ -30,8 +30,11 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 
 /**
  * Util class for CDM data stores
@@ -88,6 +91,29 @@
         }
     }
 
+    /**
+     * Read a file and return the byte array containing the bytes of the file.
+     */
+    @NonNull
+    public static byte[] fileToByteArray(@NonNull AtomicFile file) {
+        if (!file.getBaseFile().exists()) {
+            Slog.d(TAG, "File does not exist");
+            return new byte[0];
+        }
+        try (FileInputStream in = file.openRead()) {
+            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+            byte[] buffer = new byte[1024];
+            int read;
+            while ((read = in.read(buffer)) != -1) {
+                bytes.write(buffer, 0, read);
+            }
+            return bytes.toByteArray();
+        } catch (IOException e) {
+            Slog.e(TAG, "Error while reading requests file", e);
+        }
+        return new byte[0];
+    }
+
     private DataStoreUtils() {
     }
 }
diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java
index b4b9379..1ebe65c 100644
--- a/services/companion/java/com/android/server/companion/PersistentDataStore.java
+++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java
@@ -28,6 +28,7 @@
 import static com.android.server.companion.CompanionDeviceManagerService.getFirstAssociationIdForUser;
 import static com.android.server.companion.CompanionDeviceManagerService.getLastAssociationIdForUser;
 import static com.android.server.companion.DataStoreUtils.createStorageFileForUser;
+import static com.android.server.companion.DataStoreUtils.fileToByteArray;
 import static com.android.server.companion.DataStoreUtils.isEndOfTag;
 import static com.android.server.companion.DataStoreUtils.isStartOfTag;
 import static com.android.server.companion.DataStoreUtils.writeToFileSafely;
@@ -55,9 +56,11 @@
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -186,6 +189,7 @@
     private static final String XML_ATTR_SELF_MANAGED = "self_managed";
     private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby";
     private static final String XML_ATTR_REVOKED = "revoked";
+    private static final String XML_ATTR_PENDING = "pending";
     private static final String XML_ATTR_TIME_APPROVED = "time_approved";
     private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected";
     private static final String XML_ATTR_SYSTEM_DATA_SYNC_FLAGS = "system_data_sync_flags";
@@ -328,34 +332,42 @@
             @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut,
             @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) {
         try (FileInputStream in = file.openRead()) {
-            final TypedXmlPullParser parser = Xml.resolvePullParser(in);
-
-            XmlUtils.beginDocument(parser, rootTag);
-            final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0);
-            switch (version) {
-                case 0:
-                    readAssociationsV0(parser, userId, associationsOut);
-                    break;
-                case 1:
-                    while (true) {
-                        parser.nextTag();
-                        if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) {
-                            readAssociationsV1(parser, userId, associationsOut);
-                        } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) {
-                            readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut);
-                        } else if (isEndOfTag(parser, rootTag)) {
-                            break;
-                        }
-                    }
-                    break;
-            }
-            return version;
+            return readStateFromInputStream(userId, in, rootTag, associationsOut,
+                    previouslyUsedIdsPerPackageOut);
         } catch (XmlPullParserException | IOException e) {
             Slog.e(TAG, "Error while reading associations file", e);
             return -1;
         }
     }
 
+    private int readStateFromInputStream(@UserIdInt int userId, @NonNull InputStream in,
+            @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut,
+            @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut)
+            throws XmlPullParserException, IOException {
+        final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+
+        XmlUtils.beginDocument(parser, rootTag);
+        final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0);
+        switch (version) {
+            case 0:
+                readAssociationsV0(parser, userId, associationsOut);
+                break;
+            case 1:
+                while (true) {
+                    parser.nextTag();
+                    if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) {
+                        readAssociationsV1(parser, userId, associationsOut);
+                    } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) {
+                        readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut);
+                    } else if (isEndOfTag(parser, rootTag)) {
+                        break;
+                    }
+                }
+                break;
+        }
+        return version;
+    }
+
     private void persistStateToFileLocked(@NonNull AtomicFile file,
             @Nullable Collection<AssociationInfo> associations,
             @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) {
@@ -391,6 +403,26 @@
                 u -> createStorageFileForUser(userId, FILE_NAME));
     }
 
+    byte[] getBackupPayload(@UserIdInt int userId) {
+        Slog.i(TAG, "Fetching stored state data for user " + userId + " from disk");
+        final AtomicFile file = getStorageFileForUser(userId);
+
+        synchronized (file) {
+            return fileToByteArray(file);
+        }
+    }
+
+    void readStateFromPayload(byte[] payload, @UserIdInt int userId,
+                              @NonNull Set<AssociationInfo> associationsOut,
+                              @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) {
+        try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) {
+            readStateFromInputStream(userId, in, XML_TAG_STATE, associationsOut,
+                    previouslyUsedIdsPerPackageOut);
+        } catch (XmlPullParserException | IOException e) {
+            Slog.e(TAG, "Error while reading associations file", e);
+        }
+    }
+
     private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) {
         return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY);
     }
@@ -433,8 +465,8 @@
 
         out.add(new AssociationInfo(associationId, userId, appPackage, tag,
                 MacAddress.fromString(deviceAddress), null, profile, null,
-                /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved,
-                Long.MAX_VALUE, /* systemDataSyncFlags */ 0));
+                /* managedByCompanionApp */ false, notify, /* revoked */ false, /* pending */ false,
+                timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0));
     }
 
     private static void readAssociationsV1(@NonNull TypedXmlPullParser parser,
@@ -465,6 +497,7 @@
         final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED);
         final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY);
         final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false);
+        final boolean pending = readBooleanAttribute(parser, XML_ATTR_PENDING, false);
         final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L);
         final long lastTimeConnected = readLongAttribute(
                 parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE);
@@ -473,7 +506,7 @@
 
         final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId,
                 appPackage, tag, macAddress, displayName, profile, selfManaged, notify, revoked,
-                timeApproved, lastTimeConnected, systemDataSyncFlags);
+                pending, timeApproved, lastTimeConnected, systemDataSyncFlags);
         if (associationInfo != null) {
             out.add(associationInfo);
         }
@@ -527,8 +560,8 @@
         writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged());
         writeBooleanAttribute(
                 serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby());
-        writeBooleanAttribute(
-                serializer, XML_ATTR_REVOKED, a.isRevoked());
+        writeBooleanAttribute(serializer, XML_ATTR_REVOKED, a.isRevoked());
+        writeBooleanAttribute(serializer, XML_ATTR_PENDING, a.isPending());
         writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs());
         writeLongAttribute(
                 serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs());
@@ -572,14 +605,14 @@
             @UserIdInt int userId, @NonNull String appPackage, @Nullable String tag,
             @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
             @Nullable String profile, boolean selfManaged, boolean notify, boolean revoked,
-            long timeApproved, long lastTimeConnected, int systemDataSyncFlags) {
+            boolean pending, long timeApproved, long lastTimeConnected, int systemDataSyncFlags) {
         AssociationInfo associationInfo = null;
         try {
             // We do not persist AssociatedDevice, which means that AssociationInfo retrieved from
             // datastore is not guaranteed to be identical to the one from initial association.
             associationInfo = new AssociationInfo(associationId, userId, appPackage, tag,
                     macAddress, displayName, profile, null, selfManaged, notify,
-                    revoked, timeApproved, lastTimeConnected, systemDataSyncFlags);
+                    revoked, pending, timeApproved, lastTimeConnected, systemDataSyncFlags);
         } catch (Exception e) {
             if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e);
         }
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
index 9f489e8..51c5fd6 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
@@ -23,6 +23,7 @@
 import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
 import static com.android.internal.util.XmlUtils.writeIntAttribute;
 import static com.android.server.companion.DataStoreUtils.createStorageFileForUser;
+import static com.android.server.companion.DataStoreUtils.fileToByteArray;
 import static com.android.server.companion.DataStoreUtils.isEndOfTag;
 import static com.android.server.companion.DataStoreUtils.isStartOfTag;
 import static com.android.server.companion.DataStoreUtils.writeToFileSafely;
@@ -44,6 +45,7 @@
 
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.ByteArrayInputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -67,7 +69,6 @@
  *   <request
  *     association_id="1"
  *     data_type="1"
- *     user_id="12"
  *     is_user_consented="true"
  *   </request>
  * </requests>
@@ -84,7 +85,6 @@
 
     private static final String XML_ATTR_ASSOCIATION_ID = "association_id";
     private static final String XML_ATTR_DATA_TYPE = "data_type";
-    private static final String XML_ATTR_USER_ID = "user_id";
     private static final String XML_ATTR_IS_USER_CONSENTED = "is_user_consented";
 
     private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds
@@ -152,6 +152,33 @@
         mExecutor.execute(() -> writeRequestsToStore(userId, cachedRequests));
     }
 
+    /**
+     * Return the byte contents of the XML file storing current system data transfer requests.
+     */
+    public byte[] getBackupPayload(@UserIdInt int userId) {
+        final AtomicFile file = getStorageFileForUser(userId);
+
+        synchronized (file) {
+            return fileToByteArray(file);
+        }
+    }
+
+    /**
+     * Parse the byte array containing XML information of system data transfer requests into
+     * an array list of requests.
+     */
+    public List<SystemDataTransferRequest> readRequestsFromPayload(byte[] payload, int userId) {
+        try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) {
+            final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+            XmlUtils.beginDocument(parser, XML_TAG_REQUESTS);
+
+            return readRequestsFromXml(parser, userId);
+        } catch (XmlPullParserException | IOException e) {
+            Slog.e(LOG_TAG, "Error while reading requests file", e);
+            return new ArrayList<>();
+        }
+    }
+
     @GuardedBy("mLock")
     private ArrayList<SystemDataTransferRequest> readRequestsFromCache(@UserIdInt int userId) {
         ArrayList<SystemDataTransferRequest> cachedRequests = mCachedPerUser.get(userId);
@@ -197,7 +224,7 @@
                 final TypedXmlPullParser parser = Xml.resolvePullParser(in);
                 XmlUtils.beginDocument(parser, XML_TAG_REQUESTS);
 
-                return readRequestsFromXml(parser);
+                return readRequestsFromXml(parser, userId);
             } catch (XmlPullParserException | IOException e) {
                 Slog.e(LOG_TAG, "Error while reading requests file", e);
                 return new ArrayList<>();
@@ -207,7 +234,8 @@
 
     @NonNull
     private ArrayList<SystemDataTransferRequest> readRequestsFromXml(
-            @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException {
+            @NonNull TypedXmlPullParser parser, int userId)
+            throws XmlPullParserException, IOException {
         if (!isStartOfTag(parser, XML_TAG_REQUESTS)) {
             throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_REQUESTS);
         }
@@ -220,14 +248,15 @@
                 break;
             }
             if (isStartOfTag(parser, XML_TAG_REQUEST)) {
-                requests.add(readRequestFromXml(parser));
+                requests.add(readRequestFromXml(parser, userId));
             }
         }
 
         return requests;
     }
 
-    private SystemDataTransferRequest readRequestFromXml(@NonNull TypedXmlPullParser parser)
+    private SystemDataTransferRequest readRequestFromXml(@NonNull TypedXmlPullParser parser,
+            int userId)
             throws XmlPullParserException, IOException {
         if (!isStartOfTag(parser, XML_TAG_REQUEST)) {
             throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_REQUEST);
@@ -235,7 +264,6 @@
 
         final int associationId = readIntAttribute(parser, XML_ATTR_ASSOCIATION_ID);
         final int dataType = readIntAttribute(parser, XML_ATTR_DATA_TYPE);
-        final int userId = readIntAttribute(parser, XML_ATTR_USER_ID);
         final boolean isUserConsented = readBooleanAttribute(parser, XML_ATTR_IS_USER_CONSENTED);
 
         switch (dataType) {
@@ -292,7 +320,6 @@
 
         writeIntAttribute(serializer, XML_ATTR_ASSOCIATION_ID, request.getAssociationId());
         writeIntAttribute(serializer, XML_ATTR_DATA_TYPE, request.getDataType());
-        writeIntAttribute(serializer, XML_ATTR_USER_ID, request.getUserId());
         writeBooleanAttribute(serializer, XML_ATTR_IS_USER_CONSENTED, request.isUserConsented());
 
         serializer.endTag(null, XML_TAG_REQUEST);
diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
index 720687e..0e66fbc 100644
--- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
+++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
@@ -23,12 +23,12 @@
 import android.os.Build;
 import android.util.Slog;
 
-import com.google.security.cryptauth.lib.securegcm.BadHandleException;
-import com.google.security.cryptauth.lib.securegcm.CryptoException;
-import com.google.security.cryptauth.lib.securegcm.D2DConnectionContextV1;
-import com.google.security.cryptauth.lib.securegcm.D2DHandshakeContext;
-import com.google.security.cryptauth.lib.securegcm.D2DHandshakeContext.Role;
-import com.google.security.cryptauth.lib.securegcm.HandshakeException;
+import com.google.security.cryptauth.lib.securegcm.ukey2.BadHandleException;
+import com.google.security.cryptauth.lib.securegcm.ukey2.CryptoException;
+import com.google.security.cryptauth.lib.securegcm.ukey2.D2DConnectionContextV1;
+import com.google.security.cryptauth.lib.securegcm.ukey2.D2DHandshakeContext;
+import com.google.security.cryptauth.lib.securegcm.ukey2.D2DHandshakeContext.Role;
+import com.google.security.cryptauth.lib.securegcm.ukey2.HandshakeException;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
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 62c6703..3e45626 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -125,7 +125,7 @@
      * Send a message to remote devices through the transports
      */
     public void sendMessage(int message, byte[] data, int[] associationIds) {
-        Slog.i(TAG, "Sending message 0x" + Integer.toHexString(message)
+        Slog.d(TAG, "Sending message 0x" + Integer.toHexString(message)
                 + " data length " + data.length);
         synchronized (mTransports) {
             for (int i = 0; i < associationIds.length; i++) {
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 22b18ac..8a5774e 100644
--- a/services/companion/java/com/android/server/companion/transport/Transport.java
+++ b/services/companion/java/com/android/server/companion/transport/Transport.java
@@ -284,7 +284,7 @@
         if (mListeners.containsKey(message)) {
             try {
                 mListeners.get(message).onMessageReceived(getAssociationId(), data);
-                Slog.i(TAG, "Message 0x" + Integer.toHexString(message)
+                Slog.d(TAG, "Message 0x" + Integer.toHexString(message)
                         + " is received from associationId " + mAssociationId
                         + ", sending data length " + data.length + " to the listener.");
             } catch (RemoteException ignored) {
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 1f89e57b..d1274d4 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -245,8 +245,8 @@
         mInputManagerInternal.setPointerIconVisible(visible, displayId);
     }
 
-    void setPointerAcceleration(float pointerAcceleration, int displayId) {
-        mInputManagerInternal.setPointerAcceleration(pointerAcceleration, displayId);
+    void setMousePointerAccelerationEnabled(boolean enabled, int displayId) {
+        mInputManagerInternal.setMousePointerAccelerationEnabled(enabled, displayId);
     }
 
     void setDisplayEligibilityForPointerCapture(boolean isEligible, int displayId) {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 58aa2c3..44c3a8d 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -1110,7 +1110,7 @@
         final long token = Binder.clearCallingIdentity();
         try {
             mInputController.setShowPointerIcon(showPointer, displayId);
-            mInputController.setPointerAcceleration(1f, displayId);
+            mInputController.setMousePointerAccelerationEnabled(false, displayId);
             mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
                     displayId);
             // WM throws a SecurityException if the display is untrusted.
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index d089b05..2f9b6a5 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -55,9 +55,7 @@
     @GuardedBy("mCameras")
     private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>();
 
-    public VirtualCameraController() {
-        connectVirtualCameraService();
-    }
+    public VirtualCameraController() {}
 
     @VisibleForTesting
     VirtualCameraController(IVirtualCameraService virtualCameraService) {
diff --git a/services/companion/lint-baseline.xml b/services/companion/lint-baseline.xml
index 03eae39..020126f 100644
--- a/services/companion/lint-baseline.xml
+++ b/services/companion/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="7.1.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NonUserGetterCalled"
@@ -12,4 +12,4 @@
             column="14"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/services/core/Android.bp b/services/core/Android.bp
index dd001ec..a6ed498 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -203,6 +203,7 @@
         "com_android_wm_shell_flags_lib",
         "com.android.server.utils_aconfig-java",
         "service-jobscheduler-deviceidle.flags-aconfig-java",
+        "policy_flags_lib",
     ],
     javac_shard_size: 50,
     javacflags: [
@@ -233,9 +234,6 @@
 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/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java
index 9f279b1..329aac6 100644
--- a/services/core/java/com/android/server/BootReceiver.java
+++ b/services/core/java/com/android/server/BootReceiver.java
@@ -48,6 +48,8 @@
 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;
@@ -60,11 +62,14 @@
 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
@@ -332,12 +337,12 @@
      *
      * @param ctx Context
      * @param tombstone path to the tombstone
-     * @param proto whether the tombstone is stored as proto
+     * @param tombstoneProto the parsed proto tombstone
      * @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, boolean proto, String processName,
+                Context ctx, File tombstone, Tombstone tombstoneProto, String processName,
                 ReentrantLock tmpFileLock) {
         final DropBoxManager db = ctx.getSystemService(DropBoxManager.class);
         if (db == null) {
@@ -347,31 +352,33 @@
 
         // Check if we should rate limit and abort early if needed.
         DropboxRateLimiter.RateLimitResult rateLimitResult =
-                sDropboxRateLimiter.shouldRateLimit(
-                        proto ? TAG_TOMBSTONE_PROTO_WITH_HEADERS : TAG_TOMBSTONE, processName);
+                sDropboxRateLimiter.shouldRateLimit(TAG_TOMBSTONE_PROTO_WITH_HEADERS, processName);
         if (rateLimitResult.shouldRateLimit()) return;
 
         HashMap<String, Long> timestamps = readTimestamps();
         try {
-            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();
-                    }
+            // 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();
                 }
-            } 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);
@@ -380,11 +387,8 @@
     }
 
     private static void addAugmentedProtoToDropbox(
-                File tombstone, DropBoxManager db,
+                File tombstone, byte[] tombstoneBytes, 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(
@@ -417,6 +421,8 @@
         }
     }
 
+    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,
@@ -434,6 +440,31 @@
         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 e289a56..e923e30a 100644
--- a/services/core/java/com/android/server/OWNERS
+++ b/services/core/java/com/android/server/OWNERS
@@ -43,3 +43,6 @@
 per-file TelephonyRegistry.java = file:/telephony/OWNERS
 per-file UiModeManagerService.java = file:/packages/SystemUI/OWNERS
 per-file VcnManagementService.java = file:/services/core/java/com/android/server/vcn/OWNERS
+
+# SystemConfig
+per-file SystemConfig.java = file:/PACKAGE_MANAGER_OWNERS
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
new file mode 100644
index 0000000..70bd4b3
--- /dev/null
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.projection.MediaProjectionInfo;
+import android.media.projection.MediaProjectionManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.RankingMap;
+import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wm.SensitiveContentPackages.PackageInfo;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Service that monitors for notifications with sensitive content and protects content from screen
+ * sharing
+ */
+public final class SensitiveContentProtectionManagerService extends SystemService {
+    private static final String TAG = "SensitiveContentProtect";
+    private static final boolean DEBUG = false;
+
+    @VisibleForTesting
+    NotificationListener mNotificationListener;
+    private @Nullable MediaProjectionManager mProjectionManager;
+    private @Nullable WindowManagerInternal mWindowManager;
+
+    private final MediaProjectionManager.Callback mProjectionCallback =
+            new MediaProjectionManager.Callback() {
+                @Override
+                public void onStart(MediaProjectionInfo info) {
+                    if (DEBUG) Log.d(TAG, "onStart projection: " + info);
+                    onProjectionStart();
+                }
+
+                @Override
+                public void onStop(MediaProjectionInfo info) {
+                    if (DEBUG) Log.d(TAG, "onStop projection: " + info);
+                    onProjectionEnd();
+                }
+            };
+
+    public SensitiveContentProtectionManagerService(@NonNull Context context) {
+        super(context);
+        mNotificationListener = new NotificationListener();
+    }
+
+    @Override
+    public void onStart() {}
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase != SystemService.PHASE_BOOT_COMPLETED) {
+            return;
+        }
+
+        if (DEBUG) Log.d(TAG, "onBootPhase - PHASE_BOOT_COMPLETED");
+
+        init(getContext().getSystemService(MediaProjectionManager.class),
+                LocalServices.getService(WindowManagerInternal.class));
+    }
+
+    @VisibleForTesting
+    void init(MediaProjectionManager projectionManager,
+            WindowManagerInternal windowManager) {
+        if (DEBUG) Log.d(TAG, "init");
+
+        checkNotNull(projectionManager, "Failed to get valid MediaProjectionManager");
+        checkNotNull(windowManager, "Failed to get valid WindowManagerInternal");
+
+        mProjectionManager = projectionManager;
+        mWindowManager = windowManager;
+
+        // TODO(b/317250444): use MediaProjectionManagerService directly, reduces unnecessary
+        //  handler, delegate, and binder death recipient
+        mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper()));
+
+        try {
+            mNotificationListener.registerAsSystemService(getContext(),
+                    new ComponentName(getContext(), NotificationListener.class),
+                    UserHandle.USER_ALL);
+        } catch (RemoteException e) {
+            // Intra-process call, should never happen.
+        }
+    }
+
+    /** Cleanup any callbacks and listeners */
+    @VisibleForTesting
+    void onDestroy() {
+        if (mProjectionManager != null) {
+            mProjectionManager.removeCallback(mProjectionCallback);
+        }
+
+        try {
+            mNotificationListener.unregisterAsSystemService();
+        } catch (RemoteException e) {
+            // Intra-process call, should never happen.
+        }
+
+        if (mWindowManager != null) {
+            onProjectionEnd();
+        }
+    }
+
+    private void onProjectionStart() {
+        StatusBarNotification[] notifications;
+        try {
+            notifications = mNotificationListener.getActiveNotifications();
+        } catch (SecurityException e) {
+            Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e);
+            notifications = new StatusBarNotification[0];
+        }
+
+        RankingMap rankingMap;
+        try {
+            rankingMap = mNotificationListener.getCurrentRanking();
+        } catch (SecurityException e) {
+            Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e);
+            rankingMap = null;
+        }
+
+        // notify windowmanager of any currently posted sensitive content notifications
+        Set<PackageInfo> packageInfos = getSensitivePackagesFromNotifications(
+                notifications,
+                rankingMap);
+
+        mWindowManager.setShouldBlockScreenCaptureForApp(packageInfos);
+    }
+
+    private void onProjectionEnd() {
+        // notify windowmanager to clear any sensitive notifications observed during projection
+        // session
+        mWindowManager.setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    private Set<PackageInfo> getSensitivePackagesFromNotifications(
+            StatusBarNotification[] notifications, RankingMap rankingMap) {
+        if (rankingMap == null) {
+            Log.w(TAG, "Ranking map not initialized.");
+            return Collections.emptySet();
+        }
+
+        Set<PackageInfo> sensitivePackages = new ArraySet<>();
+        for (StatusBarNotification sbn : notifications) {
+            NotificationListenerService.Ranking ranking =
+                    rankingMap.getRawRankingObject(sbn.getKey());
+            if (ranking != null && ranking.hasSensitiveContent()) {
+                PackageInfo info = new PackageInfo(sbn.getPackageName(), sbn.getUid());
+                sensitivePackages.add(info);
+            }
+        }
+        return sensitivePackages;
+    }
+
+    // TODO(b/317251408): add trigger that updates on onNotificationPosted,
+    //  onNotificationRankingUpdate and onListenerConnected
+    @VisibleForTesting
+    static class NotificationListener extends NotificationListenerService {}
+}
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 39b8643..7a4ac6a 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -1356,8 +1356,8 @@
 
         final int flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
         for (UserInfo user : users) {
-            prepareUserStorageInternal(fromVolumeUuid, user.id, user.serialNumber, flags);
-            prepareUserStorageInternal(toVolumeUuid, user.id, user.serialNumber, flags);
+            prepareUserStorageInternal(fromVolumeUuid, user.id, flags);
+            prepareUserStorageInternal(toVolumeUuid, user.id, flags);
         }
     }
 
@@ -3231,12 +3231,12 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) {
+    public void createUserStorageKeys(int userId, boolean ephemeral) {
 
         super.createUserStorageKeys_enforcePermission();
 
         try {
-            mVold.createUserStorageKeys(userId, serialNumber, ephemeral);
+            mVold.createUserStorageKeys(userId, ephemeral);
             // Since the user's CE key was just created, the user's CE storage is now unlocked.
             synchronized (mLock) {
                 mCeUnlockedUsers.append(userId);
@@ -3276,12 +3276,11 @@
     /* Only for use by LockSettingsService */
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void unlockCeStorage(@UserIdInt int userId, int serialNumber, byte[] secret)
-            throws RemoteException {
+    public void unlockCeStorage(@UserIdInt int userId, byte[] secret) throws RemoteException {
         super.unlockCeStorage_enforcePermission();
 
         if (StorageManager.isFileEncrypted()) {
-            mVold.unlockCeStorage(userId, serialNumber, HexDump.toHexString(secret));
+            mVold.unlockCeStorage(userId, HexDump.toHexString(secret));
         }
         synchronized (mLock) {
             mCeUnlockedUsers.append(userId);
@@ -3348,27 +3347,27 @@
                 continue;
             }
 
-            prepareUserStorageInternal(vol.fsUuid, user.id, user.serialNumber, flags);
+            prepareUserStorageInternal(vol.fsUuid, user.id, flags);
         }
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) {
+    public void prepareUserStorage(String volumeUuid, int userId, int flags) {
 
         super.prepareUserStorage_enforcePermission();
 
         try {
-            prepareUserStorageInternal(volumeUuid, userId, serialNumber, flags);
+            prepareUserStorageInternal(volumeUuid, userId, flags);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
 
-    private void prepareUserStorageInternal(String volumeUuid, int userId, int serialNumber,
-            int flags) throws Exception {
+    private void prepareUserStorageInternal(String volumeUuid, int userId, int flags)
+            throws Exception {
         try {
-            mVold.prepareUserStorage(volumeUuid, userId, serialNumber, flags);
+            mVold.prepareUserStorage(volumeUuid, userId, flags);
             // After preparing user storage, we should check if we should mount data mirror again,
             // and we do it for user 0 only as we only need to do once for all users.
             if (volumeUuid != null) {
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 40b29d7..3483c1a 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -315,6 +315,11 @@
     private final ArraySet<String> mBugreportWhitelistedPackages = new ArraySet<>();
     private final ArraySet<String> mAppDataIsolationWhitelistedApps = new ArraySet<>();
 
+    // These packages will be set as 'prevent disable', where they are no longer possible
+    // for the end user to disable via settings. This flag should only be used for packages
+    // which meet the 'force or keep enabled apps' policy.
+    private final ArrayList<String> mPreventUserDisablePackages = new ArrayList<>();
+
     // Map of packagesNames to userTypes. Stored temporarily until cleared by UserManagerService().
     private ArrayMap<String, Set<String>> mPackageToUserTypeWhitelist = new ArrayMap<>();
     private ArrayMap<String, Set<String>> mPackageToUserTypeBlacklist = new ArrayMap<>();
@@ -504,6 +509,10 @@
         return mAppDataIsolationWhitelistedApps;
     }
 
+    public @NonNull ArrayList<String> getPreventUserDisablePackages() {
+        return mPreventUserDisablePackages;
+    }
+
     /**
      * Gets map of packagesNames to userTypes, dictating on which user types each package should be
      * initially installed, and then removes this map from SystemConfig.
@@ -1309,6 +1318,16 @@
                         }
                         XmlUtils.skipCurrentTag(parser);
                     } break;
+                    case "prevent-disable": {
+                        String pkgname = parser.getAttributeValue(null, "package");
+                        if (pkgname == null) {
+                            Slog.w(TAG, "<" + name + "> without package in " + permFile
+                                    + " at " + parser.getPositionDescription());
+                        } else {
+                            mPreventUserDisablePackages.add(pkgname);
+                        }
+                        XmlUtils.skipCurrentTag(parser);
+                    } break;
                     case "install-in-user-type": {
                         // NB: We allow any directory permission to declare install-in-user-type.
                         readInstallInUserType(parser,
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 9eb35fd..eb6fdd7 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -101,6 +101,7 @@
 import com.android.internal.telephony.IPhoneStateListener;
 import com.android.internal.telephony.ITelephonyRegistry;
 import com.android.internal.telephony.TelephonyPermissions;
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
@@ -2679,6 +2680,14 @@
         if (!checkNotifyPermission("notifyEmergencyNumberList()")) {
             return;
         }
+        if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+            if (!mContext.getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_TELEPHONY_CALLING)) {
+                // TelephonyManager.getEmergencyNumberList() throws an exception if
+                // FEATURE_TELEPHONY_CALLING is not defined.
+                return;
+            }
+        }
 
         synchronized (mRecords) {
             if (validatePhoneId(phoneId)) {
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 85abf87..130a733 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -3152,6 +3152,7 @@
                                     new AccountAuthenticatorResponse(this),
                                     authTokenType,
                                     true);
+                            mCanStartAccountManagerActivity = true;
                             Bundle bundle = new Bundle();
                             bundle.putParcelable(AccountManager.KEY_INTENT, intent);
                             onResult(bundle);
@@ -4933,6 +4934,7 @@
         IAccountAuthenticator mAuthenticator = null;
 
         private final boolean mStripAuthTokenFromResult;
+        protected boolean mCanStartAccountManagerActivity = false;
         protected final UserAccounts mAccounts;
 
         public Session(UserAccounts accounts, IAccountManagerResponse response, String accountType,
@@ -5068,9 +5070,13 @@
 
         private boolean isExportedSystemActivity(ActivityInfo activityInfo) {
             String className = activityInfo.name;
-            return "android".equals(activityInfo.packageName) &&
-                    (GrantCredentialsPermissionActivity.class.getName().equals(className)
-                    || CantAddAccountActivity.class.getName().equals(className));
+            if (!"android".equals(activityInfo.packageName)) {
+                return false;
+
+            }
+            return (mCanStartAccountManagerActivity
+                    && GrantCredentialsPermissionActivity.class.getName().equals(className))
+                    || CantAddAccountActivity.class.getName().equals(className);
         }
 
         private void close() {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 02f4485..0cff8b7 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -4464,6 +4464,12 @@
                     }
                 }
                 if (userId > 0) {
+                    if (mAm.isSystemUserOnly(sInfo.flags)) {
+                        Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user,"
+                                + " calling userId is: " + userId);
+                        return null;
+                    }
+
                     if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
                             sInfo.name, sInfo.flags)
                             && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
@@ -5453,7 +5459,7 @@
         // Force an immediate oomAdjUpdate, so the client app could be in the correct process state
         // before doing any service related transactions
         mAm.enqueueOomAdjTargetLocked(app);
-        mAm.updateOomAdjLocked(app, OOM_ADJ_REASON_START_SERVICE);
+        mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE);
 
         boolean created = false;
         try {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index fddb570..0c70bfe 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13747,6 +13747,11 @@
         return result;
     }
 
+    boolean isSystemUserOnly(int flags) {
+        return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
+                && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0;
+    }
+
     /**
      * Checks to see if the caller is in the same app as the singleton
      * component, or the component is in a special app. It allows special apps
diff --git a/services/core/java/com/android/server/am/AnrHelper.java b/services/core/java/com/android/server/am/AnrHelper.java
index e0a2246..9fc0bf9 100644
--- a/services/core/java/com/android/server/am/AnrHelper.java
+++ b/services/core/java/com/android/server/am/AnrHelper.java
@@ -63,6 +63,11 @@
     private static final long CONSECUTIVE_ANR_TIME_MS = TimeUnit.MINUTES.toMillis(2);
 
     /**
+     * Time to wait before taking dumps for other processes to reduce load at boot time.
+     */
+    private static final long SELF_ONLY_AFTER_BOOT_MS = TimeUnit.MINUTES.toMillis(10);
+
+    /**
      * The keep alive time for the threads in the helper threadpool executor
     */
     private static final int DEFAULT_THREAD_KEEP_ALIVE_SECOND = 10;
@@ -231,7 +236,8 @@
                 // If there are many ANR at the same time, the latency may be larger.
                 // If the latency is too large, the stack trace might not be meaningful.
                 final long reportLatency = startTime - r.mTimestamp;
-                final boolean onlyDumpSelf = reportLatency > EXPIRED_REPORT_TIME_MS;
+                final boolean onlyDumpSelf = reportLatency > EXPIRED_REPORT_TIME_MS
+                        || startTime < SELF_ONLY_AFTER_BOOT_MS;
                 r.appNotResponding(onlyDumpSelf);
                 final long endTime = SystemClock.uptimeMillis();
                 Slog.d(TAG, "Completed ANR of " + r.mApp.processName + " in "
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 095d907..30f21a6 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1249,9 +1249,9 @@
             ProviderInfo cpi = providers.get(i);
             boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo,
                     cpi.name, cpi.flags);
-            if (singleton && app.userId != UserHandle.USER_SYSTEM) {
-                // This is a singleton provider, but a user besides the
-                // default user is asking to initialize a process it runs
+            if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) {
+                // This is a singleton or a SYSTEM user only provider, but a user besides the
+                // SYSTEM user is asking to initialize a process it runs
                 // in...  well, no, it doesn't actually run in this process,
                 // it runs in the process of the default user.  Get rid of it.
                 providers.remove(i);
@@ -1398,8 +1398,7 @@
                                     final boolean processMatch =
                                             Objects.equals(pi.processName, app.processName)
                                             || pi.multiprocess;
-                                    final boolean userMatch = !mService.isSingleton(
-                                            pi.processName, pi.applicationInfo, pi.name, pi.flags)
+                                    final boolean userMatch = !isSingletonOrSystemUserOnly(pi)
                                             || app.userId == UserHandle.USER_SYSTEM;
                                     final boolean isInstantApp = pi.applicationInfo.isInstantApp();
                                     final boolean splitInstalled = pi.splitName == null
@@ -1985,4 +1984,13 @@
             return isAuthRedirected;
         }
     }
+
+    /**
+     * Returns true if Provider is either singleUser or systemUserOnly provider.
+     */
+    private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) {
+        return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
+                && mService.isSystemUserOnly(pi.flags))
+                || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags);
+    }
 }
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index f49e25a..ef7a0e0 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1385,6 +1385,8 @@
                         break;
                 }
 
+                // TODO: b/319163103 - limit isolated/sandbox trimming to just the processes
+                //  evaluated in the current update.
                 if (app.isolated && psr.numberOfRunningServices() <= 0
                         && app.getIsolatedEntryPoint() == null) {
                     // If this is an isolated process, there are no services
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 7cc7c51..5a3fbe9 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -724,24 +724,13 @@
 
         if (fullUpdate) {
             assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP());
-            postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime);
         } else {
             activeProcesses.clear();
             activeProcesses.addAll(targetProcesses);
             assignCachedAdjIfNecessary(activeProcesses);
-
-            for (int  i = activeUids.size() - 1; i >= 0; i--) {
-                final UidRecord uidRec = activeUids.valueAt(i);
-                uidRec.forEachProcess(this::updateAppUidRecIfNecessaryLSP);
-            }
-            updateUidsLSP(activeUids, nowElapsed);
-
-            for (int i = 0, size = targetProcesses.size(); i < size; i++) {
-                applyOomAdjLSP(targetProcesses.valueAt(i), false, now, nowElapsed, oomAdjReason);
-            }
-
             activeProcesses.clear();
         }
+        postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime);
         targetProcesses.clear();
 
         if (startProfiling) {
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 08b129e..2771572 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -39,7 +39,7 @@
 import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
-import android.compat.annotation.Disabled;
+import android.compat.annotation.EnabledAfter;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -49,6 +49,7 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
+import android.os.Build.VERSION_CODES;
 import android.os.IBinder;
 import android.os.PowerExemptionManager;
 import android.os.SystemClock;
@@ -94,16 +95,14 @@
      * (See also android.app.ForegroundServiceTypePolicy)
      */
     @ChangeId
-    // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
-    @Disabled
+    @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
     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
+    @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
     static final long USE_NEW_WIU_LOGIC_FOR_CAPABILITIES = 313677553L;
 
     /**
@@ -111,8 +110,7 @@
      * the background.
      */
     @ChangeId
-    // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
-    @Disabled
+    @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
     static final long USE_NEW_BFSL_LOGIC = 311208749L;
 
     final ActivityManagerService ams;
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index e7e721b..9db5d0a 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -176,6 +176,7 @@
         "system_performance",
         "system_sw_touch",
         "system_sw_usb",
+        "statsd",
         "test_suites",
         "text",
         "threadnetwork",
diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
index 9f31f37..5f12ce1 100644
--- a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
+++ b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
@@ -73,7 +73,8 @@
     private static final Set<Integer> DEFAULT_EVENT_SET = Sets.newHashSet(
             AmbientContextEvent.EVENT_COUGH,
             AmbientContextEvent.EVENT_SNORE,
-            AmbientContextEvent.EVENT_BACK_DOUBLE_TAP);
+            AmbientContextEvent.EVENT_BACK_DOUBLE_TAP,
+            AmbientContextEvent.EVENT_HEART_RATE);
 
     /** Default value in absence of {@link DeviceConfig} override. */
     private static final boolean DEFAULT_SERVICE_ENABLED = true;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index dada72e..f80228a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1812,6 +1812,7 @@
                                         "msg: MSG_L_SET_BT_ACTIVE_DEVICE "
                                             + "received with null profile proxy: "
                                             + btInfo)).printLog(TAG));
+                                sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, 0 /*delay*/);
                                 return;
                             }
                             @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index e05824a..bf20ae3 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -1992,7 +1992,7 @@
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address)
+                    "A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address)
                             + " now available").printLog(TAG));
         }
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index a1b6f29..4cbee2b 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -8835,6 +8835,8 @@
                 synchronized (VolumeStreamState.class) {
                     oldIndex = getIndex(device);
                     index = getValidIndex(index, hasModifyAudioSettings);
+                    // for STREAM_SYSTEM_ENFORCED, do not sync aliased streams on the enforced index
+                    int aliasIndex = index;
                     if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) {
                         index = mIndexMax;
                     }
@@ -8853,7 +8855,8 @@
                         if (streamType != mStreamType &&
                                 mStreamVolumeAlias[streamType] == mStreamType &&
                                 (changed || !aliasStreamState.hasIndexForDevice(device))) {
-                            final int scaledIndex = rescaleIndex(index, mStreamType, streamType);
+                            final int scaledIndex =
+                                    rescaleIndex(aliasIndex, mStreamType, streamType);
                             aliasStreamState.setIndex(scaledIndex, device, caller,
                                     hasModifyAudioSettings);
                             if (isCurrentDevice) {
@@ -9375,6 +9378,14 @@
             if (mIsSingleVolume && (streamState.mStreamType != AudioSystem.STREAM_MUSIC)) {
                 return;
             }
+
+            // Persisting STREAM_SYSTEM_ENFORCED index is not needed as its alias (STREAM_RING)
+            // is persisted. This can also be problematic when the enforcement is active as it will
+            // override current SYSTEM_RING persisted value given they share the same settings name
+            // (due to aliasing).
+            if (streamState.mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) {
+                return;
+            }
             if (streamState.hasValidSettingsName()) {
                 mSettings.putSystemIntForUser(mContentResolver,
                         streamState.getSettingNameForDevice(device),
@@ -13591,6 +13602,46 @@
         }
     }
 
+
+    /**
+     * @see AudioManager#shouldNotificationSoundPlay(AudioAttributes)
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.QUERY_AUDIO_STATE)
+    public boolean shouldNotificationSoundPlay(@NonNull final AudioAttributes aa) {
+        super.shouldNotificationSoundPlay_enforcePermission();
+        Objects.requireNonNull(aa);
+
+        // don't play notifications if the stream volume associated with the
+        // AudioAttributes of the notification record is 0 (non-zero volume implies
+        // not silenced by SILENT or VIBRATE ringer mode)
+        final int stream = AudioAttributes.toLegacyStreamType(aa);
+        final boolean mutingFromVolume = getStreamVolume(stream) == 0;
+        if (mutingFromVolume) {
+            if (DEBUG_VOL) {
+                Slog.d(TAG, "notification should not play due to muted stream " + stream);
+            }
+            return false;
+        }
+
+        // don't play notifications if there is a user of GAIN_TRANSIENT_EXCLUSIVE audio focus
+        // and the focus owner is recording
+        final int uid = mMediaFocusControl.getExclusiveFocusOwnerUid();
+        if (uid == -1) { // return value is -1 if focus isn't GAIN_TRANSIENT_EXCLUSIVE
+            return true;
+        }
+        // is the owner of GAIN_TRANSIENT_EXCLUSIVE focus also recording?
+        final boolean mutingFromFocusAndRecording = mRecordMonitor.isRecordingActiveForUid(uid);
+        if (mutingFromFocusAndRecording) {
+            if (DEBUG_VOL) {
+                Slog.d(TAG, "notification should not play due to exclusive focus owner recording "
+                        + " uid:" + uid);
+            }
+            return false;
+        }
+        return true;
+    }
+
     //======================
     // Audioserver state dispatch
     //======================
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index a818c30..f51043d 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -634,16 +634,17 @@
             return;
         }
         List<BluetoothDevice> activeDevices = adapter.getActiveDevices(profile);
-        if (activeDevices.isEmpty() || activeDevices.get(0) == null) {
-            return;
+        BluetoothProfileConnectionInfo bpci = new BluetoothProfileConnectionInfo(profile);
+        for (BluetoothDevice device : activeDevices) {
+            if (device == null) {
+                continue;
+            }
+            AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData(
+                    device, null, bpci, "mBluetoothProfileServiceListener");
+            AudioDeviceBroker.BtDeviceInfo info = mDeviceBroker.createBtDeviceInfo(
+                    data, device, BluetoothProfile.STATE_CONNECTED);
+            mDeviceBroker.postBluetoothActiveDevice(info, 0 /* delay */);
         }
-        AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData(
-                activeDevices.get(0), null, new BluetoothProfileConnectionInfo(profile),
-                "mBluetoothProfileServiceListener");
-        AudioDeviceBroker.BtDeviceInfo info =
-                mDeviceBroker.createBtDeviceInfo(data, activeDevices.get(0),
-                        BluetoothProfile.STATE_CONNECTED);
-        mDeviceBroker.postBluetoothActiveDevice(info, 0 /* delay */);
     }
 
     // @GuardedBy("mDeviceBroker.mSetModeLock")
@@ -678,8 +679,11 @@
         if (adapter != null) {
             List<BluetoothDevice> activeDevices =
                     adapter.getActiveDevices(BluetoothProfile.HEADSET);
-            if (activeDevices.size() > 0 && activeDevices.get(0) != null) {
-                onSetBtScoActiveDevice(activeDevices.get(0));
+            for (BluetoothDevice device : activeDevices) {
+                if (device == null) {
+                    continue;
+                }
+                onSetBtScoActiveDevice(device);
             }
         } else {
             Log.e(TAG, "onHeadsetProfileConnected: Null BluetoothAdapter");
diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java
index cbcd8f5..4d5bce5 100644
--- a/services/core/java/com/android/server/audio/FadeOutManager.java
+++ b/services/core/java/com/android/server/audio/FadeOutManager.java
@@ -29,6 +29,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.server.utils.EventLogger;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -345,7 +346,8 @@
             }
             if (apc.getPlayerProxy() != null) {
                 applyVolumeShaperInternal(apc, piid, volShaper,
-                        skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
+                        skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED, skipRamp,
+                        PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT);
                 mFadedPlayers.put(piid, volShaper);
             } else {
                 if (DEBUG) {
@@ -361,7 +363,8 @@
                 final AudioPlaybackConfiguration apc = players.get(piid);
                 if ((apc != null) && (apc.getPlayerProxy() != null)) {
                     applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null,
-                            VolumeShaper.Operation.REVERSE);
+                            VolumeShaper.Operation.REVERSE, /* skipRamp= */ false,
+                            PlaybackActivityMonitor.EVENT_TYPE_FADE_IN);
                 } else {
                     // this piid was in the list of faded players, but wasn't found
                     if (DEBUG) {
@@ -373,6 +376,7 @@
             mFadedPlayers.clear();
         }
 
+        @GuardedBy("mLock")
         void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc,
                 @Nullable VolumeShaper.Configuration config) {
             int piid = Integer.valueOf(apc.getPlayerInterfaceId());
@@ -385,10 +389,17 @@
                 return;
             }
 
+            VolumeShaper.Operation operation = VolumeShaper.Operation.REVERSE;
+            if (config != null) {
+                // replace and join the volumeshapers with (possibly) in progress fade out operation
+                // for a smoother fade in
+                operation = new VolumeShaper.Operation.Builder()
+                        .replace(mFadedPlayers.get(piid).getId(), /* join= */ true).build();
+            }
             mFadedPlayers.remove(piid);
             if (apc.getPlayerProxy() != null) {
-                applyVolumeShaperInternal(apc, piid, config,
-                        config != null ? PLAY_CREATE_IF_NEEDED : VolumeShaper.Operation.REVERSE);
+                applyVolumeShaperInternal(apc, piid, config, operation, /* skipRamp= */ false,
+                        PlaybackActivityMonitor.EVENT_TYPE_FADE_IN);
             } else {
                 if (DEBUG) {
                     Slog.v(TAG, "Error fading in player piid:" + piid
@@ -397,6 +408,7 @@
             }
         }
 
+        @GuardedBy("mLock")
         void clear() {
             if (mFadedPlayers.size() > 0) {
                 if (DEBUG) {
@@ -413,21 +425,40 @@
         }
 
         private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid,
-                VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation) {
+                VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation,
+                boolean skipRamp, String eventType) {
             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));
+                logFadeEvent(apc, piid, volShaperConfig, operation, skipRamp, eventType);
                 apc.getPlayerProxy().applyVolumeShaper(config, operation);
             } catch (Exception e) {
-                Slog.e(TAG, "Error fading player piid:" + piid + " uid:" + mUid
-                        + " operation:" + operation, e);
+                Slog.e(TAG, "Error " + eventType + " piid:" + piid + " uid:" + mUid, e);
             }
         }
+
+        private void logFadeEvent(AudioPlaybackConfiguration apc, int piid,
+                VolumeShaper.Configuration config, VolumeShaper.Operation operation,
+                boolean skipRamp, String eventType) {
+            if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT)) {
+                PlaybackActivityMonitor.sEventLogger.enqueue(
+                        (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp, config, operation))
+                                .printLog(TAG));
+                return;
+            }
+
+            if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_IN)) {
+                PlaybackActivityMonitor.sEventLogger.enqueue(
+                        (new PlaybackActivityMonitor.FadeInEvent(apc, skipRamp, config, operation))
+                                .printLog(TAG));
+                return;
+            }
+
+            PlaybackActivityMonitor.sEventLogger.enqueue(
+                    (new EventLogger.StringEvent(eventType + " piid:" + piid)).printLog(TAG));
+        }
     }
 }
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 0df0006..1376bde 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -297,6 +297,23 @@
     }
 
     /**
+     * Return the UID of the focus owner that has focus with exclusive focus gain
+     * @return -1 if nobody has exclusive focus, the UID of the owner otherwise
+     */
+    protected int getExclusiveFocusOwnerUid() {
+        synchronized (mAudioFocusLock) {
+            if (mFocusStack.empty()) {
+                return -1;
+            }
+            final FocusRequester owner = mFocusStack.peek();
+            if (owner.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+                return -1;
+            }
+            return owner.getClientUid();
+        }
+    }
+
+    /**
      * Send AUDIOFOCUS_LOSS to a specific stack entry.
      * Note this method is supporting an external API, and is restricted to LOSS in order to
      * prevent allowing the stack to be in an invalid state (e.g. entry inside stack has focus)
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index e69fbbd..08da32e 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -84,6 +84,8 @@
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID = 3;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID = 4;
+    /*package*/ static final String EVENT_TYPE_FADE_OUT = "fading out";
+    /*package*/ static final String EVENT_TYPE_FADE_IN = "fading in";
 
     // ducking settings for a "normal duck" at -14dB
     private static final VolumeShaper.Configuration DUCK_VSHAPE =
@@ -1204,11 +1206,13 @@
                     return;
                 }
                 try {
-                    sEventLogger.enqueue((new DuckEvent(apc, skipRamp, mUseStrongDuck))
-                            .printLog(TAG));
-                    apc.getPlayerProxy().applyVolumeShaper(
-                            mUseStrongDuck ? STRONG_DUCK_VSHAPE : DUCK_VSHAPE,
-                            skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
+                    VolumeShaper.Configuration config =
+                            mUseStrongDuck ? STRONG_DUCK_VSHAPE : DUCK_VSHAPE;
+                    VolumeShaper.Operation operation =
+                            skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED;
+                    sEventLogger.enqueue((new DuckEvent(apc, skipRamp, mUseStrongDuck, config,
+                            operation)).printLog(TAG));
+                    apc.getPlayerProxy().applyVolumeShaper(config, operation);
                     mDuckedPlayers.add(piid);
                 } catch (Exception e) {
                     Log.e(TAG, "Error ducking player piid:" + piid + " uid:" + mUid, e);
@@ -1363,58 +1367,41 @@
         }
     }
 
-    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;
         private final int mClientUid;
         private final int mClientPid;
+        private final int mPlayerType;
+        private final AudioAttributes mPlayerAttr;
+        private final VolumeShaper.Configuration mConfig;
+        private final VolumeShaper.Operation mOperation;
 
         abstract String getVSAction();
 
-        VolumeShaperEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+        VolumeShaperEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp,
+                VolumeShaper.Configuration config, VolumeShaper.Operation operation) {
             mPlayerIId = apc.getPlayerInterfaceId();
             mSkipRamp = skipRamp;
             mClientUid = apc.getClientUid();
             mClientPid = apc.getClientPid();
+            mPlayerAttr = apc.getAudioAttributes();
+            mPlayerType = apc.getPlayerType();
+            mConfig = config;
+            mOperation = operation;
         }
 
         @Override
         public String eventToString() {
-            return new StringBuilder(getVSAction()).append(" player piid:").append(mPlayerIId)
-                    .append(" uid/pid:").append(mClientUid).append("/").append(mClientPid)
-                    .append(" skip ramp:").append(mSkipRamp).toString();
+            return getVSAction()
+                    + " player piid:" + mPlayerIId
+                    + " uid/pid:" + mClientUid + "/" + mClientPid
+                    + " skip ramp:" + mSkipRamp
+                    + " player type:"
+                    + AudioPlaybackConfiguration.toLogFriendlyPlayerType(mPlayerType)
+                    + " attr:" + mPlayerAttr
+                    + " config:" + mConfig
+                    + " operation:" + mOperation;
         }
     }
 
@@ -1426,9 +1413,10 @@
             return mUseStrongDuck ? "ducking (strong)" : "ducking";
         }
 
-        DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, boolean useStrongDuck)
+        DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, boolean useStrongDuck,
+                VolumeShaper.Configuration config, VolumeShaper.Operation operation)
         {
-            super(apc, skipRamp);
+            super(apc, skipRamp, config, operation);
             mUseStrongDuck = useStrongDuck;
         }
     }
@@ -1436,11 +1424,24 @@
     static final class FadeOutEvent extends VolumeShaperEvent {
         @Override
         String getVSAction() {
-            return "fading out";
+            return EVENT_TYPE_FADE_OUT;
         }
 
-        FadeOutEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
-            super(apc, skipRamp);
+        FadeOutEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp,
+                VolumeShaper.Configuration config, VolumeShaper.Operation operation) {
+            super(apc, skipRamp, config, operation);
+        }
+    }
+
+    static final class FadeInEvent extends VolumeShaperEvent {
+        @Override
+        String getVSAction() {
+            return EVENT_TYPE_FADE_IN;
+        }
+
+        FadeInEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp,
+                VolumeShaper.Configuration config, VolumeShaper.Operation operation) {
+            super(apc, skipRamp, config, operation);
         }
     }
 
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index c72632f..a30cdc4 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -62,6 +62,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -147,6 +148,15 @@
 
     private static final int SAFE_MEDIA_VOLUME_UNINITIALIZED = -1;
 
+    // see {@link #recordToPersistedString(SoundDoseRecord)}
+    // this is computed conservatively to accommodate the legacy persisting of SoundDoseRecords in
+    // which we materialized more decimal values.
+    // TODO: adjust value after soaking in
+    private static final int MAX_RECORDS_STRING_LENGTH = 50;
+    private static final int MAX_SETTINGS_LENGTH = 32768;
+    private static final int MAX_NUMBER_OF_CACHED_RECORDS =
+            MAX_SETTINGS_LENGTH / MAX_RECORDS_STRING_LENGTH;
+
     private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE,
             "CSD updates");
 
@@ -893,7 +903,7 @@
             if (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC
                     && safeDevicesContains(device)) {
                 soundDose.updateAttenuation(
-                        AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC,
+                        -AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC,
                                 (newIndex + 5) / 10,
                                 device), device);
             }
@@ -923,7 +933,7 @@
         Log.v(TAG, "Initializing sound dose");
 
         try {
-            if (mCachedAudioDeviceCategories.size() > 0) {
+            if (!mCachedAudioDeviceCategories.isEmpty()) {
                 soundDose.initCachedAudioDeviceCategories(mCachedAudioDeviceCategories.toArray(
                         new ISoundDose.AudioDeviceCategory[0]));
                 mCachedAudioDeviceCategories.clear();
@@ -957,6 +967,7 @@
                         mGlobalTimeOffsetInSecs);
                 if (records != null) {
                     mDoseRecords.addAll(records);
+                    sanitizeDoseRecords_l();
                 }
             }
         }
@@ -1176,17 +1187,35 @@
                                 && r.duration == record.duration)) {
                     Log.w(TAG, "Could not find cached record to remove: " + record);
                 }
-            } else {
+            } else if (record.value > 0) {
                 mDoseRecords.add(record);
             }
         }
 
+        sanitizeDoseRecords_l();
+
         mAudioHandler.sendMessageAtTime(mAudioHandler.obtainMessage(MSG_PERSIST_CSD_VALUES,
                 /* arg1= */0, /* arg2= */0, /* obj= */null), /* delay= */0);
 
         mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
     }
 
+    @GuardedBy("mCsdStateLock")
+    private void sanitizeDoseRecords_l() {
+        if (mDoseRecords.size() > MAX_NUMBER_OF_CACHED_RECORDS) {
+            int nrToRemove = MAX_NUMBER_OF_CACHED_RECORDS - mDoseRecords.size();
+            Log.w(TAG,
+                    "Removing " + nrToRemove + " records from the total of " + mDoseRecords.size());
+            // Remove older elements to fit into persisted settings max length
+            Iterator<SoundDoseRecord> recordIterator = mDoseRecords.iterator();
+            while (recordIterator.hasNext() && nrToRemove > 0) {
+                recordIterator.next();
+                recordIterator.remove();
+                --nrToRemove;
+            }
+        }
+    }
+
     @SuppressWarnings("GuardedBy")  // avoid limitation with intra-procedural analysis of lambdas
     private void onPersistSoundDoseRecords() {
         synchronized (mCsdStateLock) {
@@ -1213,8 +1242,8 @@
             long globalTimeOffsetInSecs) {
         return convertToGlobalTime(record.timestamp, globalTimeOffsetInSecs)
                 + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.duration
-                + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.value
-                + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.averageMel;
+                + PERSIST_CSD_RECORD_FIELD_SEPARATOR + String.format("%.3f", record.value)
+                + PERSIST_CSD_RECORD_FIELD_SEPARATOR + String.format("%.3f", record.averageMel);
     }
 
     private static long convertToGlobalTime(long bootTimeInSecs, long globalTimeOffsetInSecs) {
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 89b638b..89e08c1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors;
 
+import static com.android.server.biometrics.sensors.BiometricSchedulerOperation.STATE_STARTED;
+
 import android.annotation.IntDef;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
@@ -28,6 +30,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.UserHandle;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
@@ -35,6 +38,7 @@
 import com.android.modules.expresslog.Counter;
 import com.android.server.biometrics.BiometricSchedulerProto;
 import com.android.server.biometrics.BiometricsProto;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
 import java.io.PrintWriter;
@@ -48,6 +52,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 /**
  * A scheduler for biometric HAL operations. Maintains a queue of {@link BaseClientMonitor}
@@ -56,11 +61,16 @@
  *
  * We currently assume (and require) that each biometric sensor have its own instance of a
  * {@link BiometricScheduler}.
+ *
+ * @param <T> Hal instance for starting the user.
+ * @param <U> Session associated with the current user id.
+ *
+ * TODO: (b/304604965) Update thread annotation when FLAGS_DE_HIDL is removed.
  */
 @MainThread
-public class BiometricScheduler {
+public class BiometricScheduler<T, U> {
 
-    private static final String BASE_TAG = "BiometricScheduler";
+    private static final String TAG = "BiometricScheduler";
     // Number of recent operations to keep in our logs for dumpsys
     protected static final int LOG_NUM_RECENT_OPERATIONS = 50;
 
@@ -89,30 +99,6 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface SensorType {}
 
-    public static @SensorType int sensorTypeFromFingerprintProperties(
-            @NonNull FingerprintSensorPropertiesInternal props) {
-        if (props.isAnyUdfpsType()) {
-            return SENSOR_TYPE_UDFPS;
-        }
-
-        return SENSOR_TYPE_FP_OTHER;
-    }
-
-    public static String sensorTypeToString(@SensorType int sensorType) {
-        switch (sensorType) {
-            case SENSOR_TYPE_UNKNOWN:
-                return "Unknown";
-            case SENSOR_TYPE_FACE:
-                return "Face";
-            case SENSOR_TYPE_UDFPS:
-                return "Udfps";
-            case SENSOR_TYPE_FP_OTHER:
-                return "OtherFp";
-            default:
-                return "UnknownUnknown";
-        }
-    }
-
     private static final class CrashState {
         static final int NUM_ENTRIES = 10;
         final String timestamp;
@@ -145,8 +131,8 @@
         }
     }
 
-    @NonNull protected final String mBiometricTag;
-    private final @SensorType int mSensorType;
+    @SensorType
+    private final int mSensorType;
     @Nullable private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
     @NonNull private final IBiometricService mBiometricService;
     @NonNull protected final Handler mHandler;
@@ -157,6 +143,43 @@
     private int mTotalOperationsHandled;
     private final int mRecentOperationsLimit;
     @NonNull private final List<Integer> mRecentOperations;
+    @Nullable private StopUserClient<U> mStopUserClient;
+    @NonNull private Supplier<Integer> mCurrentUserRetriever;
+    @Nullable private UserSwitchProvider<T, U> mUserSwitchProvider;
+
+    private class UserSwitchClientCallback implements ClientMonitorCallback {
+        @NonNull private final BaseClientMonitor mOwner;
+
+        UserSwitchClientCallback(@NonNull BaseClientMonitor owner) {
+            mOwner = owner;
+        }
+
+        @Override
+        public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) {
+            mHandler.post(() -> {
+                Slog.d(TAG, "[Client finished] " + clientMonitor + ", success: " + success);
+
+                // Set mStopUserClient to null when StopUserClient fails. Otherwise it's possible
+                // for that the queue will wait indefinitely until the field is cleared.
+                if (clientMonitor instanceof StopUserClient<?>) {
+                    if (!success) {
+                        Slog.w(TAG, "StopUserClient failed(), is the HAL stuck? "
+                                + "Clearing mStopUserClient");
+                    }
+                    mStopUserClient = null;
+                }
+                if (mCurrentOperation != null && mCurrentOperation.isFor(mOwner)) {
+                    mCurrentOperation = null;
+                } else {
+                    // can happen if the hal dies and is usually okay
+                    // do not unset the current operation that may be newer
+                    Slog.w(TAG, "operation is already null or different (reset?): "
+                            + mCurrentOperation);
+                }
+                startNextOperationIfIdle();
+            });
+        }
+    }
 
     // Internal callback, notified when an operation is complete. Notifies the requester
     // that the operation is complete, before performing internal scheduler work (such as
@@ -164,26 +187,26 @@
     private final ClientMonitorCallback mInternalCallback = new ClientMonitorCallback() {
         @Override
         public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
-            Slog.d(getTag(), "[Started] " + clientMonitor);
+            Slog.d(TAG, "[Started] " + clientMonitor);
         }
 
         @Override
         public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) {
             mHandler.post(() -> {
                 if (mCurrentOperation == null) {
-                    Slog.e(getTag(), "[Finishing] " + clientMonitor
+                    Slog.e(TAG, "[Finishing] " + clientMonitor
                             + " but current operation is null, success: " + success
                             + ", possible lifecycle bug in clientMonitor implementation?");
                     return;
                 }
 
                 if (!mCurrentOperation.isFor(clientMonitor)) {
-                    Slog.e(getTag(), "[Ignoring Finish] " + clientMonitor + " does not match"
+                    Slog.e(TAG, "[Ignoring Finish] " + clientMonitor + " does not match"
                             + " current: " + mCurrentOperation);
                     return;
                 }
 
-                Slog.d(getTag(), "[Finishing] " + clientMonitor + ", success: " + success);
+                Slog.d(TAG, "[Finishing] " + clientMonitor + ", success: " + success);
 
                 if (mGestureAvailabilityDispatcher != null) {
                     mGestureAvailabilityDispatcher.markSensorActive(
@@ -202,13 +225,11 @@
     };
 
     @VisibleForTesting
-    public BiometricScheduler(@NonNull String tag,
-            @NonNull Handler handler,
+    public BiometricScheduler(@NonNull Handler handler,
             @SensorType int sensorType,
             @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull IBiometricService biometricService,
             int recentOperationsLimit) {
-        mBiometricTag = tag;
         mHandler = handler;
         mSensorType = sensorType;
         mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher;
@@ -219,49 +240,140 @@
         mRecentOperations = new ArrayList<>();
     }
 
+    @VisibleForTesting
+    public BiometricScheduler(@NonNull Handler handler,
+            @SensorType int sensorType,
+            @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull IBiometricService biometricService,
+            int recentOperationsLimit,
+            @NonNull Supplier<Integer> currentUserRetriever,
+            @Nullable UserSwitchProvider<T, U> userSwitchProvider) {
+        mHandler = handler;
+        mSensorType = sensorType;
+        mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher;
+        mPendingOperations = new ArrayDeque<>();
+        mBiometricService = biometricService;
+        mCrashStates = new ArrayDeque<>();
+        mRecentOperationsLimit = recentOperationsLimit;
+        mRecentOperations = new ArrayList<>();
+        mCurrentUserRetriever = currentUserRetriever;
+        mUserSwitchProvider = userSwitchProvider;
+    }
+
+    public BiometricScheduler(@NonNull Handler handler,
+            @SensorType int sensorType,
+            @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull Supplier<Integer> currentUserRetriever,
+            @NonNull UserSwitchProvider<T, U> userSwitchProvider) {
+        this(handler, sensorType, gestureAvailabilityDispatcher,
+                IBiometricService.Stub.asInterface(ServiceManager.getService(
+                        Context.BIOMETRIC_SERVICE)), LOG_NUM_RECENT_OPERATIONS,
+                currentUserRetriever, userSwitchProvider);
+    }
+
     /**
      * Creates a new scheduler.
      *
-     * @param tag for the specific instance of the scheduler. Should be unique.
      * @param sensorType the sensorType that this scheduler is handling.
      * @param gestureAvailabilityDispatcher may be null if the sensor does not support gestures
      *                                      (such as fingerprint swipe).
      */
-    public BiometricScheduler(@NonNull String tag,
-            @SensorType int sensorType,
+    public BiometricScheduler(@SensorType int sensorType,
             @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
-        this(tag, new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher,
+        this(new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher,
                 IBiometricService.Stub.asInterface(
                         ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
                 LOG_NUM_RECENT_OPERATIONS);
     }
 
+    /**
+     * Returns sensor type for a fingerprint sensor.
+     */
+    @SensorType
+    public static int sensorTypeFromFingerprintProperties(
+            @NonNull FingerprintSensorPropertiesInternal props) {
+        if (props.isAnyUdfpsType()) {
+            return SENSOR_TYPE_UDFPS;
+        }
+
+        return SENSOR_TYPE_FP_OTHER;
+    }
+
     @VisibleForTesting
     public ClientMonitorCallback getInternalCallback() {
         return mInternalCallback;
     }
 
-    protected String getTag() {
-        return BASE_TAG + "/" + mBiometricTag;
+    protected void startNextOperationIfIdle() {
+        if (Flags.deHidl()) {
+            startNextOperation();
+        } else {
+            startNextOperationIfIdleLegacy();
+        }
     }
 
-    protected void startNextOperationIfIdle() {
+    protected void startNextOperation() {
         if (mCurrentOperation != null) {
-            Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation);
+            Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation);
             return;
         }
         if (mPendingOperations.isEmpty()) {
-            Slog.d(getTag(), "No operations, returning to idle");
+            Slog.d(TAG, "No operations, returning to idle");
+            return;
+        }
+
+        final int currentUserId = mCurrentUserRetriever.get();
+        final int nextUserId = mPendingOperations.getFirst().getTargetUserId();
+
+        if (nextUserId == currentUserId || mPendingOperations.getFirst().isStartUserOperation()) {
+            startNextOperationIfIdleLegacy();
+        } else if (currentUserId == UserHandle.USER_NULL && mUserSwitchProvider != null) {
+            final BaseClientMonitor startClient =
+                    mUserSwitchProvider.getStartUserClient(nextUserId);
+            final UserSwitchClientCallback finishedCallback =
+                    new UserSwitchClientCallback(startClient);
+
+            Slog.d(TAG, "[Starting User] " + startClient);
+            mCurrentOperation = new BiometricSchedulerOperation(
+                    startClient, finishedCallback, STATE_STARTED);
+            startClient.start(finishedCallback);
+        } else if (mUserSwitchProvider != null) {
+            if (mStopUserClient != null) {
+                Slog.d(TAG, "[Waiting for StopUser] " + mStopUserClient);
+            } else {
+                mStopUserClient = mUserSwitchProvider
+                        .getStopUserClient(currentUserId);
+                final UserSwitchClientCallback finishedCallback =
+                        new UserSwitchClientCallback(mStopUserClient);
+
+                Slog.d(TAG, "[Stopping User] current: " + currentUserId
+                        + ", next: " + nextUserId + ". " + mStopUserClient);
+                mCurrentOperation = new BiometricSchedulerOperation(
+                        mStopUserClient, finishedCallback, STATE_STARTED);
+                mStopUserClient.start(finishedCallback);
+            }
+        } else {
+            Slog.e(TAG, "Cannot start next operation.");
+        }
+    }
+
+    protected void startNextOperationIfIdleLegacy() {
+        if (mCurrentOperation != null) {
+            Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation);
+            return;
+        }
+        if (mPendingOperations.isEmpty()) {
+            Slog.d(TAG, "No operations, returning to idle");
             return;
         }
 
         mCurrentOperation = mPendingOperations.poll();
-        Slog.d(getTag(), "[Polled] " + mCurrentOperation);
+        Slog.d(TAG, "[Polled] " + mCurrentOperation);
 
         // If the operation at the front of the queue has been marked for cancellation, send
         // ERROR_CANCELED. No need to start this client.
         if (mCurrentOperation.isMarkedCanceling()) {
-            Slog.d(getTag(), "[Now Cancelling] " + mCurrentOperation);
+            Slog.d(TAG, "[Now Cancelling] " + mCurrentOperation);
             mCurrentOperation.cancel(mHandler, mInternalCallback);
             // Now we wait for the client to send its FinishCallback, which kicks off the next
             // operation.
@@ -289,7 +401,7 @@
                 // Note down current length of queue
                 final int pendingOperationsLength = mPendingOperations.size();
                 final BiometricSchedulerOperation lastOperation = mPendingOperations.peekLast();
-                Slog.e(getTag(), "[Unable To Start] " + mCurrentOperation
+                Slog.e(TAG, "[Unable To Start] " + mCurrentOperation
                         + ". Last pending operation: " + lastOperation);
 
                 // Then for each operation currently in the pending queue at the time of this
@@ -298,10 +410,10 @@
                 for (int i = 0; i < pendingOperationsLength; i++) {
                     final BiometricSchedulerOperation operation = mPendingOperations.pollFirst();
                     if (operation != null) {
-                        Slog.w(getTag(), "[Aborting Operation] " + operation);
+                        Slog.w(TAG, "[Aborting Operation] " + operation);
                         operation.abort();
                     } else {
-                        Slog.e(getTag(), "Null operation, index: " + i
+                        Slog.e(TAG, "Null operation, index: " + i
                                 + ", expected length: " + pendingOperationsLength);
                     }
                 }
@@ -317,9 +429,9 @@
                 mBiometricService.onReadyForAuthentication(
                         mCurrentOperation.getClientMonitor().getRequestId(), cookie);
             } catch (RemoteException e) {
-                Slog.e(getTag(), "Remote exception when contacting BiometricService", e);
+                Slog.e(TAG, "Remote exception when contacting BiometricService", e);
             }
-            Slog.d(getTag(), "Waiting for cookie before starting: " + mCurrentOperation);
+            Slog.d(TAG, "Waiting for cookie before starting: " + mCurrentOperation);
         }
     }
 
@@ -338,14 +450,14 @@
      */
     public void startPreparedClient(int cookie) {
         if (mCurrentOperation == null) {
-            Slog.e(getTag(), "Current operation is null");
+            Slog.e(TAG, "Current operation is null");
             return;
         }
 
         if (mCurrentOperation.startWithCookie(mInternalCallback, cookie)) {
-            Slog.d(getTag(), "[Started] Prepared client: " + mCurrentOperation);
+            Slog.d(TAG, "[Started] Prepared client: " + mCurrentOperation);
         } else {
-            Slog.e(getTag(), "[Unable To Start] Prepared client: " + mCurrentOperation);
+            Slog.e(TAG, "[Unable To Start] Prepared client: " + mCurrentOperation);
             mCurrentOperation = null;
             startNextOperationIfIdle();
         }
@@ -374,13 +486,13 @@
         if (clientMonitor.interruptsPrecedingClients()) {
             for (BiometricSchedulerOperation operation : mPendingOperations) {
                 if (operation.markCanceling()) {
-                    Slog.d(getTag(), "New client, marking pending op as canceling: " + operation);
+                    Slog.d(TAG, "New client, marking pending op as canceling: " + operation);
                 }
             }
         }
 
         mPendingOperations.add(new BiometricSchedulerOperation(clientMonitor, clientCallback));
-        Slog.d(getTag(), "[Added] " + clientMonitor
+        Slog.d(TAG, "[Added] " + clientMonitor
                 + ", new queue size: " + mPendingOperations.size());
 
         // If the new operation should interrupt preceding clients, and if the current operation is
@@ -389,7 +501,7 @@
                 && mCurrentOperation != null
                 && mCurrentOperation.isInterruptable()
                 && mCurrentOperation.isStarted()) {
-            Slog.d(getTag(), "[Cancelling Interruptable]: " + mCurrentOperation);
+            Slog.d(TAG, "[Cancelling Interruptable]: " + mCurrentOperation);
             mCurrentOperation.cancel(mHandler, mInternalCallback);
         } else {
             startNextOperationIfIdle();
@@ -401,16 +513,16 @@
      * @param token from the caller, should match the token passed in when requesting enrollment
      */
     public void cancelEnrollment(IBinder token, long requestId) {
-        Slog.d(getTag(), "cancelEnrollment, requestId: " + requestId);
+        Slog.d(TAG, "cancelEnrollment, requestId: " + requestId);
 
         if (mCurrentOperation != null
                 && canCancelEnrollOperation(mCurrentOperation, token, requestId)) {
-            Slog.d(getTag(), "Cancelling enrollment op: " + mCurrentOperation);
+            Slog.d(TAG, "Cancelling enrollment op: " + mCurrentOperation);
             mCurrentOperation.cancel(mHandler, mInternalCallback);
         } else {
             for (BiometricSchedulerOperation operation : mPendingOperations) {
                 if (canCancelEnrollOperation(operation, token, requestId)) {
-                    Slog.d(getTag(), "Cancelling pending enrollment op: " + operation);
+                    Slog.d(TAG, "Cancelling pending enrollment op: " + operation);
                     operation.markCanceling();
                 }
             }
@@ -423,16 +535,16 @@
      * @param requestId the id returned when requesting authentication
      */
     public void cancelAuthenticationOrDetection(IBinder token, long requestId) {
-        Slog.d(getTag(), "cancelAuthenticationOrDetection, requestId: " + requestId);
+        Slog.d(TAG, "cancelAuthenticationOrDetection, requestId: " + requestId);
 
         if (mCurrentOperation != null
                 && canCancelAuthOperation(mCurrentOperation, token, requestId)) {
-            Slog.d(getTag(), "Cancelling auth/detect op: " + mCurrentOperation);
+            Slog.d(TAG, "Cancelling auth/detect op: " + mCurrentOperation);
             mCurrentOperation.cancel(mHandler, mInternalCallback);
         } else {
             for (BiometricSchedulerOperation operation : mPendingOperations) {
                 if (canCancelAuthOperation(operation, token, requestId)) {
-                    Slog.d(getTag(), "Cancelling pending auth/detect op: " + operation);
+                    Slog.d(TAG, "Cancelling pending auth/detect op: " + operation);
                     operation.markCanceling();
                 }
             }
@@ -504,11 +616,11 @@
                 mCurrentOperation != null ? mCurrentOperation.toString() : null,
                 pendingOperations);
         mCrashStates.add(crashState);
-        Slog.e(getTag(), "Recorded crash state: " + crashState.toString());
+        Slog.e(TAG, "Recorded crash state: " + crashState.toString());
     }
 
     public void dump(PrintWriter pw) {
-        pw.println("Dump of BiometricScheduler " + getTag());
+        pw.println("Dump of BiometricScheduler " + TAG);
         pw.println("Type: " + mSensorType);
         pw.println("Current operation: " + mCurrentOperation);
         pw.println("Pending operations: " + mPendingOperations.size());
@@ -548,7 +660,7 @@
      * HAL dies.
      */
     public void reset() {
-        Slog.d(getTag(), "Resetting scheduler");
+        Slog.d(TAG, "Resetting scheduler");
         mPendingOperations.clear();
         mCurrentOperation = null;
     }
@@ -562,11 +674,11 @@
             return;
         }
         for (BiometricSchedulerOperation pendingOperation : mPendingOperations) {
-            Slog.d(getTag(), "[Watchdog cancelling pending] "
+            Slog.d(TAG, "[Watchdog cancelling pending] "
                     + pendingOperation.getClientMonitor());
             pendingOperation.markCancelingForWatchdog();
         }
-        Slog.d(getTag(), "[Watchdog cancelling current] "
+        Slog.d(TAG, "[Watchdog cancelling current] "
                 + mCurrentOperation.getClientMonitor());
         mCurrentOperation.cancel(mHandler, getInternalCallback());
     }
@@ -590,9 +702,23 @@
     /**
      * Handle stop user client when user switching occurs.
      */
-    public void onUserStopped() {}
+    public void onUserStopped() {
+        if (mStopUserClient == null) {
+            Slog.e(TAG, "Unexpected onUserStopped");
+            return;
+        }
+
+        Slog.d(TAG, "[OnUserStopped]: " + mStopUserClient);
+        mStopUserClient.onUserStopped();
+        mStopUserClient = null;
+    }
 
     public Handler getHandler() {
         return mHandler;
     }
+
+    @Nullable
+    public StopUserClient<?> getStopUserClient() {
+        return mStopUserClient;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java
index e8654dc..e01c4ec 100644
--- a/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java
@@ -30,7 +30,10 @@
 
 /**
  * Abstract class for stopping a user.
- * @param <T> Interface for stopping the user.
+ *
+ * @param <T> Session for stopping the user. It should be either an instance of
+ *            {@link com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession} or
+ *            {@link com.android.server.biometrics.sensors.face.aidl.AidlSession}.
  */
 public abstract class StopUserClient<T> extends HalClientMonitor<T> {
 
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 3753bbd..7ca10e3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
@@ -33,10 +33,14 @@
 
 /**
  * A user-aware scheduler that requests user-switches based on scheduled operation's targetUserId.
+ * TODO (b/304604965): Remove class when Flags.FLAG_DE_HIDL is removed.
+ *
+ * @param <T> Hal instance for starting the user.
+ * @param <U> Session associated with the current user id.
  */
-public class UserAwareBiometricScheduler extends BiometricScheduler {
+public class UserAwareBiometricScheduler<T, U> extends BiometricScheduler<T, U> {
 
-    private static final String BASE_TAG = "UaBiometricScheduler";
+    private static final String TAG = "UaBiometricScheduler";
 
     /**
      * Interface to retrieve the owner's notion of the current userId. Note that even though
@@ -66,13 +70,13 @@
         @Override
         public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) {
             mHandler.post(() -> {
-                Slog.d(getTag(), "[Client finished] " + clientMonitor + ", success: " + success);
+                Slog.d(TAG, "[Client finished] " + clientMonitor + ", success: " + success);
 
                 // Set mStopUserClient to null when StopUserClient fails. Otherwise it's possible
                 // for that the queue will wait indefinitely until the field is cleared.
                 if (clientMonitor instanceof StopUserClient<?>) {
                     if (!success) {
-                        Slog.w(getTag(), "StopUserClient failed(), is the HAL stuck? "
+                        Slog.w(TAG, "StopUserClient failed(), is the HAL stuck? "
                                 + "Clearing mStopUserClient");
                     }
                     mStopUserClient = null;
@@ -82,7 +86,7 @@
                 } else {
                     // can happen if the hal dies and is usually okay
                     // do not unset the current operation that may be newer
-                    Slog.w(getTag(), "operation is already null or different (reset?): "
+                    Slog.w(TAG, "operation is already null or different (reset?): "
                             + mCurrentOperation);
                 }
                 startNextOperationIfIdle();
@@ -98,7 +102,7 @@
             @NonNull IBiometricService biometricService,
             @NonNull CurrentUserRetriever currentUserRetriever,
             @NonNull UserSwitchCallback userSwitchCallback) {
-        super(tag, handler, sensorType, gestureAvailabilityDispatcher, biometricService,
+        super(handler, sensorType, gestureAvailabilityDispatcher, biometricService,
                 LOG_NUM_RECENT_OPERATIONS);
 
         mCurrentUserRetriever = currentUserRetriever;
@@ -117,18 +121,13 @@
     }
 
     @Override
-    protected String getTag() {
-        return BASE_TAG + "/" + mBiometricTag;
-    }
-
-    @Override
     protected void startNextOperationIfIdle() {
         if (mCurrentOperation != null) {
-            Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation);
+            Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation);
             return;
         }
         if (mPendingOperations.isEmpty()) {
-            Slog.d(getTag(), "No operations, returning to idle");
+            Slog.d(TAG, "No operations, returning to idle");
             return;
         }
 
@@ -143,20 +142,20 @@
             final ClientFinishedCallback finishedCallback =
                     new ClientFinishedCallback(startClient);
 
-            Slog.d(getTag(), "[Starting User] " + startClient);
+            Slog.d(TAG, "[Starting User] " + startClient);
             mCurrentOperation = new BiometricSchedulerOperation(
                     startClient, finishedCallback, STATE_STARTED);
             startClient.start(finishedCallback);
         } else {
             if (mStopUserClient != null) {
-                Slog.d(getTag(), "[Waiting for StopUser] " + mStopUserClient);
+                Slog.d(TAG, "[Waiting for StopUser] " + mStopUserClient);
             } else {
                 mStopUserClient = mUserSwitchCallback
                         .getStopUserClient(currentUserId);
                 final ClientFinishedCallback finishedCallback =
                         new ClientFinishedCallback(mStopUserClient);
 
-                Slog.d(getTag(), "[Stopping User] current: " + currentUserId
+                Slog.d(TAG, "[Stopping User] current: " + currentUserId
                         + ", next: " + nextUserId + ". " + mStopUserClient);
                 mCurrentOperation = new BiometricSchedulerOperation(
                         mStopUserClient, finishedCallback, STATE_STARTED);
@@ -168,11 +167,11 @@
     @Override
     public void onUserStopped() {
         if (mStopUserClient == null) {
-            Slog.e(getTag(), "Unexpected onUserStopped");
+            Slog.e(TAG, "Unexpected onUserStopped");
             return;
         }
 
-        Slog.d(getTag(), "[OnUserStopped]: " + mStopUserClient);
+        Slog.d(TAG, "[OnUserStopped]: " + mStopUserClient);
         mStopUserClient.onUserStopped();
         mStopUserClient = null;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/UserSwitchProvider.java b/services/core/java/com/android/server/biometrics/sensors/UserSwitchProvider.java
new file mode 100644
index 0000000..bc5c55b
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/UserSwitchProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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;
+
+import android.annotation.NonNull;
+
+/**
+ * Interface to get the appropriate start and stop user clients.
+ *
+ * @param <T> Hal instance for starting the user.
+ * @param <U> Session associated with the current user id.
+ */
+public interface UserSwitchProvider<T, U> {
+    @NonNull
+    StartUserClient<T, U> getStartUserClient(int newUserId);
+    @NonNull
+    StopUserClient<U> getStopUserClient(int userId);
+}
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 af46f44..3d61f99 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
@@ -53,12 +53,10 @@
         mAidlResponseHandler = aidlResponseHandler;
     }
 
-    /** The underlying {@link ISession}. */
     @NonNull public ISession getSession() {
         return mSession;
     }
 
-    /** The user id associated with the session. */
     public int getUserId() {
         return mUserId;
     }
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 9fa15b8..e4ecf1a 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
@@ -39,6 +39,7 @@
 import android.hardware.face.IFaceServiceReceiver;
 import android.os.Binder;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -88,6 +89,8 @@
  * Provider for a single instance of the {@link IFace} HAL.
  */
 public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
+
+    private static final String TAG = "FaceProvider";
     private static final int ENROLL_TIMEOUT_SEC = 75;
 
     private boolean mTestHalEnabled;
@@ -159,7 +162,7 @@
             @NonNull BiometricContext biometricContext,
             boolean resetLockoutRequiresChallenge) {
         this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
-                biometricContext, null /* daemon */, resetLockoutRequiresChallenge,
+                biometricContext, null /* daemon */, getHandler(), resetLockoutRequiresChallenge,
                 false /* testHalEnabled */);
     }
 
@@ -169,13 +172,19 @@
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull BiometricContext biometricContext,
-            @Nullable IFace daemon, boolean resetLockoutRequiresChallenge,
+            @Nullable IFace daemon,
+            @NonNull Handler handler,
+            boolean resetLockoutRequiresChallenge,
             boolean testHalEnabled) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
         mHalInstanceName = halInstanceName;
         mFaceSensors = new SensorList<>(ActivityManager.getService());
-        mHandler = new Handler(Looper.getMainLooper());
+        if (Flags.deHidl()) {
+            mHandler = handler;
+        } else {
+            mHandler = new Handler(Looper.getMainLooper());
+        }
         mUsageStats = new UsageStats(context);
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mActivityTaskManager = ActivityTaskManager.getInstance();
@@ -189,6 +198,13 @@
         initSensors(resetLockoutRequiresChallenge, props);
     }
 
+    @NonNull
+    private static Handler getHandler() {
+        HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        return new Handler(handlerThread.getLooper());
+    }
+
     private void initAuthenticationBroadcastReceiver() {
         new AuthenticationStatsBroadcastReceiver(
                 mContext,
@@ -230,8 +246,8 @@
                         prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
                         prop.supportsDetectInteraction, prop.halControlsPreview,
                         false /* resetLockoutRequiresChallenge */);
-                final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this,
-                        mContext, mHandler, internalProp, mLockoutResetDispatcher,
+                final Sensor sensor = new Sensor(this,
+                        mContext, mHandler, internalProp,
                         mBiometricContext);
                 sensor.init(mLockoutResetDispatcher, this);
                 final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
@@ -250,9 +266,8 @@
 
     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,
+        final Sensor sensor = new HidlToAidlSensorAdapter(this, mContext, mHandler, prop,
+                mLockoutResetDispatcher, mBiometricContext, resetLockoutRequiresChallenge,
                 () -> {
                     //TODO: update to make this testable
                     scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(),
@@ -279,8 +294,7 @@
 
     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,
+        final Sensor sensor = new Sensor(this, mContext, mHandler, prop, mBiometricContext,
                 resetLockoutRequiresChallenge);
         sensor.init(mLockoutResetDispatcher, this);
         final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
@@ -296,7 +310,7 @@
     }
 
     private String getTag() {
-        return "FaceProvider/" + mHalInstanceName;
+        return TAG + "/" + mHalInstanceName;
     }
 
     boolean hasHalInstance() {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java
index 0110ae9..e5ae8e3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.face.ISession;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -30,10 +31,10 @@
 
 import java.util.function.Supplier;
 
-public class FaceStopUserClient extends StopUserClient<AidlSession> {
+public class FaceStopUserClient extends StopUserClient<ISession> {
     private static final String TAG = "FaceStopUserClient";
 
-    public FaceStopUserClient(@NonNull Context context, @NonNull Supplier<AidlSession> lazyDaemon,
+    public FaceStopUserClient(@NonNull Context context, @NonNull Supplier<ISession> lazyDaemon,
             @Nullable IBinder token, int userId, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
             @NonNull UserStoppedCallback callback) {
@@ -49,7 +50,7 @@
     @Override
     protected void startHalOperation() {
         try {
-            getFreshDaemon().getSession().close();
+            getFreshDaemon().close();
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
             getCallback().onClientFinished(this, false /* 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 3e5c599..635e79a 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
@@ -58,6 +58,7 @@
 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.UserSwitchProvider;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 
 import java.util.ArrayList;
@@ -71,15 +72,16 @@
  */
 public class Sensor {
 
+    private static final String TAG = "Sensor";
+
     private boolean mTestHalEnabled;
 
-    @NonNull private final String mTag;
     @NonNull private final FaceProvider mProvider;
     @NonNull private final Context mContext;
     @NonNull private final IBinder mToken;
     @NonNull private final Handler mHandler;
     @NonNull private final FaceSensorPropertiesInternal mSensorProperties;
-    @NonNull private BiometricScheduler mScheduler;
+    @NonNull private BiometricScheduler<IFace, ISession> mScheduler;
     @Nullable private LockoutTracker mLockoutTracker;
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
 
@@ -88,11 +90,9 @@
     @NonNull BiometricContext mBiometricContext;
 
 
-    Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
+    Sensor(@NonNull FaceProvider provider, @NonNull Context context,
             @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
-            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext, @Nullable AidlSession session) {
-        mTag = tag;
+            @NonNull BiometricContext biometricContext) {
         mProvider = provider;
         mContext = context;
         mToken = new Binder();
@@ -102,105 +102,135 @@
         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,
+    public Sensor(@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,
+        this(provider, context, handler,
                 getFaceSensorPropertiesInternal(prop, resetLockoutRequiresChallenge),
-                lockoutResetDispatcher, biometricContext, null);
+                biometricContext);
     }
 
     /**
      * Initialize biometric scheduler, lockout tracker and session for the sensor.
      */
-    public void init(LockoutResetDispatcher lockoutResetDispatcher,
+    public void init(@NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull FaceProvider provider) {
+        if (Flags.deHidl()) {
+            setScheduler(getBiometricSchedulerForInit(lockoutResetDispatcher, provider));
+        } else {
+            setScheduler(getUserAwareBiometricSchedulerForInit(lockoutResetDispatcher, provider));
+        }
+        mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
+        mLockoutTracker = new LockoutCache();
+    }
+
+    private BiometricScheduler<IFace, ISession> getBiometricSchedulerForInit(
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull FaceProvider provider) {
+        return new BiometricScheduler<>(mHandler,
+                BiometricScheduler.SENSOR_TYPE_FACE,
+                null /* gestureAvailabilityDispatcher */,
+                () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
+                new UserSwitchProvider<IFace, ISession>() {
+                    @NonNull
+                    @Override
+                    public StopUserClient<ISession> getStopUserClient(int userId) {
+                        return new FaceStopUserClient(mContext,
+                                () -> mLazySession.get().getSession(), mToken, userId,
+                                mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext),
+                                mBiometricContext, () -> mCurrentSession = null);
+                    }
+
+                    @NonNull
+                    @Override
+                    public StartUserClient<IFace, ISession> getStartUserClient(int newUserId) {
+                        final int sensorId = mSensorProperties.sensorId;
+                        final AidlResponseHandler 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(TAG, "Face sensor hardware unavailable.");
+                                        mCurrentSession = null;
+                                    }
+                                });
+
+                        return Sensor.this.getStartUserClient(resultController, sensorId,
+                                newUserId, provider);
+                    }
+                });
+    }
+
+    private UserAwareBiometricScheduler<IFace, ISession> getUserAwareBiometricSchedulerForInit(
+            LockoutResetDispatcher lockoutResetDispatcher,
             FaceProvider provider) {
-        mScheduler = new UserAwareBiometricScheduler(mTag,
+        return new UserAwareBiometricScheduler<>(TAG,
                 BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityDispatcher */,
                 () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
                 new UserAwareBiometricScheduler.UserSwitchCallback() {
                     @NonNull
                     @Override
-                    public StopUserClient<?> getStopUserClient(int userId) {
-                        return new FaceStopUserClient(mContext, mLazySession, mToken, userId,
-                                mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
-                                () -> mCurrentSession = null);
+                    public StopUserClient<ISession> getStopUserClient(int userId) {
+                        return new FaceStopUserClient(mContext,
+                                () -> mLazySession.get().getSession(), mToken, userId,
+                                mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext),
+                                mBiometricContext, () -> mCurrentSession = null);
                     }
 
                     @NonNull
                     @Override
-                    public StartUserClient<?, ?> getStartUserClient(int newUserId) {
+                    public StartUserClient<IFace, ISession> getStartUserClient(int newUserId) {
                         final int sensorId = mSensorProperties.sensorId;
+                        final AidlResponseHandler resultController = new AidlResponseHandler(
+                                mContext, mScheduler, sensorId, newUserId,
+                                mLockoutTracker, lockoutResetDispatcher,
+                                mBiometricContext.getAuthSessionCoordinator(), () -> {
+                                    Slog.e(TAG, "Face sensor hardware 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) -> {
-                                    Slog.d(mTag, "New session created for user: "
-                                            + userIdStarted + " with hal version: "
-                                            + halInterfaceVersion);
-                                    mCurrentSession = new AidlSession(halInterfaceVersion,
-                                            newSession, userIdStarted, resultController);
-                                    if (FaceUtils.getLegacyInstance(sensorId)
-                                            .isInvalidationInProgress(mContext, userIdStarted)) {
-                                        Slog.w(mTag,
-                                                "Scheduling unfinished invalidation request for "
-                                                        + "sensor: "
-                                                        + sensorId
-                                                        + ", user: " + userIdStarted);
-                                        provider.scheduleInvalidationRequest(sensorId,
-                                                userIdStarted);
-                                    }
-                                };
-
-                        return new FaceStartUserClient(mContext, provider::getHalInstance,
-                                mToken, newUserId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
-                                resultController, userStartedCallback);
+                        return Sensor.this.getStartUserClient(resultController, sensorId,
+                                newUserId, provider);
                     }
                 });
-        mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
-        mLockoutTracker = new LockoutCache();
+    }
+
+    private FaceStartUserClient getStartUserClient(@NonNull AidlResponseHandler resultController,
+            int sensorId, int newUserId, @NonNull FaceProvider provider) {
+        final StartUserClient.UserStartedCallback<ISession> userStartedCallback =
+                (userIdStarted, newSession, halInterfaceVersion) -> {
+                    Slog.d(TAG, "New face session created for user: "
+                            + userIdStarted + " with hal version: "
+                            + halInterfaceVersion);
+                    mCurrentSession = new AidlSession(halInterfaceVersion,
+                            newSession, userIdStarted, resultController);
+                    if (FaceUtils.getLegacyInstance(sensorId)
+                            .isInvalidationInProgress(mContext, userIdStarted)) {
+                        Slog.w(TAG,
+                                "Scheduling unfinished invalidation request for "
+                                        + "face sensor: "
+                                        + sensorId
+                                        + ", user: " + userIdStarted);
+                        provider.scheduleInvalidationRequest(sensorId,
+                                userIdStarted);
+                    }
+                };
+
+        return new FaceStartUserClient(mContext, provider::getHalInstance, mToken, newUserId,
+                mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext,
+                resultController, userStartedCallback);
     }
 
     private static FaceSensorPropertiesInternal getFaceSensorPropertiesInternal(SensorProps prop,
@@ -213,13 +243,11 @@
                         info.softwareVersion));
             }
         }
-        final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+        return new FaceSensorPropertiesInternal(
                 prop.commonProps.sensorId, prop.commonProps.sensorStrength,
                 prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
                 prop.supportsDetectInteraction, prop.halControlsPreview,
                 resetLockoutRequiresChallenge);
-
-        return internalProp;
     }
 
     @NonNull public Supplier<AidlSession> getLazySession() {
@@ -243,7 +271,7 @@
                 mProvider, this);
     }
 
-    @NonNull public BiometricScheduler getScheduler() {
+    @NonNull public BiometricScheduler<IFace, ISession> getScheduler() {
         return mScheduler;
     }
 
@@ -259,17 +287,17 @@
     }
 
     void setTestHalEnabled(boolean enabled) {
-        Slog.w(mTag, "setTestHalEnabled: " + enabled);
+        Slog.w(TAG, "Face setTestHalEnabled: " + enabled);
         if (enabled != mTestHalEnabled) {
             // The framework should retrieve a new session from the HAL.
             try {
                 if (mCurrentSession != null) {
                     // TODO(181984005): This should be scheduled instead of directly invoked
-                    Slog.d(mTag, "Closing old session");
+                    Slog.d(TAG, "Closing old face session");
                     mCurrentSession.getSession().close();
                 }
             } catch (RemoteException e) {
-                Slog.e(mTag, "RemoteException", e);
+                Slog.e(TAG, "RemoteException", e);
             }
             mCurrentSession = null;
         }
@@ -308,7 +336,7 @@
     public void onBinderDied() {
         final BaseClientMonitor client = mScheduler.getCurrentClient();
         if (client != null && client.isInterruptable()) {
-            Slog.e(mTag, "Sending ERROR_HW_UNAVAILABLE for client: " + client);
+            Slog.e(TAG, "Sending face hardware unavailable error for client: " + client);
             final ErrorConsumer errorConsumer = (ErrorConsumer) client;
             errorConsumer.onError(FaceManager.FACE_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index 46ce0b6..5337666 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -120,7 +120,7 @@
     @NonNull private final FaceSensorPropertiesInternal mSensorProperties;
     @NonNull private final BiometricStateCallback mBiometricStateCallback;
     @NonNull private final Context mContext;
-    @NonNull private final BiometricScheduler mScheduler;
+    @NonNull private final BiometricScheduler<IBiometricsFace, AidlSession> mScheduler;
     @NonNull private final Handler mHandler;
     @NonNull private final Supplier<IBiometricsFace> mLazyDaemon;
     @NonNull private final LockoutHalImpl mLockoutTracker;
@@ -163,14 +163,15 @@
         private final int mSensorId;
         @NonNull private final Context mContext;
         @NonNull private final Handler mHandler;
-        @NonNull private final BiometricScheduler mScheduler;
+        @NonNull private final BiometricScheduler<IBiometricsFace, AidlSession> mScheduler;
         @Nullable private Callback mCallback;
         @NonNull private final LockoutHalImpl mLockoutTracker;
         @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher;
 
 
         HalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler,
-                @NonNull BiometricScheduler scheduler, @NonNull LockoutHalImpl lockoutTracker,
+                @NonNull BiometricScheduler<IBiometricsFace, AidlSession> scheduler,
+                @NonNull LockoutHalImpl lockoutTracker,
                 @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
             mSensorId = sensorId;
             mContext = context;
@@ -352,7 +353,7 @@
             @NonNull FaceSensorPropertiesInternal sensorProps,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull Handler handler,
-            @NonNull BiometricScheduler scheduler,
+            @NonNull BiometricScheduler<IBiometricsFace, AidlSession> scheduler,
             @NonNull BiometricContext biometricContext) {
         mSensorProperties = sensorProps;
         mContext = context;
@@ -395,7 +396,8 @@
             @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
         final Handler handler = new Handler(Looper.getMainLooper());
         return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher,
-                handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
+                handler, new BiometricScheduler<>(
+                        BiometricScheduler.SENSOR_TYPE_FACE,
                         null /* gestureAvailabilityTracker */),
                 BiometricContext.getInstance(context));
     }
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
index 6355cb5..a004cae4 100644
--- 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
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.SensorProps;
 import android.hardware.biometrics.face.V1_0.IBiometricsFace;
 import android.os.Handler;
@@ -67,8 +68,7 @@
             };
     private LockoutHalImpl mLockoutTracker;
 
-    public HidlToAidlSensorAdapter(@NonNull String tag,
-            @NonNull FaceProvider provider,
+    public HidlToAidlSensorAdapter(@NonNull FaceProvider provider,
             @NonNull Context context,
             @NonNull Handler handler,
             @NonNull SensorProps prop,
@@ -76,15 +76,14 @@
             @NonNull BiometricContext biometricContext,
             boolean resetLockoutRequiresChallenge,
             @NonNull Runnable internalCleanupAndGetFeatureRunnable) {
-        this(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
+        this(provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
                 resetLockoutRequiresChallenge, internalCleanupAndGetFeatureRunnable,
                 new AuthSessionCoordinator(), null /* daemon */,
                 null /* onEnrollSuccessCallback */);
     }
 
     @VisibleForTesting
-    HidlToAidlSensorAdapter(@NonNull String tag,
-            @NonNull FaceProvider provider,
+    HidlToAidlSensorAdapter(@NonNull FaceProvider provider,
             @NonNull Context context,
             @NonNull Handler handler,
             @NonNull SensorProps prop,
@@ -95,7 +94,7 @@
             @NonNull AuthSessionCoordinator authSessionCoordinator,
             @Nullable IBiometricsFace daemon,
             @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) {
-        super(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
+        super(provider, context, handler, prop, biometricContext,
                 resetLockoutRequiresChallenge);
         mInternalCleanupAndGetFeatureRunnable = internalCleanupAndGetFeatureRunnable;
         mFaceProvider = provider;
@@ -124,7 +123,7 @@
 
     @Override
     public void serviceDied(long cookie) {
-        Slog.d(TAG, "HAL died.");
+        Slog.d(TAG, "Face HAL died.");
         mDaemon = null;
     }
 
@@ -140,10 +139,12 @@
     }
 
     @Override
-    public void init(LockoutResetDispatcher lockoutResetDispatcher,
-            FaceProvider provider) {
-        setScheduler(new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
-                null /* gestureAvailabilityTracker */));
+    public void init(@NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull FaceProvider provider) {
+        setScheduler(new BiometricScheduler<ISession, AidlSession>(getHandler(),
+                BiometricScheduler.SENSOR_TYPE_FACE,
+                null /* gestureAvailabilityTracker */, () -> mCurrentUserId,
+                null /* userSwitchProvider */));
         setLazySession(this::getSession);
         mLockoutTracker = new LockoutHalImpl();
     }
@@ -188,7 +189,7 @@
             return mDaemon;
         }
 
-        Slog.d(TAG, "Daemon was null, reconnecting, current operation: "
+        Slog.d(TAG, "Face daemon was null, reconnecting, current operation: "
                 + getScheduler().getCurrentClient());
 
         try {
@@ -213,7 +214,7 @@
     }
 
     @VisibleForTesting void handleUserChanged(int newUserId) {
-        Slog.d(TAG, "User changed. Current user is " + newUserId);
+        Slog.d(TAG, "User changed. Current user for face sensor is " + newUserId);
         mSession = null;
         mCurrentUserId = newUserId;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
index 5daf2d4..fa95361 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
@@ -282,7 +282,7 @@
 
     @Override
     public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) {
-        //Unsupported in HIDL
+        Slog.e(TAG, "enrollWithOptions unsupported in HIDL");
         return null;
     }
 
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 8ff105b..0d4dac0 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
@@ -51,12 +51,10 @@
         mAidlResponseHandler = aidlResponseHandler;
     }
 
-    /** The underlying {@link ISession}. */
     @NonNull public ISession getSession() {
         return mSession;
     }
 
-    /** The user id associated with the session. */
     public int getUserId() {
         return mUserId;
     }
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 88a11d9..c0388d1 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
@@ -46,6 +46,7 @@
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Binder;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -102,6 +103,8 @@
 @SuppressWarnings("deprecation")
 public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvider {
 
+    private static final String TAG = "FingerprintProvider";
+
     private boolean mTestHalEnabled;
 
     @NonNull
@@ -172,7 +175,7 @@
             boolean resetLockoutRequiresHardwareAuthToken) {
         this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName,
                 lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext,
-                null /* daemon */, resetLockoutRequiresHardwareAuthToken,
+                null /* daemon */, getHandler(), resetLockoutRequiresHardwareAuthToken,
                 false /* testHalEnabled */);
     }
 
@@ -184,6 +187,7 @@
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext,
             @Nullable IFingerprint daemon,
+            @NonNull Handler handler,
             boolean resetLockoutRequiresHardwareAuthToken,
             boolean testHalEnabled) {
         mContext = context;
@@ -191,7 +195,11 @@
         mAuthenticationStateListeners = authenticationStateListeners;
         mHalInstanceName = halInstanceName;
         mFingerprintSensors = new SensorList<>(ActivityManager.getService());
-        mHandler = new Handler(Looper.getMainLooper());
+        if (Flags.deHidl()) {
+            mHandler = handler;
+        } else {
+            mHandler = new Handler(Looper.getMainLooper());
+        }
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mActivityTaskManager = ActivityTaskManager.getInstance();
         mTaskStackListener = new BiometricTaskStackListener();
@@ -204,6 +212,13 @@
         initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher);
     }
 
+    @NonNull
+    private static Handler getHandler() {
+        HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        return new Handler(handlerThread.getLooper());
+    }
+
     private void initAuthenticationBroadcastReceiver() {
         new AuthenticationStatsBroadcastReceiver(
                 mContext,
@@ -262,11 +277,9 @@
                                                                 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 Sensor sensor = new Sensor(this, mContext, mHandler, internalProp,
+                        mBiometricContext);
+                sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher);
                 final int sessionUserId =
                         sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
                                 sensor.getLazySession().get().getUserId();
@@ -286,10 +299,8 @@
             @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,
+        final Sensor sensor = new HidlToAidlSensorAdapter(this, mContext, mHandler, prop,
+                mLockoutResetDispatcher, mBiometricContext, resetLockoutRequiresHardwareAuthToken,
                 () -> scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(),
                         null /* callback */));
         sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher);
@@ -307,14 +318,11 @@
 
     private void addAidlSensors(@NonNull SensorProps prop,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            List<SensorLocationInternal> workaroundLocations,
+            @NonNull 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);
+        final Sensor sensor = new Sensor(this, mContext, mHandler, prop, mBiometricContext,
+                workaroundLocations, resetLockoutRequiresHardwareAuthToken);
         sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher);
         final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
                 sensor.getLazySession().get().getUserId();
@@ -329,7 +337,7 @@
     }
 
     private String getTag() {
-        return "FingerprintProvider/" + mHalInstanceName;
+        return TAG + "/" + mHalInstanceName;
     }
 
     boolean hasHalInstance() {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java
index 2cc1879..394f045 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.fingerprint.ISession;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -30,11 +31,11 @@
 
 import java.util.function.Supplier;
 
-public class FingerprintStopUserClient extends StopUserClient<AidlSession> {
+public class FingerprintStopUserClient extends StopUserClient<ISession> {
     private static final String TAG = "FingerprintStopUserClient";
 
     public FingerprintStopUserClient(@NonNull Context context,
-            @NonNull Supplier<AidlSession> lazyDaemon, @Nullable IBinder token, int userId,
+            @NonNull Supplier<ISession> lazyDaemon, @Nullable IBinder token, int userId,
             int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
             @NonNull UserStoppedCallback callback) {
@@ -50,7 +51,7 @@
     @Override
     protected void startHalOperation() {
         try {
-            getFreshDaemon().getSession().close();
+            getFreshDaemon().close();
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
             getCallback().onClientFinished(this, false /* success */);
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 dd887bb..af88c62 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
@@ -59,6 +59,7 @@
 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.UserSwitchProvider;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
@@ -77,15 +78,16 @@
 @SuppressWarnings("deprecation")
 public class Sensor {
 
+    private static final String TAG = "Sensor";
+
     private boolean mTestHalEnabled;
 
-    @NonNull private final String mTag;
     @NonNull private final FingerprintProvider mProvider;
     @NonNull private final Context mContext;
     @NonNull private final IBinder mToken;
     @NonNull private final Handler mHandler;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties;
-    @NonNull private BiometricScheduler mScheduler;
+    @NonNull private BiometricScheduler<IFingerprint, ISession> mScheduler;
     @NonNull private LockoutTracker mLockoutTracker;
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
     @NonNull private final BiometricContext mBiometricContext;
@@ -93,13 +95,10 @@
     @Nullable AidlSession mCurrentSession;
     @NonNull private Supplier<AidlSession> mLazySession;
 
-    public Sensor(@NonNull String tag, @NonNull FingerprintProvider provider,
+    public Sensor(@NonNull FingerprintProvider provider,
             @NonNull Context context, @NonNull Handler handler,
             @NonNull FingerprintSensorPropertiesInternal sensorProperties,
-            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext, AidlSession session) {
-        mTag = tag;
         mProvider = provider;
         mContext = context;
         mToken = new Binder();
@@ -110,41 +109,52 @@
         mCurrentSession = session;
     }
 
-    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+    Sensor(@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);
+        this(provider, context, handler, sensorProperties,
+                biometricContext, null);
     }
 
-    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+    Sensor(@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,
+        this(provider, context, handler, getFingerprintSensorPropertiesInternal(sensorProp,
                         workaroundLocation, resetLockoutRequiresHardwareAuthToken),
-                lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext, null);
+                biometricContext, null);
     }
 
     /**
      * Initialize biometric scheduler, lockout tracker and session for the sensor.
      */
-    public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            LockoutResetDispatcher lockoutResetDispatcher) {
-        mScheduler = new UserAwareBiometricScheduler(mTag,
+    public void init(@NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
+        if (Flags.deHidl()) {
+            setScheduler(getBiometricSchedulerForInit(gestureAvailabilityDispatcher,
+                    lockoutResetDispatcher));
+        } else {
+            setScheduler(getUserAwareBiometricSchedulerForInit(gestureAvailabilityDispatcher,
+                    lockoutResetDispatcher));
+        }
+        mLockoutTracker = new LockoutCache();
+        mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
+    }
+
+    private BiometricScheduler<IFingerprint, ISession> getBiometricSchedulerForInit(
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
+        return new BiometricScheduler<>(mHandler,
                 BiometricScheduler.sensorTypeFromFingerprintProperties(mSensorProperties),
                 gestureAvailabilityDispatcher,
                 () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
-                new UserAwareBiometricScheduler.UserSwitchCallback() {
+                new UserSwitchProvider<IFingerprint, ISession>() {
                     @NonNull
                     @Override
-                    public StopUserClient<?> getStopUserClient(int userId) {
-                        return new FingerprintStopUserClient(mContext, mLazySession, mToken,
+                    public StopUserClient<ISession> getStopUserClient(int userId) {
+                        return new FingerprintStopUserClient(mContext,
+                                () -> mLazySession.get().getSession(), mToken,
                                 userId, mSensorProperties.sensorId,
                                 BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 () -> mCurrentSession = null);
@@ -152,69 +162,100 @@
 
                     @NonNull
                     @Override
-                    public StartUserClient<?, ?> getStartUserClient(int newUserId) {
+                    public StartUserClient<IFingerprint, ISession> getStartUserClient(
+                            int newUserId) {
                         final int sensorId = mSensorProperties.sensorId;
-
-                        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) -> {
-                                    Slog.d(mTag, "New session created for user: "
-                                            + userIdStarted + " with hal version: "
-                                            + halInterfaceVersion);
-                                    mCurrentSession = new AidlSession(halInterfaceVersion,
-                                            newSession, userIdStarted, resultController);
-                                    if (FingerprintUtils.getInstance(sensorId)
-                                            .isInvalidationInProgress(mContext, userIdStarted)) {
-                                        Slog.w(mTag,
-                                                "Scheduling unfinished invalidation request for "
-                                                        + "sensor: "
-                                                        + sensorId
-                                                        + ", user: " + userIdStarted);
+                        final AidlResponseHandler 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,
-                                                userIdStarted);
+                                                newUserId);
                                     }
-                                };
 
-                        return new FingerprintStartUserClient(mContext, mProvider::getHalInstance,
-                                mToken, newUserId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
-                                resultController, userStartedCallback);
+                                    @Override
+                                    public void onHardwareUnavailable() {
+                                        Slog.e(TAG,
+                                                "Fingerprint sensor hardware unavailable.");
+                                        mCurrentSession = null;
+                                    }
+                                });
+
+                        return Sensor.this.getStartUserClient(resultController, sensorId,
+                                newUserId);
                     }
                 });
-        mLockoutTracker = new LockoutCache();
-        mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
+    }
+
+    private UserAwareBiometricScheduler<ISession, AidlSession>
+            getUserAwareBiometricSchedulerForInit(
+                    GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+                    LockoutResetDispatcher lockoutResetDispatcher) {
+        return new UserAwareBiometricScheduler<>(TAG,
+                BiometricScheduler.sensorTypeFromFingerprintProperties(mSensorProperties),
+                gestureAvailabilityDispatcher,
+                () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
+                new UserAwareBiometricScheduler.UserSwitchCallback() {
+                    @NonNull
+                    @Override
+                    public StopUserClient<ISession> getStopUserClient(int userId) {
+                        return new FingerprintStopUserClient(mContext,
+                                () -> mLazySession.get().getSession(), mToken,
+                                userId, mSensorProperties.sensorId,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
+                                () -> mCurrentSession = null);
+                    }
+
+                    @NonNull
+                    @Override
+                    public StartUserClient<IFingerprint, ISession> getStartUserClient(
+                            int newUserId) {
+                        final int sensorId = mSensorProperties.sensorId;
+
+                        final AidlResponseHandler resultController = new AidlResponseHandler(
+                                mContext, mScheduler, sensorId, newUserId,
+                                mLockoutTracker, lockoutResetDispatcher,
+                                mBiometricContext.getAuthSessionCoordinator(), () -> {
+                                    Slog.e(TAG, "Fingerprint hardware unavailable.");
+                                    mCurrentSession = null;
+                                });
+
+                        return Sensor.this.getStartUserClient(resultController, sensorId,
+                                newUserId);
+                    }
+                });
+    }
+
+    private FingerprintStartUserClient getStartUserClient(AidlResponseHandler resultController,
+            int sensorId, int newUserId) {
+        final StartUserClient.UserStartedCallback<ISession> userStartedCallback =
+                (userIdStarted, newSession, halInterfaceVersion) -> {
+                    Slog.d(TAG, "New fingerprint session created for user: "
+                            + userIdStarted + " with hal version: "
+                            + halInterfaceVersion);
+                    mCurrentSession = new AidlSession(halInterfaceVersion,
+                            newSession, userIdStarted, resultController);
+                    if (FingerprintUtils.getInstance(sensorId)
+                            .isInvalidationInProgress(mContext, userIdStarted)) {
+                        Slog.w(TAG,
+                                "Scheduling unfinished invalidation request for "
+                                        + "fingerprint sensor: "
+                                        + sensorId
+                                        + ", user: " + userIdStarted);
+                        mProvider.scheduleInvalidationRequest(sensorId,
+                                userIdStarted);
+                    }
+                };
+
+        return new FingerprintStartUserClient(mContext, mProvider::getHalInstance,
+                mToken, newUserId, mSensorProperties.sensorId,
+                BiometricLogger.ofUnknown(mContext), mBiometricContext,
+                resultController, userStartedCallback);
     }
 
     protected static FingerprintSensorPropertiesInternal getFingerprintSensorPropertiesInternal(
@@ -267,7 +308,7 @@
                 biometricStateCallback, mProvider, this);
     }
 
-    @NonNull public BiometricScheduler getScheduler() {
+    @NonNull public BiometricScheduler<IFingerprint, ISession> getScheduler() {
         return mScheduler;
     }
 
@@ -283,17 +324,17 @@
     }
 
     void setTestHalEnabled(boolean enabled) {
-        Slog.w(mTag, "setTestHalEnabled: " + enabled);
+        Slog.w(TAG, "Fingerprint setTestHalEnabled: " + enabled);
         if (enabled != mTestHalEnabled) {
             // The framework should retrieve a new session from the HAL.
             try {
                 if (mCurrentSession != null) {
                     // TODO(181984005): This should be scheduled instead of directly invoked
-                    Slog.d(mTag, "Closing old session");
+                    Slog.d(TAG, "Closing old fingerprint session");
                     mCurrentSession.getSession().close();
                 }
             } catch (RemoteException e) {
-                Slog.e(mTag, "RemoteException", e);
+                Slog.e(TAG, "RemoteException", e);
             }
             mCurrentSession = null;
         }
@@ -335,7 +376,7 @@
     public void onBinderDied() {
         final BaseClientMonitor client = mScheduler.getCurrentClient();
         if (client instanceof ErrorConsumer) {
-            Slog.e(mTag, "Sending ERROR_HW_UNAVAILABLE for client: " + client);
+            Slog.e(TAG, "Sending fingerprint hardware unavailable error for client: " + client);
             final ErrorConsumer errorConsumer = (ErrorConsumer) client;
             errorConsumer.onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index d3cecd0..4accf8f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -119,7 +119,7 @@
     @NonNull private final AuthenticationStateListeners mAuthenticationStateListeners;
     private final ActivityTaskManager mActivityTaskManager;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties;
-    private final BiometricScheduler mScheduler;
+    private final BiometricScheduler<IBiometricsFingerprint, AidlSession> mScheduler;
     private final Handler mHandler;
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     private final LockoutFrameworkImpl mLockoutTracker;
@@ -198,11 +198,11 @@
         private final int mSensorId;
         @NonNull private final Context mContext;
         @NonNull final Handler mHandler;
-        @NonNull final BiometricScheduler mScheduler;
+        @NonNull final BiometricScheduler<IBiometricsFingerprint, AidlSession> mScheduler;
         @Nullable private Callback mCallback;
 
         HalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler,
-                @NonNull BiometricScheduler scheduler) {
+                @NonNull BiometricScheduler<IBiometricsFingerprint, AidlSession> scheduler) {
             mSensorId = sensorId;
             mContext = context;
             mHandler = handler;
@@ -336,7 +336,7 @@
             @NonNull BiometricStateCallback biometricStateCallback,
             @NonNull AuthenticationStateListeners authenticationStateListeners,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
-            @NonNull BiometricScheduler scheduler,
+            @NonNull BiometricScheduler<IBiometricsFingerprint, AidlSession> scheduler,
             @NonNull Handler handler,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull HalResultController controller,
@@ -389,8 +389,8 @@
             @NonNull Handler handler,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
-        final BiometricScheduler scheduler =
-                new BiometricScheduler(TAG,
+        final BiometricScheduler<IBiometricsFingerprint, AidlSession> scheduler =
+                new BiometricScheduler<>(
                         BiometricScheduler.sensorTypeFromFingerprintProperties(sensorProps),
                         gestureAvailabilityDispatcher);
         final HalResultController controller = new HalResultController(sensorProps.sensorId,
@@ -533,8 +533,8 @@
     private void scheduleUpdateActiveUserWithoutHandler(int targetUserId, boolean force) {
         final boolean hasEnrolled =
                 !getEnrolledFingerprints(mSensorProperties.sensorId, targetUserId).isEmpty();
-        final FingerprintUpdateActiveUserClient client =
-                new FingerprintUpdateActiveUserClient(mContext, mLazyDaemon, targetUserId,
+        final FingerprintUpdateActiveUserClientLegacy client =
+                new FingerprintUpdateActiveUserClientLegacy(mContext, mLazyDaemon, targetUserId,
                         mContext.getOpPackageName(), mSensorProperties.sensorId,
                         createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
                                 BiometricsProtoEnums.CLIENT_UNKNOWN,
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
index 88dae6f..9232e11 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
@@ -140,9 +140,9 @@
     private static class TestableBiometricScheduler extends BiometricScheduler {
         @NonNull private Fingerprint21UdfpsMock mFingerprint21;
 
-        TestableBiometricScheduler(@NonNull String tag, @NonNull Handler handler,
+        TestableBiometricScheduler(
                 @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
-            super(tag, BiometricScheduler.SENSOR_TYPE_FP_OTHER, gestureAvailabilityDispatcher);
+            super(BiometricScheduler.SENSOR_TYPE_FP_OTHER, gestureAvailabilityDispatcher);
         }
 
         void init(@NonNull Fingerprint21UdfpsMock fingerprint21) {
@@ -258,7 +258,7 @@
 
         final Handler handler = new Handler(Looper.getMainLooper());
         final TestableBiometricScheduler scheduler =
-                new TestableBiometricScheduler(TAG, handler, gestureAvailabilityDispatcher);
+                new TestableBiometricScheduler(gestureAvailabilityDispatcher);
         final MockHalResultController controller =
                 new MockHalResultController(sensorProps.sensorId, context, handler, scheduler);
         return new Fingerprint21UdfpsMock(context, biometricStateCallback,
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 5c5b992..59e64cd 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
@@ -18,7 +18,7 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
-import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.hardware.biometrics.fingerprint.ISession;
 import android.os.Build;
 import android.os.Environment;
 import android.os.RemoteException;
@@ -39,8 +39,8 @@
 /**
  * Sets the HAL's current active user, and updates the framework's authenticatorId cache.
  */
-public class FingerprintUpdateActiveUserClient extends
-        StartUserClient<IBiometricsFingerprint, AidlSession> {
+public class FingerprintUpdateActiveUserClient extends StartUserClient<ISession,
+        AidlSession> {
 
     private static final String TAG = "FingerprintUpdateActiveUserClient";
     private static final String FP_DATA_DIR = "fpdata";
@@ -52,19 +52,7 @@
     private File mDirectory;
 
     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) {
-        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 Supplier<ISession> lazyDaemon, int userId,
             @NonNull String owner, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
             @NonNull Supplier<Integer> currentUserId,
@@ -132,9 +120,10 @@
         try {
             final int targetId = getTargetUserId();
             Slog.d(TAG, "Setting active user: " + targetId);
-            getFreshDaemon().setActiveGroup(targetId, mDirectory.getAbsolutePath());
+            HidlToAidlSessionAdapter sessionAdapter = (HidlToAidlSessionAdapter) getFreshDaemon();
+            sessionAdapter.setActiveGroup(targetId, mDirectory.getAbsolutePath());
             mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics
-                    ? getFreshDaemon().getAuthenticatorId() : 0L);
+                    ? sessionAdapter.getAuthenticatorIdForUpdateClient() : 0L);
             mUserStartedCallback.onUserStarted(targetId, null, 0);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClientLegacy.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClientLegacy.java
new file mode 100644
index 0000000..fc85402
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClientLegacy.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.content.Context;
+import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.os.Build;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.SELinux;
+import android.util.Slog;
+
+import com.android.server.biometrics.BiometricsProto;
+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 java.io.File;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * TODO(b/304604965): Delete this class once Flags.DE_HIDL is ready for release.
+ */
+public class FingerprintUpdateActiveUserClientLegacy extends
+        HalClientMonitor<IBiometricsFingerprint> {
+    private static final String TAG = "FingerprintUpdateActiveUserClient";
+    private static final String FP_DATA_DIR = "fpdata";
+
+    private final Supplier<Integer> mCurrentUserId;
+    private final boolean mForceUpdateAuthenticatorId;
+    private final boolean mHasEnrolledBiometrics;
+    private final Map<Integer, Long> mAuthenticatorIds;
+    private File mDirectory;
+
+    FingerprintUpdateActiveUserClientLegacy(@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) {
+        super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
+                0 /* cookie */, sensorId, logger, biometricContext);
+        mCurrentUserId = currentUserId;
+        mForceUpdateAuthenticatorId = forceUpdateAuthenticatorId;
+        mHasEnrolledBiometrics = hasEnrolledBiometrics;
+        mAuthenticatorIds = authenticatorIds;
+    }
+
+    @Override
+    public void start(@NonNull ClientMonitorCallback callback) {
+        super.start(callback);
+
+        if (mCurrentUserId.get() == getTargetUserId() && !mForceUpdateAuthenticatorId) {
+            Slog.d(TAG, "Already user: " + mCurrentUserId + ", returning");
+            callback.onClientFinished(this, true /* success */);
+            return;
+        }
+
+        int firstSdkInt = Build.VERSION.DEVICE_INITIAL_SDK_INT;
+        if (firstSdkInt < Build.VERSION_CODES.BASE) {
+            Slog.e(TAG, "First SDK version " + firstSdkInt + " is invalid; must be "
+                    + "at least VERSION_CODES.BASE");
+        }
+        File baseDir;
+        if (firstSdkInt <= Build.VERSION_CODES.O_MR1) {
+            baseDir = Environment.getUserSystemDirectory(getTargetUserId());
+        } else {
+            baseDir = Environment.getDataVendorDeDirectory(getTargetUserId());
+        }
+
+        mDirectory = new File(baseDir, FP_DATA_DIR);
+        if (!mDirectory.exists()) {
+            if (!mDirectory.mkdir()) {
+                Slog.e(TAG, "Cannot make directory: " + mDirectory.getAbsolutePath());
+                callback.onClientFinished(this, false /* success */);
+                return;
+            }
+            // Calling mkdir() from this process will create a directory with our
+            // permissions (inherited from the containing dir). This command fixes
+            // the label.
+            if (!SELinux.restorecon(mDirectory)) {
+                Slog.e(TAG, "Restorecons failed. Directory will have wrong label.");
+                callback.onClientFinished(this, false /* success */);
+                return;
+            }
+        }
+
+        startHalOperation();
+    }
+
+    @Override
+    public void unableToStart() {
+        // Nothing to do here
+    }
+
+    @Override
+    protected void startHalOperation() {
+        try {
+            final int targetId = getTargetUserId();
+            Slog.d(TAG, "Setting active user: " + targetId);
+            getFreshDaemon().setActiveGroup(targetId, mDirectory.getAbsolutePath());
+            mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics
+                    ? getFreshDaemon().getAuthenticatorId() : 0L);
+            mCallback.onClientFinished(this, true /* success */);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to setActiveGroup: " + e);
+            mCallback.onClientFinished(this, false /* success */);
+        }
+    }
+
+    @Override
+    public int getProtoEnum() {
+        return BiometricsProto.CM_UPDATE_ACTIVE_USER;
+    }
+}
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
index 90da74c..47fdcdb 100644
--- 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
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.os.Handler;
@@ -39,7 +40,7 @@
 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.UserSwitchProvider;
 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;
@@ -71,37 +72,33 @@
                 }
             };
 
-    public HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider,
-            @NonNull Context context, @NonNull Handler handler,
+    public HidlToAidlSensorAdapter(@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,
+        this(provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
                 resetLockoutRequiresHardwareAuthToken, internalCleanupRunnable,
                 new AuthSessionCoordinator(), null /* daemon */,
                 null /* onEnrollSuccessCallback */);
     }
 
     @VisibleForTesting
-    HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider,
+    HidlToAidlSensorAdapter(@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,
+        super(provider, context, handler, getFingerprintSensorPropertiesInternal(prop,
                         new ArrayList<>(), resetLockoutRequiresHardwareAuthToken),
-                lockoutResetDispatcher,
-                gestureAvailabilityDispatcher,
                 biometricContext, null /* session */);
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mInternalCleanupRunnable = internalCleanupRunnable;
@@ -127,7 +124,7 @@
 
     @Override
     public void serviceDied(long cookie) {
-        Slog.d(TAG, "HAL died.");
+        Slog.d(TAG, "Fingerprint HAL died.");
         mSession = null;
         mDaemon = null;
     }
@@ -139,12 +136,12 @@
     }
 
     @Override
-    public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            LockoutResetDispatcher lockoutResetDispatcher) {
+    public void init(@NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
         setLazySession(this::getSession);
-        setScheduler(new UserAwareBiometricScheduler(TAG,
+        setScheduler(new BiometricScheduler<ISession, AidlSession>(getHandler(),
                 BiometricScheduler.sensorTypeFromFingerprintProperties(getSensorProperties()),
-                gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback()));
+                gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchProvider()));
         mLockoutTracker = new LockoutFrameworkImpl(getContext(),
                 userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks(
                         getSensorProperties().sensorId), getHandler());
@@ -152,6 +149,7 @@
 
     @Override
     @Nullable
+    @VisibleForTesting
     protected AidlSession getSessionForUser(int userId) {
         if (mSession != null && mSession.getUserId() == userId) {
             return mSession;
@@ -217,21 +215,18 @@
         }
 
         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() {
+    private UserSwitchProvider<ISession, AidlSession> getUserSwitchProvider() {
+        return new UserSwitchProvider<>() {
             @NonNull
             @Override
-            public StopUserClient<?> getStopUserClient(int userId) {
-                return new StopUserClient<IBiometricsFingerprint>(getContext(),
-                        HidlToAidlSensorAdapter.this::getIBiometricsFingerprint,
+            public StopUserClient<AidlSession> getStopUserClient(int userId) {
+                return new StopUserClient<>(getContext(),
+                        HidlToAidlSensorAdapter.this::getSession,
                         null /* token */, userId, getSensorProperties().sensorId,
                         BiometricLogger.ofUnknown(getContext()), getBiometricContext(),
                         () -> {
@@ -258,7 +253,7 @@
 
             @NonNull
             @Override
-            public StartUserClient<?, ?> getStartUserClient(int newUserId) {
+            public StartUserClient<ISession, AidlSession> getStartUserClient(int newUserId) {
                 return getFingerprintUpdateActiveUserClient(newUserId,
                         false /* forceUpdateAuthenticatorId */);
             }
@@ -268,7 +263,7 @@
     private FingerprintUpdateActiveUserClient getFingerprintUpdateActiveUserClient(int newUserId,
             boolean forceUpdateAuthenticatorIds) {
         return new FingerprintUpdateActiveUserClient(getContext(),
-                this::getIBiometricsFingerprint, newUserId, TAG,
+                () -> getSession().getSession(), newUserId, TAG,
                 getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()),
                 getBiometricContext(), () -> mCurrentUserId,
                 !FingerprintUtils.getInstance(getSensorProperties().sensorId)
@@ -290,7 +285,7 @@
     }
 
     @VisibleForTesting void handleUserChanged(int newUserId) {
-        Slog.d(TAG, "User changed. Current user is " + newUserId);
+        Slog.d(TAG, "User changed. Current user for fingerprint sensor is " + newUserId);
         mSession = null;
         mCurrentUserId = newUserId;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
index 2fc00e1..b469752 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
@@ -209,6 +209,14 @@
         return null;
     }
 
+    public long getAuthenticatorIdForUpdateClient() throws RemoteException {
+        return mSession.get().getAuthenticatorId();
+    }
+
+    public void setActiveGroup(int userId, String absolutePath) throws RemoteException {
+        mSession.get().setActiveGroup(userId, absolutePath);
+    }
+
     private void setCallback(AidlResponseHandler aidlResponseHandler) {
         mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
         try {
diff --git a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
index 0814375..816c349 100644
--- a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
+++ b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
@@ -29,6 +29,7 @@
 import com.android.server.criticalevents.nano.CriticalEventProto;
 import com.android.server.criticalevents.nano.CriticalEventProto.AppNotResponding;
 import com.android.server.criticalevents.nano.CriticalEventProto.HalfWatchdog;
+import com.android.server.criticalevents.nano.CriticalEventProto.InstallPackages;
 import com.android.server.criticalevents.nano.CriticalEventProto.JavaCrash;
 import com.android.server.criticalevents.nano.CriticalEventProto.NativeCrash;
 import com.android.server.criticalevents.nano.CriticalEventProto.SystemServerStarted;
@@ -142,6 +143,13 @@
         return System.currentTimeMillis();
     }
 
+    /** Logs when one or more packages are installed. */
+    public void logInstallPackagesStarted() {
+        CriticalEventProto event = new CriticalEventProto();
+        event.setInstallPackages(new InstallPackages());
+        log(event);
+    }
+
     /** Logs when system server started. */
     public void logSystemServerStarted() {
         CriticalEventProto event = new CriticalEventProto();
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index 1ac3a12..e54f30f 100644
--- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
+++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
@@ -27,6 +27,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.config.layout.Layouts;
 import com.android.server.display.config.layout.XmlParser;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 
@@ -63,24 +64,40 @@
     private static final String CONFIG_FILE_PATH =
             "etc/displayconfig/display_layout_configuration.xml";
 
+    private static final String DATA_CONFIG_FILE_PATH =
+            "system/displayconfig/display_layout_configuration.xml";
+
     private final SparseArray<Layout> mLayoutMap = new SparseArray<>();
     private final DisplayIdProducer mIdProducer;
+    private final boolean mIsPortInDisplayLayoutEnabled;
 
-    DeviceStateToLayoutMap(DisplayIdProducer idProducer) {
-        this(idProducer, Environment.buildPath(
-                Environment.getVendorDirectory(), CONFIG_FILE_PATH));
+    DeviceStateToLayoutMap(DisplayIdProducer idProducer, DisplayManagerFlags flags) {
+        this(idProducer, flags, getConfigFile());
     }
 
-    DeviceStateToLayoutMap(DisplayIdProducer idProducer, File configFile) {
+    DeviceStateToLayoutMap(DisplayIdProducer idProducer, DisplayManagerFlags flags,
+            File configFile) {
+        mIsPortInDisplayLayoutEnabled = flags.isPortInDisplayLayoutEnabled();
         mIdProducer = idProducer;
         loadLayoutsFromConfig(configFile);
         createLayout(STATE_DEFAULT);
     }
 
+    static private File getConfigFile() {
+        final File configFileFromDataDir = Environment.buildPath(Environment.getDataDirectory(),
+                DATA_CONFIG_FILE_PATH);
+        if (configFileFromDataDir.exists()) {
+            return configFileFromDataDir;
+        } else {
+            return Environment.buildPath(Environment.getVendorDirectory(), CONFIG_FILE_PATH);
+        }
+    }
+
     public void dumpLocked(IndentingPrintWriter ipw) {
         ipw.println("DeviceStateToLayoutMap:");
         ipw.increaseIndent();
 
+        ipw.println("mIsPortInDisplayLayoutEnabled=" + mIsPortInDisplayLayoutEnabled);
         ipw.println("Registered Layouts:");
         for (int i = 0; i < mLayoutMap.size(); i++) {
             ipw.println("state(" + mLayoutMap.keyAt(i) + "): " + mLayoutMap.valueAt(i));
@@ -120,13 +137,15 @@
                 final Layout layout = createLayout(state);
                 for (com.android.server.display.config.layout.Display d: l.getDisplay()) {
                     assert layout != null;
+                    final DisplayAddress address = getDisplayAddressForLayoutDisplay(d);
+
                     int position = getPosition(d.getPosition());
                     BigInteger leadDisplayPhysicalId = d.getLeadDisplayAddress();
                     DisplayAddress leadDisplayAddress = leadDisplayPhysicalId == null ? null
                             : DisplayAddress.fromPhysicalDisplayId(
                                     leadDisplayPhysicalId.longValue());
                     layout.createDisplayLocked(
-                            DisplayAddress.fromPhysicalDisplayId(d.getAddress().longValue()),
+                            address,
                             d.isDefaultDisplay(),
                             d.isEnabled(),
                             d.getDisplayGroup(),
@@ -146,6 +165,20 @@
         }
     }
 
+    private DisplayAddress getDisplayAddressForLayoutDisplay(
+            @NonNull com.android.server.display.config.layout.Display display) {
+        BigInteger xmlAddress = display.getAddress_optional();
+        if (xmlAddress != null) {
+            return DisplayAddress.fromPhysicalDisplayId(xmlAddress.longValue());
+        }
+        if (!mIsPortInDisplayLayoutEnabled || display.getPort_optional() == null) {
+            throw new IllegalArgumentException(
+                  "Must specify a display identifier in display layout configuration: " + display);
+        }
+        return DisplayAddress.fromPortAndModel((int) display.getPort_optional().longValue(),
+                /* model= */ null);
+    }
+
     private int getPosition(@NonNull String position) {
         int positionInt = POSITION_UNKNOWN;
         if (FRONT_STRING.equals(position)) {
diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java
index 9fcaa1e..d50a43a 100644
--- a/services/core/java/com/android/server/display/DisplayBrightnessState.java
+++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java
@@ -33,6 +33,7 @@
     private final float mSdrBrightness;
 
     private final float mMaxBrightness;
+    private final float mMinBrightness;
     private final BrightnessReason mBrightnessReason;
     private final String mDisplayBrightnessStrategyName;
     private final boolean mShouldUseAutoBrightness;
@@ -50,6 +51,7 @@
         mShouldUseAutoBrightness = builder.getShouldUseAutoBrightness();
         mIsSlowChange = builder.isSlowChange();
         mMaxBrightness = builder.getMaxBrightness();
+        mMinBrightness = builder.getMinBrightness();
         mCustomAnimationRate = builder.getCustomAnimationRate();
         mShouldUpdateScreenBrightnessSetting = builder.shouldUpdateScreenBrightnessSetting();
     }
@@ -105,6 +107,13 @@
     }
 
     /**
+     * @return minimum allowed brightness
+     */
+    public float getMinBrightness() {
+        return mMinBrightness;
+    }
+
+    /**
      * @return custom animation rate
      */
     public float getCustomAnimationRate() {
@@ -131,6 +140,7 @@
         stringBuilder.append(getShouldUseAutoBrightness());
         stringBuilder.append("\n    isSlowChange:").append(mIsSlowChange);
         stringBuilder.append("\n    maxBrightness:").append(mMaxBrightness);
+        stringBuilder.append("\n    minBrightness:").append(mMinBrightness);
         stringBuilder.append("\n    customAnimationRate:").append(mCustomAnimationRate);
         stringBuilder.append("\n    shouldUpdateScreenBrightnessSetting:")
                 .append(mShouldUpdateScreenBrightnessSetting);
@@ -160,6 +170,7 @@
                 && mShouldUseAutoBrightness == otherState.getShouldUseAutoBrightness()
                 && mIsSlowChange == otherState.isSlowChange()
                 && mMaxBrightness == otherState.getMaxBrightness()
+                && mMinBrightness == otherState.getMinBrightness()
                 && mCustomAnimationRate == otherState.getCustomAnimationRate()
                 && mShouldUpdateScreenBrightnessSetting
                     == otherState.shouldUpdateScreenBrightnessSetting();
@@ -168,7 +179,8 @@
     @Override
     public int hashCode() {
         return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason,
-                mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mCustomAnimationRate,
+                mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness,
+                mCustomAnimationRate,
                 mShouldUpdateScreenBrightnessSetting);
     }
 
@@ -190,6 +202,7 @@
         private boolean mShouldUseAutoBrightness;
         private boolean mIsSlowChange;
         private float mMaxBrightness;
+        private float mMinBrightness;
         private float mCustomAnimationRate = CUSTOM_ANIMATION_RATE_NOT_SET;
         private boolean mShouldUpdateScreenBrightnessSetting;
 
@@ -208,6 +221,7 @@
             builder.setShouldUseAutoBrightness(state.getShouldUseAutoBrightness());
             builder.setIsSlowChange(state.isSlowChange());
             builder.setMaxBrightness(state.getMaxBrightness());
+            builder.setMinBrightness(state.getMinBrightness());
             builder.setCustomAnimationRate(state.getCustomAnimationRate());
             builder.setShouldUpdateScreenBrightnessSetting(
                     state.shouldUpdateScreenBrightnessSetting());
@@ -334,6 +348,20 @@
             return mMaxBrightness;
         }
 
+        /**
+         * See {@link DisplayBrightnessState#getMinBrightness()}.
+         */
+        public Builder setMinBrightness(float minBrightness) {
+            this.mMinBrightness = minBrightness;
+            return this;
+        }
+
+        /**
+         * See {@link DisplayBrightnessState#getMinBrightness()}.
+         */
+        public float getMinBrightness() {
+            return mMinBrightness;
+        }
 
         /**
          * See {@link DisplayBrightnessState#getCustomAnimationRate()}.
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index bd22e1d..4c4cf608 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat;
 
@@ -567,7 +568,8 @@
 
     public static final int DEFAULT_LOW_REFRESH_RATE = 60;
 
-    private static final float BRIGHTNESS_DEFAULT = 0.5f;
+    @VisibleForTesting
+    static final float BRIGHTNESS_DEFAULT = 0.5f;
     private static final String ETC_DIR = "etc";
     private static final String DISPLAY_CONFIG_DIR = "displayconfig";
     private static final String CONFIG_FILE_FORMAT = "display_%s.xml";
@@ -597,8 +599,6 @@
     // so -2 is used instead
     private static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f;
 
-    static final float NITS_INVALID = -1;
-
     // Length of the ambient light horizon used to calculate the long term estimate of ambient
     // light.
     private static final int AMBIENT_LIGHT_LONG_HORIZON_MILLIS = 10000;
@@ -1031,11 +1031,12 @@
     /**
      * Calculates the nits value for the specified backlight value if a mapping exists.
      *
-     * @return The mapped nits or {@link #NITS_INVALID} if no mapping exits.
+     * @return The mapped nits or {@link BrightnessMappingStrategy.INVALID_NITS} if no mapping
+     * exits.
      */
     public float getNitsFromBacklight(float backlight) {
         if (mBacklightToNitsSpline == null) {
-            return NITS_INVALID;
+            return INVALID_NITS;
         }
         backlight = Math.max(backlight, mBacklightMinimum);
         return mBacklightToNitsSpline.interpolate(backlight);
@@ -1061,7 +1062,7 @@
 
         float backlight = getBacklightFromBrightness(brightness);
         float nits = getNitsFromBacklight(backlight);
-        if (nits == NITS_INVALID) {
+        if (nits == INVALID_NITS) {
             return PowerManager.BRIGHTNESS_INVALID;
         }
 
diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
index 67e612d..6164154 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
@@ -129,7 +129,9 @@
     public DisplayDevice getByAddressLocked(@NonNull DisplayAddress address) {
         for (int i = mDisplayDevices.size() - 1; i >= 0; i--) {
             final DisplayDevice device = mDisplayDevices.get(i);
-            if (address.equals(device.getDisplayDeviceInfoLocked().address)) {
+            final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
+            if (address.equals(info.address)
+                    || DisplayAddress.Physical.isPortMatch(address, info.address)) {
                 return device;
             }
         }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index bc3f9dd..9cf9119 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1615,6 +1615,10 @@
                 if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) {
                     Slog.w(TAG, "Display created with home support but lacks "
                             + "VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the home support request.");
+                } else if ((flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
+                    Slog.w(TAG, "Display created with home support but has "
+                            + "VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, ignoring the home support "
+                            + "request.");
                 } else {
                     mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId,
                             Display.TYPE_VIRTUAL, true);
@@ -3386,17 +3390,10 @@
         // with the corresponding displaydevice.
         HighBrightnessModeMetadata hbmMetadata =
                 mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
-        if (mConfigParameterProvider.isNewPowerControllerFeatureEnabled()) {
-            displayPowerController = new DisplayPowerController2(
-                    mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
-                    mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
-                    () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted, mFlags);
-        } else {
-            displayPowerController = new DisplayPowerController(
-                    mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
-                    mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
-                    () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted, mFlags);
-        }
+        displayPowerController = new DisplayPowerController(
+                mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
+                mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
+                () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted, mFlags);
         mDisplayPowerControllers.append(display.getDisplayIdLocked(), displayPowerController);
         return displayPowerController;
     }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 734381b..087cacf 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * 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.
@@ -17,11 +17,12 @@
 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.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
@@ -31,8 +32,6 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.AmbientBrightnessDayStats;
 import android.hardware.display.BrightnessChangeEvent;
@@ -45,6 +44,7 @@
 import android.metrics.LogMaker;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -56,12 +56,12 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.FloatProperty;
+import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
 import android.util.MutableFloat;
 import android.util.MutableInt;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TimeUtils;
 import android.view.Display;
 
 import com.android.internal.R;
@@ -78,10 +78,15 @@
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
 import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.BrightnessUtils;
+import com.android.server.display.brightness.DisplayBrightnessController;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
+import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
 import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
 import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.state.DisplayStateController;
 import com.android.server.display.utils.DebugUtils;
 import com.android.server.display.utils.SensorUtils;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
@@ -119,12 +124,12 @@
     private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
     private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
 
-    private static final String TAG = "DisplayPowerController";
+    private static final String TAG = "DisplayPowerController2";
     // To enable these logs, run:
-    // 'adb shell setprop persist.log.tag.DisplayPowerController DEBUG && adb reboot'
+    // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot'
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
-
-    private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false;
+    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
@@ -138,36 +143,28 @@
     private static final int COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS = 400;
 
     private static final int MSG_UPDATE_POWER_STATE = 1;
-    private static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 2;
-    private static final int MSG_SCREEN_ON_UNBLOCKED = 3;
-    private static final int MSG_SCREEN_OFF_UNBLOCKED = 4;
-    private static final int MSG_CONFIGURE_BRIGHTNESS = 5;
-    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 6;
-    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 7;
-    private static final int MSG_IGNORE_PROXIMITY = 8;
-    private static final int MSG_STOP = 9;
-    private static final int MSG_UPDATE_BRIGHTNESS = 10;
-    private static final int MSG_UPDATE_RBC = 11;
-    private static final int MSG_BRIGHTNESS_RAMP_DONE = 12;
-    private static final int MSG_STATSD_HBM_BRIGHTNESS = 13;
-    private static final int MSG_SWITCH_USER = 14;
-    private static final int MSG_BOOT_COMPLETED = 15;
-    private static final int MSG_SET_DWBC_STRONG_MODE = 16;
-    private static final int MSG_SET_DWBC_COLOR_OVERRIDE = 17;
-    private static final int MSG_SET_DWBC_LOGGING_ENABLED = 18;
+    private static final int MSG_SCREEN_ON_UNBLOCKED = 2;
+    private static final int MSG_SCREEN_OFF_UNBLOCKED = 3;
+    private static final int MSG_CONFIGURE_BRIGHTNESS = 4;
+    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 5;
+    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 6;
+    private static final int MSG_STOP = 7;
+    private static final int MSG_UPDATE_BRIGHTNESS = 8;
+    private static final int MSG_UPDATE_RBC = 9;
+    private static final int MSG_BRIGHTNESS_RAMP_DONE = 10;
+    private static final int MSG_STATSD_HBM_BRIGHTNESS = 11;
+    private static final int MSG_SWITCH_USER = 12;
+    private static final int MSG_BOOT_COMPLETED = 13;
+    private static final int MSG_SET_DWBC_STRONG_MODE = 14;
+    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;
 
-    private static final int PROXIMITY_UNKNOWN = -1;
-    private static final int PROXIMITY_NEGATIVE = 0;
-    private static final int PROXIMITY_POSITIVE = 1;
 
-    // Proximity sensor debounce delay in milliseconds for positive or negative transitions.
-    private static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0;
-    private static final int PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY = 250;
 
     private static final int BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS = 500;
 
-    // Trigger proximity if distance is less than 5 cm.
-    private static final float TYPICAL_PROXIMITY_THRESHOLD = 5.0f;
 
     // State machine constants for tracking initial brightness ramp skipping when enabled.
     private static final int RAMP_STATE_SKIP_NONE = 0;
@@ -181,6 +178,7 @@
     private static final int REPORTED_TO_POLICY_SCREEN_TURNING_OFF = 3;
 
     private static final int RINGBUFFER_MAX = 100;
+    private static final int RINGBUFFER_RBC_MAX = 20;
 
     private static final float[] BRIGHTNESS_RANGE_BOUNDARIES = {
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80,
@@ -236,10 +234,6 @@
     // Our handler.
     private final DisplayControllerHandler mHandler;
 
-    // Asynchronous callbacks into the power manager service.
-    // Only invoked from the handler thread while no locks are held.
-    private final DisplayPowerCallbacks mCallbacks;
-
     // Battery stats.
     @Nullable
     private final IBatteryStats mBatteryStats;
@@ -253,10 +247,10 @@
     // The display blanker.
     private final DisplayBlanker mBlanker;
 
-    // The LogicalDisplay tied to this DisplayPowerController.
+    // The LogicalDisplay tied to this DisplayPowerController2.
     private final LogicalDisplay mLogicalDisplay;
 
-    // The ID of the LogicalDisplay tied to this DisplayPowerController.
+    // The ID of the LogicalDisplay tied to this DisplayPowerController2.
     private final int mDisplayId;
 
     // The ID of the display which this display follows for brightness purposes.
@@ -272,34 +266,12 @@
     // Tracker for brightness settings changes.
     private final SettingsObserver mSettingsObserver;
 
-    // The proximity sensor, or null if not available or needed.
-    private Sensor mProximitySensor;
-
     // The doze screen brightness.
     private final float mScreenBrightnessDozeConfig;
 
-    // The dim screen brightness.
-    private final float mScreenBrightnessDimConfig;
-
-    // The minimum dim amount to use if the screen brightness is already below
-    // mScreenBrightnessDimConfig.
-    private final float mScreenBrightnessMinimumDimAmount;
-
-    private final float mScreenBrightnessDefault;
-
     // True if auto-brightness should be used.
     private boolean mUseSoftwareAutoBrightnessConfig;
 
-    // True if should use light sensor to automatically determine doze screen brightness.
-    private final boolean mAllowAutoBrightnessWhileDozingConfig;
-
-    // True if we want to persist the brightness value in nits even if the underlying display
-    // device changes.
-    private final boolean mPersistBrightnessNitsForDefaultDisplay;
-
-    // True if the brightness config has changed and the short-term model needs to be reset
-    private boolean mShouldResetShortTermModel;
-
     // Whether or not the color fade on screen on / off is enabled.
     private final boolean mColorFadeEnabled;
 
@@ -340,10 +312,6 @@
     @GuardedBy("mLock")
     private DisplayPowerRequest mPendingRequestLocked;
 
-    // True if a request has been made to wait for the proximity sensor to go negative.
-    @GuardedBy("mLock")
-    private boolean mPendingWaitForNegativeProximityLocked;
-
     // True if the pending power request or wait for negative proximity flag
     // has been changed since the last update occurred.
     @GuardedBy("mLock")
@@ -370,67 +338,36 @@
     // Must only be accessed on the handler thread.
     private DisplayPowerState mPowerState;
 
-    // True if the device should wait for negative proximity sensor before
-    // waking up the screen.  This is set to false as soon as a negative
-    // proximity sensor measurement is observed or when the device is forced to
-    // go to sleep by the user.  While true, the screen remains off.
-    private boolean mWaitingForNegativeProximity;
 
-    // True if the device should not take into account the proximity sensor
-    // until either the proximity sensor state changes, or there is no longer a
-    // request to listen to proximity sensor.
-    private boolean mIgnoreProximityUntilChanged;
-
-    // The actual proximity sensor threshold value.
-    private float mProximityThreshold;
-
-    // Set to true if the proximity sensor listener has been registered
-    // with the sensor manager.
-    private boolean mProximitySensorEnabled;
-
-    // The debounced proximity sensor state.
-    private int mProximity = PROXIMITY_UNKNOWN;
-
-    // The raw non-debounced proximity sensor state.
-    private int mPendingProximity = PROXIMITY_UNKNOWN;
-    private long mPendingProximityDebounceTime = -1; // -1 if fully debounced
-
-    // True if the screen was turned off because of the proximity sensor.
-    // When the screen turns on again, we report user activity to the power manager.
-    private boolean mScreenOffBecauseOfProximity;
 
     // The currently active screen on unblocker.  This field is non-null whenever
     // 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
     // turning off the screen.
     private boolean mPendingScreenOff;
 
-    // True if we have unfinished business and are holding a suspend blocker.
-    private boolean mUnfinishedBusiness;
-
     // 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;
 
-    // Remembers whether certain kinds of brightness adjustments
-    // were recently applied so that we can decide how to transition.
-    private boolean mAppliedAutoBrightness;
     private boolean mAppliedDimming;
-    private boolean mAppliedLowPower;
-    private boolean mAppliedScreenBrightnessOverride;
-    private boolean mAppliedTemporaryBrightness;
-    private boolean mAppliedTemporaryAutoBrightnessAdjustment;
-    private boolean mAppliedBrightnessBoost;
+
     private boolean mAppliedThrottling;
 
     // Reason for which the brightness was last changed. See {@link BrightnessReason} for more
@@ -456,7 +393,7 @@
     private final boolean mSkipScreenOnBrightnessRamp;
 
     // Display white balance components.
-    // Critical methods must be called on DPC handler thread.
+    // Critical methods must be called on DPC2 handler thread.
     @Nullable
     private final DisplayWhiteBalanceSettings mDisplayWhiteBalanceSettings;
     @Nullable
@@ -468,21 +405,39 @@
 
     private final BrightnessRangeController mBrightnessRangeController;
 
-    @Nullable
-    private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
-
     private final BrightnessThrottler mBrightnessThrottler;
 
-    private final BrightnessSetting mBrightnessSetting;
+    private final BrightnessClamperController mBrightnessClamperController;
 
     private final Runnable mOnBrightnessChangeRunnable;
 
     private final BrightnessEvent mLastBrightnessEvent;
     private final BrightnessEvent mTempBrightnessEvent;
 
+    private final DisplayBrightnessController mDisplayBrightnessController;
+
     // Keeps a record of brightness changes for dumpsys.
     private RingBuffer<BrightnessEvent> mBrightnessEventRingBuffer;
 
+    // Keeps a record of rbc changes for dumpsys.
+    private final RingBuffer<BrightnessEvent> mRbcEventRingBuffer =
+            new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_RBC_MAX);
+
+    // Controls and tracks all the wakelocks that are acquired/released by the system. Also acts as
+    // a medium of communication between this class and the PowerManagerService.
+    private final WakelockController mWakelockController;
+
+    // Tracks and manages the proximity state of the associated display.
+    private final DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+
+    // Tracks and manages the display state of the associated display.
+    private final DisplayStateController mDisplayStateController;
+
+
+    // Responsible for evaluating and tracking the automatic brightness relevant states.
+    // Todo: This is a temporary workaround. Ideally DPC2 should never talk to the strategies
+    private final AutomaticBrightnessStrategy mAutomaticBrightnessStrategy;
+
     // A record of state for skipping brightness ramps.
     private int mSkipRampState = RAMP_STATE_SKIP_NONE;
 
@@ -500,81 +455,18 @@
     private Sensor mLightSensor;
     private Sensor mScreenOffBrightnessSensor;
 
-    // The current brightness configuration.
-    @Nullable
-    private BrightnessConfiguration mBrightnessConfiguration;
-
-    // The last brightness that was set by the user and not temporary. Set to
-    // PowerManager.BRIGHTNESS_INVALID_FLOAT when a brightness has yet to be recorded.
-    private float mLastUserSetScreenBrightness = Float.NaN;
-
-    // The screen brightness setting has changed but not taken effect yet. If this is different
-    // from the current screen brightness setting then this is coming from something other than us
-    // and should be considered a user interaction.
-    private float mPendingScreenBrightnessSetting;
-
-    // The last observed screen brightness setting, either set by us or by the settings app on
-    // behalf of the user.
-    private float mCurrentScreenBrightnessSetting;
-
-    // The temporary screen brightness. Typically set when a user is interacting with the
-    // brightness slider but hasn't settled on a choice yet. Set to
-    // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary brightness set.
-    private float mTemporaryScreenBrightness;
-
-    // This brightness value is set in concurrent displays mode. It is the brightness value
-    // of the lead display that this DPC should follow.
-    private float mBrightnessToFollow;
-
-    // Indicates whether we should ramp slowly to the brightness value to follow.
-    private boolean mBrightnessToFollowSlowChange;
-
-    // The last auto brightness adjustment that was set by the user and not temporary. Set to
-    // Float.NaN when an auto-brightness adjustment hasn't been recorded yet.
-    private float mAutoBrightnessAdjustment;
-
-    // The pending auto brightness adjustment that will take effect on the next power state update.
-    private float mPendingAutoBrightnessAdjustment;
-
-    // The temporary auto brightness adjustment. Typically set when a user is interacting with the
-    // adjustment slider but hasn't settled on a choice yet. Set to
-    // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set.
-    private float mTemporaryAutoBrightnessAdjustment;
-
-    private boolean mUseAutoBrightness;
-
     private boolean mIsRbcActive;
 
-    // Whether there's a callback to tell listeners the display has changed scheduled to run. When
-    // true it implies a wakelock is being held to guarantee the update happens before we collapse
-    // into suspend and so needs to be cleaned up if the thread is exiting.
-    // Should only be accessed on the Handler thread.
-    private boolean mOnStateChangedPending;
-
-    // Count of proximity messages currently on this DPC's Handler. Used to keep track of how many
-    // suspend blocker acquisitions are pending when shutting down this DPC.
-    // Should only be accessed on the Handler thread.
-    private int mOnProximityPositiveMessages;
-    private int mOnProximityNegativeMessages;
-
     // Animators.
     private ObjectAnimator mColorFadeOnAnimator;
     private ObjectAnimator mColorFadeOffAnimator;
     private DualRampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator;
-    private BrightnessSetting.BrightnessSettingListener mBrightnessSettingListener;
 
-    // True if this DisplayPowerController has been stopped and should no longer be running.
+    // True if this DisplayPowerController2 has been stopped and should no longer be running.
     private boolean mStopped;
 
     private DisplayDeviceConfig mDisplayDeviceConfig;
 
-    // Identifiers for suspend blocker acquisition requests
-    private final String mSuspendBlockerIdUnfinishedBusiness;
-    private final String mSuspendBlockerIdOnStateChanged;
-    private final String mSuspendBlockerIdProxPositive;
-    private final String mSuspendBlockerIdProxNegative;
-    private final String mSuspendBlockerIdProxDebounce;
-
     private boolean mIsEnabled;
     private boolean mIsInTransition;
     private boolean mIsDisplayInternal;
@@ -585,13 +477,13 @@
     // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there
     // is one lead display, the additional displays follow the brightness value of the lead display.
     @GuardedBy("mLock")
-    private final SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
-            new SparseArray<>();
+    private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
+            new SparseArray();
 
     private boolean mBootCompleted;
     private final DisplayManagerFlags mFlags;
-    private int mDozeStateOverride = Display.STATE_UNKNOWN;
-    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
+
+    private DisplayOffloadSession mDisplayOffloadSession;
 
     /**
      * Creates the display power controller.
@@ -607,26 +499,28 @@
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
         mDisplayId = mLogicalDisplay.getDisplayIdLocked();
-        mTag = TAG + "[" + mDisplayId + "]";
-        mHighBrightnessModeMetadata = hbmMetadata;
-        mSuspendBlockerIdUnfinishedBusiness = getSuspendBlockerUnfinishedBusinessId(mDisplayId);
-        mSuspendBlockerIdOnStateChanged = getSuspendBlockerOnStateChangedId(mDisplayId);
-        mSuspendBlockerIdProxPositive = getSuspendBlockerProxPositiveId(mDisplayId);
-        mSuspendBlockerIdProxNegative = getSuspendBlockerProxNegativeId(mDisplayId);
-        mSuspendBlockerIdProxDebounce = getSuspendBlockerProxDebounceId(mDisplayId);
-
-        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
-        mDisplayStatsId = mUniqueDisplayId.hashCode();
+        mSensorManager = sensorManager;
+        mHandler = new DisplayControllerHandler(handler.getLooper());
+        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
+                .getDisplayDeviceConfig();
         mIsEnabled = logicalDisplay.isEnabledLocked();
         mIsInTransition = logicalDisplay.isInTransitionLocked();
         mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked()
                 .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
-        mHandler = new DisplayControllerHandler(handler.getLooper());
-        mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
-        mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
+        mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
+        mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
+                () -> updatePowerState(), mDisplayId, mSensorManager);
+        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
+        mTag = TAG + "[" + mDisplayId + "]";
         mThermalBrightnessThrottlingDataId =
                 logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
+        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
+        mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+        mDisplayStatsId = mUniqueDisplayId.hashCode();
+
+        mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
+        mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
 
         if (mDisplayId == Display.DEFAULT_DISPLAY) {
             mBatteryStats = BatteryStatsService.getService();
@@ -635,14 +529,10 @@
         }
 
         mSettingsObserver = new SettingsObserver(mHandler);
-        mCallbacks = callbacks;
-        mSensorManager = sensorManager;
         mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class);
         mBlanker = blanker;
         mContext = context;
         mBrightnessTracker = brightnessTracker;
-        // TODO: b/186428377 update brightness setting when display changes
-        mBrightnessSetting = brightnessSetting;
         mOnBrightnessChangeRunnable = onBrightnessChangeRunnable;
 
         PowerManager pm = context.getSystemService(PowerManager.class);
@@ -650,30 +540,12 @@
         final Resources resources = context.getResources();
 
         // DOZE AND DIM SETTINGS
-        mScreenBrightnessDozeConfig = clampAbsoluteBrightness(
+        mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
                 pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE));
-        mScreenBrightnessDimConfig = clampAbsoluteBrightness(
-                pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM));
-        mScreenBrightnessMinimumDimAmount = resources.getFloat(
-                com.android.internal.R.dimen.config_screenBrightnessMinimumDimAmountFloat);
-
-
-        // NORMAL SCREEN SETTINGS
-        mScreenBrightnessDefault = clampAbsoluteBrightness(
-                mLogicalDisplay.getDisplayInfoLocked().brightnessDefault);
-
-        mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing);
-
-        mPersistBrightnessNitsForDefaultDisplay = resources.getBoolean(
-                com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay);
-
-        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceConfig();
-
         loadBrightnessRampRates();
         mSkipScreenOnBrightnessRamp = resources.getBoolean(
-                com.android.internal.R.bool.config_skipScreenOnBrightnessRamp);
+                R.bool.config_skipScreenOnBrightnessRamp);
+
         Runnable modeChangeCallback = () -> {
             sendUpdatePowerState();
             postBrightnessChangeRunnable();
@@ -683,23 +555,38 @@
             }
         };
 
-        HighBrightnessModeController hbmController = createHbmControllerLocked(modeChangeCallback);
+        HighBrightnessModeController hbmController = createHbmControllerLocked(hbmMetadata,
+                modeChangeCallback);
+        mBrightnessThrottler = createBrightnessThrottlerLocked();
 
-        mBrightnessRangeController = new BrightnessRangeController(hbmController,
+        mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
                 modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
                 mDisplayDevice.getDisplayTokenLocked(),
                 mDisplayDevice.getDisplayDeviceInfoLocked());
 
-        mBrightnessThrottler = createBrightnessThrottlerLocked();
+        mDisplayBrightnessController =
+                new DisplayBrightnessController(context, null,
+                        mDisplayId, mLogicalDisplay.getDisplayInfoLocked().brightnessDefault,
+                        brightnessSetting, () -> postBrightnessChangeRunnable(),
+                        new HandlerExecutor(mHandler), flags);
 
+        mBrightnessClamperController = mInjector.getBrightnessClamperController(
+                mHandler, modeChangeCallback::run,
+                new BrightnessClamperController.DisplayDeviceData(
+                        mUniqueDisplayId,
+                        mThermalBrightnessThrottlingDataId,
+                        logicalDisplay.getPowerThrottlingDataIdLocked(),
+                        mDisplayDeviceConfig), mContext, flags);
         // Seed the cached brightness
         saveBrightnessInfo(getScreenBrightnessSetting());
+        mAutomaticBrightnessStrategy =
+                mDisplayBrightnessController.getAutomaticBrightnessStrategy();
 
         DisplayWhiteBalanceSettings displayWhiteBalanceSettings = null;
         DisplayWhiteBalanceController displayWhiteBalanceController = null;
         if (mDisplayId == Display.DEFAULT_DISPLAY) {
             try {
-                displayWhiteBalanceController = injector.getDisplayWhiteBalanceController(
+                displayWhiteBalanceController = mInjector.getDisplayWhiteBalanceController(
                         mHandler, mSensorManager, resources);
                 displayWhiteBalanceSettings = new DisplayWhiteBalanceSettings(mContext, mHandler);
                 displayWhiteBalanceSettings.setCallbacks(this);
@@ -715,21 +602,24 @@
 
         if (mDisplayId == Display.DEFAULT_DISPLAY) {
             mCdsi = LocalServices.getService(ColorDisplayServiceInternal.class);
-            boolean active = mCdsi.setReduceBrightColorsListener(new ReduceBrightColorsListener() {
-                @Override
-                public void onReduceBrightColorsActivationChanged(boolean activated,
-                        boolean userInitiated) {
-                    applyReduceBrightColorsSplineAdjustment();
+            if (mCdsi != null) {
+                boolean active = mCdsi.setReduceBrightColorsListener(
+                        new ReduceBrightColorsListener() {
+                            @Override
+                            public void onReduceBrightColorsActivationChanged(boolean activated,
+                                    boolean userInitiated) {
+                                applyReduceBrightColorsSplineAdjustment();
 
-                }
+                            }
 
-                @Override
-                public void onReduceBrightColorsStrengthChanged(int strength) {
+                            @Override
+                            public void onReduceBrightColorsStrengthChanged(int strength) {
+                                applyReduceBrightColorsSplineAdjustment();
+                            }
+                        });
+                if (active) {
                     applyReduceBrightColorsSplineAdjustment();
                 }
-            });
-            if (active) {
-                applyReduceBrightColorsSplineAdjustment();
             }
         } else {
             mCdsi = null;
@@ -737,27 +627,17 @@
 
         setUpAutoBrightness(context, handler);
 
-        mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic()
+        mColorFadeEnabled = mInjector.isColorFadeEnabled()
                 && !resources.getBoolean(
                   com.android.internal.R.bool.config_displayColorFadeDisabled);
         mColorFadeFadesConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_animateScreenLights);
+                R.bool.config_animateScreenLights);
 
         mDisplayBlanksAfterDozeConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_displayBlanksAfterDoze);
+                R.bool.config_displayBlanksAfterDoze);
 
         mBrightnessBucketsInDozeConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_displayBrightnessBucketsInDoze);
-
-        loadProximitySensor();
-
-        loadNitBasedBrightnessSetting();
-        mBrightnessToFollow = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
-        mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+                R.bool.config_displayBrightnessBucketsInDoze);
 
         mBootCompleted = bootCompleted;
     }
@@ -785,7 +665,7 @@
      */
     @Override
     public boolean isProximitySensorAvailable() {
-        return mProximitySensor != null;
+        return mDisplayPowerProximityStateController.isProximitySensorAvailable();
     }
 
     /**
@@ -818,64 +698,6 @@
         }
     }
 
-    @Override
-    public int getDisplayId() {
-        return mDisplayId;
-    }
-
-    @Override
-    public int getLeadDisplayId() {
-        return mLeadDisplayId;
-    }
-
-    @Override
-    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
-            boolean slowChange) {
-        mBrightnessRangeController.onAmbientLuxChange(ambientLux);
-        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
-            mBrightnessToFollow = leadDisplayBrightness;
-        } else {
-            float brightness = getBrightnessFromNits(nits);
-            if (isValidBrightnessValue(brightness)) {
-                mBrightnessToFollow = brightness;
-            } else {
-                // The device does not support nits
-                mBrightnessToFollow = leadDisplayBrightness;
-            }
-        }
-        mBrightnessToFollowSlowChange = slowChange;
-        sendUpdatePowerState();
-    }
-
-    @Override
-    public void addDisplayBrightnessFollower(@NonNull DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
-            sendUpdatePowerStateLocked();
-        }
-    }
-
-    @Override
-    public void removeDisplayBrightnessFollower(@NonNull DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.remove(follower.getDisplayId());
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void clearDisplayBrightnessFollowersLocked() {
-        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-        mDisplayBrightnessFollowers.clear();
-    }
-
     @Nullable
     @Override
     public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(
@@ -923,13 +745,8 @@
                 return true;
             }
 
-            boolean changed = false;
-
-            if (waitForNegativeProximity
-                    && !mPendingWaitForNegativeProximityLocked) {
-                mPendingWaitForNegativeProximityLocked = true;
-                changed = true;
-            }
+            boolean changed = mDisplayPowerProximityStateController
+                    .setPendingWaitForNegativeProximityLocked(waitForNegativeProximity);
 
             if (mPendingRequestLocked == null) {
                 mPendingRequestLocked = new DisplayPowerRequest(request);
@@ -953,18 +770,23 @@
 
     @Override
     public void overrideDozeScreenState(int displayState) {
-        synchronized (mLock) {
-            if (mDisplayOffloadSession == null ||
-                    !DisplayOffloadSession.isSupportedOffloadState(displayState)) {
+        mHandler.postAtTime(() -> {
+            if (mDisplayOffloadSession == null
+                    || !(DisplayOffloadSession.isSupportedOffloadState(displayState)
+                    || displayState == Display.STATE_UNKNOWN)) {
                 return;
             }
-            mDozeStateOverride = displayState;
+            mDisplayStateController.overrideDozeScreenState(displayState);
             sendUpdatePowerState();
-        }
+        }, mClock.uptimeMillis());
     }
 
     @Override
     public void setDisplayOffloadSession(DisplayOffloadSession session) {
+        if (session == mDisplayOffloadSession) {
+            return;
+        }
+        unblockScreenOnByDisplayOffload();
         mDisplayOffloadSession = session;
     }
 
@@ -981,14 +803,14 @@
      * when displays get swapped on foldable devices.  For example, different brightness properties
      * of each display need to be properly reflected in AutomaticBrightnessController.
      *
-     * Make sure DisplayManagerService.mSyncRoot is held when this is called
+     * Make sure DisplayManagerService.mSyncRoot lock is held when this is called
      */
     @Override
     public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) {
         mLeadDisplayId = leadDisplayId;
         final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
         if (device == null) {
-            Slog.wtf(mTag, "Display Device is null in DisplayPowerController for display: "
+            Slog.wtf(mTag, "Display Device is null in DisplayPowerController2 for display: "
                     + mLogicalDisplay.getDisplayIdLocked());
             return;
         }
@@ -1004,6 +826,9 @@
                 .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
         final String thermalBrightnessThrottlingDataId =
                 mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
+        final String powerThrottlingDataId =
+                mLogicalDisplay.getPowerThrottlingDataIdLocked();
+
         mHandler.postAtTime(() -> {
             boolean changed = false;
             if (mDisplayDevice != device) {
@@ -1014,9 +839,9 @@
                 mDisplayDeviceConfig = config;
                 mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
                 loadFromDisplayDeviceConfig(token, info, hbmMetadata);
-                loadNitBasedBrightnessSetting();
+                mDisplayPowerProximityStateController.notifyDisplayDeviceChanged(config);
 
-                /// Since the underlying display-device changed, we really don't know the
+                // Since the underlying display-device changed, we really don't know the
                 // last command that was sent to change it's state. Let's assume it is unknown so
                 // that we trigger a change immediately.
                 mPowerState.resetScreenState();
@@ -1034,7 +859,16 @@
                 mIsEnabled = isEnabled;
                 mIsInTransition = isInTransition;
             }
+
             mIsDisplayInternal = isDisplayInternal;
+            // using local variables here, when mBrightnessThrottler is removed,
+            // mThermalBrightnessThrottlingDataId could be removed as well
+            // changed = true will be not needed - clampers are maintaining their state and
+            // will call updatePowerState if needed.
+            mBrightnessClamperController.onDisplayChanged(
+                    new BrightnessClamperController.DisplayDeviceData(uniqueId,
+                        thermalBrightnessThrottlingDataId, powerThrottlingDataId, config));
+
             if (changed) {
                 updatePowerState();
             }
@@ -1044,7 +878,7 @@
     /**
      * Unregisters all listeners and interrupts all running threads; halting future work.
      *
-     * This method should be called when the DisplayPowerController is no longer in use; i.e. when
+     * This method should be called when the DisplayPowerController2 is no longer in use; i.e. when
      * the {@link #mDisplayId display} has been removed.
      */
     @Override
@@ -1060,9 +894,7 @@
                 mAutomaticBrightnessController.stop();
             }
 
-            if (mBrightnessSetting != null) {
-                mBrightnessSetting.unregisterListener(mBrightnessSettingListener);
-            }
+            mDisplayBrightnessController.stop();
 
             mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
         }
@@ -1073,11 +905,11 @@
         // All properties that depend on the associated DisplayDevice and the DDC must be
         // updated here.
         loadBrightnessRampRates();
-        loadProximitySensor();
         loadNitsRange(mContext.getResources());
         setUpAutoBrightness(mContext, mHandler);
         reloadReduceBrightColours();
         setAnimatorRampSpeeds(/* isIdleMode= */ false);
+
         mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);
         mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
                 mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),
@@ -1126,22 +958,30 @@
         noteScreenBrightness(mPowerState.getScreenBrightness());
 
         // Initialize all of the brightness tracking state
-        final float brightness = convertToAdjustedNits(mPowerState.getScreenBrightness());
+        final float brightness = mDisplayBrightnessController.convertToAdjustedNits(
+                mPowerState.getScreenBrightness());
         if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
             mBrightnessTracker.start(brightness);
         }
-        mBrightnessSettingListener = brightnessValue -> {
+
+        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = brightnessValue -> {
             Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
             mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         };
+        mDisplayBrightnessController
+                .registerBrightnessSettingChangeListener(brightnessSettingListener);
 
-        mBrightnessSetting.registerListener(mBrightnessSettingListener);
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
                 false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
         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();
     }
 
@@ -1165,12 +1005,21 @@
         if (isIdleScreenBrightnessEnabled) {
             BrightnessMappingStrategy idleModeBrightnessMapper =
                     BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
-                            AUTO_BRIGHTNESS_MODE_IDLE, mDisplayWhiteBalanceController);
+                            AUTO_BRIGHTNESS_MODE_IDLE,
+                            mDisplayWhiteBalanceController);
             if (idleModeBrightnessMapper != null) {
-                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE, idleModeBrightnessMapper);
+                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE,
+                        idleModeBrightnessMapper);
             }
         }
 
+        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) {
@@ -1180,7 +1029,7 @@
 
         if (defaultModeBrightnessMapper != null) {
             final float dozeScaleFactor = context.getResources().getFraction(
-                    com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
+                    R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
             // Ambient Lux - Active Mode Brightness Thresholds
@@ -1264,14 +1113,14 @@
             long darkeningLightDebounceIdle = mDisplayDeviceConfig
                     .getAutoBrightnessDarkeningLightDebounceIdle();
             boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
-                    com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
+                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
 
             int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
-                    com.android.internal.R.integer.config_lightSensorWarmupTime);
+                    R.integer.config_lightSensorWarmupTime);
             int lightSensorRate = context.getResources().getInteger(
-                    com.android.internal.R.integer.config_autoBrightnessLightSensorRate);
+                    R.integer.config_autoBrightnessLightSensorRate);
             int initialLightSensorRate = context.getResources().getInteger(
-                    com.android.internal.R.integer.config_autoBrightnessInitialLightSensorRate);
+                    R.integer.config_autoBrightnessInitialLightSensorRate);
             if (initialLightSensorRate == -1) {
                 initialLightSensorRate = lightSensorRate;
             } else if (initialLightSensorRate > lightSensorRate) {
@@ -1301,7 +1150,11 @@
                     screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
                     mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
                     mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
+            mDisplayBrightnessController.setAutomaticBrightnessController(
+                    mAutomaticBrightnessController);
 
+            mAutomaticBrightnessStrategy
+                    .setAutomaticBrightnessController(mAutomaticBrightnessController);
             mBrightnessEventRingBuffer =
                     new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
 
@@ -1351,7 +1204,7 @@
         } else {
             Slog.w(mTag, "Screen brightness nits configuration is unavailable; falling back");
             mNitsRange = BrightnessMappingStrategy.getFloatArray(resources
-                    .obtainTypedArray(com.android.internal.R.array.config_screenBrightnessNits));
+                    .obtainTypedArray(R.array.config_screenBrightnessNits));
         }
     }
 
@@ -1369,7 +1222,6 @@
             mAutomaticBrightnessController.switchMode(mode);
             setAnimatorRampSpeeds(isIdle);
         }
-
         Message msg = mHandler.obtainMessage();
         msg.what = MSG_SET_DWBC_STRONG_MODE;
         msg.arg1 = isIdle ? 1 : 0;
@@ -1421,28 +1273,14 @@
 
     /** Clean up all resources that are accessed via the {@link #mHandler} thread. */
     private void cleanupHandlerThreadAfterStop() {
-        setProximitySensorEnabled(false);
+        mDisplayPowerProximityStateController.cleanup();
         mBrightnessRangeController.stop();
         mBrightnessThrottler.stop();
+        mBrightnessClamperController.stop();
         mHandler.removeCallbacksAndMessages(null);
 
         // Release any outstanding wakelocks we're still holding because of pending messages.
-        if (mUnfinishedBusiness) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
-            mUnfinishedBusiness = false;
-        }
-        if (mOnStateChangedPending) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-            mOnStateChangedPending = false;
-        }
-        for (int i = 0; i < mOnProximityPositiveMessages; i++) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
-        }
-        mOnProximityPositiveMessages = 0;
-        for (int i = 0; i < mOnProximityNegativeMessages; i++) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
-        }
-        mOnProximityNegativeMessages = 0;
+        mWakelockController.releaseAll();
 
         final float brightness = mPowerState != null
                 ? mPowerState.getScreenBrightness()
@@ -1457,10 +1295,10 @@
         if (mScreenOffBrightnessSensorController != null) {
             mScreenOffBrightnessSensorController.stop();
         }
+
         if (mDisplayWhiteBalanceController != null) {
             mDisplayWhiteBalanceController.setEnabled(false);
         }
-
     }
 
     // Call from handler thread
@@ -1476,7 +1314,6 @@
         final boolean mustNotify;
         final int previousPolicy;
         boolean mustInitialize = false;
-        int brightnessAdjustmentFlags = 0;
         mBrightnessReasonTemp.set(null);
         mTempBrightnessEvent.reset();
         SparseArray<DisplayPowerControllerInterface> displayBrightnessFollowers;
@@ -1491,7 +1328,7 @@
 
             if (mPowerRequest == null) {
                 mPowerRequest = new DisplayPowerRequest(mPendingRequestLocked);
-                updatePendingProximityRequestsLocked();
+                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
                 mPendingRequestChangedLocked = false;
                 mustInitialize = true;
                 // Assume we're on and bright until told otherwise, since that's the state we turn
@@ -1500,7 +1337,7 @@
             } else if (mPendingRequestChangedLocked) {
                 previousPolicy = mPowerRequest.policy;
                 mPowerRequest.copyFrom(mPendingRequestLocked);
-                updatePendingProximityRequestsLocked();
+                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
                 mPendingRequestChangedLocked = false;
                 mDisplayReadyLocked = false;
             } else {
@@ -1512,105 +1349,8 @@
             displayBrightnessFollowers = mDisplayBrightnessFollowers.clone();
         }
 
-        // Compute the basic display state using the policy.
-        // We might override this below based on other factors.
-        // Initialise brightness as invalid.
-        int state;
-        float brightnessState = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        boolean performScreenOffTransition = false;
-        switch (mPowerRequest.policy) {
-            case DisplayPowerRequest.POLICY_OFF:
-                state = Display.STATE_OFF;
-                performScreenOffTransition = true;
-                break;
-            case DisplayPowerRequest.POLICY_DOZE:
-                if (mDozeStateOverride != Display.STATE_UNKNOWN) {
-                    state = mDozeStateOverride;
-                } else if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
-                    state = mPowerRequest.dozeScreenState;
-                } else {
-                    state = Display.STATE_DOZE;
-                }
-                if (!mAllowAutoBrightnessWhileDozingConfig) {
-                    brightnessState = mPowerRequest.dozeScreenBrightness;
-                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE);
-                }
-                break;
-            case DisplayPowerRequest.POLICY_DIM:
-            case DisplayPowerRequest.POLICY_BRIGHT:
-            default:
-                state = Display.STATE_ON;
-                break;
-        }
-        assert (state != Display.STATE_UNKNOWN);
-
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController.setLightSensorEnabled(mUseAutoBrightness
-                    && mIsEnabled && (state == Display.STATE_OFF || (state == Display.STATE_DOZE
-                    && !mAllowAutoBrightnessWhileDozingConfig))
-                    && mLeadDisplayId == Layout.NO_LEAD_DISPLAY);
-        }
-
-        boolean skipRampBecauseOfProximityChangeToNegative = false;
-        // Apply the proximity sensor.
-        if (mProximitySensor != null) {
-            if (mPowerRequest.useProximitySensor && state != Display.STATE_OFF) {
-                // At this point the policy says that the screen should be on, but we've been
-                // asked to listen to the prox sensor to adjust the display state, so lets make
-                // sure the sensor is on.
-                setProximitySensorEnabled(true);
-                if (!mScreenOffBecauseOfProximity
-                        && mProximity == PROXIMITY_POSITIVE
-                        && !mIgnoreProximityUntilChanged) {
-                    // Prox sensor already reporting "near" so we should turn off the screen.
-                    // Also checked that we aren't currently set to ignore the proximity sensor
-                    // temporarily.
-                    mScreenOffBecauseOfProximity = true;
-                    sendOnProximityPositiveWithWakelock();
-                }
-            } else if (mWaitingForNegativeProximity
-                    && mScreenOffBecauseOfProximity
-                    && mProximity == PROXIMITY_POSITIVE
-                    && state != Display.STATE_OFF) {
-                // The policy says that we should have the screen on, but it's off due to the prox
-                // and we've been asked to wait until the screen is far from the user to turn it
-                // back on. Let keep the prox sensor on so we can tell when it's far again.
-                setProximitySensorEnabled(true);
-            } else {
-                // We haven't been asked to use the prox sensor and we're not waiting on the screen
-                // to turn back on...so lets shut down the prox sensor.
-                setProximitySensorEnabled(false);
-                mWaitingForNegativeProximity = false;
-            }
-
-            if (mScreenOffBecauseOfProximity
-                    && (mProximity != PROXIMITY_POSITIVE || mIgnoreProximityUntilChanged)) {
-                // The screen *was* off due to prox being near, but now it's "far" so lets turn
-                // the screen back on.  Also turn it back on if we've been asked to ignore the
-                // prox sensor temporarily.
-                mScreenOffBecauseOfProximity = false;
-                skipRampBecauseOfProximityChangeToNegative = true;
-                sendOnProximityNegativeWithWakelock();
-            }
-        } else {
-            setProximitySensorEnabled(false);
-            mWaitingForNegativeProximity = false;
-            mIgnoreProximityUntilChanged = false;
-
-            if (mScreenOffBecauseOfProximity) {
-                // The screen *was* off due to prox being near, but now there's no prox sensor, so
-                // let's turn the screen back on.
-                mScreenOffBecauseOfProximity = false;
-                skipRampBecauseOfProximityChangeToNegative = true;
-                sendOnProximityNegativeWithWakelock();
-            }
-        }
-
-        if (!mIsEnabled
-                || mIsInTransition
-                || mScreenOffBecauseOfProximity) {
-            state = Display.STATE_OFF;
-        }
+        int state = mDisplayStateController
+                .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition);
 
         // Initialize things the first time the power state is changed.
         if (mustInitialize) {
@@ -1620,157 +1360,100 @@
         // Animate the screen state change unless already animating.
         // The transition may be deferred, so after this point we will use the
         // actual state instead of the desired one.
-        final int oldState = mPowerState.getScreenState();
-        animateScreenStateChange(state, performScreenOffTransition);
+        animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
         state = mPowerState.getScreenState();
-        boolean slowChange = false;
 
-        if (state == Display.STATE_OFF) {
-            brightnessState = PowerManager.BRIGHTNESS_OFF_FLOAT;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_SCREEN_OFF);
+        // 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);
         }
 
-        if (Float.isNaN(brightnessState) && isValidBrightnessValue(mBrightnessToFollow)) {
-            brightnessState = mBrightnessToFollow;
-            slowChange = mBrightnessToFollowSlowChange;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_FOLLOWER);
+        final boolean userSetBrightnessChanged = mDisplayBrightnessController
+                .updateUserSetScreenBrightness();
+
+        DisplayBrightnessState displayBrightnessState = mDisplayBrightnessController
+                .updateBrightness(mPowerRequest, state);
+        float brightnessState = displayBrightnessState.getBrightness();
+        float rawBrightnessState = displayBrightnessState.getBrightness();
+        mBrightnessReasonTemp.set(displayBrightnessState.getBrightnessReason());
+        boolean slowChange = displayBrightnessState.isSlowChange();
+        // custom transition duration
+        float customAnimationRate = displayBrightnessState.getCustomAnimationRate();
+
+        // Set up the ScreenOff controller used when coming out of SCREEN_OFF and the ALS sensor
+        // doesn't yet have a valid lux value to use with auto-brightness.
+        if (mScreenOffBrightnessSensorController != null) {
+            mScreenOffBrightnessSensorController
+                    .setLightSensorEnabled(displayBrightnessState.getShouldUseAutoBrightness()
+                    && mIsEnabled && (state == Display.STATE_OFF
+                    || (state == Display.STATE_DOZE
+                    && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()))
+                    && mLeadDisplayId == Layout.NO_LEAD_DISPLAY);
         }
 
-        if ((Float.isNaN(brightnessState))
-                && isValidBrightnessValue(mPowerRequest.screenBrightnessOverride)) {
-            brightnessState = mPowerRequest.screenBrightnessOverride;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_OVERRIDE);
-            mAppliedScreenBrightnessOverride = true;
-        } else {
-            mAppliedScreenBrightnessOverride = false;
-        }
-
-        final boolean autoBrightnessEnabledInDoze =
-                mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state);
-        final boolean autoBrightnessEnabled = mUseAutoBrightness
-                && (state == Display.STATE_ON || autoBrightnessEnabledInDoze)
-                && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_OVERRIDE
-                && mAutomaticBrightnessController != null;
-        final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness
-                && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze);
-        final int autoBrightnessState = autoBrightnessEnabled
-                    && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_FOLLOWER
-                ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : autoBrightnessDisabledDueToDisplayOff
-                        ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
-                        : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED;
-
-        final boolean userSetBrightnessChanged = updateUserSetScreenBrightness();
-
-        // Use the temporary screen brightness if there isn't an override, either from
-        // WindowManager or based on the display state.
-        if (isValidBrightnessValue(mTemporaryScreenBrightness)) {
-            brightnessState = mTemporaryScreenBrightness;
-            mAppliedTemporaryBrightness = true;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_TEMPORARY);
-        } else {
-            mAppliedTemporaryBrightness = false;
-        }
-
-        final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment();
-
-        // Use the autobrightness adjustment override if set.
-        final float autoBrightnessAdjustment;
-        if (!Float.isNaN(mTemporaryAutoBrightnessAdjustment)) {
-            autoBrightnessAdjustment = mTemporaryAutoBrightnessAdjustment;
-            brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO_TEMP;
-            mAppliedTemporaryAutoBrightnessAdjustment = true;
-        } else {
-            autoBrightnessAdjustment = mAutoBrightnessAdjustment;
-            brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO;
-            mAppliedTemporaryAutoBrightnessAdjustment = false;
-        }
-        // Apply brightness boost.
-        // We do this here after deciding whether auto-brightness is enabled so that we don't
-        // disable the light sensor during this temporary state.  That way when boost ends we will
-        // be able to resume normal auto-brightness behavior without any delay.
-        if (mPowerRequest.boostScreenBrightness
-                && brightnessState != PowerManager.BRIGHTNESS_OFF_FLOAT) {
-            brightnessState = PowerManager.BRIGHTNESS_MAX;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_BOOST);
-            mAppliedBrightnessBoost = true;
-        } else {
-            mAppliedBrightnessBoost = false;
-        }
+        // Take note if the short term model was already active before applying the current
+        // request changes.
+        final boolean wasShortTermModelActive =
+                mAutomaticBrightnessStrategy.isShortTermModelActive();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(state,
+                mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig(),
+                mBrightnessReasonTemp.getReason(), mPowerRequest.policy,
+                mDisplayBrightnessController.getLastUserSetScreenBrightness(),
+                userSetBrightnessChanged);
 
         // If the brightness is already set then it's been overridden by something other than the
         // user, or is a temporary adjustment.
         boolean userInitiatedChange = (Float.isNaN(brightnessState))
-                && (autoBrightnessAdjustmentChanged || userSetBrightnessChanged);
-        boolean wasShortTermModelActive = false;
-        // Configure auto-brightness.
-        if (mAutomaticBrightnessController != null) {
-            wasShortTermModelActive = mAutomaticBrightnessController.hasUserDataPoints();
-            mAutomaticBrightnessController.configure(autoBrightnessState,
-                    mBrightnessConfiguration,
-                    mLastUserSetScreenBrightness,
-                    userSetBrightnessChanged, autoBrightnessAdjustment,
-                    autoBrightnessAdjustmentChanged, mPowerRequest.policy,
-                    mShouldResetShortTermModel);
-            mShouldResetShortTermModel = false;
-        }
-        mBrightnessRangeController.setAutoBrightnessEnabled(autoBrightnessEnabled
+                && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
+                || userSetBrightnessChanged);
+
+        mBrightnessRangeController.setAutoBrightnessEnabled(
+                mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : autoBrightnessDisabledDueToDisplayOff
+                : mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff()
                         ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
                         : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
 
-        if (mBrightnessTracker != null) {
-            mBrightnessTracker.setShouldCollectColorSample(mBrightnessConfiguration != null
-                    && mBrightnessConfiguration.shouldCollectColorSamples());
-        }
-
-        boolean updateScreenBrightnessSetting = false;
-        float rawBrightnessState = brightnessState;
-
+        boolean updateScreenBrightnessSetting =
+                displayBrightnessState.shouldUpdateScreenBrightnessSetting();
+        float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness();
         // Apply auto-brightness.
+        int brightnessAdjustmentFlags = 0;
         if (Float.isNaN(brightnessState)) {
-            float newAutoBrightnessAdjustment = autoBrightnessAdjustment;
-            if (autoBrightnessEnabled) {
-                rawBrightnessState = mAutomaticBrightnessController
-                        .getRawAutomaticScreenBrightness();
-                brightnessState = mAutomaticBrightnessController.getAutomaticScreenBrightness(
+            if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) {
+                brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
                         mTempBrightnessEvent);
-                newAutoBrightnessAdjustment =
-                        mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment();
-            }
-            if (isValidBrightnessValue(brightnessState)
-                    || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) {
-                // Use current auto-brightness value and slowly adjust to changes.
-                brightnessState = clampScreenBrightness(brightnessState);
-                if (mAppliedAutoBrightness && !autoBrightnessAdjustmentChanged) {
-                    slowChange = true; // slowly adapt to auto-brightness
+                if (BrightnessUtils.isValidBrightnessValue(brightnessState)
+                        || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) {
+                    rawBrightnessState = mAutomaticBrightnessController
+                            .getRawAutomaticScreenBrightness();
+                    brightnessState = clampScreenBrightness(brightnessState);
+                    // slowly adapt to auto-brightness
+                    // TODO(b/253226419): slowChange should be decided by strategy.updateBrightness
+                    slowChange = mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()
+                            && !mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged();
+                    brightnessAdjustmentFlags =
+                            mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags();
+                    updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
+                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
+                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
+                    if (mScreenOffBrightnessSensorController != null) {
+                        mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
+                    }
+                } else {
+                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
                 }
-                updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
-                mAppliedAutoBrightness = true;
-                mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
-                if (mScreenOffBrightnessSensorController != null) {
-                    mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
-                }
-            } else {
-                mAppliedAutoBrightness = false;
-            }
-            if (autoBrightnessAdjustment != newAutoBrightnessAdjustment) {
-                // If the autobrightness controller has decided to change the adjustment value
-                // used, make sure that's reflected in settings.
-                putAutoBrightnessAdjustmentSetting(newAutoBrightnessAdjustment);
-            } else {
-                // Adjustment values resulted in no change
-                brightnessAdjustmentFlags = 0;
             }
         } else {
             // Any non-auto-brightness values such as override or temporary should still be subject
             // to clamping so that they don't go beyond the current max as specified by HBM
             // Controller.
             brightnessState = clampScreenBrightness(brightnessState);
-            mAppliedAutoBrightness = false;
-            brightnessAdjustmentFlags = 0;
+            mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
         }
+
         // Use default brightness when dozing unless overridden.
         if ((Float.isNaN(brightnessState))
                 && Display.isDozeState(state)) {
@@ -1781,14 +1464,15 @@
 
         // The ALS is not available yet - use the screen off sensor to determine the initial
         // brightness
-        if (Float.isNaN(brightnessState) && autoBrightnessEnabled
+        if (Float.isNaN(brightnessState) && mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
                 && mScreenOffBrightnessSensorController != null) {
             rawBrightnessState =
                     mScreenOffBrightnessSensorController.getAutomaticScreenBrightness();
             brightnessState = rawBrightnessState;
-            if (isValidBrightnessValue(brightnessState)) {
+            if (BrightnessUtils.isValidBrightnessValue(brightnessState)) {
                 brightnessState = clampScreenBrightness(brightnessState);
-                updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
+                updateScreenBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness()
+                        != brightnessState;
                 mBrightnessReasonTemp.setReason(
                         BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR);
             }
@@ -1796,9 +1480,9 @@
 
         // Apply manual brightness.
         if (Float.isNaN(brightnessState)) {
-            rawBrightnessState = mCurrentScreenBrightnessSetting;
+            rawBrightnessState = currentBrightnessSetting;
             brightnessState = clampScreenBrightness(rawBrightnessState);
-            if (brightnessState != mCurrentScreenBrightnessSetting) {
+            if (brightnessState != currentBrightnessSetting) {
                 // The manually chosen screen brightness is outside of the currently allowed
                 // range (i.e., high-brightness-mode), make sure we tell the rest of the system
                 // by updating the setting.
@@ -1811,7 +1495,8 @@
                 : mAutomaticBrightnessController.getAmbientLux();
         for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
             DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
-            follower.setBrightnessToFollow(rawBrightnessState, convertToNits(rawBrightnessState),
+            follower.setBrightnessToFollow(rawBrightnessState,
+                    mDisplayBrightnessController.convertToNits(rawBrightnessState),
                     ambientLux, slowChange);
         }
 
@@ -1823,64 +1508,25 @@
         // Note throttling effectively changes the allowed brightness range, so, similarly to HBM,
         // we broadcast this change through setting.
         final float unthrottledBrightnessState = brightnessState;
-        if (mBrightnessThrottler.isThrottled()) {
-            mTempBrightnessEvent.setThermalMax(mBrightnessThrottler.getBrightnessCap());
-            brightnessState = Math.min(brightnessState, mBrightnessThrottler.getBrightnessCap());
-            mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_THROTTLED);
-            if (!mAppliedThrottling) {
-                // Brightness throttling is needed, so do so quickly.
-                // Later, when throttling is removed, we let other mechanisms decide on speed.
-                slowChange = false;
-            }
-            mAppliedThrottling = true;
-        } else if (mAppliedThrottling) {
-            mAppliedThrottling = false;
-        }
+        DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest,
+                brightnessState, slowChange);
+
+        brightnessState = clampedState.getBrightness();
+        slowChange = clampedState.isSlowChange();
+        // faster rate wins, at this point customAnimationRate == -1, strategy does not control
+        // customAnimationRate. Should be revisited if strategy start setting this value
+        customAnimationRate = Math.max(customAnimationRate, clampedState.getCustomAnimationRate());
+        mBrightnessReasonTemp.addModifier(clampedState.getBrightnessReason().getModifier());
 
         if (updateScreenBrightnessSetting) {
             // Tell the rest of the system about the new brightness in case we had to change it
             // for things like auto-brightness or high-brightness-mode. Note that we do this
-            // before applying the low power or dim transformations so that the slider
-            // accurately represents the full possible range, even if they range changes what
-            // it means in absolute terms.
-            updateScreenBrightnessSetting(brightnessState);
-        }
-
-        // Apply dimming by at least some minimum amount when user activity
-        // timeout is about to expire.
-        if (mPowerRequest.policy == DisplayPowerRequest.POLICY_DIM) {
-            if (brightnessState > PowerManager.BRIGHTNESS_MIN) {
-                brightnessState = Math.max(
-                        Math.min(brightnessState - mScreenBrightnessMinimumDimAmount,
-                                mScreenBrightnessDimConfig),
-                        PowerManager.BRIGHTNESS_MIN);
-                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_DIMMED);
-            }
-            if (!mAppliedDimming) {
-                slowChange = false;
-            }
-            mAppliedDimming = true;
-        } else if (mAppliedDimming) {
-            slowChange = false;
-            mAppliedDimming = false;
-        }
-        // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor
-        // as long as it is above the minimum threshold.
-        if (mPowerRequest.lowPowerMode) {
-            if (brightnessState > PowerManager.BRIGHTNESS_MIN) {
-                final float brightnessFactor =
-                        Math.min(mPowerRequest.screenLowPowerBrightnessFactor, 1);
-                final float lowPowerBrightnessFloat = (brightnessState * brightnessFactor);
-                brightnessState = Math.max(lowPowerBrightnessFloat, PowerManager.BRIGHTNESS_MIN);
-                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_LOW_POWER);
-            }
-            if (!mAppliedLowPower) {
-                slowChange = false;
-            }
-            mAppliedLowPower = true;
-        } else if (mAppliedLowPower) {
-            slowChange = false;
-            mAppliedLowPower = false;
+            // only considering maxBrightness (ignoring brightness modifiers like low power or dim)
+            // so that the slider accurately represents the full possible range,
+            // even if they range changes what it means in absolute terms.
+            mDisplayBrightnessController.updateScreenBrightnessSetting(
+                    MathUtils.constrain(unthrottledBrightnessState,
+                            clampedState.getMinBrightness(), clampedState.getMaxBrightness()));
         }
 
         // The current brightness to use has been calculated at this point, and HbmController should
@@ -1889,13 +1535,15 @@
         // brightness sources (such as an app override) are not saved to the setting, but should be
         // reflected in HBM calculations.
         mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
-                mBrightnessThrottler.getBrightnessMaxReason());
+                mBrightnessClamperController.getBrightnessMaxReason());
 
         // Animate the screen brightness when the screen is on or dozing.
-        // Skip the animation when the screen is off.
+        // Skip the animation when the screen is off or suspended.
         boolean brightnessAdjusted = false;
         final boolean brightnessIsTemporary =
-                mAppliedTemporaryBrightness || mAppliedTemporaryAutoBrightnessAdjustment;
+                (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_TEMPORARY)
+                        || mAutomaticBrightnessStrategy
+                        .isTemporaryAutoBrightnessAdjustmentApplied();
         if (!mPendingScreenOff) {
             if (mSkipScreenOnBrightnessRamp) {
                 if (state == Display.STATE_ON) {
@@ -1916,7 +1564,8 @@
             }
 
             final boolean initialRampSkip = (state == Display.STATE_ON && mSkipRampState
-                    != RAMP_STATE_SKIP_NONE) || skipRampBecauseOfProximityChangeToNegative;
+                    != RAMP_STATE_SKIP_NONE) || mDisplayPowerProximityStateController
+                    .shouldSkipRampBecauseOfProximityChangeToNegative();
             // While dozing, sometimes the brightness is split into buckets. Rather than animating
             // through the buckets, which is unlikely to be smooth in the first place, just jump
             // right to the suggested brightness.
@@ -1950,13 +1599,25 @@
                 // We want to scale HDR brightness level with the SDR level, we also need to restore
                 // SDR brightness immediately when entering dim or low power mode.
                 animateValue = mBrightnessRangeController.getHdrBrightnessValue();
+                customAnimationRate = Math.max(customAnimationRate,
+                        mBrightnessRangeController.getHdrTransitionRate());
                 mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_HDR);
             }
 
+            // if doze or suspend state is requested, we want to finish brightnes animation fast
+            // to allow state animation to start
+            if (mPowerRequest.policy == DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE
+                    && (mPowerRequest.dozeScreenState == Display.STATE_UNKNOWN  // dozing
+                    || mPowerRequest.dozeScreenState == Display.STATE_DOZE_SUSPEND
+                    || mPowerRequest.dozeScreenState == Display.STATE_ON_SUSPEND)) {
+                customAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
+                slowChange = false;
+            }
+
             final float currentBrightness = mPowerState.getScreenBrightness();
             final float currentSdrBrightness = mPowerState.getSdrScreenBrightness();
 
-            if (isValidBrightnessValue(animateValue)
+            if (BrightnessUtils.isValidBrightnessValue(animateValue)
                     && (animateValue != currentBrightness
                     || sdrAnimateValue != currentSdrBrightness)) {
                 boolean skipAnimation = initialRampSkip || hasBrightnessBuckets
@@ -1986,6 +1647,9 @@
                 if (skipAnimation) {
                     animateScreenBrightness(animateValue, sdrAnimateValue,
                             SCREEN_ANIMATION_RATE_MINIMUM);
+                } else if (customAnimationRate > 0) {
+                    animateScreenBrightness(animateValue, sdrAnimateValue,
+                            customAnimationRate, /* ignoreAnimationLimits = */true);
                 } else {
                     boolean isIncreasing = animateValue > currentBrightness;
                     final float rampSpeed;
@@ -2007,13 +1671,15 @@
             }
 
             notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange,
-                    wasShortTermModelActive, autoBrightnessEnabled, brightnessIsTemporary);
+                    wasShortTermModelActive, mAutomaticBrightnessStrategy.isAutoBrightnessEnabled(),
+                    brightnessIsTemporary, displayBrightnessState.getShouldUseAutoBrightness());
 
             // We save the brightness info *after* the brightness setting has been changed and
             // adjustments made so that the brightness info reflects the latest value.
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(), animateValue);
+            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(),
+                    animateValue, clampedState);
         } else {
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting());
+            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(), clampedState);
         }
 
         // Only notify if the brightness adjustment is not temporary (i.e. slider has been released)
@@ -2049,13 +1715,20 @@
                 ? mCdsi.getReduceBrightColorsStrength() : -1);
         mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
         mTempBrightnessEvent.setWasShortTermModelActive(wasShortTermModelActive);
-        mTempBrightnessEvent.setAutomaticBrightnessEnabled(mUseAutoBrightness);
+        mTempBrightnessEvent.setDisplayBrightnessStrategyName(displayBrightnessState
+                .getDisplayBrightnessStrategyName());
+        mTempBrightnessEvent.setAutomaticBrightnessEnabled(
+                displayBrightnessState.getShouldUseAutoBrightness());
         // Temporary is what we use during slider interactions. We avoid logging those so that
         // we don't spam logcat when the slider is being used.
         boolean tempToTempTransition =
                 mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY
                         && mLastBrightnessEvent.getReason().getReason()
                         == BrightnessReason.REASON_TEMPORARY;
+        // Purely for dumpsys;
+        final boolean isRbcEvent =
+                mLastBrightnessEvent.isRbcEnabled() != mTempBrightnessEvent.isRbcEnabled();
+
         if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition)
                 || brightnessAdjustmentFlags != 0) {
             mTempBrightnessEvent.setInitialBrightness(mLastBrightnessEvent.getBrightness());
@@ -2075,6 +1748,10 @@
             if (mBrightnessEventRingBuffer != null) {
                 mBrightnessEventRingBuffer.append(newEvent);
             }
+            if (isRbcEvent) {
+                mRbcEventRingBuffer.append(newEvent);
+            }
+
         }
 
         // Update display white-balance.
@@ -2092,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);
@@ -2106,12 +1784,8 @@
         }
 
         // Grab a wake lock if we have unfinished business.
-        if (!finished && !mUnfinishedBusiness) {
-            if (DEBUG) {
-                Slog.d(mTag, "Unfinished business...");
-            }
-            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
-            mUnfinishedBusiness = true;
+        if (!finished) {
+            mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
         }
 
         // Notify the power manager when ready.
@@ -2130,12 +1804,8 @@
         }
 
         // Release the wake lock when we have no unfinished business.
-        if (finished && mUnfinishedBusiness) {
-            if (DEBUG) {
-                Slog.d(mTag, "Finished business...");
-            }
-            mUnfinishedBusiness = false;
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
+        if (finished) {
+            mWakelockController.releaseWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
         }
 
         // Record if dozing for future comparison.
@@ -2166,9 +1836,9 @@
 
     private void setDwbcLoggingEnabled(int arg) {
         if (mDisplayWhiteBalanceController != null) {
-            final boolean shouldEnable = (arg == 1);
-            mDisplayWhiteBalanceController.setLoggingEnabled(shouldEnable);
-            mDisplayWhiteBalanceSettings.setLoggingEnabled(shouldEnable);
+            final boolean enabled = (arg == 1);
+            mDisplayWhiteBalanceController.setLoggingEnabled(enabled);
+            mDisplayWhiteBalanceSettings.setLoggingEnabled(enabled);
         }
     }
 
@@ -2183,7 +1853,7 @@
      */
     @Override
     public void ignoreProximitySensorUntilChanged() {
-        mHandler.sendEmptyMessage(MSG_IGNORE_PROXIMITY);
+        mDisplayPowerProximityStateController.ignoreProximitySensorUntilChanged();
     }
 
     @Override
@@ -2210,21 +1880,27 @@
 
     @Override
     public void setBrightnessFromOffload(float brightness) {
-        // The old DPC is no longer supported
+        Message msg = mHandler.obtainMessage(MSG_SET_BRIGHTNESS_FROM_OFFLOAD,
+                Float.floatToIntBits(brightness), 0 /*unused*/);
+        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
     }
 
     @Override
     public float[] getAutoBrightnessLevels(
             @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        // The old DPC is no longer supported
-        return null;
+        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
+        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
     }
 
     @Override
     public float[] getAutoBrightnessLuxLevels(
             @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        // The old DPC is no longer supported
-        return null;
+        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
+        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
     }
 
     @Override
@@ -2241,18 +1917,29 @@
         }
     }
 
-    private boolean saveBrightnessInfo(float brightness) {
-        return saveBrightnessInfo(brightness, brightness);
+    @Override
+    public void onBootCompleted() {
+        Message msg = mHandler.obtainMessage(MSG_BOOT_COMPLETED);
+        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
     }
 
-    private boolean saveBrightnessInfo(float brightness, float adjustedBrightness) {
+    private boolean saveBrightnessInfo(float brightness) {
+        return saveBrightnessInfo(brightness, /* state= */ null);
+    }
+
+    private boolean saveBrightnessInfo(float brightness, @Nullable DisplayBrightnessState state) {
+        return saveBrightnessInfo(brightness, brightness, state);
+    }
+
+    private boolean saveBrightnessInfo(float brightness, float adjustedBrightness,
+            @Nullable DisplayBrightnessState state) {
         synchronized (mCachedBrightnessInfo) {
-            final float minBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMin(),
-                    mBrightnessThrottler.getBrightnessCap());
+            float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX;
+            float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX;
+            final float minBrightness = Math.max(stateMin, Math.min(
+                    mBrightnessRangeController.getCurrentBrightnessMin(), stateMax));
             final float maxBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMax(),
-                    mBrightnessThrottler.getBrightnessCap());
+                    mBrightnessRangeController.getCurrentBrightnessMax(), stateMax);
             boolean changed = false;
 
             changed |=
@@ -2275,8 +1962,7 @@
                             mBrightnessRangeController.getTransitionPoint());
             changed |=
                     mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
-                            mBrightnessThrottler.getBrightnessMaxReason());
-
+                            mBrightnessClamperController.getBrightnessMaxReason());
             return changed;
         }
     }
@@ -2288,27 +1974,18 @@
     }
 
     private HighBrightnessModeController createHbmControllerLocked(
-            Runnable modeChangeCallback) {
-        final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
-        final IBinder displayToken =
-                mLogicalDisplay.getPrimaryDisplayDeviceLocked().getDisplayTokenLocked();
-        final String displayUniqueId =
-                mLogicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+            HighBrightnessModeMetadata hbmMetadata, Runnable modeChangeCallback) {
+        final DisplayDeviceConfig ddConfig = mDisplayDevice.getDisplayDeviceConfig();
+        final IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
+        final String displayUniqueId = mDisplayDevice.getUniqueId();
         final DisplayDeviceConfig.HighBrightnessModeData hbmData =
                 ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
-        final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
+        final DisplayDeviceInfo info = mDisplayDevice.getDisplayDeviceInfoLocked();
         return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
                 displayToken, displayUniqueId, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, hbmData,
-                new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
-                    @Override
-                    public float getHdrBrightnessFromSdr(
-                            float sdrBrightness, float maxDesiredHdrSdrRatio) {
-                        return mDisplayDeviceConfig.getHdrBrightnessFromSdr(
-                                sdrBrightness, maxDesiredHdrSdrRatio);
-                    }
-                }, modeChangeCallback, mHighBrightnessModeMetadata, mContext);
+                PowerManager.BRIGHTNESS_MAX, hbmData, (sdrBrightness, maxDesiredHdrSdrRatio) ->
+                        mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness,
+                                maxDesiredHdrSdrRatio), modeChangeCallback, hbmMetadata, mContext);
     }
 
     private BrightnessThrottler createBrightnessThrottlerLocked() {
@@ -2359,18 +2036,72 @@
         }
     }
 
+    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 && !mScreenOffBecauseOfProximity) {
+            if (isOff && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
                 if (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_ON
                         || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
                     setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF);
@@ -2383,8 +2114,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";
@@ -2410,7 +2142,7 @@
         // it is only removed once the window manager tells us that the activity has
         // finished drawing underneath.
         if (isOff && mReportedScreenStateToPolicy != REPORTED_TO_POLICY_SCREEN_OFF
-                && !mScreenOffBecauseOfProximity) {
+                && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
             setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF);
             unblockScreenOn();
             mWindowManagerPolicy.screenTurnedOff(mDisplayId, mIsInTransition);
@@ -2436,12 +2168,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() {
@@ -2456,18 +2192,6 @@
                 mDisplayDeviceConfig.getScreenOffBrightnessSensor(), SensorUtils.NO_FALLBACK);
     }
 
-    private void loadProximitySensor() {
-        if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT || mDisplayId != Display.DEFAULT_DISPLAY) {
-            return;
-        }
-        mProximitySensor = SensorUtils.findSensor(mSensorManager,
-                mDisplayDeviceConfig.getProximitySensor(), Sensor.TYPE_PROXIMITY);
-        if (mProximitySensor != null) {
-            mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
-                    TYPICAL_PROXIMITY_THRESHOLD);
-        }
-    }
-
     private float clampScreenBrightness(float value) {
         if (Float.isNaN(value)) {
             value = PowerManager.BRIGHTNESS_MIN;
@@ -2476,18 +2200,18 @@
                 mBrightnessRangeController.getCurrentBrightnessMax());
     }
 
-    // Checks whether the brightness is within the valid brightness range, not including off.
-    private boolean isValidBrightnessValue(float brightness) {
-        return brightness >= PowerManager.BRIGHTNESS_MIN
-                && brightness <= PowerManager.BRIGHTNESS_MAX;
+    private void animateScreenBrightness(float target, float sdrTarget, float rate) {
+        animateScreenBrightness(target, sdrTarget, rate, /* ignoreAnimationLimits = */false);
     }
 
-    private void animateScreenBrightness(float target, float sdrTarget, float rate) {
+    private void animateScreenBrightness(float target, float sdrTarget, float rate,
+            boolean ignoreAnimationLimits) {
         if (DEBUG) {
             Slog.d(mTag, "Animating brightness: target=" + target + ", sdrTarget=" + sdrTarget
                     + ", rate=" + rate);
         }
-        if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate, false)) {
+        if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate,
+                ignoreAnimationLimits)) {
             Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target);
 
             String propertyKey = "debug.tracing.screen_brightness";
@@ -2655,102 +2379,11 @@
 
     private final Runnable mCleanListener = this::sendUpdatePowerState;
 
-    private void setProximitySensorEnabled(boolean enable) {
-        if (enable) {
-            if (!mProximitySensorEnabled) {
-                // Register the listener.
-                // Proximity sensor state already cleared initially.
-                mProximitySensorEnabled = true;
-                mIgnoreProximityUntilChanged = false;
-                mSensorManager.registerListener(mProximitySensorListener, mProximitySensor,
-                        SensorManager.SENSOR_DELAY_NORMAL, mHandler);
-            }
-        } else {
-            if (mProximitySensorEnabled) {
-                // Unregister the listener.
-                // Clear the proximity sensor state for next time.
-                mProximitySensorEnabled = false;
-                mProximity = PROXIMITY_UNKNOWN;
-                mIgnoreProximityUntilChanged = false;
-                mPendingProximity = PROXIMITY_UNKNOWN;
-                mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-                mSensorManager.unregisterListener(mProximitySensorListener);
-                clearPendingProximityDebounceTime(); // release wake lock (must be last)
-            }
-        }
-    }
-
-    private void handleProximitySensorEvent(long time, boolean positive) {
-        if (mProximitySensorEnabled) {
-            if (mPendingProximity == PROXIMITY_NEGATIVE && !positive) {
-                return; // no change
-            }
-            if (mPendingProximity == PROXIMITY_POSITIVE && positive) {
-                return; // no change
-            }
-
-            // Only accept a proximity sensor reading if it remains
-            // stable for the entire debounce delay.  We hold a wake lock while
-            // debouncing the sensor.
-            mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-            if (positive) {
-                mPendingProximity = PROXIMITY_POSITIVE;
-                setPendingProximityDebounceTime(
-                        time + PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY); // acquire wake lock
-            } else {
-                mPendingProximity = PROXIMITY_NEGATIVE;
-                setPendingProximityDebounceTime(
-                        time + PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY); // acquire wake lock
-            }
-
-            // Debounce the new sensor reading.
-            debounceProximitySensor();
-        }
-    }
-
-    private void debounceProximitySensor() {
-        if (mProximitySensorEnabled
-                && mPendingProximity != PROXIMITY_UNKNOWN
-                && mPendingProximityDebounceTime >= 0) {
-            final long now = mClock.uptimeMillis();
-            if (mPendingProximityDebounceTime <= now) {
-                if (mProximity != mPendingProximity) {
-                    // if the status of the sensor changed, stop ignoring.
-                    mIgnoreProximityUntilChanged = false;
-                    Slog.i(mTag, "No longer ignoring proximity [" + mPendingProximity + "]");
-                }
-                // Sensor reading accepted.  Apply the change then release the wake lock.
-                mProximity = mPendingProximity;
-                updatePowerState();
-                clearPendingProximityDebounceTime(); // release wake lock (must be last)
-            } else {
-                // Need to wait a little longer.
-                // Debounce again later.  We continue holding a wake lock while waiting.
-                Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-                mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime);
-            }
-        }
-    }
-
-    private void clearPendingProximityDebounceTime() {
-        if (mPendingProximityDebounceTime >= 0) {
-            mPendingProximityDebounceTime = -1;
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxDebounce);
-        }
-    }
-
-    private void setPendingProximityDebounceTime(long debounceTime) {
-        if (mPendingProximityDebounceTime < 0) {
-            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxDebounce);
-        }
-        mPendingProximityDebounceTime = debounceTime;
-    }
-
     private void sendOnStateChangedWithWakelock() {
-        if (!mOnStateChangedPending) {
-            mOnStateChangedPending = true;
-            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-            mHandler.post(mOnStateChangedRunnable);
+        boolean wakeLockAcquired = mWakelockController.acquireWakelock(
+                WakelockController.WAKE_LOCK_STATE_CHANGED);
+        if (wakeLockAcquired) {
+            mHandler.post(mWakelockController.getOnStateChangedRunnable());
         }
     }
 
@@ -2762,12 +2395,15 @@
     }
 
     private void handleSettingsChange(boolean userSwitch) {
-        mPendingScreenBrightnessSetting = getScreenBrightnessSetting();
-        mPendingAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
+        mDisplayBrightnessController
+                .setPendingScreenBrightness(mDisplayBrightnessController
+                        .getScreenBrightnessSetting());
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(userSwitch);
         if (userSwitch) {
             // Don't treat user switches as user initiated change.
-            setCurrentScreenBrightness(mPendingScreenBrightnessSetting);
-            updateAutoBrightnessAdjustment();
+            mDisplayBrightnessController
+                    .setAndNotifyCurrentScreenBrightness(mDisplayBrightnessController
+                            .getPendingScreenBrightness());
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.resetShortTermModel();
             }
@@ -2781,129 +2417,59 @@
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
         mHandler.postAtTime(() -> {
-            mUseAutoBrightness = screenBrightnessModeSetting
-                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC;
+            mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting
+                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
             updatePowerState();
         }, mClock.uptimeMillis());
     }
 
-    private float getAutoBrightnessAdjustmentSetting() {
-        final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT);
-        return Float.isNaN(adj) ? 0.0f : clampAutoBrightnessAdjustment(adj);
-    }
 
     @Override
     public float getScreenBrightnessSetting() {
-        float brightness = mBrightnessSetting.getBrightness();
-        if (Float.isNaN(brightness)) {
-            brightness = mScreenBrightnessDefault;
-        }
-        return clampAbsoluteBrightness(brightness);
-    }
-
-    private void loadNitBasedBrightnessSetting() {
-        if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
-            float brightnessNitsForDefaultDisplay =
-                    mBrightnessSetting.getBrightnessNitsForDefaultDisplay();
-            if (brightnessNitsForDefaultDisplay >= 0) {
-                float brightnessForDefaultDisplay = getBrightnessFromNits(
-                        brightnessNitsForDefaultDisplay);
-                if (isValidBrightnessValue(brightnessForDefaultDisplay)) {
-                    mBrightnessSetting.setBrightness(brightnessForDefaultDisplay);
-                    mCurrentScreenBrightnessSetting = brightnessForDefaultDisplay;
-                    return;
-                }
-            }
-        }
-        mCurrentScreenBrightnessSetting = getScreenBrightnessSetting();
+        return mDisplayBrightnessController.getScreenBrightnessSetting();
     }
 
     @Override
     public void setBrightness(float brightnessValue, int userSerial) {
-        // Update the setting, which will eventually call back into DPC to have us actually update
-        // the display with the new value.
-        float clampedBrightnessValue = clampScreenBrightness(brightnessValue);
-        mBrightnessSetting.setUserSerial(userSerial);
-        mBrightnessSetting.setBrightness(clampedBrightnessValue);
-        if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
-            float nits = convertToNits(clampedBrightnessValue);
-            if (nits >= 0) {
-                mBrightnessSetting.setBrightnessNitsForDefaultDisplay(nits);
-            }
-        }
+        mDisplayBrightnessController.setBrightness(clampScreenBrightness(brightnessValue),
+                userSerial);
     }
 
     @Override
-    public void onBootCompleted() {
-        Message msg = mHandler.obtainMessage(MSG_BOOT_COMPLETED);
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
+    public int getDisplayId() {
+        return mDisplayId;
     }
 
-    private void updateScreenBrightnessSetting(float brightnessValue) {
-        if (!isValidBrightnessValue(brightnessValue)
-                || brightnessValue == mCurrentScreenBrightnessSetting) {
-            return;
-        }
-        setCurrentScreenBrightness(brightnessValue);
-        setBrightness(brightnessValue);
+    @Override
+    public int getLeadDisplayId() {
+        return mLeadDisplayId;
     }
 
-    private void setCurrentScreenBrightness(float brightnessValue) {
-        if (brightnessValue != mCurrentScreenBrightnessSetting) {
-            mCurrentScreenBrightnessSetting = brightnessValue;
-            postBrightnessChangeRunnable();
+    @Override
+    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
+            boolean slowChange) {
+        mBrightnessRangeController.onAmbientLuxChange(ambientLux);
+        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
+            mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
+        } else {
+            float brightness = mDisplayBrightnessController.getBrightnessFromNits(nits);
+            if (BrightnessUtils.isValidBrightnessValue(brightness)) {
+                mDisplayBrightnessController.setBrightnessToFollow(brightness, slowChange);
+            } else {
+                // The device does not support nits
+                mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness,
+                        slowChange);
+            }
         }
-    }
-
-    private void putAutoBrightnessAdjustmentSetting(float adjustment) {
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            mAutoBrightnessAdjustment = adjustment;
-            Settings.System.putFloatForUser(mContext.getContentResolver(),
-                    Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, adjustment,
-                    UserHandle.USER_CURRENT);
-        }
-    }
-
-    private boolean updateAutoBrightnessAdjustment() {
-        if (Float.isNaN(mPendingAutoBrightnessAdjustment)) {
-            return false;
-        }
-        if (mAutoBrightnessAdjustment == mPendingAutoBrightnessAdjustment) {
-            mPendingAutoBrightnessAdjustment = Float.NaN;
-            return false;
-        }
-        mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment;
-        mPendingAutoBrightnessAdjustment = Float.NaN;
-        mTemporaryAutoBrightnessAdjustment = Float.NaN;
-        return true;
-    }
-
-    // We want to return true if the user has set the screen brightness.
-    // RBC on, off, and intensity changes will return false.
-    // Slider interactions whilst in RBC will return true, just as when in non-rbc.
-    private boolean updateUserSetScreenBrightness() {
-        if ((Float.isNaN(mPendingScreenBrightnessSetting)
-                || mPendingScreenBrightnessSetting < 0.0f)) {
-            return false;
-        }
-        if (mCurrentScreenBrightnessSetting == mPendingScreenBrightnessSetting) {
-            mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            return false;
-        }
-        setCurrentScreenBrightness(mPendingScreenBrightnessSetting);
-        mLastUserSetScreenBrightness = mPendingScreenBrightnessSetting;
-        mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        return true;
+        sendUpdatePowerState();
     }
 
     private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
             boolean wasShortTermModelActive, boolean autobrightnessEnabled,
-            boolean brightnessIsTemporary) {
-        final float brightnessInNits = convertToAdjustedNits(brightness);
+            boolean brightnessIsTemporary, boolean shouldUseAutoBrightness) {
 
+        final float brightnessInNits =
+                mDisplayBrightnessController.convertToAdjustedNits(brightness);
         // Don't report brightness to brightnessTracker:
         // If brightness is temporary (ie the slider has not been released)
         // or if we are in idle screen brightness mode.
@@ -2915,12 +2481,13 @@
                 || mAutomaticBrightnessController.isInIdleMode()
                 || !autobrightnessEnabled
                 || mBrightnessTracker == null
-                || !mUseAutoBrightness
+                || !shouldUseAutoBrightness
                 || brightnessInNits < 0.0f) {
             return;
         }
 
-        if (userInitiated && !mAutomaticBrightnessController.hasValidAmbientLux()) {
+        if (userInitiated && (mAutomaticBrightnessController == null
+                || !mAutomaticBrightnessController.hasValidAmbientLux())) {
             // If we don't have a valid lux reading we can't report a valid
             // slider event so notify as if the system changed the brightness.
             userInitiated = false;
@@ -2939,96 +2506,33 @@
                 mAutomaticBrightnessController.getLastSensorTimestamps());
     }
 
-    private float convertToNits(float brightness) {
-        if (mAutomaticBrightnessController == null) {
-            return BrightnessMappingStrategy.INVALID_NITS;
+    @Override
+    public void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
+        synchronized (mLock) {
+            mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
+            sendUpdatePowerStateLocked();
         }
-        return mAutomaticBrightnessController.convertToNits(brightness);
     }
 
-    private float convertToAdjustedNits(float brightness) {
-        if (mAutomaticBrightnessController == null) {
-            return BrightnessMappingStrategy.INVALID_NITS;
+    @Override
+    public void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
+        synchronized (mLock) {
+            mDisplayBrightnessFollowers.remove(follower.getDisplayId());
+            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
-        return mAutomaticBrightnessController.convertToAdjustedNits(brightness);
-    }
-
-    private float getBrightnessFromNits(float nits) {
-        if (mAutomaticBrightnessController == null) {
-            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        }
-        return mAutomaticBrightnessController.getBrightnessFromNits(nits);
     }
 
     @GuardedBy("mLock")
-    private void updatePendingProximityRequestsLocked() {
-        mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked;
-        mPendingWaitForNegativeProximityLocked = false;
-
-        if (mIgnoreProximityUntilChanged) {
-            // Also, lets stop waiting for negative proximity if we're ignoring it.
-            mWaitingForNegativeProximity = false;
+    private void clearDisplayBrightnessFollowersLocked() {
+        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
+            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
+            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
-    }
-
-    private void ignoreProximitySensorUntilChangedInternal() {
-        if (!mIgnoreProximityUntilChanged
-                && mProximity == PROXIMITY_POSITIVE) {
-            // Only ignore if it is still reporting positive (near)
-            mIgnoreProximityUntilChanged = true;
-            Slog.i(mTag, "Ignoring proximity");
-            updatePowerState();
-        }
-    }
-
-    private final Runnable mOnStateChangedRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mOnStateChangedPending = false;
-            mCallbacks.onStateChanged();
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-        }
-    };
-
-    private void sendOnProximityPositiveWithWakelock() {
-        mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive);
-        mHandler.post(mOnProximityPositiveRunnable);
-        mOnProximityPositiveMessages++;
-    }
-
-    private final Runnable mOnProximityPositiveRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mOnProximityPositiveMessages--;
-            mCallbacks.onProximityPositive();
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
-        }
-    };
-
-    private void sendOnProximityNegativeWithWakelock() {
-        mOnProximityNegativeMessages++;
-        mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative);
-        mHandler.post(mOnProximityNegativeRunnable);
-    }
-
-    private final Runnable mOnProximityNegativeRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mOnProximityNegativeMessages--;
-            mCallbacks.onProximityNegative();
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
-        }
-    };
-
-    /**
-     * Indicates whether the display state is ready to update. If this is the default display, we
-     * want to update it right away so that we can draw the boot animation on it. If it is not
-     * the default display, drawing the boot animation on it would look incorrect, so we need
-     * to wait until boot is completed.
-     * @return True if the display state is ready to update
-     */
-    private boolean readyToUpdateDisplayState() {
-        return mDisplayId == Display.DEFAULT_DISPLAY || mBootCompleted;
+        mDisplayBrightnessFollowers.clear();
     }
 
     @Override
@@ -3040,31 +2544,23 @@
             pw.println("  mLeadDisplayId=" + mLeadDisplayId);
             pw.println("  mLightSensor=" + mLightSensor);
             pw.println("  mDisplayBrightnessFollowers=" + mDisplayBrightnessFollowers);
-            pw.println("  mDozeStateOverride=" + mDozeStateOverride);
 
             pw.println();
             pw.println("Display Power Controller Locked State:");
             pw.println("  mDisplayReadyLocked=" + mDisplayReadyLocked);
             pw.println("  mPendingRequestLocked=" + mPendingRequestLocked);
             pw.println("  mPendingRequestChangedLocked=" + mPendingRequestChangedLocked);
-            pw.println("  mPendingWaitForNegativeProximityLocked="
-                    + mPendingWaitForNegativeProximityLocked);
             pw.println("  mPendingUpdatePowerStateLocked=" + mPendingUpdatePowerStateLocked);
         }
 
         pw.println();
         pw.println("Display Power Controller Configuration:");
-        pw.println("  mScreenBrightnessRangeDefault=" + mScreenBrightnessDefault);
         pw.println("  mScreenBrightnessDozeConfig=" + mScreenBrightnessDozeConfig);
-        pw.println("  mScreenBrightnessDimConfig=" + mScreenBrightnessDimConfig);
         pw.println("  mUseSoftwareAutoBrightnessConfig=" + mUseSoftwareAutoBrightnessConfig);
-        pw.println("  mAllowAutoBrightnessWhileDozingConfig="
-                + mAllowAutoBrightnessWhileDozingConfig);
-        pw.println("  mPersistBrightnessNitsForDefaultDisplay="
-                + mPersistBrightnessNitsForDefaultDisplay);
         pw.println("  mSkipScreenOnBrightnessRamp=" + mSkipScreenOnBrightnessRamp);
         pw.println("  mColorFadeFadesConfig=" + mColorFadeFadesConfig);
         pw.println("  mColorFadeEnabled=" + mColorFadeEnabled);
+        pw.println("  mIsDisplayInternal=" + mIsDisplayInternal);
         synchronized (mCachedBrightnessInfo) {
             pw.println("  mCachedBrightnessInfo.brightness="
                     + mCachedBrightnessInfo.brightness.value);
@@ -3082,7 +2578,6 @@
         }
         pw.println("  mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig);
         pw.println("  mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig);
-
         mHandler.runWithScissors(() -> dumpLocal(pw), 1000);
     }
 
@@ -3090,35 +2585,9 @@
         pw.println();
         pw.println("Display Power Controller Thread State:");
         pw.println("  mPowerRequest=" + mPowerRequest);
-        pw.println("  mUnfinishedBusiness=" + mUnfinishedBusiness);
-        pw.println("  mWaitingForNegativeProximity=" + mWaitingForNegativeProximity);
-        pw.println("  mProximitySensor=" + mProximitySensor);
-        pw.println("  mProximitySensorEnabled=" + mProximitySensorEnabled);
-        pw.println("  mProximityThreshold=" + mProximityThreshold);
-        pw.println("  mProximity=" + proximityToString(mProximity));
-        pw.println("  mPendingProximity=" + proximityToString(mPendingProximity));
-        pw.println("  mPendingProximityDebounceTime="
-                + TimeUtils.formatUptime(mPendingProximityDebounceTime));
-        pw.println("  mScreenOffBecauseOfProximity=" + mScreenOffBecauseOfProximity);
-        pw.println("  mLastUserSetScreenBrightness=" + mLastUserSetScreenBrightness);
-        pw.println("  mPendingScreenBrightnessSetting="
-                + mPendingScreenBrightnessSetting);
-        pw.println("  mTemporaryScreenBrightness=" + mTemporaryScreenBrightness);
-        pw.println("  mBrightnessToFollow=" + mBrightnessToFollow);
-        pw.println("  mBrightnessToFollowSlowChange=" + mBrightnessToFollowSlowChange);
-        pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
         pw.println("  mBrightnessReason=" + mBrightnessReason);
-        pw.println("  mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment);
-        pw.println("  mPendingAutoBrightnessAdjustment=" + mPendingAutoBrightnessAdjustment);
-        pw.println("  mAppliedAutoBrightness=" + mAppliedAutoBrightness);
         pw.println("  mAppliedDimming=" + mAppliedDimming);
-        pw.println("  mAppliedLowPower=" + mAppliedLowPower);
         pw.println("  mAppliedThrottling=" + mAppliedThrottling);
-        pw.println("  mAppliedScreenBrightnessOverride=" + mAppliedScreenBrightnessOverride);
-        pw.println("  mAppliedTemporaryBrightness=" + mAppliedTemporaryBrightness);
-        pw.println("  mAppliedTemporaryAutoBrightnessAdjustment="
-                + mAppliedTemporaryAutoBrightnessAdjustment);
-        pw.println("  mAppliedBrightnessBoost=" + mAppliedBrightnessBoost);
         pw.println("  mDozing=" + mDozing);
         pw.println("  mSkipRampState=" + skipRampStateToString(mSkipRampState));
         pw.println("  mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime);
@@ -3129,9 +2598,8 @@
         pw.println("  mReportedToPolicy="
                 + reportedToPolicyToString(mReportedScreenStateToPolicy));
         pw.println("  mIsRbcActive=" + mIsRbcActive);
-        pw.println("  mOnStateChangePending=" + mOnStateChangedPending);
-        pw.println("  mOnProximityPositiveMessages=" + mOnProximityPositiveMessages);
-        pw.println("  mOnProximityNegativeMessages=" + mOnProximityNegativeMessages);
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "    ");
+        mAutomaticBrightnessStrategy.dump(ipw);
 
         if (mScreenBrightnessRampAnimator != null) {
             pw.println("  mScreenBrightnessRampAnimator.isAnimating()="
@@ -3156,6 +2624,8 @@
             dumpBrightnessEvents(pw);
         }
 
+        dumpRbcEvents(pw);
+
         if (mScreenOffBrightnessSensorController != null) {
             mScreenOffBrightnessSensorController.dump(pw);
         }
@@ -3173,21 +2643,30 @@
             mDisplayWhiteBalanceController.dump(pw);
             mDisplayWhiteBalanceSettings.dump(pw);
         }
-    }
 
-    private static String proximityToString(int state) {
-        switch (state) {
-            case PROXIMITY_UNKNOWN:
-                return "Unknown";
-            case PROXIMITY_NEGATIVE:
-                return "Negative";
-            case PROXIMITY_POSITIVE:
-                return "Positive";
-            default:
-                return Integer.toString(state);
+        pw.println();
+
+        if (mWakelockController != null) {
+            mWakelockController.dumpLocal(pw);
+        }
+
+        pw.println();
+        if (mDisplayBrightnessController != null) {
+            mDisplayBrightnessController.dump(pw);
+        }
+
+        pw.println();
+        if (mDisplayStateController != null) {
+            mDisplayStateController.dumpsys(pw);
+        }
+
+        pw.println();
+        if (mBrightnessClamperController != null) {
+            mBrightnessClamperController.dump(ipw);
         }
     }
 
+
     private static String reportedToPolicyToString(int state) {
         switch (state) {
             case REPORTED_TO_POLICY_SCREEN_OFF:
@@ -3228,14 +2707,20 @@
         }
     }
 
-    private static float clampAbsoluteBrightness(float value) {
-        return MathUtils.constrain(value, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX);
+    private void dumpRbcEvents(PrintWriter pw) {
+        int size = mRbcEventRingBuffer.size();
+        if (size < 1) {
+            pw.println("No Reduce Bright Colors Adjustments");
+            return;
+        }
+
+        pw.println("Reduce Bright Colors Adjustments Last " + size + " Events: ");
+        BrightnessEvent[] eventArray = mRbcEventRingBuffer.toArray();
+        for (int i = 0; i < mRbcEventRingBuffer.size(); i++) {
+            pw.println("  " + eventArray[i]);
+        }
     }
 
-    private static float clampAutoBrightnessAdjustment(float value) {
-        return MathUtils.constrain(value, -1.0f, 1.0f);
-    }
 
     private void noteScreenState(int screenState) {
         // Log screen state change with display id
@@ -3363,20 +2848,21 @@
         // It's easier to check if the brightness is at maximum level using the brightness
         // value untouched by any modifiers
         boolean brightnessIsMax = unmodifiedBrightness == event.getHbmMax();
-        float brightnessInNits = convertToAdjustedNits(event.getBrightness());
+        float brightnessInNits =
+                mDisplayBrightnessController.convertToAdjustedNits(event.getBrightness());
         float appliedLowPowerMode = event.isLowPowerModeSet() ? event.getPowerFactor() : -1f;
         int appliedRbcStrength  = event.isRbcEnabled() ? event.getRbcStrength() : -1;
         float appliedHbmMaxNits =
                 event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
-                ? -1f : convertToAdjustedNits(event.getHbmMax());
+                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getHbmMax());
         // thermalCapNits set to -1 if not currently capping max brightness
         float appliedThermalCapNits =
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
-                ? -1f : convertToAdjustedNits(event.getThermalMax());
-
+                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getThermalMax());
         if (mIsDisplayInternal) {
             FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                    convertToAdjustedNits(event.getInitialBrightness()),
+                    mDisplayBrightnessController
+                            .convertToAdjustedNits(event.getInitialBrightness()),
                     brightnessInNits,
                     event.getLux(),
                     event.getPhysicalDisplayId(),
@@ -3393,7 +2879,8 @@
                     event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
                     event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR,
                     (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0,
-                    mBrightnessThrottler.getBrightnessMaxReason(),
+                    mBrightnessClamperController.getBrightnessMaxReason(),
+                    // TODO: (flc) add brightnessMinReason here too.
                     (modifier & BrightnessReason.MODIFIER_DIMMED) > 0,
                     event.isRbcEnabled(),
                     (flags & BrightnessEvent.FLAG_INVALID_LUX) > 0,
@@ -3404,6 +2891,17 @@
         }
     }
 
+    /**
+     * Indicates whether the display state is ready to update. If this is the default display, we
+     * want to update it right away so that we can draw the boot animation on it. If it is not
+     * the default display, drawing the boot animation on it would look incorrect, so we need
+     * to wait until boot is completed.
+     * @return True if the display state is ready to update
+     */
+    private boolean readyToUpdateDisplayState() {
+        return mDisplayId == Display.DEFAULT_DISPLAY || mBootCompleted;
+    }
+
     private final class DisplayControllerHandler extends Handler {
         DisplayControllerHandler(Looper looper) {
             super(looper, null, true /*async*/);
@@ -3416,10 +2914,6 @@
                     updatePowerState();
                     break;
 
-                case MSG_PROXIMITY_SENSOR_DEBOUNCED:
-                    debounceProximitySensor();
-                    break;
-
                 case MSG_SCREEN_ON_UNBLOCKED:
                     if (mPendingScreenOnUnblocker == msg.obj) {
                         unblockScreenOn();
@@ -3432,27 +2926,38 @@
                         updatePowerState();
                     }
                     break;
+                case MSG_OFFLOADING_SCREEN_ON_UNBLOCKED:
+                    if (mDisplayOffloadSession == msg.obj) {
+                        unblockScreenOnByDisplayOffload();
+                        updatePowerState();
+                    }
+                    break;
                 case MSG_CONFIGURE_BRIGHTNESS:
-                    mBrightnessConfiguration = (BrightnessConfiguration) msg.obj;
-                    mShouldResetShortTermModel = msg.arg1 == 1;
+                    BrightnessConfiguration brightnessConfiguration =
+                            (BrightnessConfiguration) msg.obj;
+                    mAutomaticBrightnessStrategy.setBrightnessConfiguration(brightnessConfiguration,
+                            msg.arg1 == 1);
+                    if (mBrightnessTracker != null) {
+                        mBrightnessTracker
+                                .setShouldCollectColorSample(brightnessConfiguration != null
+                                        && brightnessConfiguration.shouldCollectColorSamples());
+                    }
                     updatePowerState();
                     break;
 
                 case MSG_SET_TEMPORARY_BRIGHTNESS:
                     // TODO: Should we have a a timeout for the temporary brightness?
-                    mTemporaryScreenBrightness = Float.intBitsToFloat(msg.arg1);
+                    mDisplayBrightnessController
+                            .setTemporaryBrightness(Float.intBitsToFloat(msg.arg1));
                     updatePowerState();
                     break;
 
                 case MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT:
-                    mTemporaryAutoBrightnessAdjustment = Float.intBitsToFloat(msg.arg1);
+                    mAutomaticBrightnessStrategy
+                            .setTemporaryAutoBrightnessAdjustment(Float.intBitsToFloat(msg.arg1));
                     updatePowerState();
                     break;
 
-                case MSG_IGNORE_PROXIMITY:
-                    ignoreProximitySensorUntilChangedInternal();
-                    break;
-
                 case MSG_STOP:
                     cleanupHandlerThreadAfterStop();
                     break;
@@ -3500,27 +3005,15 @@
                 case MSG_SET_DWBC_LOGGING_ENABLED:
                     setDwbcLoggingEnabled(msg.arg1);
                     break;
+                case MSG_SET_BRIGHTNESS_FROM_OFFLOAD:
+                    mDisplayBrightnessController.setBrightnessFromOffload(
+                            Float.intBitsToFloat(msg.arg1));
+                    updatePowerState();
+                    break;
             }
         }
     }
 
-    private final SensorEventListener mProximitySensorListener = new SensorEventListener() {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (mProximitySensorEnabled) {
-                final long time = mClock.uptimeMillis();
-                final float distance = event.values[0];
-                boolean positive = distance >= 0.0f && distance < mProximityThreshold;
-                handleProximitySensorEvent(time, positive);
-            }
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // Not used.
-        }
-    };
-
 
     private final class SettingsObserver extends ContentObserver {
         SettingsObserver(Handler handler) {
@@ -3531,6 +3024,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 */);
             }
@@ -3581,28 +3084,6 @@
         msg.sendToTarget();
     }
 
-    @VisibleForTesting
-    String getSuspendBlockerUnfinishedBusinessId(int displayId) {
-        return "[" + displayId + "]unfinished business";
-    }
-
-    String getSuspendBlockerOnStateChangedId(int displayId) {
-        return "[" + displayId + "]on state changed";
-    }
-
-    String getSuspendBlockerProxPositiveId(int displayId) {
-        return "[" + displayId + "]prox positive";
-    }
-
-    String getSuspendBlockerProxNegativeId(int displayId) {
-        return "[" + displayId + "]prox negative";
-    }
-
-    @VisibleForTesting
-    String getSuspendBlockerProxDebounceId(int displayId) {
-        return "[" + displayId + "]prox debounce";
-    }
-
     /** Functional interface for providing time. */
     @VisibleForTesting
     interface Clock {
@@ -3629,6 +3110,20 @@
             return new DualRampAnimator(dps, firstProperty, secondProperty);
         }
 
+        WakelockController getWakelockController(int displayId,
+                DisplayPowerCallbacks displayPowerCallbacks) {
+            return new WakelockController(displayId, displayPowerCallbacks);
+        }
+
+        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
+                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
+                Looper looper, Runnable nudgeUpdatePowerState,
+                int displayId, SensorManager sensorManager) {
+            return new DisplayPowerProximityStateController(wakelockController, displayDeviceConfig,
+                    looper, nudgeUpdatePowerState,
+                    displayId, sensorManager, /* injector= */ null);
+        }
+
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
@@ -3711,11 +3206,32 @@
                     hbmChangeCallback, hbmMetadata, context);
         }
 
+        BrightnessRangeController getBrightnessRangeController(
+                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
+                DisplayDeviceConfig displayDeviceConfig, Handler handler,
+                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
+            return new BrightnessRangeController(hbmController,
+                    modeChangeCallback, displayDeviceConfig, handler, flags, displayToken, info);
+        }
+
+        BrightnessClamperController getBrightnessClamperController(Handler handler,
+                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+                BrightnessClamperController.DisplayDeviceData data, Context context,
+                DisplayManagerFlags flags) {
+
+            return new BrightnessClamperController(handler, clamperChangeListener, data, context,
+                    flags);
+        }
+
         DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
                 SensorManager sensorManager, Resources resources) {
             return DisplayWhiteBalanceFactory.create(handler,
                     sensorManager, resources);
         }
+
+        boolean isColorFadeEnabled() {
+            return !ActivityManager.isLowRamDeviceStatic();
+        }
     }
 
     static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
deleted file mode 100644
index 7df6114..0000000
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ /dev/null
@@ -1,3266 +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.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;
-import android.annotation.SuppressLint;
-import android.annotation.UserIdInt;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ParceledListSlice;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.hardware.Sensor;
-import android.hardware.SensorManager;
-import android.hardware.display.AmbientBrightnessDayStats;
-import android.hardware.display.BrightnessChangeEvent;
-import android.hardware.display.BrightnessConfiguration;
-import android.hardware.display.BrightnessInfo;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
-import android.metrics.LogMaker;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.util.FloatProperty;
-import android.util.IndentingPrintWriter;
-import android.util.MathUtils;
-import android.util.MutableFloat;
-import android.util.MutableInt;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.view.Display;
-
-import com.android.internal.R;
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.app.IBatteryStats;
-import com.android.internal.display.BrightnessSynchronizer;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.util.RingBuffer;
-import com.android.server.LocalServices;
-import com.android.server.am.BatteryStatsService;
-import com.android.server.display.RampAnimator.DualRampAnimator;
-import com.android.server.display.brightness.BrightnessEvent;
-import com.android.server.display.brightness.BrightnessReason;
-import com.android.server.display.brightness.BrightnessUtils;
-import com.android.server.display.brightness.DisplayBrightnessController;
-import com.android.server.display.brightness.clamper.BrightnessClamperController;
-import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
-import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
-import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
-import com.android.server.display.feature.DisplayManagerFlags;
-import com.android.server.display.layout.Layout;
-import com.android.server.display.state.DisplayStateController;
-import com.android.server.display.utils.DebugUtils;
-import com.android.server.display.utils.SensorUtils;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceSettings;
-import com.android.server.policy.WindowManagerPolicy;
-
-import java.io.PrintWriter;
-import java.util.Objects;
-
-/**
- * Controls the power state of the display.
- *
- * Handles the proximity sensor, light sensor, and animations between states
- * including the screen off animation.
- *
- * This component acts independently of the rest of the power manager service.
- * In particular, it does not share any state and it only communicates
- * via asynchronous callbacks to inform the power manager that something has
- * changed.
- *
- * Everything this class does internally is serialized on its handler although
- * it may be accessed by other threads from the outside.
- *
- * Note that the power manager service guarantees that it will hold a suspend
- * blocker as long as the display is not ready.  So most of the work done here
- * does not need to worry about holding a suspend blocker unless it happens
- * independently of the display ready signal.
- *
- * For debugging, you can make the color fade and brightness animations run
- * slower by changing the "animator duration scale" option in Development Settings.
- */
-final class DisplayPowerController2 implements AutomaticBrightnessController.Callbacks,
-        DisplayWhiteBalanceController.Callbacks, DisplayPowerControllerInterface {
-    private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
-    private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
-
-    private static final String TAG = "DisplayPowerController2";
-    // To enable these logs, run:
-    // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot'
-    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
-    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
-    // actually turns on and starts showing new content after the call to set the
-    // screen state returns.  Playing the animation can also be somewhat slow.
-    private static final boolean USE_COLOR_FADE_ON_ANIMATION = false;
-
-    private static final float SCREEN_ANIMATION_RATE_MINIMUM = 0.0f;
-
-    private static final int COLOR_FADE_ON_ANIMATION_DURATION_MILLIS = 250;
-    private static final int COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS = 400;
-
-    private static final int MSG_UPDATE_POWER_STATE = 1;
-    private static final int MSG_SCREEN_ON_UNBLOCKED = 2;
-    private static final int MSG_SCREEN_OFF_UNBLOCKED = 3;
-    private static final int MSG_CONFIGURE_BRIGHTNESS = 4;
-    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 5;
-    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 6;
-    private static final int MSG_STOP = 7;
-    private static final int MSG_UPDATE_BRIGHTNESS = 8;
-    private static final int MSG_UPDATE_RBC = 9;
-    private static final int MSG_BRIGHTNESS_RAMP_DONE = 10;
-    private static final int MSG_STATSD_HBM_BRIGHTNESS = 11;
-    private static final int MSG_SWITCH_USER = 12;
-    private static final int MSG_BOOT_COMPLETED = 13;
-    private static final int MSG_SET_DWBC_STRONG_MODE = 14;
-    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;
-
-
-
-    private static final int BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS = 500;
-
-
-    // State machine constants for tracking initial brightness ramp skipping when enabled.
-    private static final int RAMP_STATE_SKIP_NONE = 0;
-    private static final int RAMP_STATE_SKIP_INITIAL = 1;
-    private static final int RAMP_STATE_SKIP_AUTOBRIGHT = 2;
-
-    private static final int REPORTED_TO_POLICY_UNREPORTED = -1;
-    private static final int REPORTED_TO_POLICY_SCREEN_OFF = 0;
-    private static final int REPORTED_TO_POLICY_SCREEN_TURNING_ON = 1;
-    private static final int REPORTED_TO_POLICY_SCREEN_ON = 2;
-    private static final int REPORTED_TO_POLICY_SCREEN_TURNING_OFF = 3;
-
-    private static final int RINGBUFFER_MAX = 100;
-    private static final int RINGBUFFER_RBC_MAX = 20;
-
-    private static final float[] BRIGHTNESS_RANGE_BOUNDARIES = {
-        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80,
-        90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200,
-        1400, 1600, 1800, 2000, 2250, 2500, 2750, 3000};
-    private static final int[] BRIGHTNESS_RANGE_INDEX = {
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_UNKNOWN,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_0_1,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1_2,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2_3,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_3_4,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_4_5,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_5_6,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_6_7,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_7_8,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_8_9,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_9_10,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_10_20,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_20_30,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_30_40,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_40_50,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_50_60,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_60_70,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_70_80,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_80_90,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_90_100,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_100_200,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_200_300,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_300_400,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_400_500,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_500_600,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_600_700,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_700_800,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_800_900,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_900_1000,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1000_1200,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1200_1400,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1400_1600,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1600_1800,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1800_2000,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2000_2250,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2250_2500,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2500_2750,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2750_3000,
-    };
-
-    private final String mTag;
-
-    private final Object mLock = new Object();
-
-    private final Context mContext;
-
-    // Our handler.
-    private final DisplayControllerHandler mHandler;
-
-    // Battery stats.
-    @Nullable
-    private final IBatteryStats mBatteryStats;
-
-    // The sensor manager.
-    private final SensorManager mSensorManager;
-
-    // The window manager policy.
-    private final WindowManagerPolicy mWindowManagerPolicy;
-
-    // The display blanker.
-    private final DisplayBlanker mBlanker;
-
-    // The LogicalDisplay tied to this DisplayPowerController2.
-    private final LogicalDisplay mLogicalDisplay;
-
-    // The ID of the LogicalDisplay tied to this DisplayPowerController2.
-    private final int mDisplayId;
-
-    // The ID of the display which this display follows for brightness purposes.
-    private int mLeadDisplayId = Layout.NO_LEAD_DISPLAY;
-
-    // The unique ID of the primary display device currently tied to this logical display
-    private String mUniqueDisplayId;
-
-    // Tracker for brightness changes.
-    @Nullable
-    private final BrightnessTracker mBrightnessTracker;
-
-    // Tracker for brightness settings changes.
-    private final SettingsObserver mSettingsObserver;
-
-    // The doze screen brightness.
-    private final float mScreenBrightnessDozeConfig;
-
-    // True if auto-brightness should be used.
-    private boolean mUseSoftwareAutoBrightnessConfig;
-
-    // Whether or not the color fade on screen on / off is enabled.
-    private final boolean mColorFadeEnabled;
-
-    @GuardedBy("mCachedBrightnessInfo")
-    private final CachedBrightnessInfo mCachedBrightnessInfo = new CachedBrightnessInfo();
-
-    private DisplayDevice mDisplayDevice;
-
-    // True if we should fade the screen while turning it off, false if we should play
-    // a stylish color fade animation instead.
-    private final boolean mColorFadeFadesConfig;
-
-    // True if we need to fake a transition to off when coming out of a doze state.
-    // Some display hardware will blank itself when coming out of doze in order to hide
-    // artifacts. For these displays we fake a transition into OFF so that policy can appropriately
-    // blank itself and begin an appropriate power on animation.
-    private final boolean mDisplayBlanksAfterDozeConfig;
-
-    // True if there are only buckets of brightness values when the display is in the doze state,
-    // rather than a full range of values. If this is true, then we'll avoid animating the screen
-    // brightness since it'd likely be multiple jarring brightness transitions instead of just one
-    // to reach the final state.
-    private final boolean mBrightnessBucketsInDozeConfig;
-
-    private final Clock mClock;
-    private final Injector mInjector;
-
-    // Maximum time a ramp animation can take.
-    private long mBrightnessRampIncreaseMaxTimeMillis;
-    private long mBrightnessRampDecreaseMaxTimeMillis;
-
-    // Maximum time a ramp animation can take in idle mode.
-    private long mBrightnessRampIncreaseMaxTimeIdleMillis;
-    private long mBrightnessRampDecreaseMaxTimeIdleMillis;
-
-    // The pending power request.
-    // Initially null until the first call to requestPowerState.
-    @GuardedBy("mLock")
-    private DisplayPowerRequest mPendingRequestLocked;
-
-    // True if the pending power request or wait for negative proximity flag
-    // has been changed since the last update occurred.
-    @GuardedBy("mLock")
-    private boolean mPendingRequestChangedLocked;
-
-    // Set to true when the important parts of the pending power request have been applied.
-    // The important parts are mainly the screen state.  Brightness changes may occur
-    // concurrently.
-    @GuardedBy("mLock")
-    private boolean mDisplayReadyLocked;
-
-    // Set to true if a power state update is required.
-    @GuardedBy("mLock")
-    private boolean mPendingUpdatePowerStateLocked;
-
-    /* The following state must only be accessed by the handler thread. */
-
-    // The currently requested power state.
-    // The power controller will progressively update its internal state to match
-    // the requested power state.  Initially null until the first update.
-    private DisplayPowerRequest mPowerRequest;
-
-    // The current power state.
-    // Must only be accessed on the handler thread.
-    private DisplayPowerState mPowerState;
-
-
-
-    // The currently active screen on unblocker.  This field is non-null whenever
-    // 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
-    // turning off the screen.
-    private boolean mPendingScreenOff;
-
-    // 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;
-
-    private boolean mAppliedDimming;
-
-    private boolean mAppliedThrottling;
-
-    // Reason for which the brightness was last changed. See {@link BrightnessReason} for more
-    // information.
-    // At the time of this writing, this value is changed within updatePowerState() only, which is
-    // limited to the thread used by DisplayControllerHandler.
-    private final BrightnessReason mBrightnessReason = new BrightnessReason();
-    private final BrightnessReason mBrightnessReasonTemp = new BrightnessReason();
-
-    // Brightness animation ramp rates in brightness units per second
-    private float mBrightnessRampRateFastDecrease;
-    private float mBrightnessRampRateFastIncrease;
-    private float mBrightnessRampRateSlowDecrease;
-    private float mBrightnessRampRateSlowIncrease;
-    private float mBrightnessRampRateSlowDecreaseIdle;
-    private float mBrightnessRampRateSlowIncreaseIdle;
-
-    // Report HBM brightness change to StatsD
-    private int mDisplayStatsId;
-    private float mLastStatsBrightness = PowerManager.BRIGHTNESS_MIN;
-
-    // Whether or not to skip the initial brightness ramps into STATE_ON.
-    private final boolean mSkipScreenOnBrightnessRamp;
-
-    // Display white balance components.
-    // Critical methods must be called on DPC2 handler thread.
-    @Nullable
-    private final DisplayWhiteBalanceSettings mDisplayWhiteBalanceSettings;
-    @Nullable
-    private final DisplayWhiteBalanceController mDisplayWhiteBalanceController;
-
-    @Nullable
-    private final ColorDisplayServiceInternal mCdsi;
-    private float[] mNitsRange;
-
-    private final BrightnessRangeController mBrightnessRangeController;
-
-    private final BrightnessThrottler mBrightnessThrottler;
-
-    private final BrightnessClamperController mBrightnessClamperController;
-
-    private final Runnable mOnBrightnessChangeRunnable;
-
-    private final BrightnessEvent mLastBrightnessEvent;
-    private final BrightnessEvent mTempBrightnessEvent;
-
-    private final DisplayBrightnessController mDisplayBrightnessController;
-
-    // Keeps a record of brightness changes for dumpsys.
-    private RingBuffer<BrightnessEvent> mBrightnessEventRingBuffer;
-
-    // Keeps a record of rbc changes for dumpsys.
-    private final RingBuffer<BrightnessEvent> mRbcEventRingBuffer =
-            new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_RBC_MAX);
-
-    // Controls and tracks all the wakelocks that are acquired/released by the system. Also acts as
-    // a medium of communication between this class and the PowerManagerService.
-    private final WakelockController mWakelockController;
-
-    // Tracks and manages the proximity state of the associated display.
-    private final DisplayPowerProximityStateController mDisplayPowerProximityStateController;
-
-    // Tracks and manages the display state of the associated display.
-    private final DisplayStateController mDisplayStateController;
-
-
-    // Responsible for evaluating and tracking the automatic brightness relevant states.
-    // Todo: This is a temporary workaround. Ideally DPC2 should never talk to the strategies
-    private final AutomaticBrightnessStrategy mAutomaticBrightnessStrategy;
-
-    // A record of state for skipping brightness ramps.
-    private int mSkipRampState = RAMP_STATE_SKIP_NONE;
-
-    // The first autobrightness value set when entering RAMP_STATE_SKIP_INITIAL.
-    private float mInitialAutoBrightness;
-
-    // The controller for the automatic brightness level.
-    @Nullable
-    private AutomaticBrightnessController mAutomaticBrightnessController;
-
-    // The controller for the sensor used to estimate ambient lux while the display is off.
-    @Nullable
-    private ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
-
-    private Sensor mLightSensor;
-    private Sensor mScreenOffBrightnessSensor;
-
-    private boolean mIsRbcActive;
-
-    // Animators.
-    private ObjectAnimator mColorFadeOnAnimator;
-    private ObjectAnimator mColorFadeOffAnimator;
-    private DualRampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator;
-
-    // True if this DisplayPowerController2 has been stopped and should no longer be running.
-    private boolean mStopped;
-
-    private DisplayDeviceConfig mDisplayDeviceConfig;
-
-    private boolean mIsEnabled;
-    private boolean mIsInTransition;
-    private boolean mIsDisplayInternal;
-
-    // The id of the thermal brightness throttling policy that should be used.
-    private String mThermalBrightnessThrottlingDataId;
-
-    // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there
-    // is one lead display, the additional displays follow the brightness value of the lead display.
-    @GuardedBy("mLock")
-    private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
-            new SparseArray();
-
-    private boolean mBootCompleted;
-    private final DisplayManagerFlags mFlags;
-
-    private DisplayOffloadSession mDisplayOffloadSession;
-
-    /**
-     * Creates the display power controller.
-     */
-    DisplayPowerController2(Context context, Injector injector,
-            DisplayPowerCallbacks callbacks, Handler handler,
-            SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay,
-            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;
-        mDisplayId = mLogicalDisplay.getDisplayIdLocked();
-        mSensorManager = sensorManager;
-        mHandler = new DisplayControllerHandler(handler.getLooper());
-        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceConfig();
-        mIsEnabled = logicalDisplay.isEnabledLocked();
-        mIsInTransition = logicalDisplay.isInTransitionLocked();
-        mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
-        mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
-        mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
-                mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
-                () -> updatePowerState(), mDisplayId, mSensorManager);
-        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
-        mTag = TAG + "[" + mDisplayId + "]";
-        mThermalBrightnessThrottlingDataId =
-                logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
-        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
-        mDisplayStatsId = mUniqueDisplayId.hashCode();
-
-        mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
-        mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
-
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            mBatteryStats = BatteryStatsService.getService();
-        } else {
-            mBatteryStats = null;
-        }
-
-        mSettingsObserver = new SettingsObserver(mHandler);
-        mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class);
-        mBlanker = blanker;
-        mContext = context;
-        mBrightnessTracker = brightnessTracker;
-        mOnBrightnessChangeRunnable = onBrightnessChangeRunnable;
-
-        PowerManager pm = context.getSystemService(PowerManager.class);
-
-        final Resources resources = context.getResources();
-
-        // DOZE AND DIM SETTINGS
-        mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
-                pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE));
-        loadBrightnessRampRates();
-        mSkipScreenOnBrightnessRamp = resources.getBoolean(
-                R.bool.config_skipScreenOnBrightnessRamp);
-
-        Runnable modeChangeCallback = () -> {
-            sendUpdatePowerState();
-            postBrightnessChangeRunnable();
-            // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern.
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.update();
-            }
-        };
-
-        HighBrightnessModeController hbmController = createHbmControllerLocked(hbmMetadata,
-                modeChangeCallback);
-        mBrightnessThrottler = createBrightnessThrottlerLocked();
-
-        mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
-                modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
-                mDisplayDevice.getDisplayTokenLocked(),
-                mDisplayDevice.getDisplayDeviceInfoLocked());
-
-        mDisplayBrightnessController =
-                new DisplayBrightnessController(context, null,
-                        mDisplayId, mLogicalDisplay.getDisplayInfoLocked().brightnessDefault,
-                        brightnessSetting, () -> postBrightnessChangeRunnable(),
-                        new HandlerExecutor(mHandler), flags);
-
-        mBrightnessClamperController = mInjector.getBrightnessClamperController(
-                mHandler, modeChangeCallback::run,
-                new BrightnessClamperController.DisplayDeviceData(
-                mUniqueDisplayId,
-                mThermalBrightnessThrottlingDataId,
-                logicalDisplay.getPowerThrottlingDataIdLocked(),
-                mDisplayDeviceConfig), mContext, flags);
-        // Seed the cached brightness
-        saveBrightnessInfo(getScreenBrightnessSetting());
-        mAutomaticBrightnessStrategy =
-                mDisplayBrightnessController.getAutomaticBrightnessStrategy();
-
-        DisplayWhiteBalanceSettings displayWhiteBalanceSettings = null;
-        DisplayWhiteBalanceController displayWhiteBalanceController = null;
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            try {
-                displayWhiteBalanceController = mInjector.getDisplayWhiteBalanceController(
-                        mHandler, mSensorManager, resources);
-                displayWhiteBalanceSettings = new DisplayWhiteBalanceSettings(mContext, mHandler);
-                displayWhiteBalanceSettings.setCallbacks(this);
-                displayWhiteBalanceController.setCallbacks(this);
-            } catch (Exception e) {
-                Slog.e(mTag, "failed to set up display white-balance: " + e);
-            }
-        }
-        mDisplayWhiteBalanceSettings = displayWhiteBalanceSettings;
-        mDisplayWhiteBalanceController = displayWhiteBalanceController;
-
-        loadNitsRange(resources);
-
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            mCdsi = LocalServices.getService(ColorDisplayServiceInternal.class);
-            if (mCdsi != null) {
-                boolean active = mCdsi.setReduceBrightColorsListener(
-                        new ReduceBrightColorsListener() {
-                            @Override
-                            public void onReduceBrightColorsActivationChanged(boolean activated,
-                                    boolean userInitiated) {
-                                applyReduceBrightColorsSplineAdjustment();
-
-                            }
-
-                            @Override
-                            public void onReduceBrightColorsStrengthChanged(int strength) {
-                                applyReduceBrightColorsSplineAdjustment();
-                            }
-                        });
-                if (active) {
-                    applyReduceBrightColorsSplineAdjustment();
-                }
-            }
-        } else {
-            mCdsi = null;
-        }
-
-        setUpAutoBrightness(context, handler);
-
-        mColorFadeEnabled = mInjector.isColorFadeEnabled()
-                && !resources.getBoolean(
-                  com.android.internal.R.bool.config_displayColorFadeDisabled);
-        mColorFadeFadesConfig = resources.getBoolean(
-                R.bool.config_animateScreenLights);
-
-        mDisplayBlanksAfterDozeConfig = resources.getBoolean(
-                R.bool.config_displayBlanksAfterDoze);
-
-        mBrightnessBucketsInDozeConfig = resources.getBoolean(
-                R.bool.config_displayBrightnessBucketsInDoze);
-
-        mBootCompleted = bootCompleted;
-    }
-
-    private void applyReduceBrightColorsSplineAdjustment() {
-        mHandler.obtainMessage(MSG_UPDATE_RBC).sendToTarget();
-        sendUpdatePowerState();
-    }
-
-    private void handleRbcChanged() {
-        if (mAutomaticBrightnessController == null) {
-            return;
-        }
-
-        float[] adjustedNits = new float[mNitsRange.length];
-        for (int i = 0; i < mNitsRange.length; i++) {
-            adjustedNits[i] = mCdsi.getReduceBrightColorsAdjustedBrightnessNits(mNitsRange[i]);
-        }
-        mIsRbcActive = mCdsi.isReduceBrightColorsActivated();
-        mAutomaticBrightnessController.recalculateSplines(mIsRbcActive, adjustedNits);
-    }
-
-    /**
-     * Returns true if the proximity sensor screen-off function is available.
-     */
-    @Override
-    public boolean isProximitySensorAvailable() {
-        return mDisplayPowerProximityStateController.isProximitySensorAvailable();
-    }
-
-    /**
-     * Get the {@link BrightnessChangeEvent}s for the specified user.
-     *
-     * @param userId         userId to fetch data for
-     * @param includePackage if false will null out the package name in events
-     */
-    @Nullable
-    @Override
-    public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents(
-            @UserIdInt int userId, boolean includePackage) {
-        if (mBrightnessTracker == null) {
-            return null;
-        }
-        return mBrightnessTracker.getEvents(userId, includePackage);
-    }
-
-    @Override
-    public void onSwitchUser(@UserIdInt int newUserId) {
-        Message msg = mHandler.obtainMessage(MSG_SWITCH_USER, newUserId);
-        mHandler.sendMessage(msg);
-    }
-
-    private void handleOnSwitchUser(@UserIdInt int newUserId) {
-        handleSettingsChange(true /* userSwitch */);
-        handleBrightnessModeChange();
-        if (mBrightnessTracker != null) {
-            mBrightnessTracker.onSwitchUser(newUserId);
-        }
-    }
-
-    @Nullable
-    @Override
-    public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(
-            @UserIdInt int userId) {
-        if (mBrightnessTracker == null) {
-            return null;
-        }
-        return mBrightnessTracker.getAmbientBrightnessStats(userId);
-    }
-
-    /**
-     * Persist the brightness slider events and ambient brightness stats to disk.
-     */
-    @Override
-    public void persistBrightnessTrackerState() {
-        if (mBrightnessTracker != null) {
-            mBrightnessTracker.persistBrightnessTrackerState();
-        }
-    }
-
-    /**
-     * Requests a new power state.
-     * The controller makes a copy of the provided object and then
-     * begins adjusting the power state to match what was requested.
-     *
-     * @param request                  The requested power state.
-     * @param waitForNegativeProximity If true, issues a request to wait for
-     *                                 negative proximity before turning the screen back on,
-     *                                 assuming the screen
-     *                                 was turned off by the proximity sensor.
-     * @return True if display is ready, false if there are important changes that must
-     * be made asynchronously (such as turning the screen on), in which case the caller
-     * should grab a wake lock, watch for {@link DisplayPowerCallbacks#onStateChanged()}
-     * then try the request again later until the state converges.
-     */
-    public boolean requestPowerState(DisplayPowerRequest request,
-            boolean waitForNegativeProximity) {
-        if (DEBUG) {
-            Slog.d(mTag, "requestPowerState: "
-                    + request + ", waitForNegativeProximity=" + waitForNegativeProximity);
-        }
-
-        synchronized (mLock) {
-            if (mStopped) {
-                return true;
-            }
-
-            boolean changed = mDisplayPowerProximityStateController
-                    .setPendingWaitForNegativeProximityLocked(waitForNegativeProximity);
-
-            if (mPendingRequestLocked == null) {
-                mPendingRequestLocked = new DisplayPowerRequest(request);
-                changed = true;
-            } else if (!mPendingRequestLocked.equals(request)) {
-                mPendingRequestLocked.copyFrom(request);
-                changed = true;
-            }
-
-            if (changed) {
-                mDisplayReadyLocked = false;
-                if (!mPendingRequestChangedLocked) {
-                    mPendingRequestChangedLocked = true;
-                    sendUpdatePowerStateLocked();
-                }
-            }
-
-            return mDisplayReadyLocked;
-        }
-    }
-
-    @Override
-    public void overrideDozeScreenState(int displayState) {
-        mHandler.postAtTime(() -> {
-            if (mDisplayOffloadSession == null
-                    || !(DisplayOffloadSession.isSupportedOffloadState(displayState)
-                    || displayState == Display.STATE_UNKNOWN)) {
-                return;
-            }
-            mDisplayStateController.overrideDozeScreenState(displayState);
-            sendUpdatePowerState();
-        }, mClock.uptimeMillis());
-    }
-
-    @Override
-    public void setDisplayOffloadSession(DisplayOffloadSession session) {
-        if (session == mDisplayOffloadSession) {
-            return;
-        }
-        unblockScreenOnByDisplayOffload();
-        mDisplayOffloadSession = session;
-    }
-
-    @Override
-    public BrightnessConfiguration getDefaultBrightnessConfiguration() {
-        if (mAutomaticBrightnessController == null) {
-            return null;
-        }
-        return mAutomaticBrightnessController.getDefaultConfig();
-    }
-
-    /**
-     * Notified when the display is changed. We use this to apply any changes that might be needed
-     * when displays get swapped on foldable devices.  For example, different brightness properties
-     * of each display need to be properly reflected in AutomaticBrightnessController.
-     *
-     * Make sure DisplayManagerService.mSyncRoot lock is held when this is called
-     */
-    @Override
-    public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) {
-        mLeadDisplayId = leadDisplayId;
-        final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        if (device == null) {
-            Slog.wtf(mTag, "Display Device is null in DisplayPowerController2 for display: "
-                    + mLogicalDisplay.getDisplayIdLocked());
-            return;
-        }
-
-        final String uniqueId = device.getUniqueId();
-        final DisplayDeviceConfig config = device.getDisplayDeviceConfig();
-        final IBinder token = device.getDisplayTokenLocked();
-        final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
-        final boolean isEnabled = mLogicalDisplay.isEnabledLocked();
-        final boolean isInTransition = mLogicalDisplay.isInTransitionLocked();
-        final boolean isDisplayInternal = mLogicalDisplay.getPrimaryDisplayDeviceLocked() != null
-                && mLogicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
-        final String thermalBrightnessThrottlingDataId =
-                mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
-        final String powerThrottlingDataId =
-                mLogicalDisplay.getPowerThrottlingDataIdLocked();
-
-        mHandler.postAtTime(() -> {
-            boolean changed = false;
-            if (mDisplayDevice != device) {
-                changed = true;
-                mDisplayDevice = device;
-                mUniqueDisplayId = uniqueId;
-                mDisplayStatsId = mUniqueDisplayId.hashCode();
-                mDisplayDeviceConfig = config;
-                mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
-                loadFromDisplayDeviceConfig(token, info, hbmMetadata);
-                mDisplayPowerProximityStateController.notifyDisplayDeviceChanged(config);
-
-                // Since the underlying display-device changed, we really don't know the
-                // last command that was sent to change it's state. Let's assume it is unknown so
-                // that we trigger a change immediately.
-                mPowerState.resetScreenState();
-            } else if (!Objects.equals(mThermalBrightnessThrottlingDataId,
-                    thermalBrightnessThrottlingDataId)) {
-                changed = true;
-                mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
-                mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
-                        config.getThermalBrightnessThrottlingDataMapByThrottlingId(),
-                        mThermalBrightnessThrottlingDataId,
-                        mUniqueDisplayId);
-            }
-            if (mIsEnabled != isEnabled || mIsInTransition != isInTransition) {
-                changed = true;
-                mIsEnabled = isEnabled;
-                mIsInTransition = isInTransition;
-            }
-
-            mIsDisplayInternal = isDisplayInternal;
-            // using local variables here, when mBrightnessThrottler is removed,
-            // mThermalBrightnessThrottlingDataId could be removed as well
-            // changed = true will be not needed - clampers are maintaining their state and
-            // will call updatePowerState if needed.
-            mBrightnessClamperController.onDisplayChanged(
-                    new BrightnessClamperController.DisplayDeviceData(uniqueId,
-                        thermalBrightnessThrottlingDataId, powerThrottlingDataId, config));
-
-            if (changed) {
-                updatePowerState();
-            }
-        }, mClock.uptimeMillis());
-    }
-
-    /**
-     * Unregisters all listeners and interrupts all running threads; halting future work.
-     *
-     * This method should be called when the DisplayPowerController2 is no longer in use; i.e. when
-     * the {@link #mDisplayId display} has been removed.
-     */
-    @Override
-    public void stop() {
-        synchronized (mLock) {
-            clearDisplayBrightnessFollowersLocked();
-
-            mStopped = true;
-            Message msg = mHandler.obtainMessage(MSG_STOP);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.stop();
-            }
-
-            mDisplayBrightnessController.stop();
-
-            mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
-        }
-    }
-
-    private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info,
-            HighBrightnessModeMetadata hbmMetadata) {
-        // All properties that depend on the associated DisplayDevice and the DDC must be
-        // updated here.
-        loadBrightnessRampRates();
-        loadNitsRange(mContext.getResources());
-        setUpAutoBrightness(mContext, mHandler);
-        reloadReduceBrightColours();
-        setAnimatorRampSpeeds(/* isIdleMode= */ false);
-
-        mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);
-        mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
-                mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),
-                mThermalBrightnessThrottlingDataId, mUniqueDisplayId);
-    }
-
-    private void sendUpdatePowerState() {
-        synchronized (mLock) {
-            sendUpdatePowerStateLocked();
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void sendUpdatePowerStateLocked() {
-        if (!mStopped && !mPendingUpdatePowerStateLocked) {
-            mPendingUpdatePowerStateLocked = true;
-            Message msg = mHandler.obtainMessage(MSG_UPDATE_POWER_STATE);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    }
-
-    private void initialize(int displayState) {
-        mPowerState = mInjector.getDisplayPowerState(mBlanker,
-                mColorFadeEnabled ? new ColorFade(mDisplayId) : null, mDisplayId, displayState);
-
-        if (mColorFadeEnabled) {
-            mColorFadeOnAnimator = ObjectAnimator.ofFloat(
-                    mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 0.0f, 1.0f);
-            mColorFadeOnAnimator.setDuration(COLOR_FADE_ON_ANIMATION_DURATION_MILLIS);
-            mColorFadeOnAnimator.addListener(mAnimatorListener);
-
-            mColorFadeOffAnimator = ObjectAnimator.ofFloat(
-                    mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 1.0f, 0.0f);
-            mColorFadeOffAnimator.setDuration(COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS);
-            mColorFadeOffAnimator.addListener(mAnimatorListener);
-        }
-
-        mScreenBrightnessRampAnimator = mInjector.getDualRampAnimator(mPowerState,
-                DisplayPowerState.SCREEN_BRIGHTNESS_FLOAT,
-                DisplayPowerState.SCREEN_SDR_BRIGHTNESS_FLOAT);
-        setAnimatorRampSpeeds(mAutomaticBrightnessController != null
-                && mAutomaticBrightnessController.isInIdleMode());
-        mScreenBrightnessRampAnimator.setListener(mRampAnimatorListener);
-
-        noteScreenState(mPowerState.getScreenState());
-        noteScreenBrightness(mPowerState.getScreenBrightness());
-
-        // Initialize all of the brightness tracking state
-        final float brightness = mDisplayBrightnessController.convertToAdjustedNits(
-                mPowerState.getScreenBrightness());
-        if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
-            mBrightnessTracker.start(brightness);
-        }
-
-        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = brightnessValue -> {
-            Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        };
-        mDisplayBrightnessController
-                .registerBrightnessSettingChangeListener(brightnessSettingListener);
-
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
-                false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
-        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(Context context, Handler handler) {
-        mUseSoftwareAutoBrightnessConfig = mDisplayDeviceConfig.isAutoBrightnessAvailable();
-
-        if (!mUseSoftwareAutoBrightnessConfig) {
-            return;
-        }
-
-        SparseArray<BrightnessMappingStrategy> brightnessMappers = new SparseArray<>();
-
-        BrightnessMappingStrategy defaultModeBrightnessMapper =
-                mInjector.getDefaultModeBrightnessMapper(context, mDisplayDeviceConfig,
-                        mDisplayWhiteBalanceController);
-        brightnessMappers.append(AUTO_BRIGHTNESS_MODE_DEFAULT,
-                defaultModeBrightnessMapper);
-
-        final boolean isIdleScreenBrightnessEnabled = context.getResources().getBoolean(
-                R.bool.config_enableIdleScreenBrightnessMode);
-        if (isIdleScreenBrightnessEnabled) {
-            BrightnessMappingStrategy idleModeBrightnessMapper =
-                    BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
-                            AUTO_BRIGHTNESS_MODE_IDLE,
-                            mDisplayWhiteBalanceController);
-            if (idleModeBrightnessMapper != null) {
-                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE,
-                        idleModeBrightnessMapper);
-            }
-        }
-
-        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);
-
-            // Ambient Lux - Active Mode Brightness Thresholds
-            float[] ambientBrighteningThresholds =
-                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
-            float[] ambientDarkeningThresholds =
-                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
-            float[] ambientBrighteningLevels =
-                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
-            float[] ambientDarkeningLevels =
-                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
-            float ambientDarkeningMinThreshold =
-                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
-            float ambientBrighteningMinThreshold =
-                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
-            HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
-                    ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
-                    ambientBrighteningMinThreshold);
-
-            // Display - Active Mode Brightness Thresholds
-            float[] screenBrighteningThresholds =
-                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
-            float[] screenDarkeningThresholds =
-                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
-            float[] screenBrighteningLevels =
-                    mDisplayDeviceConfig.getScreenBrighteningLevels();
-            float[] screenDarkeningLevels =
-                    mDisplayDeviceConfig.getScreenDarkeningLevels();
-            float screenDarkeningMinThreshold =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
-            float screenBrighteningMinThreshold =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
-            HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds,
-                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
-                    screenBrighteningMinThreshold, true);
-
-            // Ambient Lux - Idle Screen Brightness Thresholds
-            float ambientDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
-            float ambientBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
-            float[] ambientBrighteningThresholdsIdle =
-                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
-            float[] ambientDarkeningThresholdsIdle =
-                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
-            float[] ambientBrighteningLevelsIdle =
-                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
-            float[] ambientDarkeningLevelsIdle =
-                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
-            HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
-                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
-                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
-                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
-
-            // Display - Idle Screen Brightness Thresholds
-            float screenDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
-            float screenBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
-            float[] screenBrighteningThresholdsIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
-            float[] screenDarkeningThresholdsIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
-            float[] screenBrighteningLevelsIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
-            float[] screenDarkeningLevelsIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
-                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
-                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
-                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
-            long brighteningLightDebounce = mDisplayDeviceConfig
-                    .getAutoBrightnessBrighteningLightDebounce();
-            long darkeningLightDebounce = mDisplayDeviceConfig
-                    .getAutoBrightnessDarkeningLightDebounce();
-            long brighteningLightDebounceIdle = mDisplayDeviceConfig
-                    .getAutoBrightnessBrighteningLightDebounceIdle();
-            long darkeningLightDebounceIdle = mDisplayDeviceConfig
-                    .getAutoBrightnessDarkeningLightDebounceIdle();
-            boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
-                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
-
-            int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
-                    R.integer.config_lightSensorWarmupTime);
-            int lightSensorRate = context.getResources().getInteger(
-                    R.integer.config_autoBrightnessLightSensorRate);
-            int initialLightSensorRate = context.getResources().getInteger(
-                    R.integer.config_autoBrightnessInitialLightSensorRate);
-            if (initialLightSensorRate == -1) {
-                initialLightSensorRate = lightSensorRate;
-            } else if (initialLightSensorRate > lightSensorRate) {
-                Slog.w(mTag, "Expected config_autoBrightnessInitialLightSensorRate ("
-                        + initialLightSensorRate + ") to be less than or equal to "
-                        + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
-            }
-
-            loadAmbientLightSensor();
-            // BrightnessTracker should only use one light sensor, we want to use the light sensor
-            // from the default display and not e.g. temporary displays when switching layouts.
-            if (mBrightnessTracker != null && mDisplayId == Display.DEFAULT_DISPLAY) {
-                mBrightnessTracker.setLightSensor(mLightSensor);
-            }
-
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.stop();
-            }
-            mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
-                    this, handler.getLooper(), mSensorManager, mLightSensor,
-                    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);
-
-            mAutomaticBrightnessStrategy
-                    .setAutomaticBrightnessController(mAutomaticBrightnessController);
-            mBrightnessEventRingBuffer =
-                    new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
-
-            if (mScreenOffBrightnessSensorController != null) {
-                mScreenOffBrightnessSensorController.stop();
-                mScreenOffBrightnessSensorController = null;
-            }
-            loadScreenOffBrightnessSensor();
-            int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux();
-            if (mScreenOffBrightnessSensor != null && sensorValueToLux != null) {
-                mScreenOffBrightnessSensorController =
-                        mInjector.getScreenOffBrightnessSensorController(
-                                mSensorManager,
-                                mScreenOffBrightnessSensor,
-                                mHandler,
-                                SystemClock::uptimeMillis,
-                                sensorValueToLux,
-                                defaultModeBrightnessMapper);
-            }
-        } else {
-            mUseSoftwareAutoBrightnessConfig = false;
-        }
-    }
-
-    private void loadBrightnessRampRates() {
-        mBrightnessRampRateFastDecrease = mDisplayDeviceConfig.getBrightnessRampFastDecrease();
-        mBrightnessRampRateFastIncrease = mDisplayDeviceConfig.getBrightnessRampFastIncrease();
-        mBrightnessRampRateSlowDecrease = mDisplayDeviceConfig.getBrightnessRampSlowDecrease();
-        mBrightnessRampRateSlowIncrease = mDisplayDeviceConfig.getBrightnessRampSlowIncrease();
-        mBrightnessRampRateSlowDecreaseIdle =
-                mDisplayDeviceConfig.getBrightnessRampSlowDecreaseIdle();
-        mBrightnessRampRateSlowIncreaseIdle =
-                mDisplayDeviceConfig.getBrightnessRampSlowIncreaseIdle();
-        mBrightnessRampDecreaseMaxTimeMillis =
-                mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis();
-        mBrightnessRampIncreaseMaxTimeMillis =
-                mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis();
-        mBrightnessRampDecreaseMaxTimeIdleMillis =
-                mDisplayDeviceConfig.getBrightnessRampDecreaseMaxIdleMillis();
-        mBrightnessRampIncreaseMaxTimeIdleMillis =
-                mDisplayDeviceConfig.getBrightnessRampIncreaseMaxIdleMillis();
-    }
-
-    private void loadNitsRange(Resources resources) {
-        if (mDisplayDeviceConfig != null && mDisplayDeviceConfig.getNits() != null) {
-            mNitsRange = mDisplayDeviceConfig.getNits();
-        } else {
-            Slog.w(mTag, "Screen brightness nits configuration is unavailable; falling back");
-            mNitsRange = BrightnessMappingStrategy.getFloatArray(resources
-                    .obtainTypedArray(R.array.config_screenBrightnessNits));
-        }
-    }
-
-    private void reloadReduceBrightColours() {
-        if (mCdsi != null && mCdsi.isReduceBrightColorsActivated()) {
-            applyReduceBrightColorsSplineAdjustment();
-        }
-    }
-
-    @Override
-    public void setAutomaticScreenBrightnessMode(
-            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        boolean isIdle = mode == AUTO_BRIGHTNESS_MODE_IDLE;
-        if (mAutomaticBrightnessController != null) {
-            mAutomaticBrightnessController.switchMode(mode);
-            setAnimatorRampSpeeds(isIdle);
-        }
-        Message msg = mHandler.obtainMessage();
-        msg.what = MSG_SET_DWBC_STRONG_MODE;
-        msg.arg1 = isIdle ? 1 : 0;
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-    }
-
-    private void setAnimatorRampSpeeds(boolean isIdle) {
-        if (mScreenBrightnessRampAnimator == null) {
-            return;
-        }
-        if (mFlags.isAdaptiveTone1Enabled() && isIdle) {
-            mScreenBrightnessRampAnimator.setAnimationTimeLimits(
-                    mBrightnessRampIncreaseMaxTimeIdleMillis,
-                    mBrightnessRampDecreaseMaxTimeIdleMillis);
-        } else {
-            mScreenBrightnessRampAnimator.setAnimationTimeLimits(
-                    mBrightnessRampIncreaseMaxTimeMillis,
-                    mBrightnessRampDecreaseMaxTimeMillis);
-        }
-    }
-
-    private final Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() {
-        @Override
-        public void onAnimationStart(Animator animation) {
-        }
-
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            sendUpdatePowerState();
-        }
-
-        @Override
-        public void onAnimationRepeat(Animator animation) {
-        }
-
-        @Override
-        public void onAnimationCancel(Animator animation) {
-        }
-    };
-
-    private final RampAnimator.Listener mRampAnimatorListener = new RampAnimator.Listener() {
-        @Override
-        public void onAnimationEnd() {
-            sendUpdatePowerState();
-            Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    };
-
-    /** Clean up all resources that are accessed via the {@link #mHandler} thread. */
-    private void cleanupHandlerThreadAfterStop() {
-        mDisplayPowerProximityStateController.cleanup();
-        mBrightnessRangeController.stop();
-        mBrightnessThrottler.stop();
-        mBrightnessClamperController.stop();
-        mHandler.removeCallbacksAndMessages(null);
-
-        // Release any outstanding wakelocks we're still holding because of pending messages.
-        mWakelockController.releaseAll();
-
-        final float brightness = mPowerState != null
-                ? mPowerState.getScreenBrightness()
-                : PowerManager.BRIGHTNESS_MIN;
-        reportStats(brightness);
-
-        if (mPowerState != null) {
-            mPowerState.stop();
-            mPowerState = null;
-        }
-
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController.stop();
-        }
-
-        if (mDisplayWhiteBalanceController != null) {
-            mDisplayWhiteBalanceController.setEnabled(false);
-        }
-    }
-
-    // Call from handler thread
-    private void updatePowerState() {
-        Trace.traceBegin(Trace.TRACE_TAG_POWER,
-                "DisplayPowerController#updatePowerState");
-        updatePowerStateInternal();
-        Trace.traceEnd(Trace.TRACE_TAG_POWER);
-    }
-
-    private void updatePowerStateInternal() {
-        // Update the power state request.
-        final boolean mustNotify;
-        final int previousPolicy;
-        boolean mustInitialize = false;
-        mBrightnessReasonTemp.set(null);
-        mTempBrightnessEvent.reset();
-        SparseArray<DisplayPowerControllerInterface> displayBrightnessFollowers;
-        synchronized (mLock) {
-            if (mStopped) {
-                return;
-            }
-            mPendingUpdatePowerStateLocked = false;
-            if (mPendingRequestLocked == null) {
-                return; // wait until first actual power request
-            }
-
-            if (mPowerRequest == null) {
-                mPowerRequest = new DisplayPowerRequest(mPendingRequestLocked);
-                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
-                mPendingRequestChangedLocked = false;
-                mustInitialize = true;
-                // Assume we're on and bright until told otherwise, since that's the state we turn
-                // on in.
-                previousPolicy = DisplayPowerRequest.POLICY_BRIGHT;
-            } else if (mPendingRequestChangedLocked) {
-                previousPolicy = mPowerRequest.policy;
-                mPowerRequest.copyFrom(mPendingRequestLocked);
-                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
-                mPendingRequestChangedLocked = false;
-                mDisplayReadyLocked = false;
-            } else {
-                previousPolicy = mPowerRequest.policy;
-            }
-
-            mustNotify = !mDisplayReadyLocked;
-
-            displayBrightnessFollowers = mDisplayBrightnessFollowers.clone();
-        }
-
-        int state = mDisplayStateController
-                .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition);
-
-        // Initialize things the first time the power state is changed.
-        if (mustInitialize) {
-            initialize(readyToUpdateDisplayState() ? state : Display.STATE_UNKNOWN);
-        }
-
-        // Animate the screen state change unless already animating.
-        // The transition may be deferred, so after this point we will use the
-        // actual state instead of the desired one.
-        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();
-
-        DisplayBrightnessState displayBrightnessState = mDisplayBrightnessController
-                .updateBrightness(mPowerRequest, state);
-        float brightnessState = displayBrightnessState.getBrightness();
-        float rawBrightnessState = displayBrightnessState.getBrightness();
-        mBrightnessReasonTemp.set(displayBrightnessState.getBrightnessReason());
-        boolean slowChange = displayBrightnessState.isSlowChange();
-        // custom transition duration
-        float customAnimationRate = displayBrightnessState.getCustomAnimationRate();
-
-        // Set up the ScreenOff controller used when coming out of SCREEN_OFF and the ALS sensor
-        // doesn't yet have a valid lux value to use with auto-brightness.
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController
-                    .setLightSensorEnabled(displayBrightnessState.getShouldUseAutoBrightness()
-                    && mIsEnabled && (state == Display.STATE_OFF
-                    || (state == Display.STATE_DOZE
-                    && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()))
-                    && mLeadDisplayId == Layout.NO_LEAD_DISPLAY);
-        }
-
-        // Take note if the short term model was already active before applying the current
-        // request changes.
-        final boolean wasShortTermModelActive =
-                mAutomaticBrightnessStrategy.isShortTermModelActive();
-        mAutomaticBrightnessStrategy.setAutoBrightnessState(state,
-                mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig(),
-                mBrightnessReasonTemp.getReason(), mPowerRequest.policy,
-                mDisplayBrightnessController.getLastUserSetScreenBrightness(),
-                userSetBrightnessChanged);
-
-        // If the brightness is already set then it's been overridden by something other than the
-        // user, or is a temporary adjustment.
-        boolean userInitiatedChange = (Float.isNaN(brightnessState))
-                && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
-                || userSetBrightnessChanged);
-
-        mBrightnessRangeController.setAutoBrightnessEnabled(
-                mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
-                ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff()
-                        ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
-                        : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
-
-        boolean updateScreenBrightnessSetting =
-                displayBrightnessState.shouldUpdateScreenBrightnessSetting();
-        float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness();
-        // Apply auto-brightness.
-        int brightnessAdjustmentFlags = 0;
-        if (Float.isNaN(brightnessState)) {
-            if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) {
-                brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
-                        mTempBrightnessEvent);
-                if (BrightnessUtils.isValidBrightnessValue(brightnessState)
-                        || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) {
-                    rawBrightnessState = mAutomaticBrightnessController
-                            .getRawAutomaticScreenBrightness();
-                    brightnessState = clampScreenBrightness(brightnessState);
-                    // slowly adapt to auto-brightness
-                    // TODO(b/253226419): slowChange should be decided by strategy.updateBrightness
-                    slowChange = mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()
-                            && !mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged();
-                    brightnessAdjustmentFlags =
-                            mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags();
-                    updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
-                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
-                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
-                    if (mScreenOffBrightnessSensorController != null) {
-                        mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
-                    }
-                } else {
-                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
-                }
-            }
-        } else {
-            // Any non-auto-brightness values such as override or temporary should still be subject
-            // to clamping so that they don't go beyond the current max as specified by HBM
-            // Controller.
-            brightnessState = clampScreenBrightness(brightnessState);
-            mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
-        }
-
-        // Use default brightness when dozing unless overridden.
-        if ((Float.isNaN(brightnessState))
-                && Display.isDozeState(state)) {
-            rawBrightnessState = mScreenBrightnessDozeConfig;
-            brightnessState = clampScreenBrightness(rawBrightnessState);
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
-        }
-
-        // The ALS is not available yet - use the screen off sensor to determine the initial
-        // brightness
-        if (Float.isNaN(brightnessState) && mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
-                && mScreenOffBrightnessSensorController != null) {
-            rawBrightnessState =
-                    mScreenOffBrightnessSensorController.getAutomaticScreenBrightness();
-            brightnessState = rawBrightnessState;
-            if (BrightnessUtils.isValidBrightnessValue(brightnessState)) {
-                brightnessState = clampScreenBrightness(brightnessState);
-                updateScreenBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness()
-                        != brightnessState;
-                mBrightnessReasonTemp.setReason(
-                        BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR);
-            }
-        }
-
-        // Apply manual brightness.
-        if (Float.isNaN(brightnessState)) {
-            rawBrightnessState = currentBrightnessSetting;
-            brightnessState = clampScreenBrightness(rawBrightnessState);
-            if (brightnessState != currentBrightnessSetting) {
-                // The manually chosen screen brightness is outside of the currently allowed
-                // range (i.e., high-brightness-mode), make sure we tell the rest of the system
-                // by updating the setting.
-                updateScreenBrightnessSetting = true;
-            }
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_MANUAL);
-        }
-
-        float ambientLux = mAutomaticBrightnessController == null ? 0
-                : mAutomaticBrightnessController.getAmbientLux();
-        for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
-            follower.setBrightnessToFollow(rawBrightnessState,
-                    mDisplayBrightnessController.convertToNits(rawBrightnessState),
-                    ambientLux, slowChange);
-        }
-
-        // Now that a desired brightness has been calculated, apply brightness throttling. The
-        // dimming and low power transformations that follow can only dim brightness further.
-        //
-        // We didn't do this earlier through brightness clamping because we need to know both
-        // unthrottled (unclamped/ideal) and throttled brightness levels for subsequent operations.
-        // Note throttling effectively changes the allowed brightness range, so, similarly to HBM,
-        // we broadcast this change through setting.
-        final float unthrottledBrightnessState = brightnessState;
-
-        DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest,
-                brightnessState, slowChange);
-
-        brightnessState = clampedState.getBrightness();
-        slowChange = clampedState.isSlowChange();
-        // faster rate wins, at this point customAnimationRate == -1, strategy does not control
-        // customAnimationRate. Should be revisited if strategy start setting this value
-        customAnimationRate = Math.max(customAnimationRate, clampedState.getCustomAnimationRate());
-        mBrightnessReasonTemp.addModifier(clampedState.getBrightnessReason().getModifier());
-
-        if (updateScreenBrightnessSetting) {
-            // Tell the rest of the system about the new brightness in case we had to change it
-            // for things like auto-brightness or high-brightness-mode. Note that we do this
-            // only considering maxBrightness (ignroing brightness modifiers like low power or dim)
-            // so that the slider accurately represents the full possible range,
-            // even if they range changes what it means in absolute terms.
-            mDisplayBrightnessController.updateScreenBrightnessSetting(
-                    Math.min(unthrottledBrightnessState, clampedState.getMaxBrightness()));
-        }
-
-        // The current brightness to use has been calculated at this point, and HbmController should
-        // be notified so that it can accurately calculate HDR or HBM levels. We specifically do it
-        // here instead of having HbmController listen to the brightness setting because certain
-        // brightness sources (such as an app override) are not saved to the setting, but should be
-        // reflected in HBM calculations.
-        mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
-                mBrightnessClamperController.getBrightnessMaxReason());
-
-        // Animate the screen brightness when the screen is on or dozing.
-        // Skip the animation when the screen is off or suspended.
-        boolean brightnessAdjusted = false;
-        final boolean brightnessIsTemporary =
-                (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_TEMPORARY)
-                        || mAutomaticBrightnessStrategy
-                        .isTemporaryAutoBrightnessAdjustmentApplied();
-        if (!mPendingScreenOff) {
-            if (mSkipScreenOnBrightnessRamp) {
-                if (state == Display.STATE_ON) {
-                    if (mSkipRampState == RAMP_STATE_SKIP_NONE && mDozing) {
-                        mInitialAutoBrightness = brightnessState;
-                        mSkipRampState = RAMP_STATE_SKIP_INITIAL;
-                    } else if (mSkipRampState == RAMP_STATE_SKIP_INITIAL
-                            && mUseSoftwareAutoBrightnessConfig
-                            && !BrightnessSynchronizer.floatEquals(brightnessState,
-                            mInitialAutoBrightness)) {
-                        mSkipRampState = RAMP_STATE_SKIP_AUTOBRIGHT;
-                    } else if (mSkipRampState == RAMP_STATE_SKIP_AUTOBRIGHT) {
-                        mSkipRampState = RAMP_STATE_SKIP_NONE;
-                    }
-                } else {
-                    mSkipRampState = RAMP_STATE_SKIP_NONE;
-                }
-            }
-
-            final boolean initialRampSkip = (state == Display.STATE_ON && mSkipRampState
-                    != RAMP_STATE_SKIP_NONE) || mDisplayPowerProximityStateController
-                    .shouldSkipRampBecauseOfProximityChangeToNegative();
-            // While dozing, sometimes the brightness is split into buckets. Rather than animating
-            // through the buckets, which is unlikely to be smooth in the first place, just jump
-            // right to the suggested brightness.
-            final boolean hasBrightnessBuckets =
-                    Display.isDozeState(state) && mBrightnessBucketsInDozeConfig;
-            // If the color fade is totally covering the screen then we can change the backlight
-            // level without it being a noticeable jump since any actual content isn't yet visible.
-            final boolean isDisplayContentVisible =
-                    mColorFadeEnabled && mPowerState.getColorFadeLevel() == 1.0f;
-            // We only want to animate the brightness if it is between 0.0f and 1.0f.
-            // brightnessState can contain the values -1.0f and NaN, which we do not want to
-            // animate to. To avoid this, we check the value first.
-            // If the brightnessState is off (-1.0f) we still want to animate to the minimum
-            // brightness (0.0f) to accommodate for LED displays, which can appear bright to the
-            // user even when the display is all black. We also clamp here in case some
-            // transformations to the brightness have pushed it outside of the currently
-            // allowed range.
-            float animateValue = clampScreenBrightness(brightnessState);
-
-            // If there are any HDR layers on the screen, we have a special brightness value that we
-            // use instead. We still preserve the calculated brightness for Standard Dynamic Range
-            // (SDR) layers, but the main brightness value will be the one for HDR.
-            float sdrAnimateValue = animateValue;
-            // TODO(b/216365040): The decision to prevent HBM for HDR in low power mode should be
-            // done in HighBrightnessModeController.
-            if (mBrightnessRangeController.getHighBrightnessMode()
-                    == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
-                    && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_DIMMED) == 0
-                    && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_LOW_POWER)
-                    == 0) {
-                // We want to scale HDR brightness level with the SDR level, we also need to restore
-                // SDR brightness immediately when entering dim or low power mode.
-                animateValue = mBrightnessRangeController.getHdrBrightnessValue();
-                customAnimationRate = Math.max(customAnimationRate,
-                        mBrightnessRangeController.getHdrTransitionRate());
-                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_HDR);
-            }
-
-            // if doze or suspend state is requested, we want to finish brightnes animation fast
-            // to allow state animation to start
-            if (mPowerRequest.policy == DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE
-                    && (mPowerRequest.dozeScreenState == Display.STATE_UNKNOWN  // dozing
-                    || mPowerRequest.dozeScreenState == Display.STATE_DOZE_SUSPEND
-                    || mPowerRequest.dozeScreenState == Display.STATE_ON_SUSPEND)) {
-                customAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
-                slowChange = false;
-            }
-
-            final float currentBrightness = mPowerState.getScreenBrightness();
-            final float currentSdrBrightness = mPowerState.getSdrScreenBrightness();
-
-            if (BrightnessUtils.isValidBrightnessValue(animateValue)
-                    && (animateValue != currentBrightness
-                    || sdrAnimateValue != currentSdrBrightness)) {
-                boolean skipAnimation = initialRampSkip || hasBrightnessBuckets
-                        || !isDisplayContentVisible || brightnessIsTemporary;
-                final boolean isHdrOnlyChange = BrightnessSynchronizer.floatEquals(
-                        sdrAnimateValue, currentSdrBrightness);
-                if (mFlags.isFastHdrTransitionsEnabled() && !skipAnimation && isHdrOnlyChange) {
-                    // SDR brightness is unchanged, so animate quickly as this is only impacting
-                    // a likely minority amount of display content
-                    // ie, the highlights of an HDR video or UltraHDR image
-                    slowChange = false;
-
-                    // Going from HDR to no HDR; visually this should be a "no-op" anyway
-                    // as the remaining SDR content's brightness should be holding steady
-                    // due to the sdr brightness not shifting
-                    if (BrightnessSynchronizer.floatEquals(sdrAnimateValue, animateValue)) {
-                        skipAnimation = true;
-                    }
-
-                    // Going from no HDR to HDR; visually this is a significant scene change
-                    // and the animation just prevents advanced clients from doing their own
-                    // handling of enter/exit animations if they would like to do such a thing
-                    if (BrightnessSynchronizer.floatEquals(sdrAnimateValue, currentBrightness)) {
-                        skipAnimation = true;
-                    }
-                }
-                if (skipAnimation) {
-                    animateScreenBrightness(animateValue, sdrAnimateValue,
-                            SCREEN_ANIMATION_RATE_MINIMUM);
-                } else if (customAnimationRate > 0) {
-                    animateScreenBrightness(animateValue, sdrAnimateValue,
-                            customAnimationRate, /* ignoreAnimationLimits = */true);
-                } else {
-                    boolean isIncreasing = animateValue > currentBrightness;
-                    final float rampSpeed;
-                    final boolean idle = mAutomaticBrightnessController != null
-                            && mAutomaticBrightnessController.isInIdleMode();
-                    if (isIncreasing && slowChange) {
-                        rampSpeed = idle ? mBrightnessRampRateSlowIncreaseIdle
-                                : mBrightnessRampRateSlowIncrease;
-                    } else if (isIncreasing && !slowChange) {
-                        rampSpeed = mBrightnessRampRateFastIncrease;
-                    } else if (!isIncreasing && slowChange) {
-                        rampSpeed = idle ? mBrightnessRampRateSlowDecreaseIdle
-                                : mBrightnessRampRateSlowDecrease;
-                    } else {
-                        rampSpeed = mBrightnessRampRateFastDecrease;
-                    }
-                    animateScreenBrightness(animateValue, sdrAnimateValue, rampSpeed);
-                }
-            }
-
-            notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange,
-                    wasShortTermModelActive, mAutomaticBrightnessStrategy.isAutoBrightnessEnabled(),
-                    brightnessIsTemporary, displayBrightnessState.getShouldUseAutoBrightness());
-
-            // We save the brightness info *after* the brightness setting has been changed and
-            // adjustments made so that the brightness info reflects the latest value.
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(),
-                    animateValue, clampedState);
-        } else {
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(), clampedState);
-        }
-
-        // Only notify if the brightness adjustment is not temporary (i.e. slider has been released)
-        if (brightnessAdjusted && !brightnessIsTemporary) {
-            postBrightnessChangeRunnable();
-        }
-
-        // Log any changes to what is currently driving the brightness setting.
-        if (!mBrightnessReasonTemp.equals(mBrightnessReason) || brightnessAdjustmentFlags != 0) {
-            Slog.v(mTag, "Brightness [" + brightnessState + "] reason changing to: '"
-                    + mBrightnessReasonTemp.toString(brightnessAdjustmentFlags)
-                    + "', previous reason: '" + mBrightnessReason + "'.");
-            mBrightnessReason.set(mBrightnessReasonTemp);
-        } else if (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_MANUAL
-                && userSetBrightnessChanged) {
-            Slog.v(mTag, "Brightness [" + brightnessState + "] manual adjustment.");
-        }
-
-
-        // Log brightness events when a detail of significance has changed. Generally this is the
-        // brightness itself changing, but also includes data like HBM cap, thermal throttling
-        // brightness cap, RBC state, etc.
-        mTempBrightnessEvent.setTime(System.currentTimeMillis());
-        mTempBrightnessEvent.setBrightness(brightnessState);
-        mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId);
-        mTempBrightnessEvent.setReason(mBrightnessReason);
-        mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax());
-        mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
-        mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
-                | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
-                | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
-        mTempBrightnessEvent.setRbcStrength(mCdsi != null
-                ? mCdsi.getReduceBrightColorsStrength() : -1);
-        mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
-        mTempBrightnessEvent.setWasShortTermModelActive(wasShortTermModelActive);
-        mTempBrightnessEvent.setDisplayBrightnessStrategyName(displayBrightnessState
-                .getDisplayBrightnessStrategyName());
-        mTempBrightnessEvent.setAutomaticBrightnessEnabled(
-                displayBrightnessState.getShouldUseAutoBrightness());
-        // Temporary is what we use during slider interactions. We avoid logging those so that
-        // we don't spam logcat when the slider is being used.
-        boolean tempToTempTransition =
-                mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY
-                        && mLastBrightnessEvent.getReason().getReason()
-                        == BrightnessReason.REASON_TEMPORARY;
-        // Purely for dumpsys;
-        final boolean isRbcEvent =
-                mLastBrightnessEvent.isRbcEnabled() != mTempBrightnessEvent.isRbcEnabled();
-
-        if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition)
-                || brightnessAdjustmentFlags != 0) {
-            mTempBrightnessEvent.setInitialBrightness(mLastBrightnessEvent.getBrightness());
-            mLastBrightnessEvent.copyFrom(mTempBrightnessEvent);
-            BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent);
-            // Adjustment flags (and user-set flag) only get added after the equality checks since
-            // they are transient.
-            newEvent.setAdjustmentFlags(brightnessAdjustmentFlags);
-            newEvent.setFlags(newEvent.getFlags() | (userSetBrightnessChanged
-                    ? BrightnessEvent.FLAG_USER_SET : 0));
-            Slog.i(mTag, newEvent.toString(/* includeTime= */ false));
-
-            if (userSetBrightnessChanged
-                    || newEvent.getReason().getReason() != BrightnessReason.REASON_TEMPORARY) {
-                logBrightnessEvent(newEvent, unthrottledBrightnessState);
-            }
-            if (mBrightnessEventRingBuffer != null) {
-                mBrightnessEventRingBuffer.append(newEvent);
-            }
-            if (isRbcEvent) {
-                mRbcEventRingBuffer.append(newEvent);
-            }
-
-        }
-
-        // Update display white-balance.
-        if (mDisplayWhiteBalanceController != null) {
-            if (state == Display.STATE_ON && mDisplayWhiteBalanceSettings.isEnabled()) {
-                mDisplayWhiteBalanceController.setEnabled(true);
-                mDisplayWhiteBalanceController.updateDisplayColorTemperature();
-            } else {
-                mDisplayWhiteBalanceController.setEnabled(false);
-            }
-        }
-
-        // Determine whether the display is ready for use in the newly requested state.
-        // Note that we do not wait for the brightness ramp animation to complete before
-        // 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);
-        final boolean finished = ready
-                && !mScreenBrightnessRampAnimator.isAnimating();
-
-        // Notify policy about screen turned on.
-        if (ready && state != Display.STATE_OFF
-                && mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_TURNING_ON) {
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_ON);
-            mWindowManagerPolicy.screenTurnedOn(mDisplayId);
-        }
-
-        // Grab a wake lock if we have unfinished business.
-        if (!finished) {
-            mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        }
-
-        // Notify the power manager when ready.
-        if (ready && mustNotify) {
-            // Send state change.
-            synchronized (mLock) {
-                if (!mPendingRequestChangedLocked) {
-                    mDisplayReadyLocked = true;
-
-                    if (DEBUG) {
-                        Slog.d(mTag, "Display ready!");
-                    }
-                }
-            }
-            sendOnStateChangedWithWakelock();
-        }
-
-        // Release the wake lock when we have no unfinished business.
-        if (finished) {
-            mWakelockController.releaseWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        }
-
-        // Record if dozing for future comparison.
-        mDozing = state != Display.STATE_ON;
-
-        if (previousPolicy != mPowerRequest.policy) {
-            logDisplayPolicyChanged(mPowerRequest.policy);
-        }
-    }
-
-    private void setDwbcOverride(float cct) {
-        if (mDisplayWhiteBalanceController != null) {
-            mDisplayWhiteBalanceController.setAmbientColorTemperatureOverride(cct);
-            // The ambient color temperature override is only applied when the ambient color
-            // temperature changes or is updated, so it doesn't necessarily change the screen color
-            // temperature immediately. So, let's make it!
-            // We can call this directly, since we're already on the handler thread.
-            updatePowerState();
-        }
-    }
-
-    private void setDwbcStrongMode(int arg) {
-        if (mDisplayWhiteBalanceController != null) {
-            final boolean isIdle = (arg == 1);
-            mDisplayWhiteBalanceController.setStrongModeEnabled(isIdle);
-        }
-    }
-
-    private void setDwbcLoggingEnabled(int arg) {
-        if (mDisplayWhiteBalanceController != null) {
-            final boolean enabled = (arg == 1);
-            mDisplayWhiteBalanceController.setLoggingEnabled(enabled);
-            mDisplayWhiteBalanceSettings.setLoggingEnabled(enabled);
-        }
-    }
-
-    @Override
-    public void updateBrightness() {
-        sendUpdatePowerState();
-    }
-
-    /**
-     * Ignores the proximity sensor until the sensor state changes, but only if the sensor is
-     * currently enabled and forcing the screen to be dark.
-     */
-    @Override
-    public void ignoreProximitySensorUntilChanged() {
-        mDisplayPowerProximityStateController.ignoreProximitySensorUntilChanged();
-    }
-
-    @Override
-    public void setBrightnessConfiguration(BrightnessConfiguration c,
-            boolean shouldResetShortTermModel) {
-        Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS,
-                shouldResetShortTermModel ? 1 : 0, /* unused */ 0, c);
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setTemporaryBrightness(float brightness) {
-        Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_BRIGHTNESS,
-                Float.floatToIntBits(brightness), 0 /*unused*/);
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
-        Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT,
-                Float.floatToIntBits(adjustment), 0 /*unused*/);
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setBrightnessFromOffload(float brightness) {
-        Message msg = mHandler.obtainMessage(MSG_SET_BRIGHTNESS_FROM_OFFLOAD,
-                Float.floatToIntBits(brightness), 0 /*unused*/);
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-    }
-
-    @Override
-    public float[] getAutoBrightnessLevels(
-            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
-                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
-        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
-    }
-
-    @Override
-    public float[] getAutoBrightnessLuxLevels(
-            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
-                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
-        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
-    }
-
-    @Override
-    public BrightnessInfo getBrightnessInfo() {
-        synchronized (mCachedBrightnessInfo) {
-            return new BrightnessInfo(
-                    mCachedBrightnessInfo.brightness.value,
-                    mCachedBrightnessInfo.adjustedBrightness.value,
-                    mCachedBrightnessInfo.brightnessMin.value,
-                    mCachedBrightnessInfo.brightnessMax.value,
-                    mCachedBrightnessInfo.hbmMode.value,
-                    mCachedBrightnessInfo.hbmTransitionPoint.value,
-                    mCachedBrightnessInfo.brightnessMaxReason.value);
-        }
-    }
-
-    @Override
-    public void onBootCompleted() {
-        Message msg = mHandler.obtainMessage(MSG_BOOT_COMPLETED);
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-    }
-
-    private boolean saveBrightnessInfo(float brightness) {
-        return saveBrightnessInfo(brightness, /* state= */ null);
-    }
-
-    private boolean saveBrightnessInfo(float brightness, @Nullable DisplayBrightnessState state) {
-        return saveBrightnessInfo(brightness, brightness, state);
-    }
-
-    private boolean saveBrightnessInfo(float brightness, float adjustedBrightness,
-            @Nullable DisplayBrightnessState state) {
-        synchronized (mCachedBrightnessInfo) {
-            float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX;
-            final float minBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMin(), stateMax);
-            final float maxBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMax(), stateMax);
-            boolean changed = false;
-
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightness,
-                            brightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.adjustedBrightness,
-                            adjustedBrightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightnessMin,
-                            minBrightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightnessMax,
-                            maxBrightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.hbmMode,
-                            mBrightnessRangeController.getHighBrightnessMode());
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.hbmTransitionPoint,
-                            mBrightnessRangeController.getTransitionPoint());
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
-                            mBrightnessClamperController.getBrightnessMaxReason());
-
-            return changed;
-        }
-    }
-
-    void postBrightnessChangeRunnable() {
-        if (!mHandler.hasCallbacks(mOnBrightnessChangeRunnable)) {
-            mHandler.post(mOnBrightnessChangeRunnable);
-        }
-    }
-
-    private HighBrightnessModeController createHbmControllerLocked(
-            HighBrightnessModeMetadata hbmMetadata, Runnable modeChangeCallback) {
-        final DisplayDeviceConfig ddConfig = mDisplayDevice.getDisplayDeviceConfig();
-        final IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
-        final String displayUniqueId = mDisplayDevice.getUniqueId();
-        final DisplayDeviceConfig.HighBrightnessModeData hbmData =
-                ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
-        final DisplayDeviceInfo info = mDisplayDevice.getDisplayDeviceInfoLocked();
-        return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
-                displayToken, displayUniqueId, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, hbmData, (sdrBrightness, maxDesiredHdrSdrRatio) ->
-                        mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness,
-                                maxDesiredHdrSdrRatio), modeChangeCallback, hbmMetadata, mContext);
-    }
-
-    private BrightnessThrottler createBrightnessThrottlerLocked() {
-        final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
-        return new BrightnessThrottler(mHandler,
-                () -> {
-                    sendUpdatePowerState();
-                    postBrightnessChangeRunnable();
-                }, mUniqueDisplayId,
-                mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId,
-                ddConfig.getThermalBrightnessThrottlingDataMapByThrottlingId());
-    }
-
-    private void blockScreenOn() {
-        if (mPendingScreenOnUnblocker == null) {
-            Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0);
-            mPendingScreenOnUnblocker = new ScreenOnUnblocker();
-            mScreenOnBlockStartRealTime = SystemClock.elapsedRealtime();
-            Slog.i(mTag, "Blocking screen on until initial contents have been drawn.");
-        }
-    }
-
-    private void unblockScreenOn() {
-        if (mPendingScreenOnUnblocker != null) {
-            mPendingScreenOnUnblocker = null;
-            long delay = SystemClock.elapsedRealtime() - mScreenOnBlockStartRealTime;
-            Slog.i(mTag, "Unblocked screen on after " + delay + " ms");
-            Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0);
-        }
-    }
-
-    private void blockScreenOff() {
-        if (mPendingScreenOffUnblocker == null) {
-            Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_OFF_BLOCKED_TRACE_NAME, 0);
-            mPendingScreenOffUnblocker = new ScreenOffUnblocker();
-            mScreenOffBlockStartRealTime = SystemClock.elapsedRealtime();
-            Slog.i(mTag, "Blocking screen off");
-        }
-    }
-
-    private void unblockScreenOff() {
-        if (mPendingScreenOffUnblocker != null) {
-            mPendingScreenOffUnblocker = null;
-            long delay = SystemClock.elapsedRealtime() - mScreenOffBlockStartRealTime;
-            Slog.i(mTag, "Unblocked screen off after " + delay + " ms");
-            Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, SCREEN_OFF_BLOCKED_TRACE_NAME, 0);
-        }
-    }
-
-    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 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()) {
-                if (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_ON
-                        || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
-                    setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF);
-                    blockScreenOff();
-                    mWindowManagerPolicy.screenTurningOff(mDisplayId, mPendingScreenOffUnblocker);
-                    unblockScreenOff();
-                } else if (mPendingScreenOffUnblocker != null) {
-                    // Abort doing the state change until screen off is unblocked.
-                    return false;
-                }
-            }
-
-            if (!reportOnly && changed && readyToUpdateDisplayState()
-                    && mPendingScreenOffUnblocker == null
-                    && mPendingScreenOnUnblockerByDisplayOffload == null) {
-                Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state);
-
-                String propertyKey = "debug.tracing.screen_state";
-                String propertyValue = String.valueOf(state);
-                try {
-                    // TODO(b/153319140) remove when we can get this from the above trace invocation
-                    SystemProperties.set(propertyKey, propertyValue);
-                } catch (RuntimeException e) {
-                    Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
-                            + " value=" + propertyValue + " " + e.getMessage());
-                }
-
-                mPowerState.setScreenState(state);
-                // Tell battery stats about the transition.
-                noteScreenState(state);
-            }
-        }
-
-        // Tell the window manager policy when the screen is turned off or on unless it's due
-        // to the proximity sensor.  We temporarily block turning the screen on until the
-        // window manager is ready by leaving a black surface covering the screen.
-        // This surface is essentially the final state of the color fade animation and
-        // it is only removed once the window manager tells us that the activity has
-        // finished drawing underneath.
-        if (isOff && mReportedScreenStateToPolicy != REPORTED_TO_POLICY_SCREEN_OFF
-                && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF);
-            unblockScreenOn();
-            mWindowManagerPolicy.screenTurnedOff(mDisplayId, mIsInTransition);
-        } else if (!isOff
-                && mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_TURNING_OFF) {
-
-            // We told policy already that screen was turning off, but now we changed our minds.
-            // Complete the full state transition on -> turningOff -> off.
-            unblockScreenOff();
-            mWindowManagerPolicy.screenTurnedOff(mDisplayId, mIsInTransition);
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF);
-        }
-        if (!isOff
-                && (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_OFF
-                || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED)) {
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_ON);
-            if (mPowerState.getColorFadeLevel() == 0.0f) {
-                blockScreenOn();
-            } else {
-                unblockScreenOn();
-            }
-            mWindowManagerPolicy.screenTurningOn(mDisplayId, mPendingScreenOnUnblocker);
-        }
-
-        // Return true if the screen isn't blocked.
-        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() {
-        final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
-                ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
-        mLightSensor = SensorUtils.findSensor(mSensorManager,
-                mDisplayDeviceConfig.getAmbientLightSensor(), fallbackType);
-    }
-
-    private void loadScreenOffBrightnessSensor() {
-        mScreenOffBrightnessSensor = SensorUtils.findSensor(mSensorManager,
-                mDisplayDeviceConfig.getScreenOffBrightnessSensor(), SensorUtils.NO_FALLBACK);
-    }
-
-    private float clampScreenBrightness(float value) {
-        if (Float.isNaN(value)) {
-            value = PowerManager.BRIGHTNESS_MIN;
-        }
-        return MathUtils.constrain(value, mBrightnessRangeController.getCurrentBrightnessMin(),
-                mBrightnessRangeController.getCurrentBrightnessMax());
-    }
-
-    private void animateScreenBrightness(float target, float sdrTarget, float rate) {
-        animateScreenBrightness(target, sdrTarget, rate, /* ignoreAnimationLimits = */false);
-    }
-
-    private void animateScreenBrightness(float target, float sdrTarget, float rate,
-            boolean ignoreAnimationLimits) {
-        if (DEBUG) {
-            Slog.d(mTag, "Animating brightness: target=" + target + ", sdrTarget=" + sdrTarget
-                    + ", rate=" + rate);
-        }
-        if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate,
-                ignoreAnimationLimits)) {
-            Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target);
-
-            String propertyKey = "debug.tracing.screen_brightness";
-            String propertyValue = String.valueOf(target);
-            try {
-                // TODO(b/153319140) remove when we can get this from the above trace invocation
-                SystemProperties.set(propertyKey, propertyValue);
-            } catch (RuntimeException e) {
-                Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
-                        + " value=" + propertyValue + " " + e.getMessage());
-            }
-
-            noteScreenBrightness(target);
-        }
-    }
-
-    private void animateScreenStateChange(int target, boolean performScreenOffTransition) {
-        // If there is already an animation in progress, don't interfere with it.
-        if (mColorFadeEnabled
-                && (mColorFadeOnAnimator.isStarted() || mColorFadeOffAnimator.isStarted())) {
-            if (target != Display.STATE_ON) {
-                return;
-            }
-            // If display state changed to on, proceed and stop the color fade and turn screen on.
-            mPendingScreenOff = false;
-        }
-
-        if (mDisplayBlanksAfterDozeConfig
-                && Display.isDozeState(mPowerState.getScreenState())
-                && !Display.isDozeState(target)) {
-            // Skip the screen off animation and add a black surface to hide the
-            // contents of the screen.
-            mPowerState.prepareColorFade(mContext,
-                    mColorFadeFadesConfig ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP);
-            if (mColorFadeOffAnimator != null) {
-                mColorFadeOffAnimator.end();
-            }
-            // Some display hardware will blank itself on the transition between doze and non-doze
-            // but still on display states. In this case we want to report to policy that the
-            // display has turned off so it can prepare the appropriate power on animation, but we
-            // don't want to actually transition to the fully off state since that takes
-            // significantly longer to transition from.
-            setScreenState(Display.STATE_OFF, target != Display.STATE_OFF /*reportOnly*/);
-        }
-
-        // If we were in the process of turning off the screen but didn't quite
-        // finish.  Then finish up now to prevent a jarring transition back
-        // to screen on if we skipped blocking screen on as usual.
-        if (mPendingScreenOff && target != Display.STATE_OFF) {
-            setScreenState(Display.STATE_OFF);
-            mPendingScreenOff = false;
-            mPowerState.dismissColorFadeResources();
-        }
-
-        if (target == Display.STATE_ON) {
-            // Want screen on.  The contents of the screen may not yet
-            // be visible if the color fade has not been dismissed because
-            // its last frame of animation is solid black.
-            if (!setScreenState(Display.STATE_ON)) {
-                return; // screen on blocked
-            }
-            if (USE_COLOR_FADE_ON_ANIMATION && mColorFadeEnabled && mPowerRequest.isBrightOrDim()) {
-                // Perform screen on animation.
-                if (mPowerState.getColorFadeLevel() == 1.0f) {
-                    mPowerState.dismissColorFade();
-                } else if (mPowerState.prepareColorFade(mContext,
-                        mColorFadeFadesConfig
-                                ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP)) {
-                    mColorFadeOnAnimator.start();
-                } else {
-                    mColorFadeOnAnimator.end();
-                }
-            } else {
-                // Skip screen on animation.
-                mPowerState.setColorFadeLevel(1.0f);
-                mPowerState.dismissColorFade();
-            }
-        } else if (target == Display.STATE_DOZE) {
-            // Want screen dozing.
-            // Wait for brightness animation to complete beforehand when entering doze
-            // from screen on to prevent a perceptible jump because brightness may operate
-            // differently when the display is configured for dozing.
-            if (mScreenBrightnessRampAnimator.isAnimating()
-                    && mPowerState.getScreenState() == Display.STATE_ON) {
-                return;
-            }
-
-            // Set screen state.
-            if (!setScreenState(Display.STATE_DOZE)) {
-                return; // screen on blocked
-            }
-
-            // Dismiss the black surface without fanfare.
-            mPowerState.setColorFadeLevel(1.0f);
-            mPowerState.dismissColorFade();
-        } else if (target == Display.STATE_DOZE_SUSPEND) {
-            // Want screen dozing and suspended.
-            // Wait for brightness animation to complete beforehand unless already
-            // suspended because we may not be able to change it after suspension.
-            if (mScreenBrightnessRampAnimator.isAnimating()
-                    && mPowerState.getScreenState() != Display.STATE_DOZE_SUSPEND) {
-                return;
-            }
-
-            // If not already suspending, temporarily set the state to doze until the
-            // screen on is unblocked, then suspend.
-            if (mPowerState.getScreenState() != Display.STATE_DOZE_SUSPEND) {
-                if (!setScreenState(Display.STATE_DOZE)) {
-                    return; // screen on blocked
-                }
-                setScreenState(Display.STATE_DOZE_SUSPEND); // already on so can't block
-            }
-
-            // Dismiss the black surface without fanfare.
-            mPowerState.setColorFadeLevel(1.0f);
-            mPowerState.dismissColorFade();
-        } else if (target == Display.STATE_ON_SUSPEND) {
-            // Want screen full-power and suspended.
-            // Wait for brightness animation to complete beforehand unless already
-            // suspended because we may not be able to change it after suspension.
-            if (mScreenBrightnessRampAnimator.isAnimating()
-                    && mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) {
-                return;
-            }
-
-            // If not already suspending, temporarily set the state to on until the
-            // screen on is unblocked, then suspend.
-            if (mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) {
-                if (!setScreenState(Display.STATE_ON)) {
-                    return;
-                }
-                setScreenState(Display.STATE_ON_SUSPEND);
-            }
-
-            // Dismiss the black surface without fanfare.
-            mPowerState.setColorFadeLevel(1.0f);
-            mPowerState.dismissColorFade();
-        } else {
-            // Want screen off.
-            mPendingScreenOff = true;
-            if (!mColorFadeEnabled) {
-                mPowerState.setColorFadeLevel(0.0f);
-            }
-
-            if (mPowerState.getColorFadeLevel() == 0.0f) {
-                // Turn the screen off.
-                // A black surface is already hiding the contents of the screen.
-                setScreenState(Display.STATE_OFF);
-                mPendingScreenOff = false;
-                mPowerState.dismissColorFadeResources();
-            } else if (performScreenOffTransition
-                    && mPowerState.prepareColorFade(mContext,
-                    mColorFadeFadesConfig
-                            ? ColorFade.MODE_FADE : ColorFade.MODE_COOL_DOWN)
-                    && mPowerState.getScreenState() != Display.STATE_OFF) {
-                // Perform the screen off animation.
-                mColorFadeOffAnimator.start();
-            } else {
-                // Skip the screen off animation and add a black surface to hide the
-                // contents of the screen.
-                mColorFadeOffAnimator.end();
-            }
-        }
-    }
-
-    private final Runnable mCleanListener = this::sendUpdatePowerState;
-
-    private void sendOnStateChangedWithWakelock() {
-        boolean wakeLockAcquired = mWakelockController.acquireWakelock(
-                WakelockController.WAKE_LOCK_STATE_CHANGED);
-        if (wakeLockAcquired) {
-            mHandler.post(mWakelockController.getOnStateChangedRunnable());
-        }
-    }
-
-    private void logDisplayPolicyChanged(int newPolicy) {
-        LogMaker log = new LogMaker(MetricsEvent.DISPLAY_POLICY);
-        log.setType(MetricsEvent.TYPE_UPDATE);
-        log.setSubtype(newPolicy);
-        MetricsLogger.action(log);
-    }
-
-    private void handleSettingsChange(boolean userSwitch) {
-        mDisplayBrightnessController
-                .setPendingScreenBrightness(mDisplayBrightnessController
-                        .getScreenBrightnessSetting());
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(userSwitch);
-        if (userSwitch) {
-            // Don't treat user switches as user initiated change.
-            mDisplayBrightnessController
-                    .setAndNotifyCurrentScreenBrightness(mDisplayBrightnessController
-                            .getPendingScreenBrightness());
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.resetShortTermModel();
-            }
-        }
-        sendUpdatePowerState();
-    }
-
-    private void handleBrightnessModeChange() {
-        final int screenBrightnessModeSetting = Settings.System.getIntForUser(
-                mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
-        mHandler.postAtTime(() -> {
-            mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting
-                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-            updatePowerState();
-        }, mClock.uptimeMillis());
-    }
-
-
-    @Override
-    public float getScreenBrightnessSetting() {
-        return mDisplayBrightnessController.getScreenBrightnessSetting();
-    }
-
-    @Override
-    public void setBrightness(float brightnessValue, int userSerial) {
-        mDisplayBrightnessController.setBrightness(clampScreenBrightness(brightnessValue),
-                userSerial);
-    }
-
-    @Override
-    public int getDisplayId() {
-        return mDisplayId;
-    }
-
-    @Override
-    public int getLeadDisplayId() {
-        return mLeadDisplayId;
-    }
-
-    @Override
-    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
-            boolean slowChange) {
-        mBrightnessRangeController.onAmbientLuxChange(ambientLux);
-        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
-            mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
-        } else {
-            float brightness = mDisplayBrightnessController.getBrightnessFromNits(nits);
-            if (BrightnessUtils.isValidBrightnessValue(brightness)) {
-                mDisplayBrightnessController.setBrightnessToFollow(brightness, slowChange);
-            } else {
-                // The device does not support nits
-                mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness,
-                        slowChange);
-            }
-        }
-        sendUpdatePowerState();
-    }
-
-    private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
-            boolean wasShortTermModelActive, boolean autobrightnessEnabled,
-            boolean brightnessIsTemporary, boolean shouldUseAutoBrightness) {
-
-        final float brightnessInNits =
-                mDisplayBrightnessController.convertToAdjustedNits(brightness);
-        // Don't report brightness to brightnessTracker:
-        // If brightness is temporary (ie the slider has not been released)
-        // or if we are in idle screen brightness mode.
-        // or display is not on
-        // or we shouldn't be using autobrightness
-        // or the nits is invalid.
-        if (brightnessIsTemporary
-                || mAutomaticBrightnessController == null
-                || mAutomaticBrightnessController.isInIdleMode()
-                || !autobrightnessEnabled
-                || mBrightnessTracker == null
-                || !shouldUseAutoBrightness
-                || brightnessInNits < 0.0f) {
-            return;
-        }
-
-        if (userInitiated && (mAutomaticBrightnessController == null
-                || !mAutomaticBrightnessController.hasValidAmbientLux())) {
-            // If we don't have a valid lux reading we can't report a valid
-            // slider event so notify as if the system changed the brightness.
-            userInitiated = false;
-        }
-
-        // We only want to track changes on devices that can actually map the display backlight
-        // values into a physical brightness unit since the value provided by the API is in
-        // nits and not using the arbitrary backlight units.
-        final float powerFactor = mPowerRequest.lowPowerMode
-                ? mPowerRequest.screenLowPowerBrightnessFactor
-                : 1.0f;
-        mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated,
-                powerFactor, wasShortTermModelActive,
-                mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId,
-                mAutomaticBrightnessController.getLastSensorValues(),
-                mAutomaticBrightnessController.getLastSensorTimestamps());
-    }
-
-    @Override
-    public void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
-            sendUpdatePowerStateLocked();
-        }
-    }
-
-    @Override
-    public void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.remove(follower.getDisplayId());
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void clearDisplayBrightnessFollowersLocked() {
-        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-        mDisplayBrightnessFollowers.clear();
-    }
-
-    @Override
-    public void dump(final PrintWriter pw) {
-        synchronized (mLock) {
-            pw.println();
-            pw.println("Display Power Controller:");
-            pw.println("  mDisplayId=" + mDisplayId);
-            pw.println("  mLeadDisplayId=" + mLeadDisplayId);
-            pw.println("  mLightSensor=" + mLightSensor);
-            pw.println("  mDisplayBrightnessFollowers=" + mDisplayBrightnessFollowers);
-
-            pw.println();
-            pw.println("Display Power Controller Locked State:");
-            pw.println("  mDisplayReadyLocked=" + mDisplayReadyLocked);
-            pw.println("  mPendingRequestLocked=" + mPendingRequestLocked);
-            pw.println("  mPendingRequestChangedLocked=" + mPendingRequestChangedLocked);
-            pw.println("  mPendingUpdatePowerStateLocked=" + mPendingUpdatePowerStateLocked);
-        }
-
-        pw.println();
-        pw.println("Display Power Controller Configuration:");
-        pw.println("  mScreenBrightnessDozeConfig=" + mScreenBrightnessDozeConfig);
-        pw.println("  mUseSoftwareAutoBrightnessConfig=" + mUseSoftwareAutoBrightnessConfig);
-        pw.println("  mSkipScreenOnBrightnessRamp=" + mSkipScreenOnBrightnessRamp);
-        pw.println("  mColorFadeFadesConfig=" + mColorFadeFadesConfig);
-        pw.println("  mColorFadeEnabled=" + mColorFadeEnabled);
-        pw.println("  mIsDisplayInternal=" + mIsDisplayInternal);
-        synchronized (mCachedBrightnessInfo) {
-            pw.println("  mCachedBrightnessInfo.brightness="
-                    + mCachedBrightnessInfo.brightness.value);
-            pw.println("  mCachedBrightnessInfo.adjustedBrightness="
-                    + mCachedBrightnessInfo.adjustedBrightness.value);
-            pw.println("  mCachedBrightnessInfo.brightnessMin="
-                    + mCachedBrightnessInfo.brightnessMin.value);
-            pw.println("  mCachedBrightnessInfo.brightnessMax="
-                    + mCachedBrightnessInfo.brightnessMax.value);
-            pw.println("  mCachedBrightnessInfo.hbmMode=" + mCachedBrightnessInfo.hbmMode.value);
-            pw.println("  mCachedBrightnessInfo.hbmTransitionPoint="
-                    + mCachedBrightnessInfo.hbmTransitionPoint.value);
-            pw.println("  mCachedBrightnessInfo.brightnessMaxReason ="
-                    + mCachedBrightnessInfo.brightnessMaxReason.value);
-        }
-        pw.println("  mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig);
-        pw.println("  mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig);
-        mHandler.runWithScissors(() -> dumpLocal(pw), 1000);
-    }
-
-    private void dumpLocal(PrintWriter pw) {
-        pw.println();
-        pw.println("Display Power Controller Thread State:");
-        pw.println("  mPowerRequest=" + mPowerRequest);
-        pw.println("  mBrightnessReason=" + mBrightnessReason);
-        pw.println("  mAppliedDimming=" + mAppliedDimming);
-        pw.println("  mAppliedThrottling=" + mAppliedThrottling);
-        pw.println("  mDozing=" + mDozing);
-        pw.println("  mSkipRampState=" + skipRampStateToString(mSkipRampState));
-        pw.println("  mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime);
-        pw.println("  mScreenOffBlockStartRealTime=" + mScreenOffBlockStartRealTime);
-        pw.println("  mPendingScreenOnUnblocker=" + mPendingScreenOnUnblocker);
-        pw.println("  mPendingScreenOffUnblocker=" + mPendingScreenOffUnblocker);
-        pw.println("  mPendingScreenOff=" + mPendingScreenOff);
-        pw.println("  mReportedToPolicy="
-                + reportedToPolicyToString(mReportedScreenStateToPolicy));
-        pw.println("  mIsRbcActive=" + mIsRbcActive);
-        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "    ");
-        mAutomaticBrightnessStrategy.dump(ipw);
-
-        if (mScreenBrightnessRampAnimator != null) {
-            pw.println("  mScreenBrightnessRampAnimator.isAnimating()="
-                    + mScreenBrightnessRampAnimator.isAnimating());
-        }
-
-        if (mColorFadeOnAnimator != null) {
-            pw.println("  mColorFadeOnAnimator.isStarted()="
-                    + mColorFadeOnAnimator.isStarted());
-        }
-        if (mColorFadeOffAnimator != null) {
-            pw.println("  mColorFadeOffAnimator.isStarted()="
-                    + mColorFadeOffAnimator.isStarted());
-        }
-
-        if (mPowerState != null) {
-            mPowerState.dump(pw);
-        }
-
-        if (mAutomaticBrightnessController != null) {
-            mAutomaticBrightnessController.dump(pw);
-            dumpBrightnessEvents(pw);
-        }
-
-        dumpRbcEvents(pw);
-
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController.dump(pw);
-        }
-
-        if (mBrightnessRangeController != null) {
-            mBrightnessRangeController.dump(pw);
-        }
-
-        if (mBrightnessThrottler != null) {
-            mBrightnessThrottler.dump(pw);
-        }
-
-        pw.println();
-        if (mDisplayWhiteBalanceController != null) {
-            mDisplayWhiteBalanceController.dump(pw);
-            mDisplayWhiteBalanceSettings.dump(pw);
-        }
-
-        pw.println();
-
-        if (mWakelockController != null) {
-            mWakelockController.dumpLocal(pw);
-        }
-
-        pw.println();
-        if (mDisplayBrightnessController != null) {
-            mDisplayBrightnessController.dump(pw);
-        }
-
-        pw.println();
-        if (mDisplayStateController != null) {
-            mDisplayStateController.dumpsys(pw);
-        }
-
-        pw.println();
-        if (mBrightnessClamperController != null) {
-            mBrightnessClamperController.dump(ipw);
-        }
-    }
-
-
-    private static String reportedToPolicyToString(int state) {
-        switch (state) {
-            case REPORTED_TO_POLICY_SCREEN_OFF:
-                return "REPORTED_TO_POLICY_SCREEN_OFF";
-            case REPORTED_TO_POLICY_SCREEN_TURNING_ON:
-                return "REPORTED_TO_POLICY_SCREEN_TURNING_ON";
-            case REPORTED_TO_POLICY_SCREEN_ON:
-                return "REPORTED_TO_POLICY_SCREEN_ON";
-            default:
-                return Integer.toString(state);
-        }
-    }
-
-    private static String skipRampStateToString(int state) {
-        switch (state) {
-            case RAMP_STATE_SKIP_NONE:
-                return "RAMP_STATE_SKIP_NONE";
-            case RAMP_STATE_SKIP_INITIAL:
-                return "RAMP_STATE_SKIP_INITIAL";
-            case RAMP_STATE_SKIP_AUTOBRIGHT:
-                return "RAMP_STATE_SKIP_AUTOBRIGHT";
-            default:
-                return Integer.toString(state);
-        }
-    }
-
-    private void dumpBrightnessEvents(PrintWriter pw) {
-        int size = mBrightnessEventRingBuffer.size();
-        if (size < 1) {
-            pw.println("No Automatic Brightness Adjustments");
-            return;
-        }
-
-        pw.println("Automatic Brightness Adjustments Last " + size + " Events: ");
-        BrightnessEvent[] eventArray = mBrightnessEventRingBuffer.toArray();
-        for (int i = 0; i < mBrightnessEventRingBuffer.size(); i++) {
-            pw.println("  " + eventArray[i].toString());
-        }
-    }
-
-    private void dumpRbcEvents(PrintWriter pw) {
-        int size = mRbcEventRingBuffer.size();
-        if (size < 1) {
-            pw.println("No Reduce Bright Colors Adjustments");
-            return;
-        }
-
-        pw.println("Reduce Bright Colors Adjustments Last " + size + " Events: ");
-        BrightnessEvent[] eventArray = mRbcEventRingBuffer.toArray();
-        for (int i = 0; i < mRbcEventRingBuffer.size(); i++) {
-            pw.println("  " + eventArray[i]);
-        }
-    }
-
-
-    private void noteScreenState(int screenState) {
-        // Log screen state change with display id
-        FrameworkStatsLog.write(FrameworkStatsLog.SCREEN_STATE_CHANGED_V2,
-                screenState, mDisplayStatsId);
-        if (mBatteryStats != null) {
-            try {
-                // TODO(multi-display): make this multi-display
-                mBatteryStats.noteScreenState(screenState);
-            } catch (RemoteException e) {
-                // same process
-            }
-        }
-    }
-
-    @SuppressLint("AndroidFrameworkRequiresPermission")
-    private void noteScreenBrightness(float brightness) {
-        if (mBatteryStats != null) {
-            try {
-                // TODO(brightnessfloat): change BatteryStats to use float
-                int brightnessInt = mFlags.isBrightnessIntRangeUserPerceptionEnabled()
-                        ? BrightnessSynchronizer.brightnessFloatToIntSetting(mContext, brightness)
-                        : BrightnessSynchronizer.brightnessFloatToInt(brightness);
-                mBatteryStats.noteScreenBrightness(brightnessInt);
-            } catch (RemoteException e) {
-                // same process
-            }
-        }
-    }
-
-    private void reportStats(float brightness) {
-        if (mLastStatsBrightness == brightness) {
-            return;
-        }
-
-        float hbmTransitionPoint = PowerManager.BRIGHTNESS_MAX;
-        synchronized (mCachedBrightnessInfo) {
-            if (mCachedBrightnessInfo.hbmTransitionPoint == null) {
-                return;
-            }
-            hbmTransitionPoint = mCachedBrightnessInfo.hbmTransitionPoint.value;
-        }
-
-        final boolean aboveTransition = brightness > hbmTransitionPoint;
-        final boolean oldAboveTransition = mLastStatsBrightness > hbmTransitionPoint;
-
-        if (aboveTransition || oldAboveTransition) {
-            mLastStatsBrightness = brightness;
-            mHandler.removeMessages(MSG_STATSD_HBM_BRIGHTNESS);
-            if (aboveTransition != oldAboveTransition) {
-                // report immediately
-                logHbmBrightnessStats(brightness, mDisplayStatsId);
-            } else {
-                // delay for rate limiting
-                Message msg = mHandler.obtainMessage();
-                msg.what = MSG_STATSD_HBM_BRIGHTNESS;
-                msg.arg1 = Float.floatToIntBits(brightness);
-                msg.arg2 = mDisplayStatsId;
-                mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()
-                        + BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
-            }
-        }
-    }
-
-    private void logHbmBrightnessStats(float brightness, int displayStatsId) {
-        synchronized (mHandler) {
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.DISPLAY_HBM_BRIGHTNESS_CHANGED, displayStatsId, brightness);
-        }
-    }
-
-    // Return bucket index of range_[left]_[right] where
-    // left <= nits < right
-    private int nitsToRangeIndex(float nits) {
-        for (int i = 0; i < BRIGHTNESS_RANGE_BOUNDARIES.length; i++) {
-            if (nits < BRIGHTNESS_RANGE_BOUNDARIES[i]) {
-                return BRIGHTNESS_RANGE_INDEX[i];
-            }
-        }
-        return FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_3000_INF;
-    }
-
-    private int convertBrightnessReasonToStatsEnum(int brightnessReason) {
-        switch(brightnessReason) {
-            case BrightnessReason.REASON_UNKNOWN:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_UNKNOWN;
-            case BrightnessReason.REASON_MANUAL:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_MANUAL;
-            case BrightnessReason.REASON_DOZE:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_DOZE;
-            case BrightnessReason.REASON_DOZE_DEFAULT:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_DOZE_DEFAULT;
-            case BrightnessReason.REASON_AUTOMATIC:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_AUTOMATIC;
-            case BrightnessReason.REASON_SCREEN_OFF:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_SCREEN_OFF;
-            case BrightnessReason.REASON_OVERRIDE:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_OVERRIDE;
-            case BrightnessReason.REASON_TEMPORARY:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_TEMPORARY;
-            case BrightnessReason.REASON_BOOST:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_BOOST;
-            case BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_SCREEN_OFF_BRIGHTNESS_SENSOR;
-            case BrightnessReason.REASON_FOLLOWER:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_FOLLOWER;
-        }
-        return FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_UNKNOWN;
-    }
-
-    private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness) {
-        int modifier = event.getReason().getModifier();
-        int flags = event.getFlags();
-        // It's easier to check if the brightness is at maximum level using the brightness
-        // value untouched by any modifiers
-        boolean brightnessIsMax = unmodifiedBrightness == event.getHbmMax();
-        float brightnessInNits =
-                mDisplayBrightnessController.convertToAdjustedNits(event.getBrightness());
-        float appliedLowPowerMode = event.isLowPowerModeSet() ? event.getPowerFactor() : -1f;
-        int appliedRbcStrength  = event.isRbcEnabled() ? event.getRbcStrength() : -1;
-        float appliedHbmMaxNits =
-                event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
-                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getHbmMax());
-        // thermalCapNits set to -1 if not currently capping max brightness
-        float appliedThermalCapNits =
-                event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
-                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getThermalMax());
-        if (mIsDisplayInternal) {
-            FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                    mDisplayBrightnessController
-                            .convertToAdjustedNits(event.getInitialBrightness()),
-                    brightnessInNits,
-                    event.getLux(),
-                    event.getPhysicalDisplayId(),
-                    event.wasShortTermModelActive(),
-                    appliedLowPowerMode,
-                    appliedRbcStrength,
-                    appliedHbmMaxNits,
-                    appliedThermalCapNits,
-                    event.isAutomaticBrightnessEnabled(),
-                    FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL,
-                    convertBrightnessReasonToStatsEnum(event.getReason().getReason()),
-                    nitsToRangeIndex(brightnessInNits),
-                    brightnessIsMax,
-                    event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
-                    event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR,
-                    (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0,
-                    mBrightnessClamperController.getBrightnessMaxReason(),
-                    (modifier & BrightnessReason.MODIFIER_DIMMED) > 0,
-                    event.isRbcEnabled(),
-                    (flags & BrightnessEvent.FLAG_INVALID_LUX) > 0,
-                    (flags & BrightnessEvent.FLAG_DOZE_SCALE) > 0,
-                    (flags & BrightnessEvent.FLAG_USER_SET) > 0,
-                    (flags & BrightnessEvent.FLAG_IDLE_CURVE) > 0,
-                    (flags & BrightnessEvent.FLAG_LOW_POWER_MODE) > 0);
-        }
-    }
-
-    /**
-     * Indicates whether the display state is ready to update. If this is the default display, we
-     * want to update it right away so that we can draw the boot animation on it. If it is not
-     * the default display, drawing the boot animation on it would look incorrect, so we need
-     * to wait until boot is completed.
-     * @return True if the display state is ready to update
-     */
-    private boolean readyToUpdateDisplayState() {
-        return mDisplayId == Display.DEFAULT_DISPLAY || mBootCompleted;
-    }
-
-    private final class DisplayControllerHandler extends Handler {
-        DisplayControllerHandler(Looper looper) {
-            super(looper, null, true /*async*/);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_UPDATE_POWER_STATE:
-                    updatePowerState();
-                    break;
-
-                case MSG_SCREEN_ON_UNBLOCKED:
-                    if (mPendingScreenOnUnblocker == msg.obj) {
-                        unblockScreenOn();
-                        updatePowerState();
-                    }
-                    break;
-                case MSG_SCREEN_OFF_UNBLOCKED:
-                    if (mPendingScreenOffUnblocker == msg.obj) {
-                        unblockScreenOff();
-                        updatePowerState();
-                    }
-                    break;
-                case MSG_OFFLOADING_SCREEN_ON_UNBLOCKED:
-                    if (mDisplayOffloadSession == msg.obj) {
-                        unblockScreenOnByDisplayOffload();
-                        updatePowerState();
-                    }
-                    break;
-                case MSG_CONFIGURE_BRIGHTNESS:
-                    BrightnessConfiguration brightnessConfiguration =
-                            (BrightnessConfiguration) msg.obj;
-                    mAutomaticBrightnessStrategy.setBrightnessConfiguration(brightnessConfiguration,
-                            msg.arg1 == 1);
-                    if (mBrightnessTracker != null) {
-                        mBrightnessTracker
-                                .setShouldCollectColorSample(brightnessConfiguration != null
-                                        && brightnessConfiguration.shouldCollectColorSamples());
-                    }
-                    updatePowerState();
-                    break;
-
-                case MSG_SET_TEMPORARY_BRIGHTNESS:
-                    // TODO: Should we have a a timeout for the temporary brightness?
-                    mDisplayBrightnessController
-                            .setTemporaryBrightness(Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
-                    break;
-
-                case MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT:
-                    mAutomaticBrightnessStrategy
-                            .setTemporaryAutoBrightnessAdjustment(Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
-                    break;
-
-                case MSG_STOP:
-                    cleanupHandlerThreadAfterStop();
-                    break;
-
-                case MSG_UPDATE_BRIGHTNESS:
-                    if (mStopped) {
-                        return;
-                    }
-                    handleSettingsChange(false /*userSwitch*/);
-                    break;
-
-                case MSG_UPDATE_RBC:
-                    handleRbcChanged();
-                    break;
-
-                case MSG_BRIGHTNESS_RAMP_DONE:
-                    if (mPowerState != null) {
-                        final float brightness = mPowerState.getScreenBrightness();
-                        reportStats(brightness);
-                    }
-                    break;
-
-                case MSG_STATSD_HBM_BRIGHTNESS:
-                    logHbmBrightnessStats(Float.intBitsToFloat(msg.arg1), msg.arg2);
-                    break;
-
-                case MSG_SWITCH_USER:
-                    handleOnSwitchUser(msg.arg1);
-                    break;
-
-                case MSG_BOOT_COMPLETED:
-                    mBootCompleted = true;
-                    updatePowerState();
-                    break;
-
-                case MSG_SET_DWBC_STRONG_MODE:
-                    setDwbcStrongMode(msg.arg1);
-                    break;
-
-                case MSG_SET_DWBC_COLOR_OVERRIDE:
-                    final float cct = Float.intBitsToFloat(msg.arg1);
-                    setDwbcOverride(cct);
-                    break;
-
-                case MSG_SET_DWBC_LOGGING_ENABLED:
-                    setDwbcLoggingEnabled(msg.arg1);
-                    break;
-                case MSG_SET_BRIGHTNESS_FROM_OFFLOAD:
-                    mDisplayBrightnessController.setBrightnessFromOffload(
-                            Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
-                    break;
-            }
-        }
-    }
-
-
-    private final class SettingsObserver extends ContentObserver {
-        SettingsObserver(Handler handler) {
-            super(handler);
-        }
-
-        @Override
-        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 */);
-            }
-        }
-    }
-
-    private final class ScreenOnUnblocker implements WindowManagerPolicy.ScreenOnListener {
-        @Override
-        public void onScreenOn() {
-            Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    }
-
-    private final class ScreenOffUnblocker implements WindowManagerPolicy.ScreenOffListener {
-        @Override
-        public void onScreenOff() {
-            Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    }
-
-    @Override
-    public void setAutoBrightnessLoggingEnabled(boolean enabled) {
-        if (mAutomaticBrightnessController != null) {
-            mAutomaticBrightnessController.setLoggingEnabled(enabled);
-        }
-    }
-
-    @Override // DisplayWhiteBalanceController.Callbacks
-    public void updateWhiteBalance() {
-        sendUpdatePowerState();
-    }
-
-    @Override
-    public void setDisplayWhiteBalanceLoggingEnabled(boolean enabled) {
-        Message msg = mHandler.obtainMessage();
-        msg.what = MSG_SET_DWBC_LOGGING_ENABLED;
-        msg.arg1 = enabled ? 1 : 0;
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setAmbientColorTemperatureOverride(float cct) {
-        Message msg = mHandler.obtainMessage();
-        msg.what = MSG_SET_DWBC_COLOR_OVERRIDE;
-        msg.arg1 = Float.floatToIntBits(cct);
-        msg.sendToTarget();
-    }
-
-    /** Functional interface for providing time. */
-    @VisibleForTesting
-    interface Clock {
-        /**
-         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
-         */
-        long uptimeMillis();
-    }
-
-    @VisibleForTesting
-    static class Injector {
-        Clock getClock() {
-            return SystemClock::uptimeMillis;
-        }
-
-        DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
-                int displayId, int displayState) {
-            return new DisplayPowerState(blanker, colorFade, displayId, displayState);
-        }
-
-        DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
-                FloatProperty<DisplayPowerState> firstProperty,
-                FloatProperty<DisplayPowerState> secondProperty) {
-            return new DualRampAnimator(dps, firstProperty, secondProperty);
-        }
-
-        WakelockController getWakelockController(int displayId,
-                DisplayPowerCallbacks displayPowerCallbacks) {
-            return new WakelockController(displayId, displayPowerCallbacks);
-        }
-
-        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
-                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
-                Looper looper, Runnable nudgeUpdatePowerState,
-                int displayId, SensorManager sensorManager) {
-            return new DisplayPowerProximityStateController(wakelockController, displayDeviceConfig,
-                    looper, nudgeUpdatePowerState,
-                    displayId, sensorManager, /* injector= */ null);
-        }
-
-        AutomaticBrightnessController getAutomaticBrightnessController(
-                AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager, Sensor lightSensor,
-                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                boolean resetAmbientLuxAfterWarmUpConfig,
-                HysteresisLevels ambientBrightnessThresholds,
-                HysteresisLevels screenBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
-                HysteresisLevels screenBrightnessThresholdsIdle, Context context,
-                BrightnessRangeController brightnessModeController,
-                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
-            return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
-                    brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
-                    brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
-                    brighteningLightDebounceConfig, darkeningLightDebounceConfig,
-                    brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
-                    resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
-                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
-                    screenBrightnessThresholdsIdle, context, brightnessModeController,
-                    brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                    userNits);
-        }
-
-        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
-                DisplayDeviceConfig displayDeviceConfig,
-                DisplayWhiteBalanceController displayWhiteBalanceController) {
-            return BrightnessMappingStrategy.create(context, displayDeviceConfig,
-                    AUTO_BRIGHTNESS_MODE_DEFAULT, displayWhiteBalanceController);
-        }
-
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold) {
-            return new HysteresisLevels(brighteningThresholdsPercentages,
-                    darkeningThresholdsPercentages, brighteningThresholdLevels,
-                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
-        }
-
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
-            return new HysteresisLevels(brighteningThresholdsPercentages,
-                    darkeningThresholdsPercentages, brighteningThresholdLevels,
-                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
-                    potentialOldBrightnessRange);
-        }
-
-        ScreenOffBrightnessSensorController getScreenOffBrightnessSensorController(
-                SensorManager sensorManager,
-                Sensor lightSensor,
-                Handler handler,
-                ScreenOffBrightnessSensorController.Clock clock,
-                int[] sensorValueToLux,
-                BrightnessMappingStrategy brightnessMapper) {
-            return new ScreenOffBrightnessSensorController(
-                    sensorManager,
-                    lightSensor,
-                    handler,
-                    clock,
-                    sensorValueToLux,
-                    brightnessMapper
-            );
-        }
-
-        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
-                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
-                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
-                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
-                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
-                Context context) {
-            return new HighBrightnessModeController(handler, width, height, displayToken,
-                    displayUniqueId, brightnessMin, brightnessMax, hbmData, hdrBrightnessCfg,
-                    hbmChangeCallback, hbmMetadata, context);
-        }
-
-        BrightnessRangeController getBrightnessRangeController(
-                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
-                DisplayDeviceConfig displayDeviceConfig, Handler handler,
-                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
-            return new BrightnessRangeController(hbmController,
-                    modeChangeCallback, displayDeviceConfig, handler, flags, displayToken, info);
-        }
-
-        BrightnessClamperController getBrightnessClamperController(Handler handler,
-                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
-                BrightnessClamperController.DisplayDeviceData data, Context context,
-                DisplayManagerFlags flags) {
-
-            return new BrightnessClamperController(handler, clamperChangeListener, data, context,
-                    flags);
-        }
-
-        DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
-                SensorManager sensorManager, Resources resources) {
-            return DisplayWhiteBalanceFactory.create(handler,
-                    sensorManager, resources);
-        }
-
-        boolean isColorFadeEnabled() {
-            return !ActivityManager.isLowRamDeviceStatic();
-        }
-    }
-
-    static class CachedBrightnessInfo {
-        public MutableFloat brightness = new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableFloat adjustedBrightness =
-                new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableFloat brightnessMin =
-                new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableFloat brightnessMax =
-                new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableInt hbmMode = new MutableInt(BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
-        public MutableFloat hbmTransitionPoint =
-                new MutableFloat(HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID);
-        public MutableInt brightnessMaxReason =
-                new MutableInt(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
-
-        public boolean checkAndSetFloat(MutableFloat mf, float f) {
-            if (mf.value != f) {
-                mf.value = f;
-                return true;
-            }
-            return false;
-        }
-
-        public boolean checkAndSetInt(MutableInt mi, int i) {
-            if (mi.value != i) {
-                mi.value = i;
-                return true;
-            }
-            return false;
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index bcf27b4..90bad12 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -333,6 +333,8 @@
     public void stop() {
         mStopped = true;
         mPhotonicModulator.interrupt();
+        mColorFadePrepared = false;
+        mColorFadeReady = true;
         if (mColorFade != null) {
             mAsyncDestroyExecutor.execute(mColorFade::destroy);
         }
@@ -419,7 +421,8 @@
         }
     };
 
-    private final Runnable mColorFadeDrawRunnable = new Runnable() {
+    @VisibleForTesting
+    final Runnable mColorFadeDrawRunnable = new Runnable() {
         @Override
         public void run() {
             mColorFadeDrawPending = false;
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 25576ce..3a63330 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -19,6 +19,8 @@
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.Display.Mode.INVALID_MODE_ID;
 
+import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS;
+
 import android.annotation.Nullable;
 import android.app.ActivityThread;
 import android.content.Context;
@@ -956,8 +958,7 @@
 
                     void handleHdrSdrNitsChanged(float displayNits, float sdrNits) {
                         final float newHdrSdrRatio;
-                        if (displayNits != DisplayDeviceConfig.NITS_INVALID
-                                && sdrNits != DisplayDeviceConfig.NITS_INVALID) {
+                        if (displayNits != INVALID_NITS && sdrNits != INVALID_NITS) {
                             // Ensure the ratio stays >= 1.0f as values below that are nonsensical
                             newHdrSdrRatio = Math.max(1.f, displayNits / sdrNits);
                         } else {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 115111a..2e8de31 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -205,7 +205,7 @@
             @NonNull Handler handler, DisplayManagerFlags flags) {
         this(context, foldSettingProvider, repo, listener, syncRoot, handler,
                 new DeviceStateToLayoutMap((isDefault) -> isDefault ? DEFAULT_DISPLAY
-                        : sNextNonDefaultDisplayId++), flags);
+                        : sNextNonDefaultDisplayId++, flags), flags);
     }
 
     LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider,
@@ -1094,8 +1094,8 @@
             final DisplayAddress address = displayLayout.getAddress();
             final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(address);
             if (device == null) {
-                Slog.w(TAG, "The display device (" + address + "), is not available"
-                        + " for the display state " + mDeviceState);
+                Slog.w(TAG, "applyLayoutLocked: The display device (" + address + "), is not "
+                        + "available for the display state " + mDeviceState);
                 continue;
             }
 
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessReason.java b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
index 8fe5f21..bc443a8 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessReason.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
@@ -46,8 +46,10 @@
     public static final int MODIFIER_LOW_POWER = 0x2;
     public static final int MODIFIER_HDR = 0x4;
     public static final int MODIFIER_THROTTLED = 0x8;
+    public static final int MODIFIER_MIN_LUX = 0x10;
+    public static final int MODIFIER_MIN_USER_SET_LOWER_BOUND = 0x20;
     public static final int MODIFIER_MASK = MODIFIER_DIMMED | MODIFIER_LOW_POWER | MODIFIER_HDR
-            | MODIFIER_THROTTLED;
+            | MODIFIER_THROTTLED | MODIFIER_MIN_LUX | MODIFIER_MIN_USER_SET_LOWER_BOUND;
 
     // ADJUSTMENT_*
     // These things can happen at any point, even if the main brightness reason doesn't
@@ -131,6 +133,12 @@
         if ((mModifier & MODIFIER_THROTTLED) != 0) {
             sb.append(" throttled");
         }
+        if ((mModifier & MODIFIER_MIN_LUX) != 0) {
+            sb.append(" lux_lower_bound");
+        }
+        if ((mModifier & MODIFIER_MIN_USER_SET_LOWER_BOUND) != 0) {
+            sb.append(" user_min_pref");
+        }
         int strlen = sb.length();
         if (sb.charAt(strlen - 1) == '[') {
             sb.setLength(strlen - 2);
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
index 42ebc40..fab769e 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
@@ -30,6 +30,7 @@
 abstract class BrightnessClamper<T> {
 
     protected float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
+
     protected boolean mIsActive = false;
 
     @NonNull
@@ -75,6 +76,5 @@
         THERMAL,
         POWER,
         BEDTIME_MODE,
-        LUX,
     }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 01694dd..2c02fc6 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -58,13 +58,14 @@
     private final Executor mExecutor;
     private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers;
 
-    private final List<BrightnessModifier> mModifiers;
+    private final List<BrightnessStateModifier> mModifiers;
     private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener;
     private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
 
     private float mCustomAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
     @Nullable
     private Type mClamperType = null;
+
     private boolean mClamperApplied = false;
 
     public BrightnessClamperController(Handler handler,
@@ -92,7 +93,7 @@
 
         mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags,
                 context);
-        mModifiers = injector.getModifiers(context);
+        mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener);
         mOnPropertiesChangedListener =
                 properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged);
         start();
@@ -165,9 +166,10 @@
      * Used to dump ClampersController state.
      */
     public void dump(PrintWriter writer) {
-        writer.println("BrightnessClampersController:");
+        writer.println("BrightnessClamperController:");
         writer.println("  mBrightnessCap: " + mBrightnessCap);
         writer.println("  mClamperType: " + mClamperType);
+        writer.println("  mClamperApplied: " + mClamperApplied);
         IndentingPrintWriter ipw = new IndentingPrintWriter(writer, "    ");
         mClampers.forEach(clamper -> clamper.dump(ipw));
         mModifiers.forEach(modifier -> modifier.dump(ipw));
@@ -181,6 +183,7 @@
         mDeviceConfigParameterProvider.removeOnPropertiesChangedListener(
                 mOnPropertiesChangedListener);
         mClampers.forEach(BrightnessClamper::stop);
+        mModifiers.forEach(BrightnessStateModifier::stop);
     }
 
 
@@ -201,14 +204,14 @@
             customAnimationRate = minClamper.getCustomAnimationRate();
         }
 
-        if (mBrightnessCap != brightnessCap || mClamperType != clamperType
+        if (mBrightnessCap != brightnessCap
+                || mClamperType != clamperType
                 || mCustomAnimationRate != customAnimationRate) {
             mBrightnessCap = brightnessCap;
             mClamperType = clamperType;
             mCustomAnimationRate = customAnimationRate;
             mClamperChangeListenerExternal.onChanged();
         }
-
     }
 
     private void start() {
@@ -248,16 +251,17 @@
                 clampers.add(new BrightnessWearBedtimeModeClamper(handler, context,
                         clamperChangeListener, data));
             }
-            if (flags.isEvenDimmerEnabled()) {
-                clampers.add(new BrightnessMinClamper(handler, clamperChangeListener, context));
-            }
             return clampers;
         }
 
-        List<BrightnessModifier> getModifiers(Context context) {
-            List<BrightnessModifier> modifiers = new ArrayList<>();
+        List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
+                Handler handler, ClamperChangeListener listener) {
+            List<BrightnessStateModifier> modifiers = new ArrayList<>();
             modifiers.add(new DisplayDimModifier(context));
             modifiers.add(new BrightnessLowPowerModeModifier());
+            if (flags.isEvenDimmerEnabled()) {
+                modifiers.add(new BrightnessLowLuxModifier(handler, listener, context));
+            }
             return modifiers;
         }
     }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
new file mode 100644
index 0000000..7f1f7a9
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.brightness.clamper;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.hardware.display.DisplayManagerInternal;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.utils.DebugUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Class used to prevent the screen brightness dipping below a certain value, based on current
+ * lux conditions and user preferred minimum.
+ */
+public class BrightnessLowLuxModifier implements
+        BrightnessStateModifier {
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot'
+    private static final String TAG = "BrightnessLowLuxModifier";
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+    private final SettingsObserver mSettingsObserver;
+    private final ContentResolver mContentResolver;
+    private final Handler mHandler;
+    private final BrightnessClamperController.ClamperChangeListener mChangeListener;
+    protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
+    private int mReason;
+    private float mBrightnessLowerBound;
+    private boolean mIsActive;
+
+    @VisibleForTesting
+    BrightnessLowLuxModifier(Handler handler,
+            BrightnessClamperController.ClamperChangeListener listener, Context context) {
+        super();
+
+        mChangeListener = listener;
+        mHandler = handler;
+        mContentResolver = context.getContentResolver();
+        mSettingsObserver = new SettingsObserver(mHandler);
+        mHandler.post(() -> {
+            start();
+        });
+    }
+
+    /**
+     * Calculates new lower bound for brightness range, based on whether the setting is active,
+     * the user defined min brightness setting, and current lux environment.
+     */
+    @VisibleForTesting
+    public void recalculateLowerBound() {
+        int userId = UserHandle.USER_CURRENT;
+        float settingNitsLowerBound = Settings.Secure.getFloatForUser(
+                mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
+                /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
+
+        boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED,
+                /* def= */ 0, userId) == 1;
+
+        // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
+        float luxBasedNitsLowerBound = 0.0f;
+
+        // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
+                // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN;
+
+        final int reason = settingNitsLowerBound > luxBasedNitsLowerBound
+                ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND
+                : BrightnessReason.MODIFIER_MIN_LUX;
+
+        // TODO: brightnessLowerBound = nitsToBrightnessSpline(nitsLowerBound);
+        final float brightnessLowerBound = PowerManager.BRIGHTNESS_MIN;
+
+        if (mBrightnessLowerBound != brightnessLowerBound
+                || mReason != reason
+                || mIsActive != isActive) {
+            mIsActive = isActive;
+            mReason = reason;
+            if (DEBUG) {
+                Slog.i(TAG, "isActive: " + isActive
+                        + ", settingNitsLowerBound: " + settingNitsLowerBound
+                        + ", lowerBound: " + brightnessLowerBound);
+            }
+            mBrightnessLowerBound = brightnessLowerBound;
+            mChangeListener.onChanged();
+        }
+    }
+
+    @VisibleForTesting
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    @VisibleForTesting
+    public int getBrightnessReason() {
+        return mReason;
+    }
+
+    @VisibleForTesting
+    public float getBrightnessLowerBound() {
+        return mBrightnessLowerBound;
+    }
+
+    void start() {
+        recalculateLowerBound();
+    }
+
+    @Override
+    public void apply(DisplayManagerInternal.DisplayPowerRequest request,
+            DisplayBrightnessState.Builder stateBuilder) {
+        stateBuilder.setMinBrightness(mBrightnessLowerBound);
+        float boundedBrightness = Math.max(mBrightnessLowerBound, stateBuilder.getBrightness());
+        stateBuilder.setBrightness(boundedBrightness);
+
+        if (BrightnessSynchronizer.floatEquals(stateBuilder.getBrightness(),
+                mBrightnessLowerBound)) {
+            stateBuilder.getBrightnessReason().addModifier(mReason);
+        }
+    }
+
+    @Override
+    public void stop() {
+        mContentResolver.unregisterContentObserver(mSettingsObserver);
+    }
+
+    @Override
+    public void dump(PrintWriter pw) {
+        pw.println("BrightnessLowLuxModifier:");
+        pw.println("  mBrightnessLowerBound=" + mBrightnessLowerBound);
+        pw.println("  mIsActive=" + mIsActive);
+        pw.println("  mReason=" + mReason);
+    }
+
+    private final class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS),
+                    false, this);
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED),
+                    false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            recalculateLowerBound();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java
deleted file mode 100644
index 71efca1..0000000
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness.clamper;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.display.utils.DebugUtils;
-
-import java.io.PrintWriter;
-
-/**
- * Class used to prevent the screen brightness dipping below a certain value, based on current
- * lux conditions.
- */
-public class BrightnessMinClamper extends BrightnessClamper {
-
-    // To enable these logs, run:
-    // 'adb shell setprop persist.log.tag.BrightnessMinClamper DEBUG && adb reboot'
-    private static final String TAG = "BrightnessMinClamper";
-    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
-
-    private final SettingsObserver mSettingsObserver;
-
-    ContentResolver mContentResolver;
-    private float mNitsLowerBound;
-
-    @VisibleForTesting
-    BrightnessMinClamper(Handler handler,
-            BrightnessClamperController.ClamperChangeListener listener, Context context) {
-        super(handler, listener);
-
-        mContentResolver = context.getContentResolver();
-        mSettingsObserver = new SettingsObserver(mHandler);
-        mHandler.post(() -> {
-            start();
-        });
-    }
-
-    private void recalculateLowerBound() {
-        final int userId = UserHandle.USER_CURRENT;
-        float settingNitsLowerBound = Settings.Secure.getFloatForUser(
-                mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
-                /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
-
-        boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
-                Settings.Secure.EVEN_DIMMER_ACTIVATED,
-                /* def= */ 0, userId) == 1;
-
-        // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
-        float luxBasedNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
-        final float nitsLowerBound = Math.max(settingNitsLowerBound, luxBasedNitsLowerBound);
-
-        if (mNitsLowerBound != nitsLowerBound || mIsActive != isActive) {
-            mIsActive = isActive;
-            mNitsLowerBound = nitsLowerBound;
-            if (DEBUG) {
-                Slog.i(TAG, "mIsActive: " + mIsActive);
-            }
-            // TODO: mBrightnessCap = nitsToBrightnessSpline(mNitsLowerBound);
-            mChangeListener.onChanged();
-        }
-    }
-
-    void start() {
-        recalculateLowerBound();
-    }
-
-
-    @Override
-    Type getType() {
-        return Type.LUX;
-    }
-
-    @Override
-    void onDeviceConfigChanged() {
-        // TODO
-    }
-
-    @Override
-    void onDisplayChanged(Object displayData) {
-
-    }
-
-    @Override
-    void stop() {
-        mContentResolver.unregisterContentObserver(mSettingsObserver);
-    }
-
-    @Override
-    void dump(PrintWriter pw) {
-        pw.println("BrightnessMinClamper:");
-        pw.println("  mBrightnessCap=" + mBrightnessCap);
-        pw.println("  mIsActive=" + mIsActive);
-        pw.println("  mNitsLowerBound=" + mNitsLowerBound);
-        super.dump(pw);
-    }
-
-    private final class SettingsObserver extends ContentObserver {
-        SettingsObserver(Handler handler) {
-            super(handler);
-            mContentResolver.registerContentObserver(
-                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS),
-                    false, this);
-            mContentResolver.registerContentObserver(
-                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED),
-                    false, this);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            recalculateLowerBound();
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
index 112e63d..be8fa5a 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
@@ -26,7 +26,7 @@
 /**
  * Modifies current brightness based on request
  */
-abstract class BrightnessModifier {
+abstract class BrightnessModifier implements BrightnessStateModifier {
 
     private boolean mApplied = false;
 
@@ -37,7 +37,8 @@
 
     abstract int getModifier();
 
-    void apply(DisplayManagerInternal.DisplayPowerRequest request,
+    @Override
+    public void apply(DisplayManagerInternal.DisplayPowerRequest request,
             DisplayBrightnessState.Builder stateBuilder) {
         // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor
         // as long as it is above the minimum threshold.
@@ -57,8 +58,14 @@
         }
     }
 
-    void dump(PrintWriter pw) {
+    @Override
+    public void dump(PrintWriter pw) {
         pw.println("BrightnessModifier:");
         pw.println("  mApplied=" + mApplied);
     }
+
+    @Override
+    public void stop() {
+        // do nothing
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
new file mode 100644
index 0000000..441ba8f
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.brightness.clamper;
+
+import android.hardware.display.DisplayManagerInternal;
+
+import com.android.server.display.DisplayBrightnessState;
+
+import java.io.PrintWriter;
+
+public interface BrightnessStateModifier {
+    /**
+     * Applies the changes to brightness state, by modifying properties of the brightness
+     * state builder.
+     * @param request
+     * @param stateBuilder
+     */
+    void apply(DisplayManagerInternal.DisplayPowerRequest request,
+            DisplayBrightnessState.Builder stateBuilder);
+
+    /**
+     * Prints contents of this brightness state modifier
+     * @param printWriter
+     */
+    void dump(PrintWriter printWriter);
+
+    /**
+     * Called when stopped. Listeners can be unregistered here.
+     */
+    void stop();
+}
diff --git a/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
index 465584c..403dfbe 100644
--- a/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
+++ b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
@@ -41,13 +41,6 @@
         mDeviceConfig = deviceConfig;
     }
 
-    // feature: revamping_display_power_controller_feature
-    // parameter: use_newly_structured_display_power_controller
-    public boolean isNewPowerControllerFeatureEnabled() {
-        return mDeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                DisplayManager.DeviceConfig.KEY_NEW_POWER_CONTROLLER, true);
-    }
-
     // feature: hdr_output_control
     // parameter: enable_hdr_output_control
     public boolean isHdrOutputControlFeatureEnabled() {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 35991b3..be48eb4 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -37,6 +37,9 @@
     // 'adb shell setprop persist.log.tag.DisplayManagerFlags DEBUG && adb reboot'
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
+    private final FlagState mPortInDisplayLayoutFlagState = new FlagState(
+            Flags.FLAG_ENABLE_PORT_IN_DISPLAY_LAYOUT,
+            Flags::enablePortInDisplayLayout);
 
     private final FlagState mConnectedDisplayManagementFlagState = new FlagState(
             Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT,
@@ -114,6 +117,13 @@
             Flags::refreshRateVotingTelemetry
     );
 
+    /**
+     * @return {@code true} if 'port' is allowed in display layout configuration file.
+     */
+    public boolean isPortInDisplayLayoutEnabled() {
+        return mPortInDisplayLayoutFlagState.isEnabled();
+    }
+
     /** Returns whether connected display management is enabled or not. */
     public boolean isConnectedDisplayManagementEnabled() {
         return mConnectedDisplayManagementFlagState.isEnabled();
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index e735282..c9569cb 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -3,6 +3,14 @@
 # Important: Flags must be accessed through DisplayManagerFlags.
 
 flag {
+    name: "enable_port_in_display_layout"
+    namespace: "display_manager"
+    description: "Allows refering to displays by port in display layout"
+    bug: "303058435"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "enable_connected_display_management"
     namespace: "display_manager"
     description: "Feature flag for Connected Display management"
diff --git a/services/core/java/com/android/server/display/layout/Layout.java b/services/core/java/com/android/server/display/layout/Layout.java
index 40cb3303..8a362f7 100644
--- a/services/core/java/com/android/server/display/layout/Layout.java
+++ b/services/core/java/com/android/server/display/layout/Layout.java
@@ -200,13 +200,7 @@
      * @return True if the specified address is used in this layout.
      */
     public boolean contains(@NonNull DisplayAddress address) {
-        final int size = mDisplays.size();
-        for (int i = 0; i < size; i++) {
-            if (address.equals(mDisplays.get(i).getAddress())) {
-                return true;
-            }
-        }
-        return false;
+        return getByAddress(address) != null;
     }
 
     /**
@@ -237,6 +231,9 @@
             if (address.equals(display.getAddress())) {
                 return display;
             }
+            if (DisplayAddress.Physical.isPortMatch(address, display.getAddress())) {
+                return display;
+            }
         }
         return null;
     }
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 7e990c6..380106b 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -104,10 +104,13 @@
     public abstract PointF getCursorPosition();
 
     /**
-     * Sets the pointer acceleration.
-     * See {@code frameworks/native/include/input/VelocityControl.h#VelocityControlParameters}.
+     * Enables or disables pointer acceleration for mouse movements.
+     *
+     * Note that this only affects pointer movements from mice (that is, pointing devices which send
+     * relative motions, including trackballs and pointing sticks), not from other pointer devices
+     * such as touchpads and styluses.
      */
-    public abstract void setPointerAcceleration(float acceleration, int displayId);
+    public abstract void setMousePointerAccelerationEnabled(boolean enabled, int displayId);
 
     /**
      * Sets the eligibility of windows on a given display for pointer capture. If a display is
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 36dac83..bc7205a 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -64,7 +64,6 @@
 import android.os.Environment;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.IInputConstants;
 import android.os.IVibratorStateListener;
 import android.os.InputEventInjectionResult;
 import android.os.InputEventInjectionSync;
@@ -1372,9 +1371,9 @@
         mNative.setPointerSpeed(speed);
     }
 
-    private void setPointerAcceleration(float acceleration, int displayId) {
+    private void setMousePointerAccelerationEnabled(boolean enabled, int displayId) {
         updateAdditionalDisplayInputProperties(displayId,
-                properties -> properties.pointerAcceleration = acceleration);
+                properties -> properties.mousePointerAccelerationEnabled = enabled);
     }
 
     private void setPointerIconVisible(boolean visible, int displayId) {
@@ -2261,7 +2260,8 @@
                             + mAdditionalDisplayInputProperties.keyAt(i));
                     final AdditionalDisplayInputProperties properties =
                             mAdditionalDisplayInputProperties.valueAt(i);
-                    pw.println("pointerAcceleration: " + properties.pointerAcceleration);
+                    pw.println("mousePointerAccelerationEnabled: "
+                            + properties.mousePointerAccelerationEnabled);
                     pw.println("pointerIconVisible: " + properties.pointerIconVisible);
                 }
                 pw.decreaseIndent();
@@ -3289,8 +3289,8 @@
         }
 
         @Override
-        public void setPointerAcceleration(float acceleration, int displayId) {
-            InputManagerService.this.setPointerAcceleration(acceleration, displayId);
+        public void setMousePointerAccelerationEnabled(boolean enabled, int displayId) {
+            InputManagerService.this.setMousePointerAccelerationEnabled(enabled, displayId);
         }
 
         @Override
@@ -3382,11 +3382,15 @@
     private static class AdditionalDisplayInputProperties {
 
         static final boolean DEFAULT_POINTER_ICON_VISIBLE = true;
-        static final float DEFAULT_POINTER_ACCELERATION =
-                (float) IInputConstants.DEFAULT_POINTER_ACCELERATION;
+        static final boolean DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED = true;
 
-        // The pointer acceleration for this display.
-        public float pointerAcceleration;
+        /**
+         * Whether to enable mouse pointer acceleration on this display. Note that this only affects
+         * pointer movements from mice (that is, pointing devices which send relative motions,
+         * including trackballs and pointing sticks), not from other pointer devices such as
+         * touchpads and styluses.
+         */
+        public boolean mousePointerAccelerationEnabled;
 
         // Whether the pointer icon should be visible or hidden on this display.
         public boolean pointerIconVisible;
@@ -3396,12 +3400,12 @@
         }
 
         public boolean allDefaults() {
-            return Float.compare(pointerAcceleration, DEFAULT_POINTER_ACCELERATION) == 0
+            return mousePointerAccelerationEnabled == DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED
                     && pointerIconVisible == DEFAULT_POINTER_ICON_VISIBLE;
         }
 
         public void reset() {
-            pointerAcceleration = DEFAULT_POINTER_ACCELERATION;
+            mousePointerAccelerationEnabled = DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED;
             pointerIconVisible = DEFAULT_POINTER_ICON_VISIBLE;
         }
     }
@@ -3433,9 +3437,11 @@
             }
         }
 
-        if (properties.pointerAcceleration != mCurrentDisplayProperties.pointerAcceleration) {
-            mCurrentDisplayProperties.pointerAcceleration = properties.pointerAcceleration;
-            mNative.setPointerAcceleration(properties.pointerAcceleration);
+        if (properties.mousePointerAccelerationEnabled
+                != mCurrentDisplayProperties.mousePointerAccelerationEnabled) {
+            mCurrentDisplayProperties.mousePointerAccelerationEnabled =
+                    properties.mousePointerAccelerationEnabled;
+            mNative.setMousePointerAccelerationEnabled(properties.mousePointerAccelerationEnabled);
         }
     }
 
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 829b660..dd9204c 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -118,7 +118,7 @@
 
     void setPointerSpeed(int speed);
 
-    void setPointerAcceleration(float acceleration);
+    void setMousePointerAccelerationEnabled(boolean enabled);
 
     void setTouchpadPointerSpeed(int speed);
 
@@ -351,7 +351,7 @@
         public native void setPointerSpeed(int speed);
 
         @Override
-        public native void setPointerAcceleration(float acceleration);
+        public native void setMousePointerAccelerationEnabled(boolean enabled);
 
         @Override
         public native void setTouchpadPointerSpeed(int speed);
diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java
new file mode 100644
index 0000000..2934640
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/ClientController.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.content.pm.PackageManagerInternal;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+import android.view.inputmethod.InputBinding;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+
+/**
+ * Store and manage {@link InputMethodManagerService} clients. This class was designed to be a
+ * singleton in {@link InputMethodManagerService} since it stores information about all clients,
+ * still the current client will be defined per display.
+ *
+ * <p>
+ * As part of the re-architecture plan (described in go/imms-rearchitecture-plan), the following
+ * fields and methods will be moved out from IMMS and placed here:
+ * <ul>
+ * <li>mCurClient (ClientState)</li>
+ * <li>mClients (ArrayMap of ClientState indexed by IBinder)</li>
+ * <li>mLastSwitchUserId</li>
+ * </ul>
+ * <p>
+ * Nested Classes (to move from IMMS):
+ * <ul>
+ * <li>ClientDeathRecipient</li>
+ * <li>ClientState<</li>
+ * </ul>
+ * <p>
+ * Methods to rewrite and/or extract from IMMS and move here:
+ * <ul>
+ * <li>addClient</li>
+ * <li>removeClient</li>
+ * <li>verifyClientAndPackageMatch</li>
+ * <li>setImeTraceEnabledForAllClients (make it reactive)</li>
+ * <li>unbindCurrentClient</li>
+ * </ul>
+ */
+// TODO(b/314150112): Update the Javadoc above, by removing the re-architecture steps, once this
+//  class is finalized
+final class ClientController {
+
+    // TODO(b/314150112): Make this field private when breaking the cycle with IMMS.
+    @GuardedBy("ImfLock.class")
+    final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
+
+    private final PackageManagerInternal mPackageManagerInternal;
+
+    ClientController(PackageManagerInternal packageManagerInternal) {
+        mPackageManagerInternal = packageManagerInternal;
+    }
+
+    @GuardedBy("ImfLock.class")
+    void addClient(IInputMethodClientInvoker clientInvoker,
+            IRemoteInputConnection inputConnection,
+            int selfReportedDisplayId, IBinder.DeathRecipient deathRecipient, int callerUid,
+            int callerPid) {
+        // TODO: Optimize this linear search.
+        final int numClients = mClients.size();
+        for (int i = 0; i < numClients; ++i) {
+            final ClientState state = mClients.valueAt(i);
+            if (state.mUid == callerUid && state.mPid == callerPid
+                    && state.mSelfReportedDisplayId == selfReportedDisplayId) {
+                throw new SecurityException("uid=" + callerUid + "/pid=" + callerPid
+                        + "/displayId=" + selfReportedDisplayId + " is already registered");
+            }
+        }
+        try {
+            clientInvoker.asBinder().linkToDeath(deathRecipient, 0 /* flags */);
+        } catch (RemoteException e) {
+            throw new IllegalStateException(e);
+        }
+        // We cannot fully avoid race conditions where the client UID already lost the access to
+        // the given self-reported display ID, even if the client is not maliciously reporting
+        // a fake display ID. Unconditionally returning SecurityException just because the
+        // client doesn't pass display ID verification can cause many test failures hence not an
+        // option right now.  At the same time
+        //    context.getSystemService(InputMethodManager.class)
+        // is expected to return a valid non-null instance at any time if we do not choose to
+        // have the client crash.  Thus we do not verify the display ID at all here.  Instead we
+        // later check the display ID every time the client needs to interact with the specified
+        // display.
+        mClients.put(clientInvoker.asBinder(), new ClientState(clientInvoker, inputConnection,
+                callerUid, callerPid, selfReportedDisplayId, deathRecipient));
+    }
+
+    @GuardedBy("ImfLock.class")
+    boolean verifyClientAndPackageMatch(
+            @NonNull IInputMethodClient client, @NonNull String packageName) {
+        ClientState cs = mClients.get(client.asBinder());
+        if (cs == null) {
+            throw new IllegalArgumentException("unknown client " + client.asBinder());
+        }
+        return InputMethodUtils.checkIfPackageBelongsToUid(
+                mPackageManagerInternal, cs.mUid, packageName);
+    }
+
+    static final class ClientState {
+        final IInputMethodClientInvoker mClient;
+        final IRemoteInputConnection mFallbackInputConnection;
+        final int mUid;
+        final int mPid;
+        final int mSelfReportedDisplayId;
+        final InputBinding mBinding;
+        final IBinder.DeathRecipient mClientDeathRecipient;
+
+        @GuardedBy("ImfLock.class")
+        boolean mSessionRequested;
+
+        @GuardedBy("ImfLock.class")
+        boolean mSessionRequestedForAccessibility;
+
+        @GuardedBy("ImfLock.class")
+        InputMethodManagerService.SessionState mCurSession;
+
+        @GuardedBy("ImfLock.class")
+        SparseArray<InputMethodManagerService.AccessibilitySessionState> mAccessibilitySessions =
+                new SparseArray<>();
+
+        @Override
+        public String toString() {
+            return "ClientState{" + Integer.toHexString(
+                    System.identityHashCode(this)) + " mUid=" + mUid
+                    + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}";
+        }
+
+        ClientState(IInputMethodClientInvoker client,
+                IRemoteInputConnection fallbackInputConnection,
+                int uid, int pid, int selfReportedDisplayId,
+                IBinder.DeathRecipient clientDeathRecipient) {
+            mClient = client;
+            mFallbackInputConnection = fallbackInputConnection;
+            mUid = uid;
+            mPid = pid;
+            mSelfReportedDisplayId = selfReportedDisplayId;
+            mBinding = new InputBinding(null /*conn*/, mFallbackInputConnection.asBinder(), mUid,
+                    mPid);
+            mClientDeathRecipient = clientDeathRecipient;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index f0e4b0f5..898d5a5 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -19,6 +19,8 @@
 import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.util.ArrayMap;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
@@ -33,9 +35,26 @@
     @GuardedBy("ImfLock.class")
     private final ArrayList<InputMethodSubtypeHandle> mSubtypeHandles = new ArrayList<>();
 
+    @UserIdInt
+    private final int mUserId;
+
+    @AnyThread
+    @UserIdInt
+    int getUserId() {
+        return mUserId;
+    }
+
+    HardwareKeyboardShortcutController(
+            @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+        mUserId = userId;
+        reset(methodMap);
+    }
+
     @GuardedBy("ImfLock.class")
-    void reset(@NonNull InputMethodUtils.InputMethodSettings settings) {
+    void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) {
         mSubtypeHandles.clear();
+        final InputMethodUtils.InputMethodSettings settings =
+                new InputMethodUtils.InputMethodSettings(methodMap, mUserId);
         for (final InputMethodInfo imi : settings.getEnabledInputMethodListLocked()) {
             if (!imi.shouldShowInInputMethodPicker()) {
                 continue;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d722242..25ec683 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -48,6 +48,7 @@
 import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 
+import static com.android.server.inputmethod.ClientController.ClientState;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
@@ -127,7 +128,6 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.Flags;
 import android.view.inputmethod.ImeTracker;
-import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceFileProto;
@@ -273,13 +273,15 @@
     @NonNull
     private final String[] mNonPreemptibleInputMethods;
 
+    // TODO(b/314150112): Move this to ClientController.
     @UserIdInt
     private int mLastSwitchUserId;
 
     final Context mContext;
     final Resources mRes;
     private final Handler mHandler;
-    private final InputMethodSettings mSettings;
+    @NonNull
+    private InputMethodSettings mSettings;
     final SettingsObserver mSettingsObserver;
     private final SparseBooleanArray mLoggedDeniedGetInputMethodWindowVisibleHeightForUid =
             new SparseBooleanArray(0);
@@ -324,8 +326,9 @@
     // TODO: Instantiate mSwitchingController for each user.
     @NonNull
     private InputMethodSubtypeSwitchingController mSwitchingController;
-    final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController =
-            new HardwareKeyboardShortcutController();
+    // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
+    @NonNull
+    private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
 
     /**
      * Tracks how many times {@link #mMethodMap} was updated.
@@ -391,7 +394,7 @@
     /**
      * Record session state for an accessibility service.
      */
-    private static class AccessibilitySessionState {
+    static class AccessibilitySessionState {
         final ClientState mClient;
         // Id of the accessibility service.
         final int mId;
@@ -415,58 +418,10 @@
         }
     }
 
-    private static final class ClientDeathRecipient implements IBinder.DeathRecipient {
-        private final InputMethodManagerService mImms;
-        private final IInputMethodClient mClient;
-
-        ClientDeathRecipient(InputMethodManagerService imms, IInputMethodClient client) {
-            mImms = imms;
-            mClient = client;
-        }
-
-        @Override
-        public void binderDied() {
-            mImms.removeClient(mClient);
-        }
-    }
-
-    static final class ClientState {
-        final IInputMethodClientInvoker mClient;
-        final IRemoteInputConnection mFallbackInputConnection;
-        final int mUid;
-        final int mPid;
-        final int mSelfReportedDisplayId;
-        final InputBinding mBinding;
-        final ClientDeathRecipient mClientDeathRecipient;
-
-        boolean mSessionRequested;
-        boolean mSessionRequestedForAccessibility;
-        SessionState mCurSession;
-        SparseArray<AccessibilitySessionState> mAccessibilitySessions = new SparseArray<>();
-
-        @Override
-        public String toString() {
-            return "ClientState{" + Integer.toHexString(
-                    System.identityHashCode(this)) + " mUid=" + mUid
-                    + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}";
-        }
-
-        ClientState(IInputMethodClientInvoker client,
-                IRemoteInputConnection fallbackInputConnection,
-                int uid, int pid, int selfReportedDisplayId,
-                ClientDeathRecipient clientDeathRecipient) {
-            mClient = client;
-            mFallbackInputConnection = fallbackInputConnection;
-            mUid = uid;
-            mPid = pid;
-            mSelfReportedDisplayId = selfReportedDisplayId;
-            mBinding = new InputBinding(null, mFallbackInputConnection.asBinder(), mUid, mPid);
-            mClientDeathRecipient = clientDeathRecipient;
-        }
-    }
-
-    @GuardedBy("ImfLock.class")
-    final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
+    /**
+     * Manages the IME clients.
+     */
+    private final ClientController mClientController;
 
     /**
      * Set once the system is ready to run third party code.
@@ -524,6 +479,7 @@
     /**
      * The client that is currently bound to an input method.
      */
+    // TODO(b/314150112): Move this to ClientController.
     @Nullable
     private ClientState mCurClient;
 
@@ -864,8 +820,9 @@
             @Nullable
             final String mImeSurfaceParentName;
 
-            Entry(ClientState client, EditorInfo editorInfo, String focusedWindowName,
-                    @SoftInputModeFlags int softInputMode, @SoftInputShowHideReason int reason,
+            Entry(ClientState client, EditorInfo editorInfo,
+                    String focusedWindowName, @SoftInputModeFlags int softInputMode,
+                    @SoftInputShowHideReason int reason,
                     boolean inFullscreenMode, String requestWindowName,
                     @Nullable String imeControlTargetName, @Nullable String imeTargetName,
                     @Nullable String imeSurfaceParentName) {
@@ -1629,7 +1586,7 @@
             if (userId != currentUserId) {
                 return;
             }
-            mSettings.switchCurrentUser(currentUserId);
+            mSettings = new InputMethodSettings(mMethodMap, userId);
             if (mSystemReady) {
                 // We need to rebuild IMEs.
                 buildInputMethodListLocked(false /* resetDefaultEnabledIme */);
@@ -1709,7 +1666,8 @@
         mSwitchingController =
                 InputMethodSubtypeSwitchingController.createInstanceLocked(context, mMethodMap,
                         userId);
-        mHardwareKeyboardShortcutController.reset(mSettings);
+        mHardwareKeyboardShortcutController =
+                new HardwareKeyboardShortcutController(mMethodMap, userId);
         mMenuController = new InputMethodMenuController(this);
         mBindingController =
                 bindingControllerForTesting != null
@@ -1719,6 +1677,7 @@
 
         mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
         mVisibilityApplier = new DefaultImeVisibilityApplier(this);
+        mClientController = new ClientController(mPackageManagerInternal);
 
         mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
                 com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
@@ -1830,11 +1789,7 @@
         // ContentObserver should be registered again when the user is changed
         mSettingsObserver.registerContentObserverLocked(newUserId);
 
-        // If the system is not ready or the device is not yed unlocked by the user, then we use
-        // copy-on-write settings.
-        final boolean useCopyOnWriteSettings =
-                !mSystemReady || !mUserManagerInternal.isUserUnlockingOrUnlocked(newUserId);
-        mSettings.switchCurrentUser(newUserId);
+        mSettings = new InputMethodSettings(mMethodMap, newUserId);
         // Additional subtypes should be reset when the user is changed
         AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId);
         final String defaultImiId = mSettings.getSelectedInputMethod();
@@ -1876,7 +1831,8 @@
         mLastSwitchUserId = newUserId;
 
         if (mIsInteractive && clientToBeReset != null) {
-            final ClientState cs = mClients.get(clientToBeReset.asBinder());
+            final ClientState cs =
+                    mClientController.mClients.get(clientToBeReset.asBinder());
             if (cs == null) {
                 // The client is already gone.
                 return;
@@ -1896,7 +1852,6 @@
             if (!mSystemReady) {
                 mSystemReady = true;
                 final int currentUserId = mSettings.getCurrentUserId();
-                mSettings.switchCurrentUser(currentUserId);
                 mStatusBarManagerInternal =
                         LocalServices.getService(StatusBarManagerInternal.class);
                 hideStatusBarIconLocked();
@@ -2214,43 +2169,22 @@
         // actually running.
         final int callerUid = Binder.getCallingUid();
         final int callerPid = Binder.getCallingPid();
+
+        // TODO(b/314150112): Move the death recipient logic to ClientController when moving
+        //     removeClient method.
+        final IBinder.DeathRecipient deathRecipient = () -> removeClient(client);
+        final IInputMethodClientInvoker clientInvoker =
+                IInputMethodClientInvoker.create(client, mHandler);
         synchronized (ImfLock.class) {
-            // TODO: Optimize this linear search.
-            final int numClients = mClients.size();
-            for (int i = 0; i < numClients; ++i) {
-                final ClientState state = mClients.valueAt(i);
-                if (state.mUid == callerUid && state.mPid == callerPid
-                        && state.mSelfReportedDisplayId == selfReportedDisplayId) {
-                    throw new SecurityException("uid=" + callerUid + "/pid=" + callerPid
-                            + "/displayId=" + selfReportedDisplayId + " is already registered.");
-                }
-            }
-            final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
-            try {
-                client.asBinder().linkToDeath(deathRecipient, 0 /* flags */);
-            } catch (RemoteException e) {
-                throw new IllegalStateException(e);
-            }
-            // We cannot fully avoid race conditions where the client UID already lost the access to
-            // the given self-reported display ID, even if the client is not maliciously reporting
-            // a fake display ID. Unconditionally returning SecurityException just because the
-            // client doesn't pass display ID verification can cause many test failures hence not an
-            // option right now.  At the same time
-            //    context.getSystemService(InputMethodManager.class)
-            // is expected to return a valid non-null instance at any time if we do not choose to
-            // have the client crash.  Thus we do not verify the display ID at all here.  Instead we
-            // later check the display ID every time the client needs to interact with the specified
-            // display.
-            final IInputMethodClientInvoker clientInvoker =
-                    IInputMethodClientInvoker.create(client, mHandler);
-            mClients.put(client.asBinder(), new ClientState(clientInvoker, inputConnection,
-                    callerUid, callerPid, selfReportedDisplayId, deathRecipient));
+            mClientController.addClient(clientInvoker, inputConnection, selfReportedDisplayId,
+                    deathRecipient, callerUid, callerPid);
         }
     }
 
+    // TODO(b/314150112): Move this to ClientController.
     void removeClient(IInputMethodClient client) {
         synchronized (ImfLock.class) {
-            ClientState cs = mClients.remove(client.asBinder());
+            ClientState cs = mClientController.mClients.remove(client.asBinder());
             if (cs != null) {
                 client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
                 clearClientSessionLocked(cs);
@@ -2280,6 +2214,7 @@
         }
     }
 
+    // TODO(b/314150112): Move this to ClientController.
     @GuardedBy("ImfLock.class")
     void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
         if (mCurClient != null) {
@@ -2332,7 +2267,10 @@
         }
     }
 
-    /** {@code true} when a {@link ClientState} has attached from starting the input connection. */
+    /**
+     * {@code true} when a {@link ClientState} has attached from starting the
+     * input connection.
+     */
     @GuardedBy("ImfLock.class")
     boolean hasAttachedClient() {
         return mCurClient != null;
@@ -2976,10 +2914,10 @@
     @GuardedBy("ImfLock.class")
     void clearClientSessionsLocked() {
         if (getCurMethodLocked() != null) {
-            final int numClients = mClients.size();
+            final int numClients = mClientController.mClients.size();
             for (int i = 0; i < numClients; ++i) {
-                clearClientSessionLocked(mClients.valueAt(i));
-                clearClientSessionForAccessibilityLocked(mClients.valueAt(i));
+                clearClientSessionLocked(mClientController.mClients.valueAt(i));
+                clearClientSessionForAccessibilityLocked(mClientController.mClients.valueAt(i));
             }
 
             finishSessionLocked(mEnabledSession);
@@ -3305,8 +3243,13 @@
             mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
                     mContext, mMethodMap, mSettings.getCurrentUserId());
         }
-
-        mHardwareKeyboardShortcutController.reset(mSettings);
+        // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
+        if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) {
+            mHardwareKeyboardShortcutController.reset(mMethodMap);
+        } else {
+            mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
+                    mMethodMap, mSettings.getCurrentUserId());
+        }
         sendOnNavButtonFlagsChangedLocked();
     }
 
@@ -3509,9 +3452,12 @@
                     + " pref is disabled for user: " + userId);
             return;
         }
-        if (!verifyClientAndPackageMatch(client, delegatorPackageName)) {
-            Slog.w(TAG, "prepareStylusHandwritingDelegation() fail");
-            throw new IllegalArgumentException("Delegator doesn't match Uid");
+        synchronized (ImfLock.class) {
+            if (!mClientController.verifyClientAndPackageMatch(client,
+                    delegatorPackageName)) {
+                Slog.w(TAG, "prepareStylusHandwritingDelegation() fail");
+                throw new IllegalArgumentException("Delegator doesn't match Uid");
+            }
         }
         schedulePrepareStylusHandwritingDelegation(
                 userId, delegatePackageName, delegatorPackageName);
@@ -3537,30 +3483,17 @@
         return true;
     }
 
-    private boolean verifyClientAndPackageMatch(
-            @NonNull IInputMethodClient client, @NonNull String packageName) {
-        ClientState cs;
-        synchronized (ImfLock.class) {
-            cs = mClients.get(client.asBinder());
-        }
-        if (cs == null) {
-            throw new IllegalArgumentException("unknown client " + client.asBinder());
-        }
-        return InputMethodUtils.checkIfPackageBelongsToUid(
-                mPackageManagerInternal, cs.mUid, packageName);
-    }
-
     private boolean verifyDelegator(
             @NonNull IInputMethodClient client,
             @NonNull String delegatePackageName,
             @NonNull String delegatorPackageName,
             @InputMethodManager.HandwritingDelegateFlags int flags) {
-        if (!verifyClientAndPackageMatch(client, delegatePackageName)) {
-            Slog.w(TAG, "Delegate package does not belong to the same user. Ignoring"
-                    + " startStylusHandwriting");
-            return false;
-        }
         synchronized (ImfLock.class) {
+            if (!mClientController.verifyClientAndPackageMatch(client, delegatePackageName)) {
+                Slog.w(TAG, "Delegate package does not belong to the same user. Ignoring"
+                        + " startStylusHandwriting");
+                return false;
+            }
             boolean homeDelegatorAllowed =
                     (flags & InputMethodManager.HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED)
                             != 0;
@@ -3823,7 +3756,7 @@
             return InputBindResult.INVALID_USER;
         }
 
-        final ClientState cs = mClients.get(client.asBinder());
+        final ClientState cs = mClientController.mClients.get(client.asBinder());
         if (cs == null) {
             throw new IllegalArgumentException("unknown client " + client.asBinder());
         }
@@ -3997,7 +3930,8 @@
             // We need to check if this is the current client with
             // focus in the window manager, to allow this call to
             // be made before input is started in it.
-            final ClientState cs = mClients.get(client.asBinder());
+            final ClientState cs =
+                    mClientController.mClients.get(client.asBinder());
             if (cs == null) {
                 ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
                 throw new IllegalArgumentException("unknown client " + client.asBinder());
@@ -4621,7 +4555,7 @@
         ImeTracing.getInstance().startTrace(null /* printwriter */);
         ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
-            clients = new ArrayMap<>(mClients);
+            clients = new ArrayMap<>(mClientController.mClients);
         }
         for (ClientState state : clients.values()) {
             if (state != null) {
@@ -4639,7 +4573,7 @@
         ImeTracing.getInstance().stopTrace(null /* printwriter */);
         ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
-            clients = new ArrayMap<>(mClients);
+            clients = new ArrayMap<>(mClientController.mClients);
         }
         for (ClientState state : clients.values()) {
             if (state != null) {
@@ -5328,7 +5262,13 @@
             mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
                     mContext, mMethodMap, mSettings.getCurrentUserId());
         }
-        mHardwareKeyboardShortcutController.reset(mSettings);
+        // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
+        if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) {
+            mHardwareKeyboardShortcutController.reset(mMethodMap);
+        } else {
+            mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
+                    mMethodMap, mSettings.getCurrentUserId());
+        }
 
         sendOnNavButtonFlagsChangedLocked();
 
@@ -5878,10 +5818,10 @@
                 // We only have sessions when we bound to an input method. Remove this session
                 // from all clients.
                 if (getCurMethodLocked() != null) {
-                    final int numClients = mClients.size();
+                    final int numClients = mClientController.mClients.size();
                     for (int i = 0; i < numClients; ++i) {
-                        clearClientSessionForAccessibilityLocked(mClients.valueAt(i),
-                                accessibilityConnectionId);
+                        clearClientSessionForAccessibilityLocked(
+                                mClientController.mClients.valueAt(i), accessibilityConnectionId);
                     }
                     AccessibilitySessionState session = mEnabledAccessibilitySessions.get(
                             accessibilityConnectionId);
@@ -6066,9 +6006,10 @@
                 info.dump(p, "    ");
             }
             p.println("  ClientStates:");
-            final int numClients = mClients.size();
+            // TODO(b/314150112): move client related dump info to ClientController#dump
+            final int numClients = mClientController.mClients.size();
             for (int i = 0; i < numClients; ++i) {
-                final ClientState ci = mClients.valueAt(i);
+                final ClientState ci = mClientController.mClients.valueAt(i);
                 p.println("  " + ci + ":");
                 p.println("    client=" + ci.mClient);
                 p.println("    fallbackInputConnection=" + ci.mFallbackInputConnection);
@@ -6687,7 +6628,7 @@
         boolean isImeTraceEnabled = ImeTracing.getInstance().isEnabled();
         ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
-            clients = new ArrayMap<>(mClients);
+            clients = new ArrayMap<>(mClientController.mClients);
         }
         for (ClientState state : clients.values()) {
             if (state != null) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index f9b06de..fb57c09 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -215,7 +215,7 @@
         private final ArrayMap<String, InputMethodInfo> mMethodMap;
 
         @UserIdInt
-        private int mCurrentUserId;
+        private final int mCurrentUserId;
 
         private static void buildEnabledInputMethodsSettingString(
                 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
@@ -229,18 +229,6 @@
 
         InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
             mMethodMap = methodMap;
-            switchCurrentUser(userId);
-        }
-
-        /**
-         * Must be called when the current user is changed.
-         *
-         * @param userId The user ID.
-         */
-        void switchCurrentUser(@UserIdInt int userId) {
-            if (DEBUG) {
-                Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
-            }
             mCurrentUserId = userId;
             String ime = getSelectedInputMethod();
             String defaultDeviceIme = getSelectedDefaultDeviceInputMethod();
diff --git a/services/core/java/com/android/server/inputmethod/OWNERS b/services/core/java/com/android/server/inputmethod/OWNERS
index aa638aa..e507c6b 100644
--- a/services/core/java/com/android/server/inputmethod/OWNERS
+++ b/services/core/java/com/android/server/inputmethod/OWNERS
@@ -6,5 +6,8 @@
 fstern@google.com
 cosminbaies@google.com
 
+# Automotive
+kanant@google.com
+
 ogunwale@google.com #{LAST_RESORT_SUGGESTION}
 jjaggi@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
index d02b6f4..171fbb6 100644
--- a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
@@ -25,6 +25,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
+import android.location.GnssMeasurement;
 import android.location.GnssMeasurementRequest;
 import android.location.GnssMeasurementsEvent;
 import android.location.IGnssMeasurementsListener;
@@ -33,6 +34,7 @@
 import android.stats.location.LocationStatsEnums;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.server.location.gnss.GnssConfiguration.HalInterfaceVersion;
 import com.android.server.location.gnss.hal.GnssNative;
 import com.android.server.location.injector.AppOpsHelper;
@@ -40,6 +42,8 @@
 import com.android.server.location.injector.LocationUsageLogger;
 import com.android.server.location.injector.SettingsHelper;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.Collection;
 
 /**
@@ -91,6 +95,9 @@
     private final LocationUsageLogger mLogger;
     private final GnssNative mGnssNative;
 
+    @GuardedBy("mMultiplexerLock")
+    private GnssMeasurementsEvent mLastGnssMeasurementsEvent;
+
     public GnssMeasurementsProvider(Injector injector, GnssNative gnssNative) {
         super(injector);
         mAppOpsHelper = injector.getAppOpsHelper();
@@ -264,5 +271,46 @@
                 return null;
             }
         });
+        synchronized (mMultiplexerLock) {
+            mLastGnssMeasurementsEvent = event;
+        }
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        super.dump(fd, pw, args);
+        pw.print("last measurements=");
+        pw.println(getLastMeasurementEventSummary());
+    }
+
+    /**
+     * Returns a string of GnssMeasurementsEvent summary including received time, satellite count
+     * and average baseband C/No.
+     */
+    private String getLastMeasurementEventSummary() {
+        synchronized (mMultiplexerLock) {
+            if (mLastGnssMeasurementsEvent == null) {
+                return null;
+            }
+            StringBuilder builder = new StringBuilder("[");
+            builder.append("elapsedRealtimeNs=").append(
+                    mLastGnssMeasurementsEvent.getClock().getElapsedRealtimeNanos());
+            builder.append(" measurementCount=").append(
+                    mLastGnssMeasurementsEvent.getMeasurements().size());
+
+            float sumBasebandCn0 = 0;
+            int countBasebandCn0 = 0;
+            for (GnssMeasurement measurement : mLastGnssMeasurementsEvent.getMeasurements()) {
+                if (measurement.hasBasebandCn0DbHz()) {
+                    sumBasebandCn0 += measurement.getBasebandCn0DbHz();
+                    countBasebandCn0++;
+                }
+            }
+            if (countBasebandCn0 > 0) {
+                builder.append(" avgBasebandCn0=").append(sumBasebandCn0 / countBasebandCn0);
+            }
+            builder.append("]");
+            return builder.toString();
+        }
     }
 }
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index c772e08..5df0de8 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -22,12 +22,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.os.SystemClock;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
 import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.flags.Flags;
 import com.android.server.FgThread;
 
 import java.util.Objects;
@@ -104,10 +106,26 @@
         boolean isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE
                 && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs) < extensionTimeMs;
 
-        return mIsInEmergencyCall
-                || isInExtensionTime
-                || mTelephonyManager.getEmergencyCallbackMode()
-                || mTelephonyManager.isInEmergencySmsMode();
+        if (!Flags.enforceTelephonyFeatureMapping()) {
+            return mIsInEmergencyCall
+                    || isInExtensionTime
+                    || mTelephonyManager.getEmergencyCallbackMode()
+                    || mTelephonyManager.isInEmergencySmsMode();
+        } else {
+            boolean emergencyCallbackMode = false;
+            boolean emergencySmsMode = false;
+            PackageManager pm = mContext.getPackageManager();
+            if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
+                emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode();
+            }
+            if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
+                emergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+            }
+            return mIsInEmergencyCall
+                    || isInExtensionTime
+                    || emergencyCallbackMode
+                    || emergencySmsMode;
+        }
     }
 
     private class EmergencyCallTelephonyCallback extends TelephonyCallback implements
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 0c2eee5..542b3b0 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -52,6 +52,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.IActivityManager;
@@ -74,6 +75,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.database.sqlite.SQLiteDatabase;
@@ -303,7 +305,7 @@
     private boolean mThirdPartyAppsStarted;
 
     // Current password metrics for all secured users on the device. Updated when user unlocks the
-    // device or changes password. Removed when user is stopped.
+    // device or changes password. Removed if user is stopped with its CE key evicted.
     @GuardedBy("this")
     private final SparseArray<PasswordMetrics> mUserPasswordMetrics = new SparseArray<>();
     @VisibleForTesting
@@ -793,13 +795,33 @@
     }
 
     @VisibleForTesting
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS,
+            android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     void onUserStopped(int userId) {
         hideEncryptionNotification(new UserHandle(userId));
-        // User is stopped with its CE key evicted. Restore strong auth requirement to the default
-        // flags after boot since stopping and restarting a user later is equivalent to rebooting
-        // the device.
+
+        // Normally, CE storage is locked when a user is stopped, and restarting the user requires
+        // strong auth.  Therefore, reset the user's strong auth flags.  The exception is users that
+        // allow delayed locking; under some circumstances, biometric authentication is allowed to
+        // restart such users.  Don't reset the strong auth flags for such users.
+        //
+        // TODO(b/319142556): It might make more sense to reset the strong auth flags when CE
+        // storage is locked, instead of when the user is stopped.  This would ensure the flags get
+        // reset if CE storage is locked later for a user that allows delayed locking.
+        if (android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()) {
+            UserProperties userProperties = mUserManager.getUserProperties(UserHandle.of(userId));
+            if (userProperties != null && userProperties.getAllowStoppingUserWithDelayedLocking()) {
+                return;
+            }
+        }
         int strongAuthRequired = LockPatternUtils.StrongAuthTracker.getDefaultFlags(mContext);
         requireStrongAuth(strongAuthRequired, userId);
+
+        // Don't keep the password metrics in memory for a stopped user that will require strong
+        // auth to start again, since strong auth will make the password metrics available again.
         synchronized (this) {
             mUserPasswordMetrics.remove(userId);
         }
@@ -2118,11 +2140,10 @@
             Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
             return;
         }
-        final UserInfo userInfo = mUserManager.getUserInfo(userId);
         final String userType = isUserSecure(userId) ? "secured" : "unsecured";
         final byte[] secret = sp.deriveFileBasedEncryptionKey();
         try {
-            mStorageManager.unlockCeStorage(userId, userInfo.serialNumber, secret);
+            mStorageManager.unlockCeStorage(userId, secret);
             Slogf.i(TAG, "Unlocked CE storage for %s user %d", userType, userId);
         } catch (RemoteException e) {
             Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 06a8d98..e048522 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -774,7 +774,7 @@
                                 .generateDeviceRouteSelectedSessionInfo(packageName);
                     } else {
                         sessionInfos = userRecord.mHandler.mSystemProvider.getSessionInfos();
-                        if (sessionInfos != null && !sessionInfos.isEmpty()) {
+                        if (!sessionInfos.isEmpty()) {
                             // Return a copy of the current system session with no modification,
                             // except setting the client package name.
                             return new RoutingSessionInfo.Builder(sessionInfos.get(0))
@@ -1158,14 +1158,7 @@
         } else {
             if (route.isSystemRoute()
                     && !routerRecord.hasSystemRoutingPermission()
-                    && !TextUtils.equals(
-                            route.getId(),
-                            routerRecord
-                                    .mUserRecord
-                                    .mHandler
-                                    .mSystemProvider
-                                    .getDefaultRoute()
-                                    .getId())) {
+                    && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
                 Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to"
                         + route);
                 routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
@@ -1252,11 +1245,9 @@
                         "transferToRouteWithRouter2 | router: %s(id: %d), route: %s",
                         routerRecord.mPackageName, routerRecord.mRouterId, route.getId()));
 
-        String defaultRouteId =
-                routerRecord.mUserRecord.mHandler.mSystemProvider.getDefaultRoute().getId();
         if (route.isSystemRoute()
                 && !routerRecord.hasSystemRoutingPermission()
-                && !TextUtils.equals(route.getId(), defaultRouteId)) {
+                && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
             routerRecord.mUserRecord.mHandler.sendMessage(
                     obtainMessage(UserHandler::notifySessionCreationFailedToRouter,
                             routerRecord.mUserRecord.mHandler,
@@ -2761,11 +2752,10 @@
             if (manager != null) {
                 notifyRequestFailedToManager(
                         manager.mManager, toOriginalRequestId(uniqueRequestId), reason);
-                return;
             }
 
-            // Currently, only the manager can get notified of failures.
-            // TODO: Notify router too when the related callback is introduced.
+            // Currently, only manager records can get notified of failures.
+            // TODO(b/282936553): Notify regular routers of request failures.
         }
 
         private boolean handleSessionCreationRequestFailed(@NonNull MediaRoute2Provider provider,
@@ -2909,11 +2899,9 @@
                 currentSystemSessionInfo = mSystemProvider.getDefaultSessionInfo();
             }
 
-            if (currentRoutes.size() == 0) {
-                return;
+            if (!currentRoutes.isEmpty()) {
+                routerRecord.notifyRegistered(currentRoutes, currentSystemSessionInfo);
             }
-
-            routerRecord.notifyRegistered(currentRoutes, currentSystemSessionInfo);
         }
 
         private static void notifyRoutesUpdatedToRouterRecords(
diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java
index 07b333a..393e7ef 100644
--- a/services/core/java/com/android/server/media/MediaSession2Record.java
+++ b/services/core/java/com/android/server/media/MediaSession2Record.java
@@ -198,7 +198,12 @@
                 mIsConnected = true;
                 service = mService;
             }
-            service.onSessionActiveStateChanged(MediaSession2Record.this);
+
+            // TODO (b/318745416): Add support for FGS in MediaSession2. Passing a
+            // null playback state means the owning process will not be allowed to
+            // run in the foreground.
+            service.onSessionActiveStateChanged(MediaSession2Record.this,
+                    /* playbackState= */ null);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index cce66e2..53f780e 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -1144,7 +1144,7 @@
             mIsActive = active;
             long token = Binder.clearCallingIdentity();
             try {
-                mService.onSessionActiveStateChanged(MediaSessionRecord.this);
+                mService.onSessionActiveStateChanged(MediaSessionRecord.this, mPlaybackState);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 2affdfc..2cd3ab1 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -260,7 +260,8 @@
         return mGlobalPrioritySession != null && mGlobalPrioritySession.isActive();
     }
 
-    void onSessionActiveStateChanged(MediaSessionRecordImpl record) {
+    void onSessionActiveStateChanged(
+            MediaSessionRecordImpl record, @Nullable PlaybackState playbackState) {
         synchronized (mLock) {
             FullUserRecord user = getFullUserRecordLocked(record.getUserId());
             if (user == null) {
@@ -287,7 +288,9 @@
                 user.mPriorityStack.onSessionActiveStateChanged(record);
             }
             setForegroundServiceAllowance(
-                    record, /* allowRunningInForeground= */ record.isActive());
+                    record,
+                    /* allowRunningInForeground= */ record.isActive()
+                            && (playbackState == null || playbackState.isActive()));
             mHandler.postSessionsChanged(record);
         }
     }
@@ -386,7 +389,9 @@
             user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority);
             if (playbackState != null) {
                 setForegroundServiceAllowance(
-                        record, playbackState.shouldAllowServiceToRunInForeground());
+                        record,
+                        /* allowRunningInForeground= */ playbackState.isActive()
+                                && record.isActive());
             }
         }
     }
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 6deda46..f6571d9 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -653,14 +653,14 @@
         }
 
         @Override // Binder call
-        public boolean hasProjectionPermission(int uid, String packageName) {
+        public boolean hasProjectionPermission(int processUid, String packageName) {
             final long token = Binder.clearCallingIdentity();
             boolean hasPermission = false;
             try {
                 hasPermission |= checkPermission(packageName,
                         android.Manifest.permission.CAPTURE_VIDEO_OUTPUT)
                         || mAppOps.noteOpNoThrow(
-                                AppOpsManager.OP_PROJECT_MEDIA, uid, packageName)
+                                AppOpsManager.OP_PROJECT_MEDIA, processUid, packageName)
                         == AppOpsManager.MODE_ALLOWED;
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -669,7 +669,7 @@
         }
 
         @Override // Binder call
-        public IMediaProjection createProjection(int uid, String packageName, int type,
+        public IMediaProjection createProjection(int processUid, String packageName, int type,
                 boolean isPermanentGrant) {
             if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION)
                         != PackageManager.PERMISSION_GRANTED) {
@@ -680,13 +680,13 @@
                 throw new IllegalArgumentException("package name must not be empty");
             }
             final UserHandle callingUser = Binder.getCallingUserHandle();
-            return createProjectionInternal(uid, packageName, type, isPermanentGrant,
+            return createProjectionInternal(processUid, packageName, type, isPermanentGrant,
                     callingUser);
         }
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public IMediaProjection getProjection(int uid, String packageName) {
+        public IMediaProjection getProjection(int processUid, String packageName) {
             getProjection_enforcePermission();
             if (packageName == null || packageName.isEmpty()) {
                 throw new IllegalArgumentException("package name must not be empty");
@@ -695,7 +695,7 @@
             MediaProjection projection;
             final long callingToken = Binder.clearCallingIdentity();
             try {
-                projection = getProjectionInternal(uid, packageName);
+                projection = getProjectionInternal(processUid, packageName);
             } finally {
                 Binder.restoreCallingIdentity(callingToken);
             }
@@ -869,12 +869,13 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyPermissionRequestInitiated(int hostUid, int sessionCreationSource) {
+        public void notifyPermissionRequestInitiated(
+                int hostProcessUid, int sessionCreationSource) {
             notifyPermissionRequestInitiated_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 MediaProjectionManagerService.this.notifyPermissionRequestInitiated(
-                        hostUid, sessionCreationSource);
+                        hostProcessUid, sessionCreationSource);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -882,11 +883,11 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyPermissionRequestDisplayed(int hostUid) {
+        public void notifyPermissionRequestDisplayed(int hostProcessUid) {
             notifyPermissionRequestDisplayed_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
-                MediaProjectionManagerService.this.notifyPermissionRequestDisplayed(hostUid);
+                MediaProjectionManagerService.this.notifyPermissionRequestDisplayed(hostProcessUid);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -894,11 +895,11 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyPermissionRequestCancelled(int hostUid) {
+        public void notifyPermissionRequestCancelled(int hostProcessUid) {
             notifyPermissionRequestCancelled_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
-                MediaProjectionManagerService.this.notifyPermissionRequestCancelled(hostUid);
+                MediaProjectionManagerService.this.notifyPermissionRequestCancelled(hostProcessUid);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -906,11 +907,11 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyAppSelectorDisplayed(int hostUid) {
+        public void notifyAppSelectorDisplayed(int hostProcessUid) {
             notifyAppSelectorDisplayed_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
-                MediaProjectionManagerService.this.notifyAppSelectorDisplayed(hostUid);
+                MediaProjectionManagerService.this.notifyAppSelectorDisplayed(hostProcessUid);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -919,12 +920,12 @@
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
         public void notifyWindowingModeChanged(
-                int contentToRecord, int targetUid, int windowingMode) {
+                int contentToRecord, int targetProcessUid, int windowingMode) {
             notifyWindowingModeChanged_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 MediaProjectionManagerService.this.notifyWindowingModeChanged(
-                        contentToRecord, targetUid, windowingMode);
+                        contentToRecord, targetProcessUid, windowingMode);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 6b7db2d..85c4ffe 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -22,6 +22,7 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
+import static android.media.audio.Flags.focusExclusiveWithRecording;
 import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS;
 import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
 import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS;
@@ -94,8 +95,6 @@
 
     private static final float DEFAULT_VOLUME = 1.0f;
     // TODO (b/291899544): remove for release
-    private static final String POLITE_STRATEGY1 = "rule1";
-    private static final String POLITE_STRATEGY2 = "rule2";
     private static final int DEFAULT_NOTIFICATION_COOLDOWN_ENABLED = 1;
     private static final int DEFAULT_NOTIFICATION_COOLDOWN_ENABLED_FOR_WORK = 0;
     private static final int DEFAULT_NOTIFICATION_COOLDOWN_ALL = 1;
@@ -146,7 +145,6 @@
     private boolean mNotificationCooldownApplyToAll;
     private boolean mNotificationCooldownVibrateUnlocked;
 
-    private boolean mEnablePoliteNotificationsFeature;
     private final PolitenessStrategy mStrategy;
     private int mCurrentWorkProfileId = UserHandle.USER_NULL;
 
@@ -192,9 +190,7 @@
                 .build();
         mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume);
 
-        mEnablePoliteNotificationsFeature = Flags.politeNotifications();
-
-        if (mEnablePoliteNotificationsFeature) {
+        if (Flags.politeNotifications()) {
             mStrategy = getPolitenessStrategy();
         } else {
             mStrategy = null;
@@ -205,21 +201,23 @@
     }
 
     private PolitenessStrategy getPolitenessStrategy() {
-        final String politenessStrategy = mFlagResolver.getStringValue(
-                NotificationFlags.NOTIF_COOLDOWN_RULE);
-
-        if (POLITE_STRATEGY2.equals(politenessStrategy)) {
-            return new Strategy2(mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
+        if (Flags.crossAppPoliteNotifications()) {
+            PolitenessStrategy appStrategy = new StrategyPerApp(
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
-                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2));
-        } else {
-            if (!POLITE_STRATEGY1.equals(politenessStrategy)) {
-                Log.w(TAG, "Invalid cooldown strategy: " + politenessStrategy + ". Defaulting to "
-                        + POLITE_STRATEGY1);
-            }
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET));
 
-            return new Strategy1(mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
+            return new StrategyGlobal(
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2),
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
+                    appStrategy);
+        } else {
+            return new StrategyPerApp(
+                    mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
@@ -266,7 +264,7 @@
         mContext.getContentResolver().registerContentObserver(
                 SettingsObserver.NOTIFICATION_LIGHT_PULSE_URI, false, mSettingsObserver,
                 UserHandle.USER_ALL);
-        if (mEnablePoliteNotificationsFeature) {
+        if (Flags.politeNotifications()) {
             mContext.getContentResolver().registerContentObserver(
                     SettingsObserver.NOTIFICATION_COOLDOWN_ENABLED_URI, false, mSettingsObserver,
                     UserHandle.USER_ALL);
@@ -280,7 +278,7 @@
     }
 
     private void loadUserSettings() {
-        if (mEnablePoliteNotificationsFeature) {
+        if (Flags.politeNotifications()) {
             try {
                 mCurrentWorkProfileId = getManagedProfileId(ActivityManager.getCurrentUser());
 
@@ -301,11 +299,14 @@
                     mContext.getContentResolver(),
                     Settings.System.NOTIFICATION_COOLDOWN_ALL, DEFAULT_NOTIFICATION_COOLDOWN_ALL,
                     UserHandle.USER_CURRENT) != 0;
-                mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser(
-                    mContext.getContentResolver(),
-                    Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED,
-                    DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED,
-                    UserHandle.USER_CURRENT) != 0;
+                mStrategy.setApplyCooldownPerPackage(mNotificationCooldownApplyToAll);
+                if (Flags.vibrateWhileUnlocked()) {
+                    mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser(
+                        mContext.getContentResolver(),
+                        Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED,
+                        DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED,
+                        UserHandle.USER_CURRENT) != 0;
+                }
             } catch (Exception e) {
                 Log.e(TAG, "Failed to read Settings: " + e);
             }
@@ -482,10 +483,10 @@
                     getPolitenessState(record));
         }
         record.setAudiblyAlerted(buzz || beep);
-        if (mEnablePoliteNotificationsFeature) {
+        if (Flags.politeNotifications()) {
             // Update last alert time
             if (buzz || beep) {
-                record.getChannel().setLastNotificationUpdateTimeMs(System.currentTimeMillis());
+                mStrategy.setLastNotificationUpdateTimeMs(record, System.currentTimeMillis());
             }
         }
         return buzzBeepBlinkLoggingCode;
@@ -588,37 +589,48 @@
     }
 
     private boolean playSound(final NotificationRecord record, Uri soundUri) {
+        final boolean shouldPlay;
+        if (focusExclusiveWithRecording()) {
+            // flagged path
+            shouldPlay = mAudioManager.shouldNotificationSoundPlay(record.getAudioAttributes());
+        } else {
+            // legacy path
+            // play notifications if there is no user of exclusive audio focus
+            // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or
+            //   VIBRATE ringer mode)
+            shouldPlay = !mAudioManager.isAudioFocusExclusive()
+                    && (mAudioManager.getStreamVolume(
+                    AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0);
+        }
+        if (!shouldPlay) {
+            if (DEBUG) Slog.v(TAG, "Not playing sound " + soundUri + " due to focus/volume");
+            return false;
+        }
+
         boolean looping = (record.getNotification().flags & FLAG_INSISTENT) != 0;
-        // play notifications if there is no user of exclusive audio focus
-        // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or
-        //   VIBRATE ringer mode)
-        if (!mAudioManager.isAudioFocusExclusive()
-                && (mAudioManager.getStreamVolume(
-                AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0)) {
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
-                if (player != null) {
-                    if (DEBUG) {
-                        Slog.v(TAG, "Playing sound " + soundUri + " with attributes "
-                                + record.getAudioAttributes());
-                    }
-                    player.playAsync(soundUri, record.getSbn().getUser(), looping,
-                            record.getAudioAttributes(), getSoundVolume(record));
-                    return true;
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+            if (player != null) {
+                if (DEBUG) {
+                    Slog.v(TAG, "Playing sound " + soundUri + " with attributes "
+                            + record.getAudioAttributes());
                 }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed playSound: " + e);
-            } finally {
-                Binder.restoreCallingIdentity(identity);
+                player.playAsync(soundUri, record.getSbn().getUser(), looping,
+                        record.getAudioAttributes(), getSoundVolume(record));
+                return true;
             }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed playSound: " + e);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
         }
         return false;
     }
 
     private boolean isPoliteNotificationFeatureEnabled(final NotificationRecord record) {
         // Check feature flag
-        if (!mEnablePoliteNotificationsFeature) {
+        if (!Flags.politeNotifications()) {
             return false;
         }
 
@@ -1064,9 +1076,13 @@
         // Volume for muted state
         protected final float mVolumeMuted;
 
+        protected boolean mApplyPerPackage;
+        protected final Map<String, Long> mLastUpdatedTimestampByPackage;
+
         public PolitenessStrategy(int timeoutPolite, int timeoutMuted, int volumePolite,
                 int volumeMuted) {
             mVolumeStates = new HashMap<>();
+            mLastUpdatedTimestampByPackage = new HashMap<>();
 
             this.mTimeoutPolite = timeoutPolite;
             this.mTimeoutMuted = timeoutMuted;
@@ -1076,10 +1092,38 @@
 
         abstract void onNotificationPosted(NotificationRecord record);
 
+        /**
+         *  Set true if the cooldown strategy should apply per app(package).
+         *  Otherwise apply per conversation channel.
+         * @param applyPerPackage if the cooldown should be applied per app
+         */
+        void setApplyCooldownPerPackage(boolean applyPerPackage) {
+            mApplyPerPackage = applyPerPackage;
+        }
+
+        boolean shouldIgnoreNotification(final NotificationRecord record) {
+            // Ignore group summaries
+            return (record.getSbn().isGroup() && record.getSbn().getNotification()
+                    .isGroupSummary());
+        }
+
+        /**
+         * Get the key that determines the grouping for the cooldown behavior.
+         *
+         * @param record the notification being posted
+         * @return the key to group this notification under
+         */
         String getChannelKey(final NotificationRecord record) {
-            // use conversationId if it's a conversation
+            // Use conversationId if it's a conversation
             String channelId = record.getChannel().getConversationId() != null
                     ? record.getChannel().getConversationId() : record.getChannel().getId();
+
+            // Use only the package name to apply cooldown per app, unless the user explicitly
+            // changed the channel notification sound => treat separately
+            if (mApplyPerPackage && !record.getChannel().hasUserSetSound()) {
+                channelId = "";
+            }
+
             return record.getSbn().getNormalizedUserId() + ":" + record.getSbn().getPackageName()
                     + ":" + channelId;
         }
@@ -1121,12 +1165,59 @@
             final String key = getChannelKey(record);
             // reset to default state after user interaction
             mVolumeStates.put(key, POLITE_STATE_DEFAULT);
-            record.getChannel().setLastNotificationUpdateTimeMs(0);
+            setLastNotificationUpdateTimeMs(record, 0);
         }
 
         public final @PolitenessState int getPolitenessState(final NotificationRecord record) {
             return mVolumeStates.getOrDefault(getChannelKey(record), POLITE_STATE_DEFAULT);
         }
+
+        void setLastNotificationUpdateTimeMs(final NotificationRecord record,
+                long timestampMillis) {
+            record.getChannel().setLastNotificationUpdateTimeMs(timestampMillis);
+            mLastUpdatedTimestampByPackage.put(record.getSbn().getPackageName(), timestampMillis);
+        }
+
+        long getLastNotificationUpdateTimeMs(final NotificationRecord record) {
+            if (record.getChannel().hasUserSetSound() || !mApplyPerPackage) {
+                return record.getChannel().getLastNotificationUpdateTimeMs();
+            } else {
+                return mLastUpdatedTimestampByPackage.getOrDefault(record.getSbn().getPackageName(),
+                        0L);
+            }
+        }
+
+        @PolitenessState int getNextState(@PolitenessState final int currState,
+                final long timeSinceLastNotif) {
+            @PolitenessState int nextState = currState;
+            switch (currState) {
+                case POLITE_STATE_DEFAULT:
+                    if (timeSinceLastNotif < mTimeoutPolite) {
+                        nextState = POLITE_STATE_POLITE;
+                    }
+                    break;
+                case POLITE_STATE_POLITE:
+                    if (timeSinceLastNotif < mTimeoutMuted) {
+                        nextState = POLITE_STATE_MUTED;
+                    } else if (timeSinceLastNotif > mTimeoutPolite) {
+                        nextState = POLITE_STATE_DEFAULT;
+                    } else {
+                        nextState = POLITE_STATE_POLITE;
+                    }
+                    break;
+                case POLITE_STATE_MUTED:
+                    if (timeSinceLastNotif > mTimeoutMuted) {
+                        nextState = POLITE_STATE_POLITE;
+                    } else {
+                        nextState = POLITE_STATE_MUTED;
+                    }
+                    break;
+                default:
+                    Log.w(TAG, "getNextState unexpected volume state: " + currState);
+                    break;
+            }
+            return nextState;
+        }
     }
 
     // TODO b/270456865: Only one of the two strategies will be released.
@@ -1143,72 +1234,51 @@
      *  after timeoutMuted.
      *  - Transitions back to the default state after a user interaction with a notification.
      */
-    public static class Strategy1 extends PolitenessStrategy {
+    private static class StrategyPerApp extends PolitenessStrategy {
         // Keep track of the number of notifications posted per channel
         private final Map<String, Integer> mNumPosted;
         // Reset to default state if number of posted notifications exceed this value when muted
         private final int mMaxPostedForReset;
 
-        public Strategy1(int timeoutPolite, int timeoutMuted, int volumePolite, int volumeMuted,
-                int maxPosted) {
+        public StrategyPerApp(int timeoutPolite, int timeoutMuted, int volumePolite,
+                int volumeMuted, int maxPosted) {
             super(timeoutPolite, timeoutMuted, volumePolite, volumeMuted);
 
             mNumPosted = new HashMap<>();
             mMaxPostedForReset = maxPosted;
 
             if (DEBUG) {
-                Log.i(TAG, "Strategy1: " + timeoutPolite + " " + timeoutMuted);
+                Log.i(TAG, "StrategyPerApp: " + timeoutPolite + " " + timeoutMuted);
             }
         }
 
         @Override
         public void onNotificationPosted(final NotificationRecord record) {
-            long timeSinceLastNotif = System.currentTimeMillis()
-                    - record.getChannel().getLastNotificationUpdateTimeMs();
+            if (shouldIgnoreNotification(record)) {
+                return;
+            }
+
+            long timeSinceLastNotif =
+                    System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record);
 
             final String key = getChannelKey(record);
-            @PolitenessState int volState = getPolitenessState(record);
+            @PolitenessState final int currState = getPolitenessState(record);
+            @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif);
 
+            // Reset to default state if number of posted notifications exceed this value when muted
             int numPosted = mNumPosted.getOrDefault(key, 0) + 1;
             mNumPosted.put(key, numPosted);
-
-            switch (volState) {
-                case POLITE_STATE_DEFAULT:
-                    if (timeSinceLastNotif < mTimeoutPolite) {
-                        volState = POLITE_STATE_POLITE;
-                    }
-                    break;
-                case POLITE_STATE_POLITE:
-                    if (timeSinceLastNotif < mTimeoutMuted) {
-                        volState = POLITE_STATE_MUTED;
-                    } else if (timeSinceLastNotif > mTimeoutPolite) {
-                        volState = POLITE_STATE_DEFAULT;
-                    } else {
-                        volState = POLITE_STATE_POLITE;
-                    }
-                    break;
-                case POLITE_STATE_MUTED:
-                    if (timeSinceLastNotif > mTimeoutMuted) {
-                        volState = POLITE_STATE_POLITE;
-                    } else {
-                        volState = POLITE_STATE_MUTED;
-                    }
-                    if (numPosted >= mMaxPostedForReset) {
-                        volState = POLITE_STATE_DEFAULT;
-                        mNumPosted.put(key, 0);
-                    }
-                    break;
-                default:
-                    Log.w(TAG, "onNotificationPosted unexpected volume state: " + volState);
-                    break;
+            if (currState == POLITE_STATE_MUTED && numPosted >= mMaxPostedForReset) {
+                nextState = POLITE_STATE_DEFAULT;
+                mNumPosted.put(key, 0);
             }
 
             if (DEBUG) {
                 Log.i(TAG, "onNotificationPosted time delta: " + timeSinceLastNotif + " vol state: "
-                        + volState + " key: " + key + " numposted " + numPosted);
+                        + nextState + " key: " + key + " numposted " + numPosted);
             }
 
-            mVolumeStates.put(key, volState);
+            mVolumeStates.put(key, nextState);
         }
 
         @Override
@@ -1219,61 +1289,98 @@
     }
 
     /**
-     *  Polite notification strategy 2:
-     *   - Transitions from default (loud) => muted state if a notification
-     *   alerts the same channel before timeoutPolite.
-     *   - Transitions from polite => default state if a notification
-     *  alerts the same channel before timeoutMuted.
-     *   - Transitions from muted => default state if a notification alerts after timeoutMuted,
-     *  otherwise transitions to the polite state.
-     *   - Transitions back to the default state after a user interaction with a notification.
+     * Global (cross-app) strategy.
      */
-    public static class Strategy2 extends PolitenessStrategy {
-        public Strategy2(int timeoutPolite, int timeoutMuted, int volumePolite, int volumeMuted) {
+    private static class StrategyGlobal extends PolitenessStrategy {
+        private static final String COMMON_KEY = "cross_app_common_key";
+
+        private final PolitenessStrategy mAppStrategy;
+        private long mLastNotificationTimestamp = 0;
+
+        public StrategyGlobal(int timeoutPolite, int timeoutMuted, int volumePolite,
+                int volumeMuted, PolitenessStrategy appStrategy) {
             super(timeoutPolite, timeoutMuted, volumePolite, volumeMuted);
 
+            mAppStrategy = appStrategy;
+
             if (DEBUG) {
-                Log.i(TAG, "Strategy2: " + timeoutPolite + " " + timeoutMuted);
+                Log.i(TAG, "StrategyGlobal: " + timeoutPolite + " " + timeoutMuted);
             }
         }
 
         @Override
-        public void onNotificationPosted(final NotificationRecord record) {
-            long timeSinceLastNotif = System.currentTimeMillis()
-                    - record.getChannel().getLastNotificationUpdateTimeMs();
+        void onNotificationPosted(NotificationRecord record) {
+            if (shouldIgnoreNotification(record)) {
+                return;
+            }
+
+            long timeSinceLastNotif =
+                    System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record);
 
             final String key = getChannelKey(record);
-            @PolitenessState int volState = getPolitenessState(record);
-
-            switch (volState) {
-                case POLITE_STATE_DEFAULT:
-                    if (timeSinceLastNotif < mTimeoutPolite) {
-                        volState = POLITE_STATE_MUTED;
-                    }
-                    break;
-                case POLITE_STATE_POLITE:
-                    if (timeSinceLastNotif > mTimeoutMuted) {
-                        volState = POLITE_STATE_DEFAULT;
-                    }
-                    break;
-                case POLITE_STATE_MUTED:
-                    if (timeSinceLastNotif > mTimeoutMuted) {
-                        volState = POLITE_STATE_DEFAULT;
-                    } else {
-                        volState = POLITE_STATE_POLITE;
-                    }
-                    break;
-                default:
-                    Log.w(TAG, "onNotificationPosted unexpected volume state: " + volState);
-                    break;
-            }
+            @PolitenessState final int currState = getPolitenessState(record);
+            @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif);
 
             if (DEBUG) {
-                Log.i(TAG, "onNotificationPosted time delta: " + timeSinceLastNotif + " vol state: "
-                        + volState + " key: " + key);
+                Log.i(TAG, "StrategyGlobal onNotificationPosted time delta: " + timeSinceLastNotif
+                        + " vol state: " + nextState + " key: " + key);
             }
 
-            mVolumeStates.put(key, volState);
+            mVolumeStates.put(key, nextState);
+
+            mAppStrategy.onNotificationPosted(record);
+        }
+
+        @Override
+        public float getSoundVolume(final NotificationRecord record) {
+            final @PolitenessState int globalVolState = getPolitenessState(record);
+            final @PolitenessState int appVolState = mAppStrategy.getPolitenessState(record);
+
+            // Prioritize the most polite outcome
+            if (globalVolState > appVolState) {
+                return super.getSoundVolume(record);
+            } else {
+                return mAppStrategy.getSoundVolume(record);
+            }
+        }
+
+        @Override
+        public void onUserInteraction(final NotificationRecord record) {
+            super.onUserInteraction(record);
+            mAppStrategy.onUserInteraction(record);
+        }
+
+        @Override
+        String getChannelKey(final NotificationRecord record) {
+            // If the user explicitly changed the channel notification sound:
+            // handle as a separate channel
+            if (record.getChannel().hasUserSetSound()) {
+                return super.getChannelKey(record);
+            } else {
+                // Use one global key per user
+                return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY;
+            }
+        }
+
+        @Override
+        public void setLastNotificationUpdateTimeMs(NotificationRecord record,
+                long timestampMillis) {
+            super.setLastNotificationUpdateTimeMs(record, timestampMillis);
+            mLastNotificationTimestamp = timestampMillis;
+        }
+
+        long getLastNotificationUpdateTimeMs(final NotificationRecord record) {
+            if (record.getChannel().hasUserSetSound()) {
+                return super.getLastNotificationUpdateTimeMs(record);
+            } else {
+                return mLastNotificationTimestamp;
+            }
+        }
+
+        @Override
+        void setApplyCooldownPerPackage(boolean applyPerPackage) {
+            super.setApplyCooldownPerPackage(applyPerPackage);
+            mAppStrategy.setApplyCooldownPerPackage(applyPerPackage);
         }
     }
 
@@ -1338,7 +1445,7 @@
                     updateLightsLocked();
                 }
             }
-            if (mEnablePoliteNotificationsFeature) {
+            if (Flags.politeNotifications()) {
                 if (NOTIFICATION_COOLDOWN_ENABLED_URI.equals(uri)) {
                     mNotificationCooldownEnabled = Settings.System.getIntForUser(
                             mContext.getContentResolver(),
@@ -1363,13 +1470,16 @@
                             Settings.System.NOTIFICATION_COOLDOWN_ALL,
                             DEFAULT_NOTIFICATION_COOLDOWN_ALL, UserHandle.USER_CURRENT)
                             != 0;
+                    mStrategy.setApplyCooldownPerPackage(mNotificationCooldownApplyToAll);
                 }
-                if (NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED_URI.equals(uri)) {
-                    mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser(
+                if (Flags.vibrateWhileUnlocked()) {
+                    if (NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED_URI.equals(uri)) {
+                        mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser(
                             mContext.getContentResolver(),
                             Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED,
                             DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED,
                             UserHandle.USER_CURRENT) != 0;
+                    }
                 }
             }
         }
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
index 6a46048..deb95d8 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryManager.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
@@ -108,7 +108,7 @@
                 for (int i = 0; i < pendingPackageRemovals.size(); i++) {
                     userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
                 }
-                mUserPendingPackageRemovals.put(userId, null);
+                mUserPendingPackageRemovals.remove(userId);
             }
 
             // delete history if it was disabled when the user was locked
@@ -118,6 +118,10 @@
         }
     }
 
+    public void onUserAdded(@UserIdInt int userId) {
+        mSettingsObserver.update(null, userId);
+    }
+
     public void onUserStopped(@UserIdInt int userId) {
         synchronized (mLock) {
             mUserUnlockedStates.put(userId, false);
@@ -129,7 +133,7 @@
         synchronized (mLock) {
             // Actual data deletion is handled by other parts of the system (the entire directory is
             // removed) - we just need clean up our internal state for GC
-            mUserPendingPackageRemovals.put(userId, null);
+            mUserPendingPackageRemovals.remove(userId);
             mHistoryEnabled.put(userId, false);
             mUserPendingHistoryDisables.put(userId, false);
             onUserStopped(userId);
@@ -401,9 +405,7 @@
                     false, this, UserHandle.USER_ALL);
             synchronized (mLock) {
                 for (UserInfo userInfo : mUserManager.getUsers()) {
-                    if (!userInfo.isProfile()) {
-                        update(null, userInfo.id);
-                    }
+                    update(null, userInfo.id);
                 }
             }
         }
@@ -424,10 +426,7 @@
                 boolean historyEnabled = Settings.Secure.getIntForUser(resolver,
                         Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, userId)
                         != 0;
-                int[] profiles = mUserManager.getProfileIds(userId, true);
-                for (int profileId : profiles) {
-                    onHistoryEnabledChanged(profileId, historyEnabled);
-                }
+                onHistoryEnabledChanged(userId, historyEnabled);
             }
         }
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 7fbc085..9ddc362 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -83,6 +83,7 @@
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.media.audio.Flags.focusExclusiveWithRecording;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
 import static android.os.Flags.allowPrivateProfile;
 import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL;
@@ -214,7 +215,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManagerInternal;
@@ -372,6 +372,7 @@
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
+import java.time.Clock;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -2025,6 +2026,8 @@
                     if (!mUserProfiles.isProfileUser(userId)) {
                         allowDefaultApprovedServices(userId);
                     }
+                    mHistoryManager.onUserAdded(userId);
+                    mSettingsObserver.update(null, userId);
                 }
             } else if (action.equals(Intent.ACTION_USER_REMOVED)) {
                 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
@@ -2137,13 +2140,8 @@
                 mPreferencesHelper.updateBubblesEnabled();
             }
             if (uri == null || NOTIFICATION_HISTORY_ENABLED.equals(uri)) {
-                final IntArray userIds = mUserProfiles.getCurrentProfileIds();
-
-                for (int i = 0; i < userIds.size(); i++) {
-                    mArchive.updateHistoryEnabled(userIds.get(i),
-                            Settings.Secure.getIntForUser(resolver,
-                                    Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0,
-                                    userIds.get(i)) == 1);
+                for (UserInfo userInfo : mUm.getUsers()) {
+                    update(uri, userInfo.id);
                 }
             }
             if (uri == null || NOTIFICATION_SHOW_MEDIA_ON_QUICK_SETTINGS_URI.equals(uri)) {
@@ -2164,6 +2162,17 @@
                 }
             }
         }
+
+        public void update(Uri uri, int userId) {
+            ContentResolver resolver = getContext().getContentResolver();
+            if (uri == null || NOTIFICATION_HISTORY_ENABLED.equals(uri)) {
+                mArchive.updateHistoryEnabled(userId,
+                        Settings.Secure.getIntForUser(resolver,
+                                Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0,
+                                userId) == 1);
+                // note: this setting is also handled in NotificationHistoryManager
+            }
+        }
     }
 
     private SettingsObserver mSettingsObserver;
@@ -2457,8 +2466,8 @@
         mMetricsLogger = new MetricsLogger();
         mRankingHandler = rankingHandler;
         mConditionProviders = conditionProviders;
-        mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders,
-                flagResolver, new ZenModeEventLogger(mPackageManagerClient));
+        mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), Clock.systemUTC(),
+                mConditionProviders, flagResolver, new ZenModeEventLogger(mPackageManagerClient));
         mZenModeHelper.addCallback(new ZenModeHelper.Callback() {
             @Override
             public void onConfigChanged() {
@@ -9096,27 +9105,40 @@
     }
 
     private boolean playSound(final NotificationRecord record, Uri soundUri) {
+        final boolean shouldPlay;
+        if (focusExclusiveWithRecording()) {
+            // flagged path
+            shouldPlay = mAudioManager.shouldNotificationSoundPlay(record.getAudioAttributes());
+        } else {
+            // legacy path
+            // play notifications if there is no user of exclusive audio focus
+            // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or
+            //   VIBRATE ringer mode)
+            shouldPlay = !mAudioManager.isAudioFocusExclusive()
+                    && (mAudioManager.getStreamVolume(
+                        AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0);
+        }
+        if (!shouldPlay) {
+            if (DBG) Slog.v(TAG, "Not playing sound " + soundUri + " due to focus/volume");
+            return false;
+        }
+
         boolean looping = (record.getNotification().flags & FLAG_INSISTENT) != 0;
-        // play notifications if there is no user of exclusive audio focus
-        // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or
-        //   VIBRATE ringer mode)
-        if (!mAudioManager.isAudioFocusExclusive()
-                && (mAudioManager.getStreamVolume(
-                        AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0)) {
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
-                if (player != null) {
-                    if (DBG) Slog.v(TAG, "Playing sound " + soundUri
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+            if (player != null) {
+                if (DBG) {
+                    Slog.v(TAG, "Playing sound " + soundUri
                             + " with attributes " + record.getAudioAttributes());
-                    player.playAsync(soundUri, record.getSbn().getUser(), looping,
-                            record.getAudioAttributes(), 1.0f);
-                    return true;
                 }
-            } catch (RemoteException e) {
-            } finally {
-                Binder.restoreCallingIdentity(identity);
+                player.playAsync(soundUri, record.getSbn().getUser(), looping,
+                        record.getAudioAttributes(), 1.0f);
+                return true;
             }
+        } catch (RemoteException e) {
+        } finally {
+            Binder.restoreCallingIdentity(identity);
         }
         return false;
     }
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 64d3a20..1786ac5 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -25,6 +25,7 @@
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;
 
 import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Flags;
 import android.app.KeyguardManager;
@@ -167,7 +168,7 @@
     private boolean mPreChannelsNotification = true;
     private Uri mSound;
     private VibrationEffect mVibration;
-    private AudioAttributes mAttributes;
+    private @NonNull AudioAttributes mAttributes;
     private NotificationChannel mChannel;
     private ArrayList<String> mPeopleOverride;
     private ArrayList<SnoozeCriterion> mSnoozeCriteria;
@@ -334,7 +335,7 @@
         return vibration;
     }
 
-    private AudioAttributes calculateAttributes() {
+    private @NonNull AudioAttributes calculateAttributes() {
         final Notification n = getSbn().getNotification();
         AudioAttributes attributes = getChannel().getAudioAttributes();
         if (attributes == null) {
@@ -1003,7 +1004,7 @@
     }
 
     public boolean isAudioAttributesUsage(int usage) {
-        return mAttributes != null && mAttributes.getUsage() == usage;
+        return mAttributes.getUsage() == usage;
     }
 
     /**
@@ -1172,7 +1173,7 @@
         return mVibration;
     }
 
-    public AudioAttributes getAudioAttributes() {
+    public @NonNull AudioAttributes getAudioAttributes() {
         return mAttributes;
     }
 
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 3244aff..afbf08d 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -117,6 +117,9 @@
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -130,9 +133,12 @@
     static final String TAG = "ZenModeHelper";
     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
+    private static final String PACKAGE_ANDROID = "android";
+
     // The amount of time rules instances can exist without their owning app being installed.
     private static final int RULE_INSTANCE_GRACE_PERIOD = 1000 * 60 * 60 * 72;
     static final int RULE_LIMIT_PER_PACKAGE = 100;
+    private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30);
 
     private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name
 
@@ -148,6 +154,7 @@
 
     private final Context mContext;
     private final H mHandler;
+    private final Clock mClock;
     private final SettingsObserver mSettingsObserver;
     private final AppOpsManager mAppOps;
     private final NotificationManager mNotificationManager;
@@ -189,11 +196,13 @@
 
     private String[] mPriorityOnlyDndExemptPackages;
 
-    public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders,
+    public ZenModeHelper(Context context, Looper looper, Clock clock,
+            ConditionProviders conditionProviders,
             SystemUiSystemPropertiesFlags.FlagResolver flagResolver,
             ZenModeEventLogger zenModeEventLogger) {
         mContext = context;
         mHandler = new H(looper);
+        mClock = clock;
         addCallback(mMetrics);
         mAppOps = context.getSystemService(AppOpsManager.class);
         mNotificationManager = context.getSystemService(NotificationManager.class);
@@ -452,7 +461,10 @@
             newConfig = mConfig.copy();
             ZenRule rule = new ZenRule();
             populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true);
+            rule = maybeRestoreRemovedRule(newConfig, rule, automaticZenRule, origin);
             newConfig.automaticRules.put(rule.id, rule);
+            maybeReplaceDefaultRule(newConfig, automaticZenRule);
+
             if (setConfigLocked(newConfig, origin, reason, rule.component, true, callingUid)) {
                 return rule.id;
             } else {
@@ -461,6 +473,56 @@
         }
     }
 
+    private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd,
+            AutomaticZenRule azrToAdd, @ConfigChangeOrigin int origin) {
+        if (!Flags.modesApi()) {
+            return ruleToAdd;
+        }
+        String deletedKey = ZenModeConfig.deletedRuleKey(ruleToAdd);
+        if (deletedKey == null) {
+            // Couldn't calculate the deletedRuleKey (condition or pkg null?). This should
+            // never happen for an app-provided rule because NMS validates both.
+            return ruleToAdd;
+        }
+        ZenRule ruleToRestore = config.deletedRules.get(deletedKey);
+        if (ruleToRestore == null) {
+            return ruleToAdd; // Cannot restore.
+        }
+
+        // We have found a previous rule to maybe restore. Whether we do that or not, we don't need
+        // to keep it around (if not restored now, it won't be in future calls either).
+        config.deletedRules.remove(deletedKey);
+        ruleToRestore.deletionInstant = null;
+
+        if (origin != UPDATE_ORIGIN_APP) {
+            return ruleToAdd; // Okay to create anew.
+        }
+
+        // "Preserve" the previous rule by considering the azrToAdd an update instead.
+        // Only app-modifiable fields will actually be modified.
+        populateZenRule(ruleToRestore.pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false);
+        return ruleToRestore;
+    }
+
+    private static void maybeReplaceDefaultRule(ZenModeConfig config, AutomaticZenRule addedRule) {
+        if (!Flags.modesApi()) {
+            return;
+        }
+        if (addedRule.getType() == AutomaticZenRule.TYPE_BEDTIME) {
+            // Delete a built-in disabled "Sleeping" rule when a BEDTIME rule is added; it may have
+            // smarter triggers and it will prevent confusion about which one to use.
+            // Note: we must not verify canManageAutomaticZenRule here, since most likely they
+            // won't have the same owner (sleeping - system; bedtime - DWB).
+            ZenRule sleepingRule = config.automaticRules.get(
+                    ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
+            if (sleepingRule != null
+                    && !sleepingRule.enabled
+                    && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) {
+                config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
+            }
+        }
+    }
+
     public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule,
             @ConfigChangeOrigin int origin, String reason, int callingUid) {
         ZenModeConfig newConfig;
@@ -623,7 +685,7 @@
         ZenRule rule = new ZenRule();
         rule.id = implicitRuleId(pkg);
         rule.pkg = pkg;
-        rule.creationTime = System.currentTimeMillis();
+        rule.creationTime = mClock.millis();
 
         Binder.withCleanCallingIdentity(() -> {
             try {
@@ -643,7 +705,7 @@
         rule.condition = null;
         rule.conditionId = new Uri.Builder()
                 .scheme(Condition.SCHEME)
-                .authority("android")
+                .authority(PACKAGE_ANDROID)
                 .appendPath("implicit")
                 .appendPath(pkg)
                 .build();
@@ -672,7 +734,9 @@
             if (ruleToRemove == null) return false;
             if (canManageAutomaticZenRule(ruleToRemove)) {
                 newConfig.automaticRules.remove(id);
-                if (ruleToRemove.getPkg() != null && !"android".equals(ruleToRemove.getPkg())) {
+                maybePreserveRemovedRule(newConfig, ruleToRemove, origin);
+                if (ruleToRemove.getPkg() != null
+                        && !PACKAGE_ANDROID.equals(ruleToRemove.getPkg())) {
                     for (ZenRule currRule : newConfig.automaticRules.values()) {
                         if (currRule.getPkg() != null
                                 && currRule.getPkg().equals(ruleToRemove.getPkg())) {
@@ -702,12 +766,44 @@
                 ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i));
                 if (Objects.equals(rule.getPkg(), packageName) && canManageAutomaticZenRule(rule)) {
                     newConfig.automaticRules.removeAt(i);
+                    maybePreserveRemovedRule(newConfig, rule, origin);
+                }
+            }
+            // If the system is clearing all rules this means DND access is revoked or the package
+            // was uninstalled, so also clear the preserved-deleted rules.
+            if (origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI) {
+                for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
+                    ZenRule rule = newConfig.deletedRules.get(newConfig.deletedRules.keyAt(i));
+                    if (Objects.equals(rule.getPkg(), packageName)) {
+                        newConfig.deletedRules.removeAt(i);
+                    }
                 }
             }
             return setConfigLocked(newConfig, origin, reason, null, true, callingUid);
         }
     }
 
+    private void maybePreserveRemovedRule(ZenModeConfig config, ZenRule ruleToRemove,
+            @ConfigChangeOrigin int origin) {
+        if (!Flags.modesApi()) {
+            return;
+        }
+        // If an app deletes a previously customized rule, keep it around to preserve
+        // the user's customization when/if it's recreated later.
+        // We don't try to preserve system-owned rules because their conditionIds (used as
+        // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot
+        // delete a system-owned rule.
+        if (origin == UPDATE_ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp()
+                && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) {
+            String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove);
+            if (deletedKey != null) {
+                ruleToRemove.deletionInstant = Instant.now(mClock);
+                // Overwrites a previously-deleted rule with the same conditionId, but that's okay.
+                config.deletedRules.put(deletedKey, ruleToRemove);
+            }
+        }
+    }
+
     void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin,
             int callingUid) {
         ZenModeConfig newConfig;
@@ -893,48 +989,206 @@
     }
 
     void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
-            @ConfigChangeOrigin int origin, boolean isNew) {
-        // TODO: b/308671593,b/311406021 - Handle origins more precisely:
-        //  - USER can override anything and updates bitmask of user-modified fields;
-        //  - SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
-        //  - APP can only update if not user-modified.
-        if (rule.enabled != automaticZenRule.isEnabled()) {
-            rule.snoozing = false;
-        }
-        rule.name = automaticZenRule.getName();
-        rule.condition = null;
-        rule.conditionId = automaticZenRule.getConditionId();
-        rule.enabled = automaticZenRule.isEnabled();
-        rule.modified = automaticZenRule.isModified();
-        rule.zenPolicy = automaticZenRule.getZenPolicy();
+                         @ConfigChangeOrigin int origin, boolean isNew) {
         if (Flags.modesApi()) {
-            rule.zenDeviceEffects = fixZenDeviceEffects(
-                    rule.zenDeviceEffects,
-                    automaticZenRule.getDeviceEffects(),
-                    origin);
-        }
-        rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
-                automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
-        rule.configurationActivity = automaticZenRule.getConfigurationActivity();
+            // These values can always be edited by the app, so we apply changes immediately.
+            if (isNew) {
+                rule.id = ZenModeConfig.newRuleId();
+                rule.creationTime = mClock.millis();
+                rule.component = automaticZenRule.getOwner();
+                rule.pkg = pkg;
+            }
 
-        if (isNew) {
-            rule.id = ZenModeConfig.newRuleId();
-            rule.creationTime = System.currentTimeMillis();
-            rule.component = automaticZenRule.getOwner();
-            rule.pkg = pkg;
-        }
-
-        if (Flags.modesApi()) {
+            rule.condition = null;
+            rule.conditionId = automaticZenRule.getConditionId();
+            if (rule.enabled != automaticZenRule.isEnabled()) {
+                rule.snoozing = false;
+            }
+            rule.enabled = automaticZenRule.isEnabled();
+            rule.configurationActivity = automaticZenRule.getConfigurationActivity();
             rule.allowManualInvocation = automaticZenRule.isManualInvocationAllowed();
-            rule.iconResName = drawableResIdToResName(rule.pkg, automaticZenRule.getIconResId());
+            rule.iconResName =
+                    drawableResIdToResName(rule.pkg, automaticZenRule.getIconResId());
             rule.triggerDescription = automaticZenRule.getTriggerDescription();
             rule.type = automaticZenRule.getType();
+            // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
+            rule.modified = automaticZenRule.isModified();
+
+            // Name is treated differently than other values:
+            // App is allowed to update name if the name was not modified by the user (even if
+            // other values have been modified). In this way, if the locale of an app changes,
+            // i18n of the rule name can still occur even if the user has customized the rule
+            // contents.
+            String previousName = rule.name;
+            if (isNew || doesOriginAlwaysUpdateValues(origin)
+                    || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) {
+                rule.name = automaticZenRule.getName();
+            }
+
+            // For the remaining values, rules can always have all values updated if:
+            // * the rule is newly added, or
+            // * the request comes from an origin that can always update values, like the user, or
+            // * the rule has not yet been user modified, and thus can be updated by the app.
+            boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin)
+                    || rule.canBeUpdatedByApp();
+
+            // For all other values, if updates are not allowed, we discard the update.
+            if (!updateValues) {
+                return;
+            }
+
+            // Updates the bitmasks if the origin of the change is the user.
+            boolean updateBitmask = (origin == UPDATE_ORIGIN_USER);
+
+            if (updateBitmask && !TextUtils.equals(previousName, automaticZenRule.getName())) {
+                rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
+            }
+            int newZenMode = NotificationManager.zenModeFromInterruptionFilter(
+                    automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+            if (updateBitmask && rule.zenMode != newZenMode) {
+                rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+            }
+
+            // Updates the values in the ZenRule itself.
+            rule.zenMode = newZenMode;
+
+            // Updates the bitmask and values for all policy fields, based on the origin.
+            rule.zenPolicy = updatePolicy(rule.zenPolicy, automaticZenRule.getZenPolicy(),
+                    updateBitmask);
+            // Updates the bitmask and values for all device effect fields, based on the origin.
+            rule.zenDeviceEffects = updateZenDeviceEffects(
+                    rule.zenDeviceEffects, automaticZenRule.getDeviceEffects(),
+                    origin == UPDATE_ORIGIN_APP, updateBitmask);
+        } else {
+            if (rule.enabled != automaticZenRule.isEnabled()) {
+                rule.snoozing = false;
+            }
+            rule.name = automaticZenRule.getName();
+            rule.condition = null;
+            rule.conditionId = automaticZenRule.getConditionId();
+            rule.enabled = automaticZenRule.isEnabled();
+            rule.modified = automaticZenRule.isModified();
+            rule.zenPolicy = automaticZenRule.getZenPolicy();
+            if (Flags.modesApi()) {
+                rule.zenDeviceEffects = updateZenDeviceEffects(
+                        rule.zenDeviceEffects,
+                        automaticZenRule.getDeviceEffects(),
+                        origin == UPDATE_ORIGIN_APP,
+                        origin == UPDATE_ORIGIN_USER);
+            }
+            rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
+                    automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+            rule.configurationActivity = automaticZenRule.getConfigurationActivity();
+
+            if (isNew) {
+                rule.id = ZenModeConfig.newRuleId();
+                rule.creationTime = System.currentTimeMillis();
+                rule.component = automaticZenRule.getOwner();
+                rule.pkg = pkg;
+            }
         }
     }
 
     /**
-     * Fix {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule.
-     *
+     * Returns true when fields can always be updated, based on the provided origin of an AZR
+     * change. (Note that regardless of origin, fields can always be updated if they're not already
+     * user modified.)
+     */
+    private static boolean doesOriginAlwaysUpdateValues(@ConfigChangeOrigin int origin) {
+        return origin == UPDATE_ORIGIN_USER || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI;
+    }
+
+    /**
+     * Modifies {@link ZenPolicy} that is being stored as part of a new or updated ZenRule.
+     * Returns a policy based on {@code oldPolicy}, but with fields updated to match
+     * {@code newPolicy} where they differ, and updating the internal user-modified bitmask to
+     * track these changes, if applicable based on {@code origin}.
+     */
+    @Nullable
+    private ZenPolicy updatePolicy(@Nullable ZenPolicy oldPolicy, @Nullable ZenPolicy newPolicy,
+                                   boolean updateBitmask) {
+        // If the update is to make the policy null, we don't need to update the bitmask,
+        // because it won't be stored anywhere anyway.
+        if (newPolicy == null) {
+            return null;
+        }
+
+        // If oldPolicy is null, we compare against the default policy when determining which
+        // fields in the bitmask should be marked as updated.
+        if (oldPolicy == null) {
+            oldPolicy = mDefaultConfig.toZenPolicy();
+        }
+
+        int userModifiedFields = oldPolicy.getUserModifiedFields();
+        if (updateBitmask) {
+            if (oldPolicy.getPriorityMessageSenders() != newPolicy.getPriorityMessageSenders()) {
+                userModifiedFields |= ZenPolicy.FIELD_MESSAGES;
+            }
+            if (oldPolicy.getPriorityCallSenders() != newPolicy.getPriorityCallSenders()) {
+                userModifiedFields |= ZenPolicy.FIELD_CALLS;
+            }
+            if (oldPolicy.getPriorityConversationSenders()
+                    != newPolicy.getPriorityConversationSenders()) {
+                userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS;
+            }
+            if (oldPolicy.getAllowedChannels() != newPolicy.getAllowedChannels()) {
+                userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS;
+            }
+            if (oldPolicy.getPriorityCategoryReminders()
+                    != newPolicy.getPriorityCategoryReminders()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS;
+            }
+            if (oldPolicy.getPriorityCategoryEvents() != newPolicy.getPriorityCategoryEvents()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS;
+            }
+            if (oldPolicy.getPriorityCategoryRepeatCallers()
+                    != newPolicy.getPriorityCategoryRepeatCallers()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS;
+            }
+            if (oldPolicy.getPriorityCategoryAlarms() != newPolicy.getPriorityCategoryAlarms()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_ALARMS;
+            }
+            if (oldPolicy.getPriorityCategoryMedia() != newPolicy.getPriorityCategoryMedia()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_MEDIA;
+            }
+            if (oldPolicy.getPriorityCategorySystem() != newPolicy.getPriorityCategorySystem()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM;
+            }
+            // Visual effects
+            if (oldPolicy.getVisualEffectFullScreenIntent()
+                    != newPolicy.getVisualEffectFullScreenIntent()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT;
+            }
+            if (oldPolicy.getVisualEffectLights() != newPolicy.getVisualEffectLights()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS;
+            }
+            if (oldPolicy.getVisualEffectPeek() != newPolicy.getVisualEffectPeek()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_PEEK;
+            }
+            if (oldPolicy.getVisualEffectStatusBar() != newPolicy.getVisualEffectStatusBar()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_STATUS_BAR;
+            }
+            if (oldPolicy.getVisualEffectBadge() != newPolicy.getVisualEffectBadge()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_BADGE;
+            }
+            if (oldPolicy.getVisualEffectAmbient() != newPolicy.getVisualEffectAmbient()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT;
+            }
+            if (oldPolicy.getVisualEffectNotificationList()
+                    != newPolicy.getVisualEffectNotificationList()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_NOTIFICATION_LIST;
+            }
+        }
+
+        // After all bitmask changes have been made, sets the bitmask.
+        return new ZenPolicy.Builder(newPolicy).setUserModifiedFields(userModifiedFields).build();
+    }
+
+    /**
+     * Modifies {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule.
+     * Returns a {@link ZenDeviceEffects} based on {@code oldEffects}, but with fields updated to
+     * match {@code newEffects} where they differ, and updating the internal user-modified bitmask
+     * to track these changes, if applicable based on {@code origin}.
      * <ul>
      *     <li> Apps cannot turn on hidden effects (those tagged as {@code @hide}) since they are
      *     intended for platform-specific rules (e.g. wearables). If it's a new rule, we blank them
@@ -942,38 +1196,85 @@
      * </ul>
      */
     @Nullable
-    private static ZenDeviceEffects fixZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects,
-            @Nullable ZenDeviceEffects newEffects, @ConfigChangeOrigin int origin) {
-        // TODO: b/308671593,b/311406021 - Handle origins more precisely:
-        //  - USER can override anything and updates bitmask of user-modified fields;
-        //  - SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
-        //  - APP can only update if not user-modified.
-        if (origin != UPDATE_ORIGIN_APP) {
-            return newEffects;
-        }
-
+    private static ZenDeviceEffects updateZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects,
+                                                           @Nullable ZenDeviceEffects newEffects,
+                                                           boolean isFromApp,
+                                                           boolean updateBitmask) {
         if (newEffects == null) {
             return null;
         }
-        if (oldEffects != null) {
-            return new ZenDeviceEffects.Builder(newEffects)
-                    .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
-                    .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
-                    .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
-                    .setShouldDisableTouch(oldEffects.shouldDisableTouch())
-                    .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
-                    .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
-                    .build();
-        } else {
-            return new ZenDeviceEffects.Builder(newEffects)
-                    .setShouldDisableAutoBrightness(false)
-                    .setShouldDisableTapToWake(false)
-                    .setShouldDisableTiltToWake(false)
-                    .setShouldDisableTouch(false)
-                    .setShouldMinimizeRadioUsage(false)
-                    .setShouldMaximizeDoze(false)
-                    .build();
+
+        // Since newEffects is not null, we want to adopt all the new provided device effects.
+        ZenDeviceEffects.Builder builder = new ZenDeviceEffects.Builder(newEffects);
+
+        if (isFromApp) {
+            if (oldEffects != null) {
+                // We can do this because we know we don't need to update the bitmask FROM_APP.
+                return builder
+                        .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
+                        .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
+                        .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
+                        .setShouldDisableTouch(oldEffects.shouldDisableTouch())
+                        .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
+                        .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
+                        .build();
+            } else {
+                return builder
+                        .setShouldDisableAutoBrightness(false)
+                        .setShouldDisableTapToWake(false)
+                        .setShouldDisableTiltToWake(false)
+                        .setShouldDisableTouch(false)
+                        .setShouldMinimizeRadioUsage(false)
+                        .setShouldMaximizeDoze(false)
+                        .build();
+            }
         }
+
+        // If oldEffects is null, we compare against the default device effects object when
+        // determining which fields in the bitmask should be marked as updated.
+        if (oldEffects == null) {
+            oldEffects = new ZenDeviceEffects.Builder().build();
+        }
+
+        int userModifiedFields = oldEffects.getUserModifiedFields();
+        if (updateBitmask) {
+            if (oldEffects.shouldDisplayGrayscale() != newEffects.shouldDisplayGrayscale()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_GRAYSCALE;
+            }
+            if (oldEffects.shouldSuppressAmbientDisplay()
+                    != newEffects.shouldSuppressAmbientDisplay()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_SUPPRESS_AMBIENT_DISPLAY;
+            }
+            if (oldEffects.shouldDimWallpaper() != newEffects.shouldDimWallpaper()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DIM_WALLPAPER;
+            }
+            if (oldEffects.shouldUseNightMode() != newEffects.shouldUseNightMode()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_NIGHT_MODE;
+            }
+            if (oldEffects.shouldDisableAutoBrightness()
+                    != newEffects.shouldDisableAutoBrightness()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_AUTO_BRIGHTNESS;
+            }
+            if (oldEffects.shouldDisableTapToWake() != newEffects.shouldDisableTapToWake()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TAP_TO_WAKE;
+            }
+            if (oldEffects.shouldDisableTiltToWake() != newEffects.shouldDisableTiltToWake()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TILT_TO_WAKE;
+            }
+            if (oldEffects.shouldDisableTouch() != newEffects.shouldDisableTouch()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TOUCH;
+            }
+            if (oldEffects.shouldMinimizeRadioUsage() != newEffects.shouldMinimizeRadioUsage()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_MINIMIZE_RADIO_USAGE;
+            }
+            if (oldEffects.shouldMaximizeDoze() != newEffects.shouldMaximizeDoze()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_MAXIMIZE_DOZE;
+            }
+        }
+
+        // Since newEffects is not null, we want to adopt all the new provided device effects.
+        // Set the usermodifiedFields value separately, to reflect the updated bitmask.
+        return builder.setUserModifiedFields(userModifiedFields).build();
     }
 
     private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
@@ -992,6 +1293,7 @@
                     .setOwner(rule.component)
                     .setConfigurationActivity(rule.configurationActivity)
                     .setTriggerDescription(rule.triggerDescription)
+                    .setUserModifiedFields(rule.userModifiedFields)
                     .build();
         } else {
             azr = new AutomaticZenRule(rule.name, rule.component,
@@ -1152,7 +1454,7 @@
             boolean hasDefaultRules = config.automaticRules.containsAll(
                     ZenModeConfig.DEFAULT_RULE_IDS);
 
-            long time = System.currentTimeMillis();
+            long time = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
             if (config.automaticRules != null && config.automaticRules.size() > 0) {
                 for (ZenRule automaticRule : config.automaticRules.values()) {
                     if (forRestore) {
@@ -1171,6 +1473,10 @@
                 // reset zen automatic rules to default on restore or upgrade if:
                 // - doesn't already have default rules and
                 // - all previous automatic rules were disabled
+                //
+                // Note: we don't need to check to avoid restoring the Sleeping rule if there is a
+                // TYPE_BEDTIME rule because the config is from an old version and thus by
+                // definition cannot have a rule with TYPE_BEDTIME (or any other type).
                 config.automaticRules = new ArrayMap<>();
                 for (ZenRule rule : mDefaultConfig.automaticRules.values()) {
                     config.automaticRules.put(rule.id, rule);
@@ -1188,6 +1494,12 @@
                 Settings.Secure.putIntForUser(mContext.getContentResolver(),
                         Settings.Secure.ZEN_SETTINGS_UPDATED, 1, userId);
             }
+
+            if (Flags.modesApi() && forRestore) {
+                // Note: forBackup doesn't write deletedRules, but just in case.
+                config.deletedRules.clear();
+            }
+
             if (DEBUG) Log.d(TAG, reason);
             synchronized (mConfigLock) {
                 setConfigLocked(config, null,
@@ -1205,7 +1517,7 @@
                 if (forBackup && mConfigs.keyAt(i) != userId) {
                     continue;
                 }
-                mConfigs.valueAt(i).writeXml(out, version);
+                mConfigs.valueAt(i).writeXml(out, version, forBackup);
             }
         }
     }
@@ -1237,28 +1549,51 @@
     }
 
     /**
-     * Removes old rule instances whose owner is not installed.
+     * Cleans up obsolete rules:
+     * <ul>
+     *     <li>Rule instances whose owner is not installed.
+     *     <li>Deleted rules that were deleted more than 30 days ago.
+     * </ul>
      */
     private void cleanUpZenRules() {
-        long currentTime = System.currentTimeMillis();
+        Instant keptRuleThreshold = mClock.instant().minus(DELETED_RULE_KEPT_FOR);
         synchronized (mConfigLock) {
             final ZenModeConfig newConfig = mConfig.copy();
-            if (newConfig.automaticRules != null) {
-                for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) {
-                    ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i));
-                    if (RULE_INSTANCE_GRACE_PERIOD < (currentTime - rule.creationTime)) {
-                        try {
-                            if (rule.getPkg() != null) {
-                                mPm.getPackageInfo(rule.getPkg(), PackageManager.MATCH_ANY_USER);
-                            }
-                        } catch (PackageManager.NameNotFoundException e) {
-                            newConfig.automaticRules.removeAt(i);
-                        }
+
+            deleteRulesWithoutOwner(newConfig.automaticRules);
+            if (Flags.modesApi()) {
+                deleteRulesWithoutOwner(newConfig.deletedRules);
+                for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
+                    ZenRule deletedRule = newConfig.deletedRules.valueAt(i);
+                    if (deletedRule.deletionInstant == null
+                            || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) {
+                        newConfig.deletedRules.removeAt(i);
                     }
                 }
             }
-            setConfigLocked(newConfig, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "cleanUpZenRules",
-                    Process.SYSTEM_UID);
+
+            if (!newConfig.equals(mConfig)) {
+                setConfigLocked(newConfig, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+                        "cleanUpZenRules", Process.SYSTEM_UID);
+            }
+        }
+    }
+
+    private void deleteRulesWithoutOwner(ArrayMap<String, ZenRule> ruleList) {
+        long currentTime = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
+        if (ruleList != null) {
+            for (int i = ruleList.size() - 1; i >= 0; i--) {
+                ZenRule rule = ruleList.valueAt(i);
+                if (RULE_INSTANCE_GRACE_PERIOD < (currentTime - rule.creationTime)) {
+                    try {
+                        if (rule.getPkg() != null) {
+                            mPm.getPackageInfo(rule.getPkg(), PackageManager.MATCH_ANY_USER);
+                        }
+                    } catch (PackageManager.NameNotFoundException e) {
+                        ruleList.removeAt(i);
+                    }
+                }
+            }
         }
     }
 
@@ -2023,6 +2358,7 @@
         if (resId == 0) {
             return null;
         }
+        Objects.requireNonNull(packageName);
         try {
             final Resources res = mPm.getResourcesForApplication(packageName);
             return res.getResourceName(resId);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 49db7fc..47967db 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -56,4 +56,11 @@
   namespace: "systemui"
   description: "When this flag is on, NMS will no longer call removeMessage() and hasCallbacks() on Handler"
   bug: "311051285"
+}
+
+flag {
+  name: "notification_test"
+  namespace: "systemui"
+  description: "Timing test, no functionality"
+  bug: "316931130"
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java
index f6e7ef3..9ce3cb3 100644
--- a/services/core/java/com/android/server/os/NativeTombstoneManager.java
+++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java
@@ -41,14 +41,13 @@
 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;
 
@@ -130,18 +129,21 @@
             return;
         }
 
-        String processName = "UNKNOWN";
         final boolean isProtoFile = filename.endsWith(".pb");
-        File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb");
-
-        Optional<TombstoneFile> parsedTombstone = handleProtoTombstone(protoPath, isProtoFile);
-        if (parsedTombstone.isPresent()) {
-            processName = parsedTombstone.get().getProcessName();
+        if (!isProtoFile) {
+            return;
         }
-        BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile, processName, mTmpFileLock);
+
+        Optional<ParsedTombstone> parsedTombstone = handleProtoTombstone(path, true);
+        if (parsedTombstone.isPresent()) {
+            BootReceiver.addTombstoneToDropBox(
+                    mContext, path, parsedTombstone.get().getTombstone(),
+                    parsedTombstone.get().getProcessName(), mTmpFileLock);
+        }
     }
 
-    private Optional<TombstoneFile> handleProtoTombstone(File path, boolean addToList) {
+    private Optional<ParsedTombstone> handleProtoTombstone(
+            File path, boolean addToList) {
         final String filename = path.getName();
         if (!filename.endsWith(".pb")) {
             Slog.w(TAG, "unexpected tombstone name: " + path);
@@ -171,7 +173,7 @@
             return Optional.empty();
         }
 
-        final Optional<TombstoneFile> parsedTombstone = TombstoneFile.parse(pfd);
+        final Optional<ParsedTombstone> parsedTombstone = TombstoneFile.parse(pfd);
         if (!parsedTombstone.isPresent()) {
             IoUtils.closeQuietly(pfd);
             return Optional.empty();
@@ -184,7 +186,7 @@
                     previous.dispose();
                 }
 
-                mTombstones.put(number, parsedTombstone.get());
+                mTombstones.put(number, parsedTombstone.get().getTombstoneFile());
             }
         }
 
@@ -332,6 +334,27 @@
         }
     }
 
+    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;
 
@@ -414,67 +437,21 @@
             }
         }
 
-        static Optional<TombstoneFile> parse(ParcelFileDescriptor pfd) {
-            final FileInputStream is = new FileInputStream(pfd.getFileDescriptor());
-            final ProtoInputStream stream = new ProtoInputStream(is);
+        static Optional<ParsedTombstone> parse(ParcelFileDescriptor pfd) {
+            Tombstone tombstoneProto;
+            try (FileInputStream is = new FileInputStream(pfd.getFileDescriptor())) {
+                final byte[] tombstoneBytes = is.readAllBytes();
 
-            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) {
+                tombstoneProto = Tombstone.parseFrom(
+                        CodedInputStream.newInstance(tombstoneBytes));
+            } catch (IOException 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();
@@ -491,6 +468,7 @@
             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();
@@ -502,11 +480,30 @@
             result.mAppId = appId;
             result.mPid = pid;
             result.mUid = uid;
-            result.mProcessName = processName == null ? "" : processName;
+            result.mProcessName = getCmdLineProcessName(tombstoneProto);
             result.mTimestampMs = timestampMs;
-            result.mCrashReason = crashReason;
+            result.mCrashReason = getCrashReason(tombstoneProto);
 
-            return Optional.of(result);
+            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 "";
         }
 
         public IParcelFileDescriptorRetriever getPfdRetriever() {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 992d8eb..92be4ee 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -175,6 +175,7 @@
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.DexoptParams;
 import com.android.server.art.model.DexoptResult;
+import com.android.server.criticalevents.CriticalEventLog;
 import com.android.server.pm.Installer.LegacyDexoptDisabledException;
 import com.android.server.pm.dex.ArtManagerService;
 import com.android.server.pm.dex.DexManager;
@@ -697,7 +698,7 @@
                     pkgSetting.setUninstallReason(PackageManager.UNINSTALL_REASON_UNKNOWN, userId);
                     pkgSetting.setFirstInstallTime(System.currentTimeMillis(), userId);
                     // Clear any existing archive state.
-                    pkgSetting.setArchiveState(null, userId);
+                    mPm.mInstallerService.mPackageArchiver.clearArchiveState(packageName, userId);
                     mPm.mSettings.writePackageRestrictionsLPr(userId);
                     mPm.mSettings.writeKernelMappingLPr(pkgSetting);
                     installed = true;
@@ -957,6 +958,7 @@
         final Set<String> scannedPackages = new ArraySet<>(requests.size());
         final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size());
         final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size());
+        CriticalEventLog.getInstance().logInstallPackagesStarted();
         boolean success = false;
         try {
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI");
@@ -2279,7 +2281,7 @@
                                 installerPackageName);
                     }
                     // Clear any existing archive state.
-                    ps.setArchiveState(null, userId);
+                    mPm.mInstallerService.mPackageArchiver.clearArchiveState(pkgName, userId);
                 } else if (allUsers != null) {
                     // The caller explicitly specified INSTALL_ALL_USERS flag.
                     // Thus, updating the settings to install the app for all users.
@@ -2303,7 +2305,8 @@
                                         installerPackageName);
                             }
                             // Clear any existing archive state.
-                            ps.setArchiveState(null, currentUserId);
+                            mPm.mInstallerService.mPackageArchiver.clearArchiveState(pkgName,
+                                    currentUserId);
                         } else {
                             ps.setInstalled(false, currentUserId);
                         }
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 127bf49..9915554 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -1610,21 +1610,117 @@
                     "Can't access AppMarketActivity for another user")) {
                 return null;
             }
+            final int callingUser = getCallingUserId();
             final long identity = Binder.clearCallingIdentity();
+
             try {
-                // TODO(b/316118005): Add code to launch the app installer for the packageName.
-                Intent appMarketIntent = new Intent(Intent.ACTION_MAIN);
-                appMarketIntent.addCategory(Intent.CATEGORY_APP_MARKET);
-                final PendingIntent pi = PendingIntent.getActivityAsUser(
-                        mContext, /* requestCode */ 0, appMarketIntent, PendingIntent.FLAG_ONE_SHOT
-                                | PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_CANCEL_CURRENT,
-                        /* options */ null, user);
-                return pi == null ? null : pi.getIntentSender();
+                if (packageName == null) {
+                    return buildAppMarketIntentSenderForUser(user);
+                }
+
+                String installerPackageName = getInstallerPackage(packageName, callingUser);
+                if (installerPackageName == null
+                        || mPackageManagerInternal.getPackageUid(
+                                        installerPackageName, /* flags= */ 0, user.getIdentifier())
+                                < 0) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "Can't find installer for "
+                                        + packageName
+                                        + " in user: "
+                                        + user.getIdentifier());
+                    }
+                    return buildAppMarketIntentSenderForUser(user);
+                }
+
+                Intent packageInfoIntent =
+                        buildMarketPackageInfoIntent(
+                                packageName, installerPackageName, callingPackage);
+                if (mPackageManagerInternal
+                        .queryIntentActivities(
+                                packageInfoIntent,
+                                packageInfoIntent.resolveTypeIfNeeded(
+                                        mContext.getContentResolver()),
+                                PackageManager.MATCH_ALL,
+                                Process.myUid(),
+                                user.getIdentifier())
+                        .isEmpty()) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "Can't resolve package info intent for package "
+                                        + packageName
+                                        + " and installer:  "
+                                        + installerPackageName);
+                    }
+                    return buildAppMarketIntentSenderForUser(user);
+                }
+
+                return buildIntentSenderForUser(packageInfoIntent, user);
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
         }
 
+        @Nullable
+        private IntentSender buildAppMarketIntentSenderForUser(@NonNull UserHandle user) {
+            Intent appMarketIntent = new Intent(Intent.ACTION_MAIN);
+            appMarketIntent.addCategory(Intent.CATEGORY_APP_MARKET);
+            return buildIntentSenderForUser(appMarketIntent, user);
+        }
+
+        @Nullable
+        private IntentSender buildIntentSenderForUser(
+                @NonNull Intent intent, @NonNull UserHandle user) {
+            final PendingIntent pi =
+                    PendingIntent.getActivityAsUser(
+                            mContext,
+                            /* requestCode */ 0,
+                            intent,
+                            PendingIntent.FLAG_ONE_SHOT
+                                    | PendingIntent.FLAG_IMMUTABLE
+                                    | PendingIntent.FLAG_CANCEL_CURRENT,
+                            /* options */ null,
+                            user);
+            return pi == null ? null : pi.getIntentSender();
+        }
+
+        @Nullable
+        private String getInstallerPackage(@NonNull String packageName, int callingUserId) {
+            String installerPackageName = null;
+            try {
+                installerPackageName =
+                        mIPM.getInstallSourceInfo(packageName, callingUserId)
+                                .getInstallingPackageName();
+            } catch (RemoteException re) {
+                Slog.e(TAG, "Couldn't find installer for " + packageName, re);
+            }
+
+            return installerPackageName;
+        }
+
+        @NonNull
+        private Intent buildMarketPackageInfoIntent(
+                @NonNull String packageName,
+                @NonNull String installerPackageName,
+                @NonNull String callingPackage) {
+            return new Intent(Intent.ACTION_VIEW)
+                    .setData(
+                            new Uri.Builder()
+                                    .scheme("market")
+                                    .authority("details")
+                                    .appendQueryParameter("id", packageName)
+                                    .build())
+                    .putExtra(
+                            Intent.EXTRA_REFERRER,
+                            new Uri.Builder()
+                                    .scheme("android-app")
+                                    .authority(callingPackage)
+                                    .build())
+                    .setPackage(installerPackageName);
+        }
+
         @Override
         public void startActivityAsUser(IApplicationThread caller, String callingPackage,
                 String callingFeatureId, ComponentName component, Rect sourceBounds,
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 376b061..3e5759a 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -27,6 +27,7 @@
 import static android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_STATUS;
 import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
 import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET;
 import static android.content.pm.PackageManager.DELETE_ARCHIVE;
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
 import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
@@ -70,6 +71,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.FileUtils;
 import android.os.IBinder;
 import android.os.ParcelableException;
 import android.os.Process;
@@ -100,6 +102,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -210,7 +213,6 @@
                                 return;
                             }
 
-                            // TODO(b/278553670) Add special strings for the delete dialog
                             mPm.mInstallerService.uninstall(
                                     new VersionedPackage(packageName,
                                             PackageManager.VERSION_CODE_HIGHEST),
@@ -264,7 +266,7 @@
         try {
             // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options.
             requestUnarchive(packageName, callerPackageName,
-                    getOrCreateUnarchiveIntentSender(userId, packageName),
+                    getOrCreateLauncherListener(userId, packageName),
                     UserHandle.of(userId),
                     false /* showUnarchivalConfirmation= */);
         } catch (Throwable t) {
@@ -308,6 +310,26 @@
         return false;
     }
 
+    void clearArchiveState(String packageName, int userId) {
+        synchronized (mPm.mLock) {
+            PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
+            if (ps != null) {
+                ps.setArchiveState(/* archiveState= */ null, userId);
+            }
+        }
+        mPm.mBackgroundHandler.post(
+                () -> {
+                    File iconsDir = getIconsDir(packageName, userId);
+                    if (!iconsDir.exists()) {
+                        return;
+                    }
+                    // TODO(b/319238030) Move this into installd.
+                    if (!FileUtils.deleteContentsAndDir(iconsDir)) {
+                        Slog.e(TAG, "Failed to clean up archive files for " + packageName);
+                    }
+                });
+    }
+
     @Nullable
     private String getCurrentLauncherPackageName(int userId) {
         ComponentName defaultLauncherComponent = mPm.snapshotComputer().getDefaultHomeActivity(
@@ -329,7 +351,7 @@
         return true;
     }
 
-    private IntentSender getOrCreateUnarchiveIntentSender(int userId, String packageName) {
+    private IntentSender getOrCreateLauncherListener(int userId, String packageName) {
         Pair<Integer, String> key = Pair.create(userId, packageName);
         synchronized (mLauncherIntentSenders) {
             IntentSender intentSender = mLauncherIntentSenders.get(key);
@@ -436,8 +458,8 @@
         if (mainActivity.iconBitmap == null) {
             return null;
         }
-        File iconsDir = createIconsDir(userId);
-        File iconFile = new File(iconsDir, packageName + "-" + index + ".png");
+        File iconsDir = createIconsDir(packageName, userId);
+        File iconFile = new File(iconsDir, index + ".png");
         try (FileOutputStream out = new FileOutputStream(iconFile)) {
             out.write(mainActivity.iconBitmap);
             out.flush();
@@ -453,14 +475,14 @@
             // The app doesn't define an icon. No need to store anything.
             return null;
         }
-        File iconsDir = createIconsDir(userId);
-        File iconFile = new File(iconsDir, packageName + "-" + index + ".png");
+        File iconsDir = createIconsDir(packageName, userId);
+        File iconFile = new File(iconsDir, index + ".png");
         Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0), iconSize);
         try (FileOutputStream out = new FileOutputStream(iconFile)) {
             // Note: Quality is ignored for PNGs.
             if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) {
                 throw new IOException(TextUtils.formatSimple("Failure to store icon file %s",
-                        iconFile.getName()));
+                        iconFile.getAbsolutePath()));
             }
             out.flush();
         }
@@ -515,7 +537,6 @@
     /**
      * Returns true if the app is archivable.
      */
-    // TODO(b/299299569) Exclude system apps
     public boolean isAppArchivable(@NonNull String packageName, @NonNull UserHandle user) {
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(user);
@@ -685,15 +706,14 @@
                 PackageInstaller.SessionParams.MODE_FULL_INSTALL);
         sessionParams.setAppPackageName(packageName);
         sessionParams.installFlags = INSTALL_UNARCHIVE_DRAFT;
-        sessionParams.unarchiveIntentSender = statusReceiver;
 
         int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId);
         // Handles case of repeated unarchival calls for the same package.
-        // TODO(b/316881759) Allow attaching multiple intentSenders to one session.
         int existingSessionId = mPm.mInstallerService.getExistingDraftSessionId(installerUid,
                 sessionParams,
                 userId);
         if (existingSessionId != PackageInstaller.SessionInfo.INVALID_ID) {
+            attachListenerToSession(statusReceiver, existingSessionId, userId);
             return existingSessionId;
         }
 
@@ -702,12 +722,34 @@
                 installerPackage, mContext.getAttributionTag(),
                 installerUid,
                 userId);
+        attachListenerToSession(statusReceiver, sessionId, userId);
+
         // TODO(b/297358628) Also cleanup sessions upon device restart.
         mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId),
                 getUnarchiveForegroundTimeout());
         return sessionId;
     }
 
+    private void attachListenerToSession(IntentSender statusReceiver, int existingSessionId,
+            int userId) {
+        PackageInstallerSession session = mPm.mInstallerService.getSession(existingSessionId);
+        int status = session.getUnarchivalStatus();
+        // Here we handle a race condition that might happen when an installer reports UNARCHIVAL_OK
+        // but hasn't created a session yet. Without this the listener would never receive a success
+        // response.
+        if (status == UNARCHIVAL_OK) {
+            notifyUnarchivalListener(UNARCHIVAL_OK, session.getInstallerPackageName(),
+                    session.params.appPackageName, /* requiredStorageBytes= */ 0,
+                    /* userActionIntent= */ null, Set.of(statusReceiver), userId);
+            return;
+        } else if (status != UNARCHIVAL_STATUS_UNSET) {
+            throw new IllegalStateException(TextUtils.formatSimple("Session %s has unarchive status"
+                    + "%s but is still active.", session.sessionId, status));
+        }
+
+        session.registerUnarchivalListener(statusReceiver);
+    }
+
     /**
      * Returns the icon of an archived app. This is the icon of the main activity of the app.
      *
@@ -772,8 +814,20 @@
     }
 
     @VisibleForTesting
-    Bitmap decodeIcon(ArchiveActivityInfo archiveActivityInfo) {
-        return BitmapFactory.decodeFile(archiveActivityInfo.getIconBitmap().toString());
+    @Nullable
+    Bitmap decodeIcon(ArchiveActivityInfo activityInfo) {
+        Path iconBitmap = activityInfo.getIconBitmap();
+        if (iconBitmap == null) {
+            return null;
+        }
+        Bitmap bitmap = BitmapFactory.decodeFile(iconBitmap.toString());
+        // TODO(b/278553670) We should throw here after some time. Failing graciously now because
+        // we've just changed the place where we store icons.
+        if (bitmap == null) {
+            Slog.e(TAG, "Archived icon cannot be decoded " + iconBitmap.toAbsolutePath());
+            return null;
+        }
+        return bitmap;
     }
 
     Bitmap includeCloudOverlay(Bitmap bitmap) {
@@ -883,13 +937,7 @@
 
     void notifyUnarchivalListener(int status, String installerPackageName, String appPackageName,
             long requiredStorageBytes, @Nullable PendingIntent userActionIntent,
-            @Nullable IntentSender unarchiveIntentSender, int userId) {
-        if (unarchiveIntentSender == null) {
-            // Maybe this can happen if the installer calls reportUnarchivalStatus twice in quick
-            // succession.
-            return;
-        }
-
+            Set<IntentSender> unarchiveIntentSenders, int userId) {
         final Intent broadcastIntent = new Intent();
         broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, appPackageName);
         broadcastIntent.putExtra(EXTRA_UNARCHIVE_STATUS, status);
@@ -909,15 +957,16 @@
         final BroadcastOptions options = BroadcastOptions.makeBasic();
         options.setPendingIntentBackgroundActivityStartMode(
                 MODE_BACKGROUND_ACTIVITY_START_DENIED);
-        try {
-            unarchiveIntentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null,
-                    /* handler= */ null, /* requiredPermission= */ null,
-                    options.toBundle());
-        } catch (IntentSender.SendIntentException e) {
-            Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e);
-        } finally {
-            synchronized (mLauncherIntentSenders) {
-                mLauncherIntentSenders.remove(Pair.create(userId, appPackageName));
+        for (IntentSender intentSender : unarchiveIntentSenders) {
+            try {
+                intentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null,
+                        /* handler= */ null, /* requiredPermission= */ null, options.toBundle());
+            } catch (IntentSender.SendIntentException e) {
+                Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e);
+            } finally {
+                synchronized (mLauncherIntentSenders) {
+                    mLauncherIntentSenders.remove(Pair.create(userId, appPackageName));
+                }
             }
         }
     }
@@ -1059,8 +1108,9 @@
         }
     }
 
-    private static File createIconsDir(@UserIdInt int userId) throws IOException {
-        File iconsDir = getIconsDir(userId);
+    private static File createIconsDir(String packageName, @UserIdInt int userId)
+            throws IOException {
+        File iconsDir = getIconsDir(packageName, userId);
         if (!iconsDir.isDirectory()) {
             iconsDir.delete();
             iconsDir.mkdirs();
@@ -1072,8 +1122,10 @@
         return iconsDir;
     }
 
-    private static File getIconsDir(int userId) {
-        return new File(Environment.getDataSystemCeDirectory(userId), ARCHIVE_ICONS_DIR);
+    private static File getIconsDir(String packageName, int userId) {
+        return new File(
+                new File(Environment.getDataSystemCeDirectory(userId), ARCHIVE_ICONS_DIR),
+                packageName);
     }
 
     private static byte[] bytesFromBitmapFile(Path path) throws IOException {
@@ -1179,7 +1231,7 @@
             UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class);
             if (extraIntent != null && user != null
                     && mAppStateHelper.isAppTopVisible(
-                            getCurrentLauncherPackageName(user.getIdentifier()))) {
+                    getCurrentLauncherPackageName(user.getIdentifier()))) {
                 extraIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                 mContext.startActivityAsUser(extraIntent, user);
             }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index cbd65a4..fdcd28b 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -25,6 +25,7 @@
 import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED;
 import static android.content.pm.PackageInstaller.UNARCHIVAL_GENERIC_ERROR;
 import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
+import static android.content.pm.PackageManager.DELETE_ARCHIVE;
 import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.SYSTEM_UID;
@@ -291,9 +292,7 @@
     @NonNull
     private final RequestThrottle mSettingsWriteRequest = new RequestThrottle(IoThread.getHandler(),
             () -> {
-                synchronized (mSessions) {
-                    return writeSessionsLocked();
-                }
+                return writeSessions();
             });
 
     public PackageInstallerService(Context context, PackageManagerService pm,
@@ -600,9 +599,16 @@
                 mHistoricalSessionsByInstaller.get(installerUid) + 1);
     }
 
-    @GuardedBy("mSessions")
-    private boolean writeSessionsLocked() {
-        if (LOGD) Slog.v(TAG, "writeSessionsLocked()");
+    private boolean writeSessions() {
+        if (LOGD) Slog.v(TAG, "writeSessions()");
+        final PackageInstallerSession[] sessions;
+        synchronized (mSessions) {
+            final int size = mSessions.size();
+            sessions = new PackageInstallerSession[size];
+            for (int i = 0; i < size; i++) {
+                sessions[i] = mSessions.valueAt(i);
+            }
+        }
 
         FileOutputStream fos = null;
         try {
@@ -611,9 +617,7 @@
             final TypedXmlSerializer out = Xml.resolveSerializer(fos);
             out.startDocument(null, true);
             out.startTag(null, TAG_SESSIONS);
-            final int size = mSessions.size();
-            for (int i = 0; i < size; i++) {
-                final PackageInstallerSession session = mSessions.valueAt(i);
+            for (var session : sessions) {
                 session.write(out, mSessionsDir);
             }
             out.endTag(null, TAG_SESSIONS);
@@ -1402,7 +1406,7 @@
 
         final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
                 statusReceiver, versionedPackage.getPackageName(),
-                canSilentlyInstallPackage, userId);
+                canSilentlyInstallPackage, userId, mPackageArchiver, flags);
         final boolean shouldShowConfirmationDialog =
                 (flags & PackageManager.DELETE_SHOW_DIALOG) != 0;
         if (!shouldShowConfirmationDialog
@@ -1756,26 +1760,8 @@
                         binderUid, unarchiveId));
             }
 
-            IntentSender unarchiveIntentSender = session.params.unarchiveIntentSender;
-            if (unarchiveIntentSender == null) {
-                throw new IllegalStateException(
-                        TextUtils.formatSimple(
-                                "Unarchival status for ID %s has already been set or a "
-                                        + "session has been created for it already by the "
-                                        + "caller.",
-                                unarchiveId));
-            }
-
-            // Execute expensive calls outside the sync block.
-            mPm.mHandler.post(
-                    () -> mPackageArchiver.notifyUnarchivalListener(status,
-                            session.getInstallerPackageName(),
-                            session.params.appPackageName, requiredStorageBytes, userActionIntent,
-                            unarchiveIntentSender, userId));
-            session.params.unarchiveIntentSender = null;
-            if (status != UNARCHIVAL_OK) {
-                Binder.withCleanCallingIdentity(session::abandon);
-            }
+            session.reportUnarchivalStatus(status, unarchiveId, requiredStorageBytes,
+                    userActionIntent);
         }
     }
 
@@ -1843,9 +1829,23 @@
         private final IntentSender mTarget;
         private final String mPackageName;
         private final Notification mNotification;
+        private final int mUserId;
 
-        public PackageDeleteObserverAdapter(Context context, IntentSender target,
+        @DeleteFlags
+        private final int mFlags;
+
+        @Nullable
+        private final PackageArchiver mPackageArchiver;
+
+        PackageDeleteObserverAdapter(Context context, IntentSender target,
                 String packageName, boolean showNotification, int userId) {
+            this(context, target, packageName, showNotification, userId,
+                    /* packageArchiver= */ null, /* flags= */ 0);
+        }
+
+        PackageDeleteObserverAdapter(Context context, IntentSender target,
+                String packageName, boolean showNotification, int userId,
+                PackageArchiver packageArchiver, @DeleteFlags int flags) {
             mContext = context;
             mTarget = target;
             mPackageName = packageName;
@@ -1857,6 +1857,9 @@
             } else {
                 mNotification = null;
             }
+            mUserId = userId;
+            mPackageArchiver = packageArchiver;
+            mFlags = flags;
         }
 
         private String getDeviceOwnerDeletedPackageMsg() {
@@ -1898,6 +1901,11 @@
                         SystemMessage.NOTE_PACKAGE_STATE,
                         mNotification);
             }
+            if (mPackageArchiver != null
+                    && PackageManager.DELETE_SUCCEEDED != returnCode
+                    && (mFlags & DELETE_ARCHIVE) != 0) {
+                mPackageArchiver.clearArchiveState(mPackageName, mUserId);
+            }
             if (mTarget == null) {
                 return;
             }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 4adb60c..117d03f 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -22,6 +22,8 @@
 import static android.content.pm.DataLoaderType.INCREMENTAL;
 import static android.content.pm.DataLoaderType.STREAMING;
 import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
+import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET;
 import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH;
 import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED;
 import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE;
@@ -65,6 +67,7 @@
 import android.app.BroadcastOptions;
 import android.app.Notification;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.app.admin.DevicePolicyEventLogger;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManagerInternal;
@@ -97,6 +100,7 @@
 import android.content.pm.PackageInstaller.PreapprovalDetails;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.PackageInstaller.UnarchivalStatus;
 import android.content.pm.PackageInstaller.UserActionReason;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.PackageInfoFlags;
@@ -771,6 +775,10 @@
     private final List<String> mResolvedInstructionSets = new ArrayList<>();
     @GuardedBy("mLock")
     private final List<String> mResolvedNativeLibPaths = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private final Set<IntentSender> mUnarchivalListeners = new ArraySet<>();
+
     @GuardedBy("mLock")
     private File mInheritedFilesBase;
     @GuardedBy("mLock")
@@ -796,6 +804,9 @@
     @GuardedBy("mLock")
     private int mValidatedTargetSdk = INVALID_TARGET_SDK_VERSION;
 
+    @UnarchivalStatus
+    private int mUnarchivalStatus = UNARCHIVAL_STATUS_UNSET;
+
     private static final FileFilter sAddedApkFilter = new FileFilter() {
         @Override
         public boolean accept(File file) {
@@ -5088,6 +5099,44 @@
         }
     }
 
+    void registerUnarchivalListener(IntentSender intentSender) {
+        synchronized (mLock) {
+            this.mUnarchivalListeners.add(intentSender);
+        }
+    }
+
+    Set<IntentSender> getUnarchivalListeners() {
+        synchronized (mLock) {
+            return new ArraySet<>(mUnarchivalListeners);
+        }
+    }
+
+    void reportUnarchivalStatus(@UnarchivalStatus int status, int unarchiveId,
+            long requiredStorageBytes, PendingIntent userActionIntent) {
+        if (getUnarchivalStatus() != UNARCHIVAL_STATUS_UNSET) {
+            throw new IllegalStateException(
+                    TextUtils.formatSimple(
+                            "Unarchival status for ID %s has already been set or a session has "
+                                    + "been created for it already by the caller.",
+                            unarchiveId));
+        }
+        mUnarchivalStatus = status;
+
+        // Execute expensive calls outside the sync block.
+        mPm.mHandler.post(
+                () -> mPm.mInstallerService.mPackageArchiver.notifyUnarchivalListener(status,
+                        getInstallerPackageName(), params.appPackageName, requiredStorageBytes,
+                        userActionIntent, getUnarchivalListeners(), userId));
+        if (status != UNARCHIVAL_OK) {
+            Binder.withCleanCallingIdentity(this::abandon);
+        }
+    }
+
+    @UnarchivalStatus
+    int getUnarchivalStatus() {
+        return this.mUnarchivalStatus;
+    }
+
     /**
      * Free up storage used by this session and its children.
      * Must not be called on a child session.
diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
index d05e4c6..cf5de89 100644
--- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
+++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
@@ -48,6 +48,7 @@
 class PackageMonitorCallbackHelper {
 
     private static final boolean DEBUG = false;
+    private static final String TAG = "PackageMonitorCallbackHelper";
 
     @NonNull
     private final Object mLock = new Object();
@@ -243,25 +244,33 @@
                 return;
             }
             int registerUid = registerUser.getUid();
+            if (allowUids != null && registerUid != Process.SYSTEM_UID
+                    && !ArrayUtils.contains(allowUids, registerUid)) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Skip invoke PackageMonitorCallback for " + intent.getAction()
+                            + ", uid " + registerUid);
+                }
+                return;
+            }
+            Intent newIntent = intent;
             if (filterExtrasFunction != null) {
                 final Bundle extras = intent.getExtras();
                 if (extras != null) {
                     final Bundle filteredExtras = filterExtrasFunction.apply(registerUid, extras);
-                    if (filteredExtras != null) {
-                        intent.replaceExtras(filteredExtras);
+                    if (filteredExtras == null) {
+                        // caller is unable to access this intent
+                        if (DEBUG) {
+                            Slog.w(TAG,
+                                    "Skip invoke PackageMonitorCallback for " + intent.getAction()
+                                            + " because null filteredExtras");
+                        }
+                        return;
                     }
+                    newIntent = new Intent(newIntent);
+                    newIntent.replaceExtras(filteredExtras);
                 }
             }
-            if (allowUids != null && registerUid != Process.SYSTEM_UID
-                    && !ArrayUtils.contains(allowUids, registerUid)) {
-                if (DEBUG) {
-                    Slog.w("PackageMonitorCallbackHelper",
-                            "Skip invoke PackageMonitorCallback for " + intent.getAction()
-                                    + ", uid " + registerUid);
-                }
-                return;
-            }
-            invokeCallback(callback, intent);
+            invokeCallback(callback, newIntent);
         }));
     }
 
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 3adeb4b..446c629 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -77,6 +77,7 @@
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.LocaleList;
 import android.os.Looper;
@@ -113,7 +114,6 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.logging.MetricsLogger;
-import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
@@ -485,7 +485,14 @@
     }
 
     public ShortcutService(Context context) {
-        this(context, BackgroundThread.get().getLooper(), /*onyForPackgeManagerApis*/ false);
+        this(context, getBgLooper(), /*onyForPackgeManagerApis*/ false);
+    }
+
+    private static Looper getBgLooper() {
+        final HandlerThread handlerThread = new HandlerThread("shortcut",
+                android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        handlerThread.start();
+        return handlerThread.getLooper();
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index 7d87d1b..cef3244 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -193,7 +193,7 @@
             }
 
             try {
-                sm.prepareUserStorage(volumeUuid, user.id, user.serialNumber, flags);
+                sm.prepareUserStorage(volumeUuid, user.id, flags);
                 synchronized (mPm.mInstallLock) {
                     appDataHelper.reconcileAppsDataLI(volumeUuid, user.id, flags,
                             true /* migrateAppData */);
diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java
index 8adb566..4c42c2d 100644
--- a/services/core/java/com/android/server/pm/UserDataPreparer.java
+++ b/services/core/java/com/android/server/pm/UserDataPreparer.java
@@ -92,7 +92,7 @@
                 volumeUuid, userId, flags, isNewUser);
         try {
             // Prepare CE and/or DE storage.
-            storage.prepareUserStorage(volumeUuid, userId, userSerial, flags);
+            storage.prepareUserStorage(volumeUuid, userId, flags);
 
             // Ensure that the data directories of a removed user with the same ID are not being
             // reused.  New users must get fresh data directories, to avoid leaking data.
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 75b4531..c1b7489 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2337,8 +2337,18 @@
         final long identity = Binder.clearCallingIdentity();
         try {
             final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
-            if (telecomManager != null && telecomManager.isInCall()) {
-                flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+            if (com.android.internal.telephony.flags
+                    .Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+                if (mContext.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_TELECOM)) {
+                    if (telecomManager != null && telecomManager.isInCall()) {
+                        flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+                    }
+                }
+            } else {
+                if (telecomManager != null && telecomManager.isInCall()) {
+                    flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(identity);
@@ -5136,7 +5146,7 @@
 
             t.traceBegin("createUserStorageKeys");
             final StorageManager storage = mContext.getSystemService(StorageManager.class);
-            storage.createUserStorageKeys(userId, userInfo.serialNumber, userInfo.isEphemeral());
+            storage.createUserStorageKeys(userId, userInfo.isEphemeral());
             t.traceEnd();
 
             // Only prepare DE storage here.  CE storage will be prepared later, when the user is
diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java
index 752eb53..17c901e 100644
--- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java
+++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java
@@ -254,6 +254,14 @@
             String packageName = verifications.valueAt(index).second;
             AndroidPackage pkg = mConnection.getPackage(packageName);
 
+            if (pkg == null) {
+                if (DEBUG_BROADCASTS) {
+                    Slog.d(TAG,
+                            "Skip sendBroadcasts because null AndroidPackage for " + packageName);
+                }
+                continue;
+            }
+
             String hostsString = buildHostsString(pkg);
 
             Intent intent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION)
diff --git a/services/core/java/com/android/server/policy/Android.bp b/services/core/java/com/android/server/policy/Android.bp
new file mode 100644
index 0000000..fa55bf0
--- /dev/null
+++ b/services/core/java/com/android/server/policy/Android.bp
@@ -0,0 +1,10 @@
+aconfig_declarations {
+    name: "policy_flags",
+    package: "com.android.server.policy",
+    srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "policy_flags_lib",
+    aconfig_declarations: "policy_flags",
+}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index edce3ec..bf669fb 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1301,7 +1301,7 @@
                     Settings.Global.putInt(mContext.getContentResolver(),
                             Settings.Global.THEATER_MODE_ON, 0);
                     if (!interactive) {
-                        wakeUpFromWakeKey(eventTime, KEYCODE_POWER);
+                        wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false);
                     }
                 } else {
                     Slog.i(TAG, "Toggling theater mode on.");
@@ -1317,7 +1317,7 @@
             case MULTI_PRESS_POWER_BRIGHTNESS_BOOST:
                 Slog.i(TAG, "Starting brightness boost.");
                 if (!interactive) {
-                    wakeUpFromWakeKey(eventTime, KEYCODE_POWER);
+                    wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false);
                 }
                 mPowerManager.boostScreenBrightness(eventTime);
                 break;
@@ -4211,6 +4211,14 @@
         return redoLayout;
     }
 
+    /**
+     * Shows the keyguard without immediately locking the device.
+     */
+    @Override
+    public void showDismissibleKeyguard() {
+        mKeyguardDelegate.showDismissibleKeyguard();
+    }
+
     // There are several different flavors of "assistant" that can be launched from
     // various parts of the UI.
 
@@ -5177,7 +5185,8 @@
     public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
             long whenNanos, int policyFlags) {
         if ((policyFlags & FLAG_WAKE) != 0) {
-            if (mWindowWakeUpPolicy.wakeUpFromMotion(whenNanos / 1000000)) {
+            if (mWindowWakeUpPolicy.wakeUpFromMotion(
+                        whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
                 // Woke up. Pass motion events to user.
                 return ACTION_PASS_TO_USER;
             }
@@ -5191,7 +5200,8 @@
         // there will be no dream to intercept the touch and wake into ambient.  The device should
         // wake up in this case.
         if (isTheaterModeEnabled() && (policyFlags & FLAG_WAKE) != 0) {
-            if (mWindowWakeUpPolicy.wakeUpFromMotion(whenNanos / 1000000)) {
+            if (mWindowWakeUpPolicy.wakeUpFromMotion(
+                        whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
                 // Woke up. Pass motion events to user.
                 return ACTION_PASS_TO_USER;
             }
@@ -5526,11 +5536,14 @@
     }
 
     private void wakeUpFromWakeKey(KeyEvent event) {
-        wakeUpFromWakeKey(event.getEventTime(), event.getKeyCode());
+        wakeUpFromWakeKey(
+                event.getEventTime(),
+                event.getKeyCode(),
+                event.getAction() == KeyEvent.ACTION_DOWN);
     }
 
-    private void wakeUpFromWakeKey(long eventTime, int keyCode) {
-        if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode)) {
+    private void wakeUpFromWakeKey(long eventTime, int keyCode, boolean isDown) {
+        if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode, isDown)) {
             final boolean keyCanLaunchHome = keyCode == KEYCODE_HOME || keyCode == KEYCODE_POWER;
             // Start HOME with "reason" extra if sleeping for more than mWakeUpToLastStateTimeout
             if (shouldWakeUpWithHomeIntent() &&  keyCanLaunchHome) {
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 3016b39..2174fd6 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -178,6 +178,12 @@
     int applyKeyguardOcclusionChange();
 
     /**
+     * Shows the keyguard immediately if not already shown.
+     * Does NOT immediately request the device to lock.
+     */
+    void showDismissibleKeyguard();
+
+    /**
      * Interface to the Window Manager state associated with a particular
      * window. You can hold on to an instance of this interface from the call
      * to prepareAddWindow() until removeWindow().
diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
index 392d0d4..a790950 100644
--- a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
@@ -24,6 +24,9 @@
 import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION;
 import static android.view.KeyEvent.KEYCODE_POWER;
 
+import static com.android.server.policy.Flags.supportInputWakeupDelegate;
+
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.PowerManager;
@@ -31,7 +34,11 @@
 import android.os.SystemClock;
 import android.provider.Settings;
 import android.util.Slog;
+import android.view.KeyEvent;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.Clock;
+import com.android.server.LocalServices;
 
 /** Policy controlling the decision and execution of window-related wake ups. */
 class WindowWakeUpPolicy {
@@ -41,18 +48,27 @@
 
     private final Context mContext;
     private final PowerManager mPowerManager;
+    private final Clock mClock;
 
     private final boolean mAllowTheaterModeWakeFromKey;
     private final boolean mAllowTheaterModeWakeFromPowerKey;
     private final boolean mAllowTheaterModeWakeFromMotion;
-    private final boolean mAllowTheaterModeWakeFromMotionWhenNotDreaming;
     private final boolean mAllowTheaterModeWakeFromCameraLens;
     private final boolean mAllowTheaterModeWakeFromLidSwitch;
     private final boolean mAllowTheaterModeWakeFromWakeGesture;
 
+    // The policy will handle input-based wake ups if this delegate is null.
+    @Nullable private WindowWakeUpPolicyInternal.InputWakeUpDelegate mInputWakeUpDelegate;
+
     WindowWakeUpPolicy(Context context) {
+        this(context, Clock.SYSTEM_CLOCK);
+    }
+
+    @VisibleForTesting
+    WindowWakeUpPolicy(Context context, Clock clock) {
         mContext = context;
         mPowerManager = context.getSystemService(PowerManager.class);
+        mClock = clock;
 
         final Resources res = context.getResources();
         mAllowTheaterModeWakeFromKey = res.getBoolean(
@@ -62,14 +78,26 @@
                     com.android.internal.R.bool.config_allowTheaterModeWakeFromPowerKey);
         mAllowTheaterModeWakeFromMotion = res.getBoolean(
                 com.android.internal.R.bool.config_allowTheaterModeWakeFromMotion);
-        mAllowTheaterModeWakeFromMotionWhenNotDreaming = res.getBoolean(
-                com.android.internal.R.bool.config_allowTheaterModeWakeFromMotionWhenNotDreaming);
         mAllowTheaterModeWakeFromCameraLens = res.getBoolean(
                 com.android.internal.R.bool.config_allowTheaterModeWakeFromCameraLens);
         mAllowTheaterModeWakeFromLidSwitch = res.getBoolean(
                 com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch);
         mAllowTheaterModeWakeFromWakeGesture = res.getBoolean(
                 com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture);
+        if (supportInputWakeupDelegate()) {
+            LocalServices.addService(WindowWakeUpPolicyInternal.class, new LocalService());
+        }
+    }
+
+    private final class LocalService implements WindowWakeUpPolicyInternal {
+        @Override
+        public void setInputWakeUpDelegate(@Nullable InputWakeUpDelegate delegate) {
+            if (!supportInputWakeupDelegate()) {
+                Slog.w(TAG, "Input wake up delegates not supported.");
+                return;
+            }
+            mInputWakeUpDelegate = delegate;
+        }
     }
 
     /**
@@ -77,31 +105,49 @@
      *
      * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
      * @param keyCode the {@link android.view.KeyEvent} key code of the key event.
+     * @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}.
      * @return {@code true} if the policy allows the requested wake up and the request has been
      *      executed; {@code false} otherwise.
      */
-    boolean wakeUpFromKey(long eventTime, int keyCode) {
+    boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown) {
         final boolean wakeAllowedDuringTheaterMode =
                 keyCode == KEYCODE_POWER
                         ? mAllowTheaterModeWakeFromPowerKey
                         : mAllowTheaterModeWakeFromKey;
-        return wakeUp(
+        if (!canWakeUp(wakeAllowedDuringTheaterMode)) {
+            if (DEBUG) Slog.d(TAG, "Unable to wake up from " + KeyEvent.keyCodeToString(keyCode));
+            return false;
+        }
+        if (mInputWakeUpDelegate != null
+                && mInputWakeUpDelegate.wakeUpFromKey(eventTime, keyCode, isDown)) {
+            return true;
+        }
+        wakeUp(
                 eventTime,
-                wakeAllowedDuringTheaterMode,
                 keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY,
                 keyCode == KEYCODE_POWER ? "POWER" : "KEY");
+        return true;
     }
 
     /**
      * Wakes up from a motion event.
      *
      * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
+     * @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}.
      * @return {@code true} if the policy allows the requested wake up and the request has been
      *      executed; {@code false} otherwise.
      */
-    boolean wakeUpFromMotion(long eventTime) {
-        return wakeUp(
-                eventTime, mAllowTheaterModeWakeFromMotion, WAKE_REASON_WAKE_MOTION, "MOTION");
+    boolean wakeUpFromMotion(long eventTime, int source, boolean isDown) {
+        if (!canWakeUp(mAllowTheaterModeWakeFromMotion)) {
+            if (DEBUG) Slog.d(TAG, "Unable to wake up from motion.");
+            return false;
+        }
+        if (mInputWakeUpDelegate != null
+                && mInputWakeUpDelegate.wakeUpFromMotion(eventTime, source, isDown)) {
+            return true;
+        }
+        wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
+        return true;
     }
 
     /**
@@ -112,11 +158,12 @@
      *      executed; {@code false} otherwise.
      */
     boolean wakeUpFromCameraCover(long eventTime) {
-        return wakeUp(
-                eventTime,
-                mAllowTheaterModeWakeFromCameraLens,
-                WAKE_REASON_CAMERA_LAUNCH,
-                "CAMERA_COVER");
+        if (!canWakeUp(mAllowTheaterModeWakeFromCameraLens)) {
+            if (DEBUG) Slog.d(TAG, "Unable to wake up from camera cover.");
+            return false;
+        }
+        wakeUp(eventTime, WAKE_REASON_CAMERA_LAUNCH, "CAMERA_COVER");
+        return true;
     }
 
     /**
@@ -126,11 +173,12 @@
      *      executed; {@code false} otherwise.
      */
     boolean wakeUpFromLid() {
-        return wakeUp(
-                SystemClock.uptimeMillis(),
-                mAllowTheaterModeWakeFromLidSwitch,
-                WAKE_REASON_LID,
-                "LID");
+        if (!canWakeUp(mAllowTheaterModeWakeFromLidSwitch)) {
+            if (DEBUG) Slog.d(TAG, "Unable to wake up from lid.");
+            return false;
+        }
+        wakeUp(mClock.uptimeMillis(), WAKE_REASON_LID, "LID");
+        return true;
     }
 
     /**
@@ -140,11 +188,12 @@
      *      executed; {@code false} otherwise.
      */
     boolean wakeUpFromPowerKeyCameraGesture() {
-        return wakeUp(
-                SystemClock.uptimeMillis(),
-                mAllowTheaterModeWakeFromPowerKey,
-                WAKE_REASON_CAMERA_LAUNCH,
-                "CAMERA_GESTURE_PREVENT_LOCK");
+        if (!canWakeUp(mAllowTheaterModeWakeFromPowerKey)) {
+            if (DEBUG) Slog.d(TAG, "Unable to wake up from power key camera gesture.");
+            return false;
+        }
+        wakeUp(mClock.uptimeMillis(), WAKE_REASON_CAMERA_LAUNCH, "CAMERA_GESTURE_PREVENT_LOCK");
+        return true;
     }
 
     /**
@@ -154,23 +203,23 @@
      *      executed; {@code false} otherwise.
      */
     boolean wakeUpFromWakeGesture() {
-        return wakeUp(
-                SystemClock.uptimeMillis(),
-                mAllowTheaterModeWakeFromWakeGesture,
-                WAKE_REASON_GESTURE,
-                "GESTURE");
+        if (!canWakeUp(mAllowTheaterModeWakeFromWakeGesture)) {
+            if (DEBUG) Slog.d(TAG, "Unable to wake up from gesture.");
+            return false;
+        }
+        wakeUp(mClock.uptimeMillis(), WAKE_REASON_GESTURE, "GESTURE");
+        return true;
     }
 
-    private boolean wakeUp(
-            long wakeTime, boolean wakeInTheaterMode, @WakeReason int reason, String details) {
+    private boolean canWakeUp(boolean wakeInTheaterMode) {
         final boolean isTheaterModeEnabled =
                 Settings.Global.getInt(
                         mContext.getContentResolver(), Settings.Global.THEATER_MODE_ON, 0) == 1;
-        if (!wakeInTheaterMode && isTheaterModeEnabled) {
-            if (DEBUG) Slog.d(TAG, "Unable to wake up from " + details);
-            return false;
-        }
+        return wakeInTheaterMode || !isTheaterModeEnabled;
+    }
+
+    /** Wakes up {@link PowerManager}. */
+    private void wakeUp(long wakeTime, @WakeReason int reason, String details) {
         mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details);
-        return true;
     }
 }
diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicyInternal.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicyInternal.java
new file mode 100644
index 0000000..66a0035
--- /dev/null
+++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicyInternal.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import android.annotation.Nullable;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.Keep;
+import com.android.server.LocalServices;
+
+/** Policy controlling the decision and execution of window-related wake ups. */
+@Keep
+public interface WindowWakeUpPolicyInternal {
+
+    /**
+     * A delegate that can choose to intercept Input-related wake ups.
+     *
+     * <p>This delegate is not meant to control policy decisions on whether or not to wake up. The
+     * policy makes that decision, and forwards the wake up request to the delegate as necessary.
+     * Therefore, the role of the delegate is to handle the actual "waking" of the device in
+     * response to the respective input event.
+     */
+    @Keep
+    interface InputWakeUpDelegate {
+        /**
+         * Wakes up the device in response to a key event.
+         *
+         * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
+         * @param keyCode the {@link android.view.KeyEvent} key code of the key event.
+         * @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}.
+         * @return {@code true} if the delegate handled the wake up. {@code false} if the delegate
+         *      decided not to handle the wake up. The policy will execute the wake up in this case.
+         */
+        boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown);
+        /**
+         * Wakes up the device in response to a motion event.
+         *
+         * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
+         * @param source the {@link android.view.InputDevice} source that caused the event.
+         * @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}.
+         * @return {@code true} if the delegate handled the wake up. {@code false} if the delegate
+         *      decided not to handle the wake up. The policy will execute the wake up in this case.
+         */
+        boolean wakeUpFromMotion(long eventTime, int source, boolean isDown);
+    }
+
+    /**
+     * Allows injecting a delegate for controlling input-based wake ups.
+     *
+     * <p>A delegate can be injected to the policy by system_server components only, and should be
+     * done via the {@link LocalServices} interface.
+     *
+     * <p>There can at most be one active delegate. If there's no delegate set (or if a {@code null}
+     * delegate is set), the policy will handle waking up the device in response to input events.
+     *
+     * @param delegate an implementation of {@link InputWakeUpDelegate} that handles input-based
+     *      wake up requests. {@code null} to let the policy handle these wake ups.
+     */
+    @Keep
+    void setInputWakeUpDelegate(@Nullable InputWakeUpDelegate delegate);
+}
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
index 495e239..d0b70c3 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
@@ -410,6 +410,15 @@
         }
     }
 
+    /**
+     * Request to show the keyguard immediately without immediately locking the device.
+     */
+    public void showDismissibleKeyguard() {
+        if (mKeyguardService != null) {
+            mKeyguardService.showDismissibleKeyguard();
+        }
+    }
+
     public void setCurrentUser(int newUserId) {
         if (mKeyguardService != null) {
             mKeyguardService.setCurrentUser(newUserId);
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
index 774e261..cd789ea 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
@@ -209,6 +209,16 @@
         }
     }
 
+    // Binder interface
+    @Override
+    public void showDismissibleKeyguard() {
+        try {
+            mService.showDismissibleKeyguard();
+        } catch (RemoteException e) {
+            Slog.w(TAG , "Remote Exception", e);
+        }
+    }
+
     @Override // Binder interface
     public void setSwitchingUser(boolean switching) {
         try {
diff --git a/services/core/java/com/android/server/policy/window_policy_flags.aconfig b/services/core/java/com/android/server/policy/window_policy_flags.aconfig
new file mode 100644
index 0000000..ed981e0
--- /dev/null
+++ b/services/core/java/com/android/server/policy/window_policy_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.policy"
+
+flag {
+    name: "support_input_wakeup_delegate"
+    namespace: "wear_frameworks"
+    description: "Whether or not window policy allows injecting input wake-up delegate."
+    bug: "298055811"
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index fdff89a..652cf18 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -411,6 +411,11 @@
         mWakeLockLog.onWakeLockReleased(tag, ownerUid);
     }
 
+    /** Shows the keyguard without requesting the device to immediately lock. */
+    public void showDismissibleKeyguard() {
+        mPolicy.showDismissibleKeyguard();
+    }
+
     private int getBatteryStatsWakeLockMonitorType(int flags) {
         switch (flags & PowerManager.WAKE_LOCK_LEVEL_MASK) {
             case PowerManager.PARTIAL_WAKE_LOCK:
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index ec5172f..2128c991 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -115,6 +115,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.display.BrightnessSynchronizer;
+import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FrameworkStatsLog;
@@ -308,6 +309,7 @@
     private final Context mContext;
     private final ServiceThread mHandlerThread;
     private final Handler mHandler;
+    private final FoldGracePeriodProvider mFoldGracePeriodProvider;
     private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
     @Nullable
     private final BatterySaverStateMachine mBatterySaverStateMachine;
@@ -1007,6 +1009,10 @@
             return new InattentiveSleepWarningController();
         }
 
+        FoldGracePeriodProvider createFoldGracePeriodProvider() {
+            return new FoldGracePeriodProvider();
+        }
+
         public SystemPropertiesWrapper createSystemPropertiesWrapper() {
             return new SystemPropertiesWrapper() {
                 @Override
@@ -1147,6 +1153,7 @@
         mHandler = injector.createHandler(mHandlerThread.getLooper(),
                 new PowerManagerHandlerCallback());
         mConstants = new Constants(mHandler);
+        mFoldGracePeriodProvider = injector.createFoldGracePeriodProvider();
         mAmbientDisplayConfiguration = mInjector.createAmbientDisplayConfiguration(context);
         mAmbientDisplaySuppressionController =
                 mInjector.createAmbientDisplaySuppressionController(
@@ -6933,8 +6940,15 @@
                                 + ") doesn't exist");
                     }
                     if ((flags & PowerManager.GO_TO_SLEEP_FLAG_SOFT_SLEEP) != 0) {
-                        if (powerGroup.hasWakeLockKeepingScreenOnLocked()) {
-                            continue;
+                        if (mFoldGracePeriodProvider.isEnabled()) {
+                            if (!powerGroup.hasWakeLockKeepingScreenOnLocked()) {
+                                mNotifier.showDismissibleKeyguard();
+                            }
+                            continue; // never actually goes to sleep for SOFT_SLEEP
+                        } else {
+                            if (powerGroup.hasWakeLockKeepingScreenOnLocked()) {
+                                continue;
+                            }
                         }
                     }
                     if (isNoDoze) {
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 03f3763..09b19e6 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -190,15 +190,11 @@
     // The maximum number of names wakelocks we will keep track of
     // per uid; once the limit is reached, we batch the remaining wakelocks
     // in to one common name.
-    private static final int MAX_WAKELOCKS_PER_UID;
+    private static final int MAX_WAKELOCKS_PER_UID = isLowRamDevice() ? 40 : 200;
 
-    static {
-        if (ActivityManager.isLowRamDeviceStatic()) {
-            MAX_WAKELOCKS_PER_UID = 40;
-        } else {
-            MAX_WAKELOCKS_PER_UID = 200;
-        }
-    }
+    private static final int CELL_SIGNAL_STRENGTH_LEVEL_COUNT = getCellSignalStrengthLevelCount();
+
+    private static final int MODEM_TX_POWER_LEVEL_COUNT = getModemTxPowerLevelCount();
 
     // Number of transmit power states the Wifi controller can be in.
     private static final int NUM_WIFI_TX_LEVELS = 1;
@@ -278,11 +274,9 @@
     @VisibleForTesting
     protected KernelSingleUidTimeReader mKernelSingleUidTimeReader;
     @VisibleForTesting
-    protected SystemServerCpuThreadReader mSystemServerCpuThreadReader =
-            SystemServerCpuThreadReader.create();
+    protected SystemServerCpuThreadReader mSystemServerCpuThreadReader;
 
-    private final KernelMemoryBandwidthStats mKernelMemoryBandwidthStats
-            = new KernelMemoryBandwidthStats();
+    private KernelMemoryBandwidthStats mKernelMemoryBandwidthStats;
     private final LongSparseArray<SamplingTimer> mKernelMemoryStats = new LongSparseArray<>();
     private int[] mCpuPowerBracketMap;
     private final CpuPowerStatsCollector mCpuPowerStatsCollector;
@@ -323,7 +317,7 @@
     private long mLastRpmStatsUpdateTimeMs = -RPM_STATS_UPDATE_FREQ_MS;
 
     /** Container for Rail Energy Data stats. */
-    private final RailStats mTmpRailStats = new RailStats();
+    private RailStats mTmpRailStats;
 
     /**
      * Estimate UID modem power usage based on their estimated mobile radio active time.
@@ -1031,7 +1025,7 @@
     int mPhoneSignalStrengthBin = -1;
     int mPhoneSignalStrengthBinRaw = -1;
     final StopwatchTimer[] mPhoneSignalStrengthsTimer =
-            new StopwatchTimer[CellSignalStrength.getNumSignalStrengthLevels()];
+            new StopwatchTimer[CELL_SIGNAL_STRENGTH_LEVEL_COUNT];
 
     StopwatchTimer mPhoneSignalScanningTimer;
 
@@ -1723,7 +1717,8 @@
     @VisibleForTesting
     public BatteryStatsImpl(Clock clock, File historyDirectory, @NonNull Handler handler,
             @NonNull PowerStatsUidResolver powerStatsUidResolver) {
-        init(clock);
+        mClock = clock;
+        initKernelStatsReaders();
         mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
         mHandler = handler;
         mPowerStatsUidResolver = powerStatsUidResolver;
@@ -1748,13 +1743,19 @@
         mCpuPowerStatsCollector = null;
     }
 
-    private void init(Clock clock) {
-        mClock = clock;
-        mCpuUidUserSysTimeReader = new KernelCpuUidUserSysTimeReader(true, clock);
-        mCpuUidFreqTimeReader = new KernelCpuUidFreqTimeReader(true, clock);
-        mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, clock);
-        mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, clock);
+    private void initKernelStatsReaders() {
+        if (!isKernelStatsAvailable()) {
+            return;
+        }
+
+        mCpuUidUserSysTimeReader = new KernelCpuUidUserSysTimeReader(true, mClock);
+        mCpuUidFreqTimeReader = new KernelCpuUidFreqTimeReader(true, mClock);
+        mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, mClock);
+        mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, mClock);
         mKernelWakelockReader = new KernelWakelockReader();
+        mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create();
+        mKernelMemoryBandwidthStats = new KernelMemoryBandwidthStats();
+        mTmpRailStats = new RailStats();
     }
 
     /**
@@ -5878,7 +5879,7 @@
 
     @GuardedBy("this")
     void stopAllPhoneSignalStrengthTimersLocked(int except, long elapsedRealtimeMs) {
-        for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) {
+        for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) {
             if (i == except) {
                 continue;
             }
@@ -8450,7 +8451,7 @@
         public ControllerActivityCounterImpl getOrCreateModemControllerActivityLocked() {
             if (mModemControllerActivity == null) {
                 mModemControllerActivity = new ControllerActivityCounterImpl(mBsi.mClock,
-                        mBsi.mOnBatteryTimeBase, ModemActivityInfo.getNumTxPowerLevels());
+                        mBsi.mOnBatteryTimeBase, mBsi.MODEM_TX_POWER_LEVEL_COUNT);
             }
             return mModemControllerActivity;
         }
@@ -10896,7 +10897,8 @@
             @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile,
             @NonNull CpuScalingPolicies cpuScalingPolicies,
             @NonNull PowerStatsUidResolver powerStatsUidResolver) {
-        init(clock);
+        mClock = clock;
+        initKernelStatsReaders();
 
         mBatteryStatsConfig = config;
         mMonotonicClock = monotonicClock;
@@ -10992,7 +10994,7 @@
         mDeviceLightIdlingTimer = new StopwatchTimer(mClock, null, -15, null, mOnBatteryTimeBase);
         mDeviceIdlingTimer = new StopwatchTimer(mClock, null, -12, null, mOnBatteryTimeBase);
         mPhoneOnTimer = new StopwatchTimer(mClock, null, -3, null, mOnBatteryTimeBase);
-        for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) {
+        for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) {
             mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(mClock, null, -200 - i, null,
                     mOnBatteryTimeBase);
         }
@@ -11012,7 +11014,7 @@
         mBluetoothActivity = new ControllerActivityCounterImpl(mClock, mOnBatteryTimeBase,
                 NUM_BT_TX_LEVELS);
         mModemActivity = new ControllerActivityCounterImpl(mClock, mOnBatteryTimeBase,
-                ModemActivityInfo.getNumTxPowerLevels());
+                MODEM_TX_POWER_LEVEL_COUNT);
         mMobileRadioActiveTimer = new StopwatchTimer(mClock, null, -400, null, mOnBatteryTimeBase);
         mMobileRadioActivePerAppTimer = new StopwatchTimer(mClock, null, -401, null,
                 mOnBatteryTimeBase);
@@ -11611,7 +11613,7 @@
         mFlashlightOnTimer.reset(false, elapsedRealtimeUs);
         mCameraOnTimer.reset(false, elapsedRealtimeUs);
         mBluetoothScanTimer.reset(false, elapsedRealtimeUs);
-        for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) {
+        for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) {
             mPhoneSignalStrengthsTimer[i].reset(false, elapsedRealtimeUs);
         }
         mPhoneSignalScanningTimer.reset(false, elapsedRealtimeUs);
@@ -11834,7 +11836,7 @@
     private String[] mWifiIfaces = EmptyArray.STRING;
 
     @GuardedBy("mWifiNetworkLock")
-    private NetworkStats mLastWifiNetworkStats = new NetworkStats(0, -1);
+    private NetworkStats mLastWifiNetworkStats;
 
     private final Object mModemNetworkLock = new Object();
 
@@ -11842,7 +11844,7 @@
     private String[] mModemIfaces = EmptyArray.STRING;
 
     @GuardedBy("mModemNetworkLock")
-    private NetworkStats mLastModemNetworkStats = new NetworkStats(0, -1);
+    private NetworkStats mLastModemNetworkStats;
 
     @VisibleForTesting
     protected NetworkStats readMobileNetworkStatsLocked(
@@ -11875,7 +11877,9 @@
         synchronized (mWifiNetworkLock) {
             final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager);
             if (latestStats != null) {
-                delta = latestStats.subtract(mLastWifiNetworkStats);
+                delta = mLastWifiNetworkStats != null
+                        ? latestStats.subtract(mLastWifiNetworkStats)
+                        : latestStats.subtract(new NetworkStats(0, -1));
                 mLastWifiNetworkStats = latestStats;
             }
         }
@@ -12248,7 +12252,9 @@
         synchronized (mModemNetworkLock) {
             final NetworkStats latestStats = readMobileNetworkStatsLocked(networkStatsManager);
             if (latestStats != null) {
-                delta = latestStats.subtract(mLastModemNetworkStats);
+                delta = latestStats.subtract(mLastModemNetworkStats != null
+                        ? mLastModemNetworkStats
+                        : new NetworkStats(0, -1));
                 mLastModemNetworkStats = latestStats;
             }
         }
@@ -12301,7 +12307,7 @@
                         deltaInfo.getSleepTimeMillis());
                 mModemActivity.getOrCreateRxTimeCounter()
                         .increment(deltaInfo.getReceiveTimeMillis(), elapsedRealtimeMs);
-                for (int lvl = 0; lvl < ModemActivityInfo.getNumTxPowerLevels(); lvl++) {
+                for (int lvl = 0; lvl < MODEM_TX_POWER_LEVEL_COUNT; lvl++) {
                     mModemActivity.getOrCreateTxTimeCounters()[lvl]
                             .increment(deltaInfo.getTransmitDurationMillisAtPowerLevel(lvl),
                                     elapsedRealtimeMs);
@@ -12318,8 +12324,8 @@
                             mPowerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_IDLE)
                             + deltaInfo.getReceiveTimeMillis() *
                             mPowerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX);
-                    for (int i = 0; i < Math.min(ModemActivityInfo.getNumTxPowerLevels(),
-                            CellSignalStrength.getNumSignalStrengthLevels()); i++) {
+                    for (int i = 0; i < Math.min(MODEM_TX_POWER_LEVEL_COUNT,
+                            CELL_SIGNAL_STRENGTH_LEVEL_COUNT); i++) {
                         energyUsed += deltaInfo.getTransmitDurationMillisAtPowerLevel(i)
                                 * mPowerProfile.getAveragePower(
                                         PowerProfile.POWER_MODEM_CONTROLLER_TX, i);
@@ -12441,7 +12447,7 @@
                             }
 
                             if (totalTxPackets > 0 && entry.getTxPackets() > 0) {
-                                for (int lvl = 0; lvl < ModemActivityInfo.getNumTxPowerLevels();
+                                for (int lvl = 0; lvl < MODEM_TX_POWER_LEVEL_COUNT;
                                         lvl++) {
                                     long txMs = entry.getTxPackets()
                                             * deltaInfo.getTransmitDurationMillisAtPowerLevel(lvl);
@@ -12550,7 +12556,7 @@
                 && deltaInfo.getSpecificInfoFrequencyRange(0)
                 == ServiceState.FREQUENCY_RANGE_UNKNOWN) {
             // Specific info data unavailable. Proportionally smear Rx and Tx times across each RAT.
-            final int levelCount = CellSignalStrength.getNumSignalStrengthLevels();
+            final int levelCount = CELL_SIGNAL_STRENGTH_LEVEL_COUNT;
             long[] perSignalStrengthActiveTimeMs = new long[levelCount];
             long totalActiveTimeMs = 0;
 
@@ -12726,13 +12732,13 @@
             return;
         }
         int levelMaxTimeSpent = 0;
-        for (int i = 1; i < ModemActivityInfo.getNumTxPowerLevels(); i++) {
+        for (int i = 1; i < MODEM_TX_POWER_LEVEL_COUNT; i++) {
             if (activityInfo.getTransmitDurationMillisAtPowerLevel(i)
                     > activityInfo.getTransmitDurationMillisAtPowerLevel(levelMaxTimeSpent)) {
                 levelMaxTimeSpent = i;
             }
         }
-        if (levelMaxTimeSpent == ModemActivityInfo.getNumTxPowerLevels() - 1) {
+        if (levelMaxTimeSpent == MODEM_TX_POWER_LEVEL_COUNT - 1) {
             mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
                     HistoryItem.STATE2_CELLULAR_HIGH_TX_POWER_FLAG);
         }
@@ -14821,12 +14827,12 @@
             timeInRatMs[i] = getPhoneDataConnectionTime(i, rawRealTimeUs, which) / 1000;
         }
         long[] timeInRxSignalStrengthLevelMs =
-                new long[CellSignalStrength.getNumSignalStrengthLevels()];
+                new long[CELL_SIGNAL_STRENGTH_LEVEL_COUNT];
         for (int i = 0; i < timeInRxSignalStrengthLevelMs.length; i++) {
             timeInRxSignalStrengthLevelMs[i] =
                 getPhoneSignalStrengthTime(i, rawRealTimeUs, which) / 1000;
         }
-        long[] txTimeMs = new long[Math.min(ModemActivityInfo.getNumTxPowerLevels(),
+        long[] txTimeMs = new long[Math.min(MODEM_TX_POWER_LEVEL_COUNT,
             counter.getTxTimeCounters().length)];
         long totalTxTimeMs = 0;
         for (int i = 0; i < txTimeMs.length; i++) {
@@ -15458,7 +15464,7 @@
 
         public Constants(Handler handler) {
             super(handler);
-            if (ActivityManager.isLowRamDeviceStatic()) {
+            if (isLowRamDevice()) {
                 MAX_HISTORY_FILES = DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE;
                 MAX_HISTORY_BUFFER = DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB * 1024;
             } else {
@@ -15528,12 +15534,10 @@
                         KEY_PROC_STATE_CHANGE_COLLECTION_DELAY_MS,
                         DEFAULT_PROC_STATE_CHANGE_COLLECTION_DELAY_MS);
                 MAX_HISTORY_FILES = mParser.getInt(KEY_MAX_HISTORY_FILES,
-                        ActivityManager.isLowRamDeviceStatic() ?
-                                DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE
-                        : DEFAULT_MAX_HISTORY_FILES);
+                        isLowRamDevice() ? DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE
+                                : DEFAULT_MAX_HISTORY_FILES);
                 MAX_HISTORY_BUFFER = mParser.getInt(KEY_MAX_HISTORY_BUFFER_KB,
-                        ActivityManager.isLowRamDeviceStatic() ?
-                                DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB
+                        isLowRamDevice() ? DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB
                                 : DEFAULT_MAX_HISTORY_BUFFER_KB)
                         * 1024;
                 final String perUidModemModel = mParser.getString(KEY_PER_UID_MODEM_POWER_MODEL,
@@ -16014,7 +16018,7 @@
         mDeviceLightIdlingTimer.readSummaryFromParcelLocked(in);
         mDeviceIdlingTimer.readSummaryFromParcelLocked(in);
         mPhoneOnTimer.readSummaryFromParcelLocked(in);
-        for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) {
+        for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) {
             mPhoneSignalStrengthsTimer[i].readSummaryFromParcelLocked(in);
         }
         mPhoneSignalScanningTimer.readSummaryFromParcelLocked(in);
@@ -16520,7 +16524,7 @@
         mDeviceLightIdlingTimer.writeSummaryFromParcelLocked(out, nowRealtime);
         mDeviceIdlingTimer.writeSummaryFromParcelLocked(out, nowRealtime);
         mPhoneOnTimer.writeSummaryFromParcelLocked(out, nowRealtime);
-        for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) {
+        for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) {
             mPhoneSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, nowRealtime);
         }
         mPhoneSignalScanningTimer.writeSummaryFromParcelLocked(out, nowRealtime);
@@ -17015,7 +17019,7 @@
             mDeviceIdlingTimer.logState(pr, "  ");
             pr.println("*** Phone timer:");
             mPhoneOnTimer.logState(pr, "  ");
-            for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) {
+            for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) {
                 pr.println("*** Phone signal strength #" + i + ":");
                 mPhoneSignalStrengthsTimer[i].logState(pr, "  ");
             }
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 8d93408..13f1141 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -1279,8 +1279,8 @@
                 ipw.println();
             }
 
-            PackageWatchdog.getInstance(mContext).dump(ipw);
         });
+        PackageWatchdog.getInstance(mContext).dump(ipw);
     }
 
     @AnyThread
diff --git a/services/core/java/com/android/server/trust/TEST_MAPPING b/services/core/java/com/android/server/trust/TEST_MAPPING
index fa46acd..0de7c28 100644
--- a/services/core/java/com/android/server/trust/TEST_MAPPING
+++ b/services/core/java/com/android/server/trust/TEST_MAPPING
@@ -12,6 +12,19 @@
         ]
       }
     ],
+    "postsubmit": [
+      {
+        "name": "FrameworksMockingServicesTests",
+        "options": [
+          {
+            "include-filter": "com.android.server.trust"
+          },
+          {
+            "exclude-annotation": "androidx.test.filters.FlakyTest"
+          }
+        ]
+      }
+    ],
     "trust-tablet": [
       {
         "name": "TrustTests",
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 9a85c42..e3eb5b5 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -74,6 +74,7 @@
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.util.DumpUtils;
@@ -1439,6 +1440,13 @@
         if (biometricManager == null) {
             return new long[0];
         }
+        if (android.security.Flags.fixUnlockedDeviceRequiredKeysV2()
+                && mLockPatternUtils.isProfileWithUnifiedChallenge(userId)) {
+            // Profiles with unified challenge have their own set of biometrics, but the device
+            // unlock happens via the parent user.  In this case Keystore needs to be given the list
+            // of biometric SIDs from the parent user, not the profile.
+            userId = resolveProfileParent(userId);
+        }
         return biometricManager.getAuthenticatorIds(userId);
     }
 
@@ -1758,6 +1766,11 @@
         }
     };
 
+    @VisibleForTesting
+    void waitForIdle() {
+        mHandler.runWithScissors(() -> {}, 0);
+    }
+
     private boolean isTrustUsuallyManagedInternal(int userId) {
         synchronized (mTrustUsuallyManagedForUser) {
             int i = mTrustUsuallyManagedForUser.indexOfKey(userId);
@@ -1878,7 +1891,8 @@
         };
     }
 
-    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
+    @VisibleForTesting
+    final PackageMonitor mPackageMonitor = new PackageMonitor() {
         @Override
         public void onSomePackagesChanged() {
             refreshAgentList(UserHandle.USER_ALL);
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index 58acbe0..c0e3308 100755
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -1339,16 +1339,22 @@
                     String inputId = mHardwareInputIdMap.get(deviceId);
 
                     if (inputId != null) {
-                        if (connection.updateCableConnectionStatusLocked(cableConnectionStatus)) {
-                            if (previousCableConnectionStatus != connection.getInputStateLocked()) {
-                                mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
-                                    connection.getInputStateLocked(), 0, inputId).sendToTarget();
-                            }
-                        } else {
-                            if ((previousConfigsLength == 0)
-                                    != (connection.getConfigsLengthLocked() == 0)) {
-                                mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
-                                    connection.getInputStateLocked(), 0, inputId).sendToTarget();
+                        synchronized (mLock) {
+                            if (connection.updateCableConnectionStatusLocked(
+                                        cableConnectionStatus)) {
+                                if (previousCableConnectionStatus
+                                        != connection.getInputStateLocked()) {
+                                    mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
+                                                    connection.getInputStateLocked(), 0, inputId)
+                                            .sendToTarget();
+                                }
+                            } else {
+                                if ((previousConfigsLength == 0)
+                                        != (connection.getConfigsLengthLocked() == 0)) {
+                                    mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
+                                                    connection.getInputStateLocked(), 0, inputId)
+                                            .sendToTarget();
+                                }
                             }
                         }
                     }
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 9213d96..ed04e5f 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -34,6 +34,7 @@
     @NonNull private final Looper mLooper;
     @NonNull private final VcnNetworkProvider mVcnNetworkProvider;
     @NonNull private final FeatureFlags mFeatureFlags;
+    @NonNull private final com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
     private final boolean mIsInTestMode;
 
     public VcnContext(
@@ -48,6 +49,7 @@
 
         // Auto-generated class
         mFeatureFlags = new FeatureFlagsImpl();
+        mCoreNetFeatureFlags = new com.android.net.flags.FeatureFlagsImpl();
     }
 
     @NonNull
@@ -69,11 +71,23 @@
         return mIsInTestMode;
     }
 
+    public boolean isFlagNetworkMetricMonitorEnabled() {
+        return mFeatureFlags.networkMetricMonitor();
+    }
+
+    public boolean isFlagIpSecTransformStateEnabled() {
+        return mCoreNetFeatureFlags.ipsecTransformState();
+    }
+
     @NonNull
     public FeatureFlags getFeatureFlags() {
         return mFeatureFlags;
     }
 
+    public boolean isFlagSafeModeTimeoutConfigEnabled() {
+        return mFeatureFlags.safeModeTimeoutConfig();
+    }
+
     /**
      * Verifies that the caller is running on the VcnContext Thread.
      *
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 54c97dd..fcc0de1 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -915,9 +915,11 @@
             // TODO(b/180132994): explore safely removing this Thread check
             mVcnContext.ensureRunningOnLooperThread();
 
-            logInfo(
-                    "Selected underlying network changed: "
-                            + (underlying == null ? null : underlying.network));
+            if (!UnderlyingNetworkRecord.isSameNetwork(mUnderlying, underlying)) {
+                logInfo(
+                        "Selected underlying network changed: "
+                                + (underlying == null ? null : underlying.network));
+            }
 
             // TODO(b/179091925): Move the delayed-message handling to BaseState
 
@@ -1242,9 +1244,28 @@
                 createScheduledAlarm(
                         SAFEMODE_TIMEOUT_ALARM,
                         delayedMessage,
-                        mVcnContext.isInTestMode()
-                                ? TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS_TEST_MODE)
-                                : TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
+                        getSafeModeTimeoutMs(mVcnContext, mLastSnapshot, mSubscriptionGroup));
+    }
+
+    /** Gets the safe mode timeout */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static long getSafeModeTimeoutMs(
+            VcnContext vcnContext, TelephonySubscriptionSnapshot snapshot, ParcelUuid subGrp) {
+        final int defaultSeconds =
+                vcnContext.isInTestMode()
+                        ? SAFEMODE_TIMEOUT_SECONDS_TEST_MODE
+                        : SAFEMODE_TIMEOUT_SECONDS;
+
+        final PersistableBundleWrapper carrierConfig = snapshot.getCarrierConfigForSubGrp(subGrp);
+        int resultSeconds = defaultSeconds;
+
+        if (vcnContext.isFlagSafeModeTimeoutConfigEnabled() && carrierConfig != null) {
+            resultSeconds =
+                    carrierConfig.getInt(
+                            VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY, defaultSeconds);
+        }
+
+        return TimeUnit.SECONDS.toMillis(resultSeconds);
     }
 
     private void cancelSafeModeAlarm() {
diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
new file mode 100644
index 0000000..5f4852f
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.vcn.routeselection;
+
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.IpSecTransformState;
+import android.net.Network;
+import android.net.vcn.VcnManager;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.OutcomeReceiver;
+import android.os.PowerManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.server.vcn.VcnContext;
+
+import java.util.BitSet;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * IpSecPacketLossDetector is responsible for continuously monitoring IPsec packet loss
+ *
+ * <p>When the packet loss rate surpass the threshold, IpSecPacketLossDetector will report it to the
+ * caller
+ *
+ * <p>IpSecPacketLossDetector will start monitoring when the network being monitored is selected AND
+ * an inbound IpSecTransform has been applied to this network.
+ *
+ * <p>This class is flag gated by "network_metric_monitor" and "ipsec_tramsform_state"
+ */
+public class IpSecPacketLossDetector extends NetworkMetricMonitor {
+    private static final String TAG = IpSecPacketLossDetector.class.getSimpleName();
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PACKET_LOSS_UNAVALAIBLE = -1;
+
+    // For VoIP, losses between 5% and 10% of the total packet stream will affect the quality
+    // significantly (as per "Computer Networking for LANS to WANS: Hardware, Software and
+    // Security"). For audio and video streaming, above 10-12% packet loss is unacceptable (as per
+    // "ICTP-SDU: About PingER"). Thus choose 12% as a conservative default threshold to declare a
+    // validation failure.
+    private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT = 12;
+
+    private static final int POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT = 20;
+
+    private long mPollIpSecStateIntervalMs;
+    private final int mPacketLossRatePercentThreshold;
+
+    @NonNull private final Handler mHandler;
+    @NonNull private final PowerManager mPowerManager;
+    @NonNull private final Object mCancellationToken = new Object();
+    @NonNull private final PacketLossCalculator mPacketLossCalculator;
+
+    @Nullable private IpSecTransformWrapper mInboundTransform;
+    @Nullable private IpSecTransformState mLastIpSecTransformState;
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public IpSecPacketLossDetector(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkMetricMonitorCallback callback,
+            @NonNull Dependencies deps)
+            throws IllegalAccessException {
+        super(vcnContext, network, carrierConfig, callback);
+
+        Objects.requireNonNull(deps, "Missing deps");
+
+        if (!vcnContext.isFlagIpSecTransformStateEnabled()) {
+            // Caller error
+            logWtf("ipsecTransformState flag disabled");
+            throw new IllegalAccessException("ipsecTransformState flag disabled");
+        }
+
+        mHandler = new Handler(getVcnContext().getLooper());
+
+        mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class);
+
+        mPacketLossCalculator = deps.getPacketLossCalculator();
+
+        mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
+        mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
+
+        // Register for system broadcasts to monitor idle mode change
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+        getVcnContext()
+                .getContext()
+                .registerReceiver(
+                        new BroadcastReceiver() {
+                            @Override
+                            public void onReceive(Context context, Intent intent) {
+                                if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(
+                                                intent.getAction())
+                                        && mPowerManager.isDeviceIdleMode()) {
+                                    mLastIpSecTransformState = null;
+                                }
+                            }
+                        },
+                        intentFilter,
+                        null /* broadcastPermission not required */,
+                        mHandler);
+    }
+
+    public IpSecPacketLossDetector(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkMetricMonitorCallback callback)
+            throws IllegalAccessException {
+        this(vcnContext, network, carrierConfig, callback, new Dependencies());
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class Dependencies {
+        public PacketLossCalculator getPacketLossCalculator() {
+            return new PacketLossCalculator();
+        }
+    }
+
+    private static long getPollIpSecStateIntervalMs(
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        final int seconds;
+
+        if (carrierConfig != null) {
+            seconds =
+                    carrierConfig.getInt(
+                            VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
+                            POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT);
+        } else {
+            seconds = POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT;
+        }
+
+        return TimeUnit.SECONDS.toMillis(seconds);
+    }
+
+    private static int getPacketLossRatePercentThreshold(
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        if (carrierConfig != null) {
+            return carrierConfig.getInt(
+                    VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
+                    IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT);
+        }
+        return IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT;
+    }
+
+    @Override
+    protected void onSelectedUnderlyingNetworkChanged() {
+        if (!isSelectedUnderlyingNetwork()) {
+            mInboundTransform = null;
+            stop();
+        }
+
+        // No action when the underlying network got selected. Wait for the inbound transform to
+        // start the monitor
+    }
+
+    @Override
+    public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inboundTransform) {
+        Objects.requireNonNull(inboundTransform, "inboundTransform is null");
+
+        if (Objects.equals(inboundTransform, mInboundTransform)) {
+            return;
+        }
+
+        if (!isSelectedUnderlyingNetwork()) {
+            logWtf("setInboundTransform called but network not selected");
+            return;
+        }
+
+        // When multiple parallel inbound transforms are created, NetworkMetricMonitor will be
+        // enabled on the last one as a sample
+        mInboundTransform = inboundTransform;
+        start();
+    }
+
+    @Override
+    public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) {
+        // The already scheduled event will not be affected. The followup events will be scheduled
+        // with the new interval
+        mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
+    }
+
+    @Override
+    protected void start() {
+        super.start();
+        clearTransformStateAndPollingEvents();
+        mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L);
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+        clearTransformStateAndPollingEvents();
+    }
+
+    private void clearTransformStateAndPollingEvents() {
+        mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
+        mLastIpSecTransformState = null;
+    }
+
+    @Override
+    public void close() {
+        super.close();
+
+        if (mInboundTransform != null) {
+            mInboundTransform.close();
+        }
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    @Nullable
+    public IpSecTransformState getLastTransformState() {
+        return mLastIpSecTransformState;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PROTECTED)
+    @Nullable
+    public IpSecTransformWrapper getInboundTransformInternal() {
+        return mInboundTransform;
+    }
+
+    private class PollIpSecStateRunnable implements Runnable {
+        @Override
+        public void run() {
+            if (!isStarted()) {
+                logWtf("Monitor stopped but PollIpSecStateRunnable not removed from Handler");
+                return;
+            }
+
+            getInboundTransformInternal()
+                    .getIpSecTransformState(
+                            new HandlerExecutor(mHandler), new IpSecTransformStateReceiver());
+
+            // Schedule for next poll
+            mHandler.postDelayed(
+                    new PollIpSecStateRunnable(), mCancellationToken, mPollIpSecStateIntervalMs);
+        }
+    }
+
+    private class IpSecTransformStateReceiver
+            implements OutcomeReceiver<IpSecTransformState, RuntimeException> {
+        @Override
+        public void onResult(@NonNull IpSecTransformState state) {
+            getVcnContext().ensureRunningOnLooperThread();
+
+            if (!isStarted()) {
+                return;
+            }
+
+            onIpSecTransformStateReceived(state);
+        }
+
+        @Override
+        public void onError(@NonNull RuntimeException error) {
+            getVcnContext().ensureRunningOnLooperThread();
+
+            // Nothing we can do here
+            logW("TransformStateReceiver#onError " + error.toString());
+        }
+    }
+
+    private void onIpSecTransformStateReceived(@NonNull IpSecTransformState state) {
+        if (mLastIpSecTransformState == null) {
+            // This is first time to poll the state
+            mLastIpSecTransformState = state;
+            return;
+        }
+
+        final int packetLossRate =
+                mPacketLossCalculator.getPacketLossRatePercentage(
+                        mLastIpSecTransformState, state, getLogPrefix());
+
+        if (packetLossRate == PACKET_LOSS_UNAVALAIBLE) {
+            return;
+        }
+
+        final String logMsg =
+                "packetLossRate: "
+                        + packetLossRate
+                        + "% in the past "
+                        + (state.getTimestamp() - mLastIpSecTransformState.getTimestamp())
+                        + "ms";
+
+        mLastIpSecTransformState = state;
+        if (packetLossRate < mPacketLossRatePercentThreshold) {
+            logV(logMsg);
+            onValidationResultReceivedInternal(false /* isFailed */);
+        } else {
+            logInfo(logMsg);
+            onValidationResultReceivedInternal(true /* isFailed */);
+        }
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class PacketLossCalculator {
+        /** Calculate the packet loss rate between two timestamps */
+        public int getPacketLossRatePercentage(
+                @NonNull IpSecTransformState oldState,
+                @NonNull IpSecTransformState newState,
+                String logPrefix) {
+            logVIpSecTransform("oldState", oldState, logPrefix);
+            logVIpSecTransform("newState", newState, logPrefix);
+
+            final int replayWindowSize = oldState.getReplayBitmap().length * 8;
+            final long oldSeqHi = oldState.getRxHighestSequenceNumber();
+            final long oldSeqLow = Math.max(0L, oldSeqHi - replayWindowSize + 1);
+            final long newSeqHi = newState.getRxHighestSequenceNumber();
+            final long newSeqLow = Math.max(0L, newSeqHi - replayWindowSize + 1);
+
+            if (oldSeqHi == newSeqHi || newSeqHi < replayWindowSize) {
+                // The replay window did not proceed and all packets might have been delivered out
+                // of order
+                return PACKET_LOSS_UNAVALAIBLE;
+            }
+
+            // Get the expected packet count by assuming there is no packet loss. In this case, SA
+            // should receive all packets whose sequence numbers are smaller than the lower bound of
+            // the replay window AND the packets received within the window.
+            // When the lower bound is 0, it's not possible to tell whether packet with seqNo 0 is
+            // received or not. For simplicity just assume that packet is received.
+            final long newExpectedPktCnt = newSeqLow + getPacketCntInReplayWindow(newState);
+            final long oldExpectedPktCnt = oldSeqLow + getPacketCntInReplayWindow(oldState);
+
+            final long expectedPktCntDiff = newExpectedPktCnt - oldExpectedPktCnt;
+            final long actualPktCntDiff = newState.getPacketCount() - oldState.getPacketCount();
+
+            logV(
+                    TAG,
+                    logPrefix
+                            + " expectedPktCntDiff: "
+                            + expectedPktCntDiff
+                            + " actualPktCntDiff: "
+                            + actualPktCntDiff);
+
+            if (expectedPktCntDiff < 0
+                    || expectedPktCntDiff == 0
+                    || actualPktCntDiff < 0
+                    || actualPktCntDiff > expectedPktCntDiff) {
+                logWtf(TAG, "Impossible values for expectedPktCntDiff or" + " actualPktCntDiff");
+                return PACKET_LOSS_UNAVALAIBLE;
+            }
+
+            return 100 - (int) (actualPktCntDiff * 100 / expectedPktCntDiff);
+        }
+    }
+
+    private static void logVIpSecTransform(
+            String transformTag, IpSecTransformState state, String logPrefix) {
+        final String stateString =
+                " seqNo: "
+                        + state.getRxHighestSequenceNumber()
+                        + " | pktCnt: "
+                        + state.getPacketCount()
+                        + " | pktCntInWindow: "
+                        + getPacketCntInReplayWindow(state);
+        logV(TAG, logPrefix + " " + transformTag + stateString);
+    }
+
+    /** Get the number of received packets within the replay window */
+    private static long getPacketCntInReplayWindow(@NonNull IpSecTransformState state) {
+        return BitSet.valueOf(state.getReplayBitmap()).cardinality();
+    }
+}
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
new file mode 100644
index 0000000..a79f188
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.vcn.routeselection;
+
+import static com.android.server.VcnManagementService.LOCAL_LOG;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IpSecTransform;
+import android.net.IpSecTransformState;
+import android.net.Network;
+import android.os.OutcomeReceiver;
+import android.util.CloseGuard;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.server.vcn.VcnContext;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * NetworkMetricMonitor is responsible for managing metric monitoring and tracking validation
+ * results.
+ *
+ * <p>This class is flag gated by "network_metric_monitor"
+ */
+public abstract class NetworkMetricMonitor implements AutoCloseable {
+    private static final String TAG = NetworkMetricMonitor.class.getSimpleName();
+
+    private static final boolean VDBG = false; // STOPSHIP: if true
+
+    @NonNull private final CloseGuard mCloseGuard = new CloseGuard();
+
+    @NonNull private final VcnContext mVcnContext;
+    @NonNull private final Network mNetwork;
+    @NonNull private final NetworkMetricMonitorCallback mCallback;
+
+    private boolean mIsSelectedUnderlyingNetwork;
+    private boolean mIsStarted;
+    private boolean mIsValidationFailed;
+
+    protected NetworkMetricMonitor(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkMetricMonitorCallback callback)
+            throws IllegalAccessException {
+        if (!vcnContext.isFlagNetworkMetricMonitorEnabled()) {
+            // Caller error
+            logWtf("networkMetricMonitor flag disabled");
+            throw new IllegalAccessException("networkMetricMonitor flag disabled");
+        }
+
+        mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+        mNetwork = Objects.requireNonNull(network, "Missing network");
+        mCallback = Objects.requireNonNull(callback, "Missing callback");
+
+        mIsSelectedUnderlyingNetwork = false;
+        mIsStarted = false;
+        mIsValidationFailed = false;
+    }
+
+    /** Callback to notify caller of the validation result */
+    public interface NetworkMetricMonitorCallback {
+        /** Called when there is a validation result is ready */
+        void onValidationResultReceived();
+    }
+
+    /**
+     * Start monitoring
+     *
+     * <p>This method might be called on a an already started monitor for updating monitor
+     * properties (e.g. IpSecTransform, carrier config)
+     *
+     * <p>Subclasses MUST call super.start() when overriding this method
+     */
+    protected void start() {
+        mIsStarted = true;
+    }
+
+    /**
+     * Stop monitoring
+     *
+     * <p>Subclasses MUST call super.stop() when overriding this method
+     */
+    public void stop() {
+        mIsValidationFailed = false;
+        mIsStarted = false;
+    }
+
+    /** Called by the subclasses when the validation result is ready */
+    protected void onValidationResultReceivedInternal(boolean isFailed) {
+        mIsValidationFailed = isFailed;
+        mCallback.onValidationResultReceived();
+    }
+
+    /** Called when the underlying network changes to selected or unselected */
+    protected abstract void onSelectedUnderlyingNetworkChanged();
+
+    /**
+     * Mark the network being monitored selected or unselected
+     *
+     * <p>Subclasses MUST call super when overriding this method
+     */
+    public void setIsSelectedUnderlyingNetwork(boolean isSelectedUnderlyingNetwork) {
+        if (mIsSelectedUnderlyingNetwork == isSelectedUnderlyingNetwork) {
+            return;
+        }
+
+        mIsSelectedUnderlyingNetwork = isSelectedUnderlyingNetwork;
+        onSelectedUnderlyingNetworkChanged();
+    }
+
+    /** Wrapper that allows injection for testing purposes */
+    @VisibleForTesting(visibility = Visibility.PROTECTED)
+    public static class IpSecTransformWrapper {
+        @NonNull public final IpSecTransform ipSecTransform;
+
+        public IpSecTransformWrapper(@NonNull IpSecTransform ipSecTransform) {
+            this.ipSecTransform = ipSecTransform;
+        }
+
+        /** Poll an IpSecTransformState */
+        public void getIpSecTransformState(
+                @NonNull Executor executor,
+                @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
+            ipSecTransform.getIpSecTransformState(executor, callback);
+        }
+
+        /** Close this instance and release the underlying resources */
+        public void close() {
+            ipSecTransform.close();
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(ipSecTransform);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof IpSecTransformWrapper)) {
+                return false;
+            }
+
+            final IpSecTransformWrapper other = (IpSecTransformWrapper) o;
+
+            return Objects.equals(ipSecTransform, other.ipSecTransform);
+        }
+    }
+
+    /** Set the IpSecTransform that applied to the Network being monitored */
+    public void setInboundTransform(@NonNull IpSecTransform inTransform) {
+        setInboundTransformInternal(new IpSecTransformWrapper(inTransform));
+    }
+
+    /**
+     * Set the IpSecTransform that applied to the Network being monitored *
+     *
+     * <p>Subclasses MUST call super when overriding this method
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inTransform) {
+        // Subclasses MUST override it if they care
+    }
+
+    /** Update the carrierconfig */
+    public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) {
+        // Subclasses MUST override it if they care
+    }
+
+    public boolean isValidationFailed() {
+        return mIsValidationFailed;
+    }
+
+    public boolean isSelectedUnderlyingNetwork() {
+        return mIsSelectedUnderlyingNetwork;
+    }
+
+    public boolean isStarted() {
+        return mIsStarted;
+    }
+
+    @NonNull
+    public VcnContext getVcnContext() {
+        return mVcnContext;
+    }
+
+    // Override methods for AutoCloseable. Subclasses MUST call super when overriding this method
+    @Override
+    public void close() {
+        mCloseGuard.close();
+
+        stop();
+    }
+
+    // Override #finalize() to use closeGuard for flagging that #close() was not called
+    @SuppressWarnings("Finalize")
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    private String getClassName() {
+        return this.getClass().getSimpleName();
+    }
+
+    protected String getLogPrefix() {
+        return " [Network " + mNetwork + "] ";
+    }
+
+    protected void logV(String msg) {
+        if (VDBG) {
+            Slog.v(getClassName(), getLogPrefix() + msg);
+            LOCAL_LOG.log("[VERBOSE ] " + getClassName() + getLogPrefix() + msg);
+        }
+    }
+
+    protected void logInfo(String msg) {
+        Slog.i(getClassName(), getLogPrefix() + msg);
+        LOCAL_LOG.log("[INFO ] " + getClassName() + getLogPrefix() + msg);
+    }
+
+    protected void logW(String msg) {
+        Slog.w(getClassName(), getLogPrefix() + msg);
+        LOCAL_LOG.log("[WARN ] " + getClassName() + getLogPrefix() + msg);
+    }
+
+    protected void logWtf(String msg) {
+        Slog.wtf(getClassName(), getLogPrefix() + msg);
+        LOCAL_LOG.log("[WTF ] " + getClassName() + getLogPrefix() + msg);
+    }
+
+    protected static void logV(String className, String msgWithPrefix) {
+        if (VDBG) {
+            Slog.wtf(className, msgWithPrefix);
+            LOCAL_LOG.log("[VERBOSE ] " + className + msgWithPrefix);
+        }
+    }
+
+    protected static void logWtf(String className, String msgWithPrefix) {
+        Slog.wtf(className, msgWithPrefix);
+        LOCAL_LOG.log("[WTF ] " + className + msgWithPrefix);
+    }
+}
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
index 7f129ea..d32e5cc 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -47,7 +47,6 @@
 
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 
 /** @hide */
@@ -86,7 +85,6 @@
      * <p>VCN MUST never select a non-INTERNET network that are unvalidated or fail to match any
      * template as the underlying network.
      */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
     static final int PRIORITY_INVALID = -1;
 
     /** Gives networks a priority class, based on configured VcnGatewayConnectionConfig */
@@ -96,7 +94,7 @@
             List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
 
@@ -118,7 +116,7 @@
                     networkRecord,
                     subscriptionGroup,
                     snapshot,
-                    currentlySelected,
+                    isSelected,
                     carrierConfig)) {
                 return priorityIndex;
             }
@@ -140,12 +138,9 @@
             UnderlyingNetworkRecord networkRecord,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
-        final boolean isSelectedUnderlyingNetwork =
-                currentlySelected != null
-                        && Objects.equals(currentlySelected.network, networkRecord.network);
 
         final int meteredMatch = networkPriority.getMetered();
         final boolean isMetered = !caps.hasCapability(NET_CAPABILITY_NOT_METERED);
@@ -159,7 +154,7 @@
         if (caps.getLinkUpstreamBandwidthKbps() < networkPriority.getMinExitUpstreamBandwidthKbps()
                 || (caps.getLinkUpstreamBandwidthKbps()
                                 < networkPriority.getMinEntryUpstreamBandwidthKbps()
-                        && !isSelectedUnderlyingNetwork)) {
+                        && !isSelected)) {
             return false;
         }
 
@@ -167,7 +162,7 @@
                         < networkPriority.getMinExitDownstreamBandwidthKbps()
                 || (caps.getLinkDownstreamBandwidthKbps()
                                 < networkPriority.getMinEntryDownstreamBandwidthKbps()
-                        && !isSelectedUnderlyingNetwork)) {
+                        && !isSelected)) {
             return false;
         }
 
@@ -191,7 +186,7 @@
             return checkMatchesWifiPriorityRule(
                     (VcnWifiUnderlyingNetworkTemplate) networkPriority,
                     networkRecord,
-                    currentlySelected,
+                    isSelected,
                     carrierConfig);
         }
 
@@ -214,7 +209,7 @@
     public static boolean checkMatchesWifiPriorityRule(
             VcnWifiUnderlyingNetworkTemplate networkPriority,
             UnderlyingNetworkRecord networkRecord,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
 
@@ -223,7 +218,7 @@
         }
 
         // TODO: Move the Network Quality check to the network metric monitor framework.
-        if (!isWifiRssiAcceptable(networkRecord, currentlySelected, carrierConfig)) {
+        if (!isWifiRssiAcceptable(networkRecord, isSelected, carrierConfig)) {
             return false;
         }
 
@@ -237,15 +232,11 @@
 
     private static boolean isWifiRssiAcceptable(
             UnderlyingNetworkRecord networkRecord,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
-        final boolean isSelectedNetwork =
-                currentlySelected != null
-                        && networkRecord.network.equals(currentlySelected.network);
 
-        if (isSelectedNetwork
-                && caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) {
+        if (isSelected && caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) {
             return true;
         }
 
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 6afa795..48df44b 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -48,6 +48,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
@@ -83,6 +84,9 @@
     @NonNull private final TelephonyCallback mActiveDataSubIdListener =
             new VcnActiveDataSubscriptionIdListener();
 
+    private final Map<Network, UnderlyingNetworkEvaluator> mUnderlyingNetworkRecords =
+            new ArrayMap<>();
+
     @NonNull private final List<NetworkCallback> mCellBringupCallbacks = new ArrayList<>();
     @Nullable private NetworkCallback mWifiBringupCallback;
     @Nullable private NetworkCallback mWifiEntryRssiThresholdCallback;
@@ -105,7 +109,8 @@
         this(vcnContext, connectionConfig, subscriptionGroup, snapshot, cb, new Dependencies());
     }
 
-    private UnderlyingNetworkController(
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
             @NonNull VcnGatewayConnectionConfig connectionConfig,
             @NonNull ParcelUuid subscriptionGroup,
@@ -196,6 +201,7 @@
         NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback;
         List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
         mCellBringupCallbacks.clear();
+        mUnderlyingNetworkRecords.clear();
 
         // Register new callbacks. Make-before-break; always register new callbacks before removal
         // of old callbacks
@@ -395,6 +401,18 @@
         // Update carrier config
         mCarrierConfig = mLastSnapshot.getCarrierConfigForSubGrp(mSubscriptionGroup);
 
+        // Make sure all evaluators use the same updated TelephonySubscriptionSnapshot and carrier
+        // config to calculate their cached priority classes. For simplicity, the
+        // UnderlyingNetworkController does not listen for changes in VCN-related carrier config
+        // keys, and changes are applied at restart of the VcnGatewayConnection
+        for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+            evaluator.reevaluate(
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+        }
+
         // Only trigger re-registration if subIds in this group have changed
         if (oldSnapshot
                 .getAllSubIdsInGroup(mSubscriptionGroup)
@@ -418,32 +436,62 @@
                 .unregisterTelephonyCallback(mActiveDataSubIdListener);
     }
 
+    private TreeSet<UnderlyingNetworkEvaluator> getSortedUnderlyingNetworks() {
+        TreeSet<UnderlyingNetworkEvaluator> sorted =
+                new TreeSet<>(UnderlyingNetworkEvaluator.getComparator());
+
+        for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+            if (evaluator.getPriorityClass() != NetworkPriorityClassifier.PRIORITY_INVALID) {
+                sorted.add(evaluator);
+            }
+        }
+
+        return sorted;
+    }
+
     private void reevaluateNetworks() {
         if (mIsQuitting || mRouteSelectionCallback == null) {
             return; // UnderlyingNetworkController has quit.
         }
 
-        TreeSet<UnderlyingNetworkRecord> sorted =
-                mRouteSelectionCallback.getSortedUnderlyingNetworks();
-        UnderlyingNetworkRecord candidate = sorted.isEmpty() ? null : sorted.first();
+        TreeSet<UnderlyingNetworkEvaluator> sorted = getSortedUnderlyingNetworks();
+
+        UnderlyingNetworkEvaluator candidateEvaluator = sorted.isEmpty() ? null : sorted.first();
+        UnderlyingNetworkRecord candidate =
+                candidateEvaluator == null ? null : candidateEvaluator.getNetworkRecord();
         if (Objects.equals(mCurrentRecord, candidate)) {
             return;
         }
 
         String allNetworkPriorities = "";
-        for (UnderlyingNetworkRecord record : sorted) {
+        for (UnderlyingNetworkEvaluator recordEvaluator : sorted) {
             if (!allNetworkPriorities.isEmpty()) {
                 allNetworkPriorities += ", ";
             }
-            allNetworkPriorities += record.network + ": " + record.priorityClass;
+            allNetworkPriorities +=
+                    recordEvaluator.getNetwork() + ": " + recordEvaluator.getPriorityClass();
         }
-        logInfo(
-                "Selected network changed to "
-                        + (candidate == null ? null : candidate.network)
-                        + ", selected from list: "
-                        + allNetworkPriorities);
+
+        if (!UnderlyingNetworkRecord.isSameNetwork(mCurrentRecord, candidate)) {
+            logInfo(
+                    "Selected network changed to "
+                            + (candidate == null ? null : candidate.network)
+                            + ", selected from list: "
+                            + allNetworkPriorities);
+        }
+
         mCurrentRecord = candidate;
         mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
+
+        // Need to update all evaluators to ensure the previously selected one is unselected
+        for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+            evaluator.setIsSelected(
+                    candidateEvaluator == evaluator,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+        }
     }
 
     /**
@@ -463,46 +511,26 @@
      */
     @VisibleForTesting
     class UnderlyingNetworkListener extends NetworkCallback {
-        private final Map<Network, UnderlyingNetworkRecord.Builder>
-                mUnderlyingNetworkRecordBuilders = new ArrayMap<>();
-
         UnderlyingNetworkListener() {
             super(NetworkCallback.FLAG_INCLUDE_LOCATION_INFO);
         }
 
-        private TreeSet<UnderlyingNetworkRecord> getSortedUnderlyingNetworks() {
-            TreeSet<UnderlyingNetworkRecord> sorted =
-                    new TreeSet<>(UnderlyingNetworkRecord.getComparator());
-
-            for (UnderlyingNetworkRecord.Builder builder :
-                    mUnderlyingNetworkRecordBuilders.values()) {
-                if (builder.isValid()) {
-                    final UnderlyingNetworkRecord record =
-                            builder.build(
-                                    mVcnContext,
-                                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
-                                    mSubscriptionGroup,
-                                    mLastSnapshot,
-                                    mCurrentRecord,
-                                    mCarrierConfig);
-                    if (record.priorityClass != NetworkPriorityClassifier.PRIORITY_INVALID) {
-                        sorted.add(record);
-                    }
-                }
-            }
-
-            return sorted;
-        }
-
         @Override
         public void onAvailable(@NonNull Network network) {
-            mUnderlyingNetworkRecordBuilders.put(
-                    network, new UnderlyingNetworkRecord.Builder(network));
+            mUnderlyingNetworkRecords.put(
+                    network,
+                    mDeps.newUnderlyingNetworkEvaluator(
+                            mVcnContext,
+                            network,
+                            mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                            mSubscriptionGroup,
+                            mLastSnapshot,
+                            mCarrierConfig));
         }
 
         @Override
         public void onLost(@NonNull Network network) {
-            mUnderlyingNetworkRecordBuilders.remove(network);
+            mUnderlyingNetworkRecords.remove(network);
 
             reevaluateNetworks();
         }
@@ -510,15 +538,20 @@
         @Override
         public void onCapabilitiesChanged(
                 @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
-            final UnderlyingNetworkRecord.Builder builder =
-                    mUnderlyingNetworkRecordBuilders.get(network);
-            if (builder == null) {
+            final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network);
+            if (evaluator == null) {
                 logWtf("Got capabilities change for unknown key: " + network);
                 return;
             }
 
-            builder.setNetworkCapabilities(networkCapabilities);
-            if (builder.isValid()) {
+            evaluator.setNetworkCapabilities(
+                    networkCapabilities,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+
+            if (evaluator.isValid()) {
                 reevaluateNetworks();
             }
         }
@@ -526,30 +559,40 @@
         @Override
         public void onLinkPropertiesChanged(
                 @NonNull Network network, @NonNull LinkProperties linkProperties) {
-            final UnderlyingNetworkRecord.Builder builder =
-                    mUnderlyingNetworkRecordBuilders.get(network);
-            if (builder == null) {
+            final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network);
+            if (evaluator == null) {
                 logWtf("Got link properties change for unknown key: " + network);
                 return;
             }
 
-            builder.setLinkProperties(linkProperties);
-            if (builder.isValid()) {
+            evaluator.setLinkProperties(
+                    linkProperties,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+
+            if (evaluator.isValid()) {
                 reevaluateNetworks();
             }
         }
 
         @Override
         public void onBlockedStatusChanged(@NonNull Network network, boolean isBlocked) {
-            final UnderlyingNetworkRecord.Builder builder =
-                    mUnderlyingNetworkRecordBuilders.get(network);
-            if (builder == null) {
+            final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network);
+            if (evaluator == null) {
                 logWtf("Got blocked status change for unknown key: " + network);
                 return;
             }
 
-            builder.setIsBlocked(isBlocked);
-            if (builder.isValid()) {
+            evaluator.setIsBlocked(
+                    isBlocked,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+
+            if (evaluator.isValid()) {
                 reevaluateNetworks();
             }
         }
@@ -614,16 +657,8 @@
         pw.println("Underlying networks:");
         pw.increaseIndent();
         if (mRouteSelectionCallback != null) {
-            for (UnderlyingNetworkRecord record :
-                    mRouteSelectionCallback.getSortedUnderlyingNetworks()) {
-                record.dump(
-                        mVcnContext,
-                        pw,
-                        mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
-                        mSubscriptionGroup,
-                        mLastSnapshot,
-                        mCurrentRecord,
-                        mCarrierConfig);
+            for (UnderlyingNetworkEvaluator recordEvaluator : getSortedUnderlyingNetworks()) {
+                recordEvaluator.dump(pw);
             }
         }
         pw.decreaseIndent();
@@ -653,5 +688,23 @@
                 @Nullable UnderlyingNetworkRecord underlyingNetworkRecord);
     }
 
-    private static class Dependencies {}
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class Dependencies {
+        /** Construct a new UnderlyingNetworkEvaluator */
+        public UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator(
+                @NonNull VcnContext vcnContext,
+                @NonNull Network network,
+                @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+                @NonNull ParcelUuid subscriptionGroup,
+                @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+                @Nullable PersistableBundleWrapper carrierConfig) {
+            return new UnderlyingNetworkEvaluator(
+                    vcnContext,
+                    network,
+                    underlyingNetworkTemplates,
+                    subscriptionGroup,
+                    lastSnapshot,
+                    carrierConfig);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
new file mode 100644
index 0000000..c124a19
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
@@ -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.server.vcn.routeselection;
+
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.vcn.VcnUnderlyingNetworkTemplate;
+import android.os.ParcelUuid;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * UnderlyingNetworkEvaluator evaluates the quality and priority class of a network candidate for
+ * route selection.
+ *
+ * @hide
+ */
+public class UnderlyingNetworkEvaluator {
+    private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName();
+
+    @NonNull private final VcnContext mVcnContext;
+    @NonNull private final UnderlyingNetworkRecord.Builder mNetworkRecordBuilder;
+
+    private boolean mIsSelected;
+    private int mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID;
+
+    public UnderlyingNetworkEvaluator(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+
+        Objects.requireNonNull(underlyingNetworkTemplates, "Missing underlyingNetworkTemplates");
+        Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
+        Objects.requireNonNull(lastSnapshot, "Missing lastSnapshot");
+
+        mNetworkRecordBuilder =
+                new UnderlyingNetworkRecord.Builder(
+                        Objects.requireNonNull(network, "Missing network"));
+        mIsSelected = false;
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    private void updatePriorityClass(
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        if (mNetworkRecordBuilder.isValid()) {
+            mPriorityClass =
+                    NetworkPriorityClassifier.calculatePriorityClass(
+                            mVcnContext,
+                            mNetworkRecordBuilder.build(),
+                            underlyingNetworkTemplates,
+                            subscriptionGroup,
+                            lastSnapshot,
+                            mIsSelected,
+                            carrierConfig);
+        } else {
+            mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID;
+        }
+    }
+
+    public static Comparator<UnderlyingNetworkEvaluator> getComparator() {
+        return (left, right) -> {
+            final int leftIndex = left.mPriorityClass;
+            final int rightIndex = right.mPriorityClass;
+
+            // In the case of networks in the same priority class, prioritize based on other
+            // criteria (eg. actively selected network, link metrics, etc)
+            if (leftIndex == rightIndex) {
+                // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord
+                // fall into the same priority class.
+                if (left.mIsSelected) {
+                    return -1;
+                }
+                if (right.mIsSelected) {
+                    return 1;
+                }
+            }
+            return Integer.compare(leftIndex, rightIndex);
+        };
+    }
+
+    /** Set the NetworkCapabilities */
+    public void setNetworkCapabilities(
+            @NonNull NetworkCapabilities nc,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mNetworkRecordBuilder.setNetworkCapabilities(nc);
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Set the LinkProperties */
+    public void setLinkProperties(
+            @NonNull LinkProperties lp,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mNetworkRecordBuilder.setLinkProperties(lp);
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Set whether the network is blocked */
+    public void setIsBlocked(
+            boolean isBlocked,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mNetworkRecordBuilder.setIsBlocked(isBlocked);
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Set whether the network is selected as VCN's underlying network */
+    public void setIsSelected(
+            boolean isSelected,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mIsSelected = isSelected;
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /**
+     * Update the last TelephonySubscriptionSnapshot and carrier config to reevaluate the network
+     */
+    public void reevaluate(
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Return whether this network evaluator is valid */
+    public boolean isValid() {
+        return mNetworkRecordBuilder.isValid();
+    }
+
+    /** Return the network */
+    public Network getNetwork() {
+        return mNetworkRecordBuilder.getNetwork();
+    }
+
+    /** Return the network record */
+    public UnderlyingNetworkRecord getNetworkRecord() {
+        return mNetworkRecordBuilder.build();
+    }
+
+    /** Return the priority class for network selection */
+    public int getPriorityClass() {
+        return mPriorityClass;
+    }
+
+    /** Dump the information of this instance */
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("UnderlyingNetworkEvaluator:");
+        pw.increaseIndent();
+
+        if (mNetworkRecordBuilder.isValid()) {
+            getNetworkRecord().dump(pw);
+        } else {
+            pw.println(
+                    "UnderlyingNetworkRecord incomplete: mNetwork: "
+                            + mNetworkRecordBuilder.getNetwork());
+        }
+
+        pw.println("mIsSelected: " + mIsSelected);
+        pw.println("mPriorityClass: " + mPriorityClass);
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
index aea9f4d..7ab8e55 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
@@ -16,24 +16,17 @@
 
 package com.android.server.vcn.routeselection;
 
-import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
-import android.net.vcn.VcnUnderlyingNetworkTemplate;
-import android.os.ParcelUuid;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
-import com.android.server.vcn.VcnContext;
 
-import java.util.Comparator;
-import java.util.List;
 import java.util.Objects;
 
 /**
@@ -46,54 +39,17 @@
     @NonNull public final NetworkCapabilities networkCapabilities;
     @NonNull public final LinkProperties linkProperties;
     public final boolean isBlocked;
-    public final boolean isSelected;
-    public final int priorityClass;
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public UnderlyingNetworkRecord(
             @NonNull Network network,
             @NonNull NetworkCapabilities networkCapabilities,
             @NonNull LinkProperties linkProperties,
-            boolean isBlocked,
-            VcnContext vcnContext,
-            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-            ParcelUuid subscriptionGroup,
-            TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
-            PersistableBundleWrapper carrierConfig) {
+            boolean isBlocked) {
         this.network = network;
         this.networkCapabilities = networkCapabilities;
         this.linkProperties = linkProperties;
         this.isBlocked = isBlocked;
-
-        this.isSelected = isSelected(this.network, currentlySelected);
-
-        priorityClass =
-                NetworkPriorityClassifier.calculatePriorityClass(
-                        vcnContext,
-                        this,
-                        underlyingNetworkTemplates,
-                        subscriptionGroup,
-                        snapshot,
-                        currentlySelected,
-                        carrierConfig);
-    }
-
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    public UnderlyingNetworkRecord(
-            @NonNull Network network,
-            @NonNull NetworkCapabilities networkCapabilities,
-            @NonNull LinkProperties linkProperties,
-            boolean isBlocked,
-            boolean isSelected,
-            int priorityClass) {
-        this.network = network;
-        this.networkCapabilities = networkCapabilities;
-        this.linkProperties = linkProperties;
-        this.isBlocked = isBlocked;
-        this.isSelected = isSelected;
-
-        this.priorityClass = priorityClass;
     }
 
     @Override
@@ -113,64 +69,20 @@
         return Objects.hash(network, networkCapabilities, linkProperties, isBlocked);
     }
 
-    /** Returns if two records are equal including their priority classes. */
-    public static boolean isEqualIncludingPriorities(
-            UnderlyingNetworkRecord left, UnderlyingNetworkRecord right) {
-        if (left != null && right != null) {
-            return left.equals(right)
-                    && left.isSelected == right.isSelected
-                    && left.priorityClass == right.priorityClass;
-        }
-
-        return left == right;
-    }
-
-    static Comparator<UnderlyingNetworkRecord> getComparator() {
-        return (left, right) -> {
-            final int leftIndex = left.priorityClass;
-            final int rightIndex = right.priorityClass;
-
-            // In the case of networks in the same priority class, prioritize based on other
-            // criteria (eg. actively selected network, link metrics, etc)
-            if (leftIndex == rightIndex) {
-                // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord
-                // fall into the same priority class.
-                if (left.isSelected) {
-                    return -1;
-                }
-                if (right.isSelected) {
-                    return 1;
-                }
-            }
-            return Integer.compare(leftIndex, rightIndex);
-        };
-    }
-
-    private static boolean isSelected(
-            Network networkToCheck, UnderlyingNetworkRecord currentlySelected) {
-        if (currentlySelected == null) {
-            return false;
-        }
-        if (currentlySelected.network.equals(networkToCheck)) {
-            return true;
-        }
-        return false;
+    /** Return whether two records represent the same network */
+    public static boolean isSameNetwork(
+            @Nullable UnderlyingNetworkRecord leftRecord,
+            @Nullable UnderlyingNetworkRecord rightRecord) {
+        final Network left = leftRecord == null ? null : leftRecord.network;
+        final Network right = rightRecord == null ? null : rightRecord.network;
+        return Objects.equals(left, right);
     }
 
     /** Dumps the state of this record for logging and debugging purposes. */
-    void dump(
-            VcnContext vcnContext,
-            IndentingPrintWriter pw,
-            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-            ParcelUuid subscriptionGroup,
-            TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
-            PersistableBundleWrapper carrierConfig) {
+    void dump(IndentingPrintWriter pw) {
         pw.println("UnderlyingNetworkRecord:");
         pw.increaseIndent();
 
-        pw.println("priorityClass: " + priorityClass);
-        pw.println("isSelected: " + isSelected);
         pw.println("mNetwork: " + network);
         pw.println("mNetworkCapabilities: " + networkCapabilities);
         pw.println("mLinkProperties: " + linkProperties);
@@ -218,29 +130,14 @@
             return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet;
         }
 
-        UnderlyingNetworkRecord build(
-                VcnContext vcnContext,
-                List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-                ParcelUuid subscriptionGroup,
-                TelephonySubscriptionSnapshot snapshot,
-                UnderlyingNetworkRecord currentlySelected,
-                PersistableBundleWrapper carrierConfig) {
+        UnderlyingNetworkRecord build() {
             if (!isValid()) {
                 throw new IllegalArgumentException(
                         "Called build before UnderlyingNetworkRecord was valid");
             }
 
             return new UnderlyingNetworkRecord(
-                    mNetwork,
-                    mNetworkCapabilities,
-                    mLinkProperties,
-                    mIsBlocked,
-                    vcnContext,
-                    underlyingNetworkTemplates,
-                    subscriptionGroup,
-                    snapshot,
-                    currentlySelected,
-                    carrierConfig);
+                    mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index b0b66cf..5c86701 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -130,6 +130,28 @@
      */
     final Rect cropHint = new Rect(0, 0, 0, 0);
 
+    // Describes the context of a call to WallpaperManagerService#bindWallpaperComponentLocked
+    enum BindSource {
+        UNKNOWN,
+        CONNECT_LOCKED,
+        CONNECTION_TRY_TO_REBIND,
+        INITIALIZE_FALLBACK,
+        PACKAGE_UPDATE_FINISHED,
+        RESTORE_SETTINGS_LIVE_FAILURE,
+        RESTORE_SETTINGS_LIVE_SUCCESS,
+        RESTORE_SETTINGS_STATIC,
+        SET_LIVE,
+        SET_LIVE_TO_CLEAR,
+        SET_STATIC,
+        SWITCH_WALLPAPER_FAILURE,
+        SWITCH_WALLPAPER_SWITCH_USER,
+        SWITCH_WALLPAPER_UNLOCK_USER,
+    }
+
+    // Context in which this wallpaper was bound. Intended for use in resolving b/301073479 but may
+    // be useful after the issue is resolved as well.
+    BindSource mBindSource = BindSource.UNKNOWN;
+
     // map of which -> File
     private final SparseArray<File> mWallpaperFiles = new SparseArray<>();
     private final SparseArray<File> mCropFiles = new SparseArray<>();
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index 5f8bbe5..de98df5 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -46,6 +46,7 @@
 import com.android.internal.util.JournaledFile;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.wallpaper.WallpaperData.BindSource;
 
 import libcore.io.IoUtils;
 
@@ -314,6 +315,14 @@
         wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0);
         wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0);
         wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f);
+        BindSource bindSource;
+        try {
+            bindSource = Enum.valueOf(BindSource.class,
+                    getAttributeString(parser, "bindSource", BindSource.UNKNOWN.name()));
+        } catch (IllegalArgumentException | NullPointerException e) {
+            bindSource = BindSource.UNKNOWN;
+        }
+        wallpaper.mBindSource = bindSource;
         int dimAmountsCount = getAttributeInt(parser, "dimAmountsCount", 0);
         if (dimAmountsCount > 0) {
             SparseArray<Float> allDimAmounts = new SparseArray<>(dimAmountsCount);
@@ -364,6 +373,11 @@
         return parser.getAttributeFloat(null, name, defValue);
     }
 
+    private String getAttributeString(XmlPullParser parser, String name, String defValue) {
+        String s = parser.getAttributeValue(null, name);
+        return (s != null) ? s : defValue;
+    }
+
     void saveSettingsLocked(int userId, WallpaperData wallpaper, WallpaperData lockWallpaper) {
         JournaledFile journal = makeJournaledFile(userId);
         FileOutputStream fstream = null;
@@ -423,6 +437,7 @@
         }
 
         out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount);
+        out.attribute(null, "bindSource", wallpaper.mBindSource.name());
         int dimAmountsCount = wallpaper.mUidToDimAmount.size();
         out.attributeInt(null, "dimAmountsCount", dimAmountsCount);
         if (dimAmountsCount > 0) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 1485b96..3782b42 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -122,6 +122,7 @@
 import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.utils.TimingsTraceAndSlog;
+import com.android.server.wallpaper.WallpaperData.BindSource;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
@@ -335,6 +336,7 @@
                     };
 
                     // If this was the system wallpaper, rebind...
+                    wallpaper.mBindSource = BindSource.SET_STATIC;
                     bindWallpaperComponentLocked(mImageWallpaper, true, false, wallpaper,
                             callback);
                 }
@@ -354,6 +356,7 @@
                         }
                     };
 
+                    wallpaper.mBindSource = BindSource.SET_STATIC;
                     bindWallpaperComponentLocked(mImageWallpaper, true /* force */,
                             false /* fromUser */, wallpaper, callback);
                 } else if (isAppliedToLock) {
@@ -811,6 +814,7 @@
                 Slog.w(TAG, "Failed attaching wallpaper on display", e);
                 if (wallpaper != null && !wallpaper.wallpaperUpdating
                         && connection.getConnectedEngineSize() == 0) {
+                    wallpaper.mBindSource = BindSource.CONNECT_LOCKED;
                     bindWallpaperComponentLocked(null /* componentName */, false /* force */,
                             false /* fromUser */, wallpaper, null /* reply */);
                 }
@@ -1035,6 +1039,7 @@
                 final ComponentName wpService = mWallpaper.wallpaperComponent;
                 // The broadcast of package update could be delayed after service disconnected. Try
                 // to re-bind the service for 10 seconds.
+                mWallpaper.mBindSource = BindSource.CONNECTION_TRY_TO_REBIND;
                 if (bindWallpaperComponentLocked(
                         wpService, true, false, mWallpaper, null)) {
                     mWallpaper.connection.scheduleTimeoutLocked();
@@ -1321,6 +1326,7 @@
                         }
                         wallpaper.wallpaperUpdating = false;
                         clearWallpaperComponentLocked(wallpaper);
+                        wallpaper.mBindSource = BindSource.PACKAGE_UPDATE_FINISHED;
                         if (!bindWallpaperComponentLocked(wpService, false, false,
                                 wallpaper, null)) {
                             Slog.w(TAG, "Wallpaper " + wpService
@@ -1711,6 +1717,7 @@
                 if (mHomeWallpaperWaitingForUnlock) {
                     final WallpaperData systemWallpaper =
                             getWallpaperSafeLocked(userId, FLAG_SYSTEM);
+                    systemWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER;
                     switchWallpaper(systemWallpaper, null);
                     // TODO(b/278261563): call notifyCallbacksLocked inside switchWallpaper
                     notifyCallbacksLocked(systemWallpaper);
@@ -1718,6 +1725,7 @@
                 if (mLockWallpaperWaitingForUnlock) {
                     final WallpaperData lockWallpaper =
                             getWallpaperSafeLocked(userId, FLAG_LOCK);
+                    lockWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER;
                     switchWallpaper(lockWallpaper, null);
                     notifyCallbacksLocked(lockWallpaper);
                 }
@@ -1838,6 +1846,7 @@
         // delete them in order to show the default wallpaper.
         clearWallpaperBitmaps(wallpaper);
 
+        fallback.mBindSource = BindSource.SWITCH_WALLPAPER_FAILURE;
         bindWallpaperComponentLocked(mImageWallpaper, true, false, fallback, reply);
         if ((wallpaper.mWhich & FLAG_SYSTEM) != 0) mHomeWallpaperWaitingForUnlock = true;
         if ((wallpaper.mWhich & FLAG_LOCK) != 0) mLockWallpaperWaitingForUnlock = true;
@@ -2963,6 +2972,8 @@
                  */
                 boolean forceRebind = force || (same && systemIsBoth && which == FLAG_SYSTEM);
 
+                newWallpaper.mBindSource =
+                        (name == null) ? BindSource.SET_LIVE_TO_CLEAR : BindSource.SET_LIVE;
                 bindSuccess = bindWallpaperComponentLocked(name, /* force */
                         forceRebind, /* fromUser */ true, newWallpaper, reply);
                 if (bindSuccess) {
@@ -3530,6 +3541,7 @@
             mFallbackWallpaper = new WallpaperData(systemUserId, FLAG_SYSTEM);
             mFallbackWallpaper.allowBackup = false;
             mFallbackWallpaper.wallpaperId = makeWallpaperIdLocked();
+            mFallbackWallpaper.mBindSource = BindSource.INITIALIZE_FALLBACK;
             bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false,
                     mFallbackWallpaper, null);
         }
@@ -3553,11 +3565,13 @@
             wallpaper.allowBackup = true;   // by definition if it was restored
             if (wallpaper.nextWallpaperComponent != null
                     && !wallpaper.nextWallpaperComponent.equals(mImageWallpaper)) {
+                wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_SUCCESS;
                 if (!bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, false, false,
                         wallpaper, null)) {
                     // No such live wallpaper or other failure; fall back to the default
                     // live wallpaper (since the profile being restored indicated that the
                     // user had selected a live rather than static one).
+                    wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_FAILURE;
                     bindWallpaperComponentLocked(null, false, false, wallpaper, null);
                 }
                 success = true;
@@ -3575,6 +3589,7 @@
                         + " id=" + wallpaper.wallpaperId);
                 if (success) {
                     mWallpaperCropper.generateCrop(wallpaper); // based on the new image + metadata
+                    wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_STATIC;
                     bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, true, false,
                             wallpaper, null);
                 }
@@ -3608,7 +3623,8 @@
         pw.print(" User "); pw.print(wallpaper.userId);
         pw.print(": id="); pw.print(wallpaper.wallpaperId);
         pw.print(": mWhich="); pw.print(wallpaper.mWhich);
-        pw.print(": mSystemWasBoth="); pw.println(wallpaper.mSystemWasBoth);
+        pw.print(": mSystemWasBoth="); pw.print(wallpaper.mSystemWasBoth);
+        pw.print(": mBindSource="); pw.println(wallpaper.mBindSource.name());
         pw.println(" Display state:");
         mWallpaperDisplayHelper.forEachDisplayData(wpSize -> {
             pw.print("  displayId=");
diff --git a/services/core/java/com/android/server/wearable/OWNERS b/services/core/java/com/android/server/wearable/OWNERS
index 073e2d7..eca48b7 100644
--- a/services/core/java/com/android/server/wearable/OWNERS
+++ b/services/core/java/com/android/server/wearable/OWNERS
@@ -1,3 +1 @@
-charliewang@google.com
-oni@google.com
-volnov@google.com
\ No newline at end of file
+include /core/java/android/app/wearable/OWNERS
\ No newline at end of file
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index cd48f5d..106be5f 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -218,7 +218,7 @@
                 PersistableBundle data,
                 SharedMemory sharedMemory,
                 RemoteCallback callback) {
-            Slog.i(TAG, "WearableSensingManagerInternal provideData.");
+            Slog.d(TAG, "WearableSensingManagerInternal provideData.");
             Objects.requireNonNull(data);
             Objects.requireNonNull(callback);
             mContext.enforceCallingOrSelfPermission(
diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
index 05da9df..e5c743c 100644
--- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
@@ -18,6 +18,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -180,16 +181,8 @@
         if (snapshot == null) {
             return null;
         }
-        final HardwareBuffer buffer = snapshot.getHardwareBuffer();
-        if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
-            buffer.close();
-            Slog.e(TAG, "Invalid snapshot dimensions " + buffer.getWidth() + "x"
-                    + buffer.getHeight());
-            return null;
-        } else {
-            mCache.putSnapshot(source, snapshot);
-            return snapshot;
-        }
+        mCache.putSnapshot(source, snapshot);
+        return snapshot;
     }
 
     @VisibleForTesting
@@ -210,6 +203,11 @@
 
     @Nullable
     TaskSnapshot snapshot(TYPE source) {
+        return snapshot(source, mHighResSnapshotScale);
+    }
+
+    @Nullable
+    TaskSnapshot snapshot(TYPE source, float scale) {
         TaskSnapshot.Builder builder = new TaskSnapshot.Builder();
         final Rect crop = prepareTaskSnapshot(source, builder);
         if (crop == null) {
@@ -218,7 +216,7 @@
         }
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "createSnapshot");
         final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = createSnapshot(source,
-                mHighResSnapshotScale, crop, builder);
+                scale, crop, builder);
         Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
         if (screenshotBuffer == null) {
             // Failed to acquire image. Has been logged.
@@ -227,7 +225,19 @@
         builder.setCaptureTime(SystemClock.elapsedRealtimeNanos());
         builder.setSnapshot(screenshotBuffer.getHardwareBuffer());
         builder.setColorSpace(screenshotBuffer.getColorSpace());
-        return builder.build();
+        final TaskSnapshot snapshot = builder.build();
+        return validateSnapshot(snapshot);
+    }
+
+    private static TaskSnapshot validateSnapshot(@NonNull TaskSnapshot snapshot) {
+        final HardwareBuffer buffer = snapshot.getHardwareBuffer();
+        if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
+            buffer.close();
+            Slog.e(TAG, "Invalid snapshot dimensions " + buffer.getWidth() + "x"
+                    + buffer.getHeight());
+            return null;
+        }
+        return snapshot;
     }
 
     @Nullable
@@ -432,7 +442,7 @@
         InsetUtils.addInsets(contentInsets, letterboxInsets);
         // Note, the app theme snapshot is never translucent because we enforce a non-translucent
         // color above
-        return new TaskSnapshot(
+        final TaskSnapshot taskSnapshot = new TaskSnapshot(
                 System.currentTimeMillis() /* id */,
                 SystemClock.elapsedRealtimeNanos() /* captureTime */,
                 topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(),
@@ -441,6 +451,7 @@
                 contentInsets, letterboxInsets, false /* isLowResolution */,
                 false /* isRealSnapshot */, source.getWindowingMode(),
                 getAppearance(source), false /* isTranslucent */, false /* hasImeSurface */);
+        return validateSnapshot(taskSnapshot);
     }
 
     static Rect getSystemBarInsets(Rect frame, InsetsState state) {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 4b55bec..676203b 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -1166,11 +1166,12 @@
             transition.abort();
             return;
         }
-        transition.collect(topFocusedRootTask);
-        executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
-        r.mTransitionController.requestStartTransition(transition, topFocusedRootTask,
+        final Task requestingTask = r.getTask();
+        transition.collect(requestingTask);
+        executeMultiWindowFullscreenRequest(fullscreenRequest, requestingTask);
+        r.mTransitionController.requestStartTransition(transition, requestingTask,
                 null /* remoteTransition */, null /* displayChange */);
-        transition.setReady(topFocusedRootTask, true);
+        transition.setReady(requestingTask, true);
     }
 
     private static void reportMultiwindowFullscreenRequestValidatingResult(IRemoteCallback callback,
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index febcc05..3a792d0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2181,7 +2181,8 @@
             // The result receiver is the transition receiver, which will handle the shared element
             // exit transition.
             mHasSceneTransition = options.getAnimationType() == ANIM_SCENE_TRANSITION
-                    && options.getResultReceiver() != null;
+                    && options.getSceneTransitionInfo() != null
+                    && options.getSceneTransitionInfo().getResultReceiver() != null;
             final PendingIntent usageReport = options.getUsageTimeReport();
             if (usageReport != null) {
                 appTimeTracker = new AppTimeTracker(usageReport);
@@ -2494,7 +2495,14 @@
 
         ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SnapshotStartingData");
         mStartingData = new SnapshotStartingData(mWmService, snapshot, typeParams);
-        if (task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+        if ((!mStyleFillsParent && task.getChildCount() > 1)
+                || task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+            // Case 1:
+            // If it is moving a Task{[0]=main activity, [1]=translucent activity} to front, use
+            // shared starting window so that the transition doesn't need to wait for the activity
+            // behind the translucent activity. Also, onFirstWindowDrawn will check all visible
+            // activities are drawn in the task to remove the snapshot starting window.
+            // Case 2:
             // Associate with the task so if this activity is resized by task fragment later, the
             // starting window can keep the same bounds as the task.
             associateStartingDataWithTask();
@@ -5178,16 +5186,15 @@
         return mPendingOptions;
     }
 
-    ActivityOptions takeOptions() {
-        if (DEBUG_TRANSITION) Slog.i(TAG, "Taking options for " + this + " callers="
-                + Debug.getCallers(6));
+    ActivityOptions.SceneTransitionInfo takeSceneTransitionInfo() {
+        if (DEBUG_TRANSITION) {
+            Slog.i(TAG, "Taking SceneTransitionInfo for " + this + " callers="
+                    + Debug.getCallers(6));
+        }
         if (mPendingOptions == null) return null;
         final ActivityOptions opts = mPendingOptions;
         mPendingOptions = null;
-        // Strip sensitive information from options before sending it to app.
-        opts.setRemoteTransition(null);
-        opts.setRemoteAnimationAdapter(null);
-        return opts;
+        return opts.getSceneTransitionInfo();
     }
 
     RemoteTransition takeRemoteTransition() {
@@ -6153,7 +6160,7 @@
 
             try {
                 mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
-                        StartActivityItem.obtain(token, takeOptions()));
+                        StartActivityItem.obtain(token, takeSceneTransitionInfo()));
             } catch (Exception e) {
                 Slog.w(TAG, "Exception thrown sending start: " + intent.getComponent(), e);
             }
@@ -6258,8 +6265,11 @@
 
     void handleAlreadyVisible() {
         try {
-            if (returningOptions != null) {
-                app.getThread().scheduleOnNewActivityOptions(token, returningOptions.toBundle());
+            if (returningOptions != null
+                    && returningOptions.getAnimationType() == ANIM_SCENE_TRANSITION
+                    && returningOptions.getSceneTransitionInfo() != null) {
+                app.getThread().scheduleOnNewSceneTransitionInfo(token,
+                        returningOptions.getSceneTransitionInfo());
             }
         } catch(RemoteException e) {
         }
@@ -6608,10 +6618,6 @@
         return hasProcess() && !app.isCrashing() && !app.isNotResponding();
     }
 
-    void startFreezingScreenLocked(int configChanges) {
-        startFreezingScreenLocked(app, configChanges);
-    }
-
     void startFreezingScreenLocked(WindowProcessController app, int configChanges) {
         if (mayFreezeScreenLocked(app)) {
             if (getParent() == null) {
@@ -8095,12 +8101,8 @@
      * Set the last reported configuration to the client. Should be called whenever
      * a new merged configuration is sent to the client for this activity.
      */
-    void setLastReportedConfiguration(@NonNull MergedConfiguration config) {
-        setLastReportedConfiguration(config.getGlobalConfiguration(),
-            config.getOverrideConfiguration());
-    }
-
-    private void setLastReportedConfiguration(Configuration global, Configuration override) {
+    void setLastReportedConfiguration(@NonNull Configuration global,
+            @NonNull Configuration override) {
         mLastReportedConfiguration.setConfiguration(global, override);
     }
 
@@ -9634,7 +9636,7 @@
             configChangeFlags |= changes;
             if (mVisible && mAtmService.mTmpUpdateConfigurationResult.mIsUpdating
                     && !mTransitionController.isShellTransitionsEnabled()) {
-                startFreezingScreenLocked(mAtmService.mTmpUpdateConfigurationResult.changes);
+                startFreezingScreenLocked(app, mAtmService.mTmpUpdateConfigurationResult.changes);
             }
             final boolean displayMayChange = mTmpConfig.windowConfiguration.getDisplayRotation()
                     != getWindowConfiguration().getDisplayRotation()
@@ -10621,6 +10623,14 @@
 
     @Override
     boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) {
+        if (task != null && task.mSharedStartingData != null) {
+            final WindowState startingWin = task.topStartingWindow();
+            if (startingWin != null && startingWin.mSyncState == SYNC_STATE_READY
+                    && mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
+                // The sync is ready if a drawn starting window covered the task.
+                return true;
+            }
+        }
         if (!super.isSyncFinished(group)) return false;
         if (mDisplayContent != null && mDisplayContent.mUnknownAppVisibilityController
                 .isVisibilityUnknown(this)) {
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index 7af494c..a692167 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -25,6 +25,7 @@
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.util.ArraySet;
+import android.util.IntArray;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.window.TaskSnapshot;
@@ -36,6 +37,7 @@
 import com.android.window.flags.Flags;
 
 import java.io.File;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 
 /**
@@ -136,11 +138,26 @@
                 false /* enableLowResSnapshots */, 0 /* lowResScaleFactor */, use16BitFormat);
     }
 
-    /** Retrieves a snapshot for an activity from cache. */
+    /**
+     * Retrieves a snapshot for a set of activities from cache.
+     * This will only return the snapshot IFF input activities exist entirely in the snapshot.
+     * Sample: If the snapshot was captured with activity A and B, here will return null if the
+     * input activity is only [A] or [B], it must be [A, B]
+     */
     @Nullable
-    TaskSnapshot getSnapshot(ActivityRecord ar) {
-        final int code = getSystemHashCode(ar);
-        return mCache.getSnapshot(code);
+    TaskSnapshot getSnapshot(@NonNull ActivityRecord[] activities) {
+        if (activities.length == 0) {
+            return null;
+        }
+        final UserSavedFile tmpUsf = findSavedFile(activities[0]);
+        if (tmpUsf == null || tmpUsf.mActivityIds.size() != activities.length) {
+            return null;
+        }
+        int fileId = 0;
+        for (int i = activities.length - 1; i >= 0; --i) {
+            fileId ^= getSystemHashCode(activities[i]);
+        }
+        return tmpUsf.mFileId == fileId ? mCache.getSnapshot(tmpUsf.mActivityIds.get(0)) : null;
     }
 
     private void cleanUpUserFiles(int userId) {
@@ -229,33 +246,16 @@
                     + " load " + mPendingLoadActivity);
         }
         // load snapshot to cache
-        for (int i = mPendingLoadActivity.size() - 1; i >= 0; i--) {
-            final ActivityRecord ar = mPendingLoadActivity.valueAt(i);
-            final int code = getSystemHashCode(ar);
-            final int userId = ar.mUserId;
-            if (mCache.getSnapshot(code) != null) {
-                // already in cache, skip
-                continue;
-            }
-            if (containsFile(code, userId)) {
-                synchronized (mSnapshotPersistQueue.getLock()) {
-                    mSnapshotPersistQueue.insertQueueAtFirstLocked(
-                            new LoadActivitySnapshotItem(ar, code, userId, mPersistInfoProvider));
-                }
-            }
-        }
+        loadActivitySnapshot();
         // clear mTmpRemoveActivity from cache
         for (int i = mPendingRemoveActivity.size() - 1; i >= 0; i--) {
             final ActivityRecord ar = mPendingRemoveActivity.valueAt(i);
-            final int code = getSystemHashCode(ar);
-            mCache.onIdRemoved(code);
+            removeCachedFiles(ar);
         }
         // clear snapshot on cache and delete files
         for (int i = mPendingDeleteActivity.size() - 1; i >= 0; i--) {
             final ActivityRecord ar = mPendingDeleteActivity.valueAt(i);
-            final int code = getSystemHashCode(ar);
-            mCache.onIdRemoved(code);
-            removeIfUserSavedFileExist(code, ar.mUserId);
+            removeIfUserSavedFileExist(ar);
         }
         // don't keep any reference
         resetTmpFields();
@@ -264,28 +264,38 @@
     class LoadActivitySnapshotItem extends SnapshotPersistQueue.WriteQueueItem {
         private final int mCode;
         private final int mUserId;
-        private final ActivityRecord mActivityRecord;
+        private final ActivityRecord[] mActivities;
 
-        LoadActivitySnapshotItem(@NonNull ActivityRecord ar, int code, int userId,
+        LoadActivitySnapshotItem(@NonNull ActivityRecord[] activities, int code, int userId,
                 @NonNull PersistInfoProvider persistInfoProvider) {
             super(persistInfoProvider);
-            mActivityRecord = ar;
+            mActivities = activities;
             mCode = code;
             mUserId = userId;
         }
 
         @Override
         void write() {
-            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
-                    "load_activity_snapshot");
-            final TaskSnapshot snapshot = mSnapshotLoader.loadTask(mCode,
-                    mUserId, false /* loadLowResolutionBitmap */);
-            synchronized (mService.getWindowManagerLock()) {
-                if (snapshot != null && !mActivityRecord.finishing) {
-                    mCache.putSnapshot(mActivityRecord, snapshot);
+            try {
+                Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
+                        "load_activity_snapshot");
+                final TaskSnapshot snapshot = mSnapshotLoader.loadTask(mCode,
+                        mUserId, false /* loadLowResolutionBitmap */);
+                if (snapshot == null) {
+                    return;
                 }
+                synchronized (mService.getWindowManagerLock()) {
+                    // Verify the snapshot is still needed, and the activity is not finishing
+                    if (!hasRecord(mActivities[0])) {
+                        return;
+                    }
+                    for (ActivityRecord ar : mActivities) {
+                        mCache.putSnapshot(ar, snapshot);
+                    }
+                }
+            } finally {
+                Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             }
-            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         }
 
         @Override
@@ -297,18 +307,81 @@
         }
     }
 
-    void recordSnapshot(ActivityRecord activity) {
-        if (shouldDisableSnapshots()) {
+    void loadActivitySnapshot() {
+        if (mPendingLoadActivity.isEmpty()) {
+            return;
+        }
+        // Only load if saved file exists.
+        final ArraySet<UserSavedFile> loadingFiles = new ArraySet<>();
+        for (int i = mPendingLoadActivity.size() - 1; i >= 0; i--) {
+            final ActivityRecord ar = mPendingLoadActivity.valueAt(i);
+            final UserSavedFile usf = findSavedFile(ar);
+            if (usf != null) {
+                loadingFiles.add(usf);
+            }
+        }
+        // Filter out the activity if the snapshot was removed.
+        for (int i = loadingFiles.size() - 1; i >= 0; i--) {
+            final UserSavedFile usf = loadingFiles.valueAt(i);
+            final ActivityRecord[] activities = usf.filterExistActivities(mPendingLoadActivity);
+            if (activities == null) {
+                continue;
+            }
+            if (getSnapshot(activities) != null) {
+                // Found the cache in memory, so skip loading from file.
+                continue;
+            }
+            loadSnapshotInner(activities, usf);
+        }
+    }
+
+    @VisibleForTesting
+    void loadSnapshotInner(ActivityRecord[] activities, UserSavedFile usf) {
+        synchronized (mSnapshotPersistQueue.getLock()) {
+            mSnapshotPersistQueue.insertQueueAtFirstLocked(new LoadActivitySnapshotItem(
+                    activities, usf.mFileId, usf.mUserId, mPersistInfoProvider));
+        }
+    }
+
+    /**
+     * Record one or multiple activities within a snapshot where those activities must belong to
+     * the same task.
+     * @param activity If the request activity is more than one, try to record those activities
+     *                 as a single snapshot, so those activities should belong to the same task.
+     */
+    void recordSnapshot(@NonNull ArrayList<ActivityRecord> activity) {
+        if (shouldDisableSnapshots() || activity.isEmpty()) {
             return;
         }
         if (DEBUG) {
             Slog.d(TAG, "ActivitySnapshotController#recordSnapshot " + activity);
         }
-        final TaskSnapshot snapshot = recordSnapshotInner(activity);
-        if (snapshot != null) {
-            final int code = getSystemHashCode(activity);
-            addUserSavedFile(code, activity.mUserId, snapshot);
+        final int size = activity.size();
+        final int[] mixedCode = new int[size];
+        if (size == 1) {
+            final ActivityRecord singleActivity = activity.get(0);
+            final TaskSnapshot snapshot = recordSnapshotInner(singleActivity);
+            if (snapshot != null) {
+                mixedCode[0] = getSystemHashCode(singleActivity);
+                addUserSavedFile(singleActivity.mUserId, snapshot, mixedCode);
+            }
+            return;
         }
+
+        final Task mainTask = activity.get(0).getTask();
+        // Snapshot by task controller with activity's scale.
+        final TaskSnapshot snapshot = mService.mTaskSnapshotController
+                .snapshot(mainTask, mHighResSnapshotScale);
+        if (snapshot == null) {
+            return;
+        }
+
+        for (int i = 0; i < activity.size(); ++i) {
+            final ActivityRecord next = activity.get(i);
+            mCache.putSnapshot(next, snapshot);
+            mixedCode[i] = getSystemHashCode(next);
+        }
+        addUserSavedFile(mainTask.mUserId, snapshot, mixedCode);
     }
 
     /**
@@ -331,7 +404,8 @@
         }
     }
 
-    private static int getSystemHashCode(ActivityRecord activity) {
+    @VisibleForTesting
+    static int getSystemHashCode(ActivityRecord activity) {
         return System.identityHashCode(activity);
     }
 
@@ -362,7 +436,13 @@
         if (ar.isVisibleRequested()) {
             mPendingDeleteActivity.add(ar);
             // load next one if exists.
-            addBelowActivityIfExist(ar, mPendingLoadActivity, true, "load-snapshot");
+            // Note if this transition is happen between two TaskFragment, the next N - 1 activity
+            // may not participant in this transition.
+            // Sample:
+            //   [TF1] close
+            //   [TF2] open
+            //   Bottom Activity <- Able to load this even it didn't participant the transition.
+            addBelowActivityIfExist(ar, mPendingLoadActivity, false, "load-snapshot");
         } else {
             // remove the snapshot for the one below close
             addBelowActivityIfExist(ar, mPendingRemoveActivity, true, "remove-snapshot");
@@ -478,10 +558,8 @@
     }
 
     private void adjustSavedFileOrder(Task nextTopTask) {
-        final int userId = nextTopTask.mUserId;
         nextTopTask.forAllActivities(ar -> {
-            final int code = getSystemHashCode(ar);
-            final UserSavedFile usf = getUserFiles(userId).get(code);
+            final UserSavedFile usf = findSavedFile(ar);
             if (usf != null) {
                 mSavedFilesInOrder.remove(usf);
                 mSavedFilesInOrder.add(usf);
@@ -494,9 +572,7 @@
         if (shouldDisableSnapshots()) {
             return;
         }
-        super.onAppRemoved(activity);
-        final int code = getSystemHashCode(activity);
-        removeIfUserSavedFileExist(code, activity.mUserId);
+        removeIfUserSavedFileExist(activity);
         if (DEBUG) {
             Slog.d(TAG, "ActivitySnapshotController#onAppRemoved delete snapshot " + activity);
         }
@@ -507,9 +583,7 @@
         if (shouldDisableSnapshots()) {
             return;
         }
-        super.onAppDied(activity);
-        final int code = getSystemHashCode(activity);
-        removeIfUserSavedFileExist(code, activity.mUserId);
+        removeIfUserSavedFileExist(activity);
         if (DEBUG) {
             Slog.d(TAG, "ActivitySnapshotController#onAppDied delete snapshot " + activity);
         }
@@ -558,55 +632,92 @@
         return mUserSavedFiles.get(userId);
     }
 
-    private void removeIfUserSavedFileExist(int code, int userId) {
-        final UserSavedFile usf = getUserFiles(userId).get(code);
+    UserSavedFile findSavedFile(@NonNull ActivityRecord ar) {
+        final int code = getSystemHashCode(ar);
+        return findSavedFile(ar.mUserId, code);
+    }
+
+    UserSavedFile findSavedFile(int userId, int code) {
+        final SparseArray<UserSavedFile> usfs = getUserFiles(userId);
+        return usfs.get(code);
+    }
+
+    private void removeCachedFiles(ActivityRecord ar) {
+        final UserSavedFile usf = findSavedFile(ar);
         if (usf != null) {
-            mUserSavedFiles.get(userId).remove(code);
-            mSavedFilesInOrder.remove(usf);
-            mPersister.removeSnapshot(code, userId);
+            for (int i = usf.mActivityIds.size() - 1; i >= 0; --i) {
+                final int activityId = usf.mActivityIds.get(i);
+                mCache.onIdRemoved(activityId);
+            }
         }
     }
 
-    private boolean containsFile(int code, int userId) {
-        return getUserFiles(userId).get(code) != null;
+    private void removeIfUserSavedFileExist(ActivityRecord ar) {
+        final UserSavedFile usf = findSavedFile(ar);
+        if (usf != null) {
+            final SparseArray<UserSavedFile> usfs = getUserFiles(ar.mUserId);
+            for (int i = usf.mActivityIds.size() - 1; i >= 0; --i) {
+                final int activityId = usf.mActivityIds.get(i);
+                usf.remove(activityId);
+                mCache.onIdRemoved(activityId);
+                usfs.remove(activityId);
+            }
+            mSavedFilesInOrder.remove(usf);
+            mPersister.removeSnapshot(usf.mFileId, ar.mUserId);
+        }
     }
 
-    private void addUserSavedFile(int code, int userId, TaskSnapshot snapshot) {
-        final SparseArray<UserSavedFile> savedFiles = getUserFiles(userId);
-        final UserSavedFile savedFile = savedFiles.get(code);
-        if (savedFile == null) {
-            final UserSavedFile usf = new UserSavedFile(code, userId);
-            savedFiles.put(code, usf);
-            mSavedFilesInOrder.add(usf);
-            mPersister.persistSnapshot(code, userId, snapshot);
+    @VisibleForTesting
+    boolean hasRecord(@NonNull ActivityRecord ar) {
+        return findSavedFile(ar) != null;
+    }
 
-            if (mSavedFilesInOrder.size() > MAX_PERSIST_SNAPSHOT_COUNT * 2) {
-                purgeSavedFile();
-            }
+    @VisibleForTesting
+    void addUserSavedFile(int userId, TaskSnapshot snapshot, @NonNull int[] code) {
+        final UserSavedFile savedFile = findSavedFile(userId, code[0]);
+        if (savedFile != null) {
+            Slog.w(TAG, "Duplicate request for recording activity snapshot " + savedFile);
+            return;
+        }
+        int fileId = 0;
+        for (int i = code.length - 1; i >= 0; --i) {
+            fileId ^= code[i];
+        }
+        final UserSavedFile usf = new UserSavedFile(fileId, userId);
+        SparseArray<UserSavedFile> usfs = getUserFiles(userId);
+        for (int i = code.length - 1; i >= 0; --i) {
+            usfs.put(code[i], usf);
+        }
+        usf.mActivityIds.addAll(code);
+        mSavedFilesInOrder.add(usf);
+        mPersister.persistSnapshot(fileId, userId, snapshot);
+
+        if (mSavedFilesInOrder.size() > MAX_PERSIST_SNAPSHOT_COUNT * 2) {
+            purgeSavedFile();
         }
     }
 
     private void purgeSavedFile() {
         final int savedFileCount = mSavedFilesInOrder.size();
         final int removeCount = savedFileCount - MAX_PERSIST_SNAPSHOT_COUNT;
-        final ArrayList<UserSavedFile> usfs = new ArrayList<>();
-        if (removeCount > 0) {
-            final int removeTillIndex = savedFileCount - removeCount;
-            for (int i = savedFileCount - 1; i > removeTillIndex; --i) {
-                final UserSavedFile usf = mSavedFilesInOrder.remove(i);
-                if (usf != null) {
-                    final SparseArray<UserSavedFile> records = getUserFiles(usf.mUserId);
-                    records.remove(usf.mFileId);
-                    usfs.add(usf);
-                }
+        if (removeCount < 1) {
+            return;
+        }
+
+        final ArrayList<UserSavedFile> removeTargets = new ArrayList<>();
+        for (int i = removeCount - 1; i >= 0; --i) {
+            final UserSavedFile usf = mSavedFilesInOrder.remove(i);
+            final SparseArray<UserSavedFile> files = mUserSavedFiles.get(usf.mUserId);
+            for (int j = usf.mActivityIds.size() - 1; j >= 0; --j) {
+                mCache.removeRunningEntry(usf.mActivityIds.get(j));
+                files.remove(usf.mActivityIds.get(j));
             }
+            removeTargets.add(usf);
         }
-        if (usfs.size() > 0) {
-            removeSnapshotFiles(usfs);
-        }
+        removeSnapshotFiles(removeTargets);
     }
 
-    private void removeSnapshotFiles(ArrayList<UserSavedFile> files) {
+    private void removeSnapshotFiles(@NonNull ArrayList<UserSavedFile> files) {
         synchronized (mSnapshotPersistQueue.getLock()) {
             mSnapshotPersistQueue.sendToQueueLocked(
                     new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) {
@@ -624,12 +735,85 @@
         }
     }
 
+    @Override
+    void dump(PrintWriter pw, String prefix) {
+        super.dump(pw, prefix);
+        final String doublePrefix = prefix + "  ";
+        final String triplePrefix = doublePrefix + "  ";
+        for (int i = mUserSavedFiles.size() - 1; i >= 0; --i) {
+            final SparseArray<UserSavedFile> usfs = mUserSavedFiles.valueAt(i);
+            pw.println(doublePrefix + "UserSavedFile userId=" + mUserSavedFiles.keyAt(i));
+            final ArraySet<UserSavedFile> sets = new ArraySet<>();
+            for (int j = usfs.size() - 1; j >= 0; --j) {
+                sets.add(usfs.valueAt(j));
+            }
+            for (int j = sets.size() - 1; j >= 0; --j) {
+                pw.println(triplePrefix + "SavedFile=" + sets.valueAt(j));
+            }
+        }
+    }
+
     static class UserSavedFile {
-        int mFileId;
-        int mUserId;
+        // The unique id as filename.
+        final int mFileId;
+        final int mUserId;
+
+        /**
+         * The Id of all activities which are includes in the snapshot.
+         */
+        final IntArray mActivityIds = new IntArray();
+
         UserSavedFile(int fileId, int userId) {
             mFileId = fileId;
             mUserId = userId;
         }
+
+        boolean contains(int code) {
+            return mActivityIds.contains(code);
+        }
+
+        void remove(int code) {
+            final int index = mActivityIds.indexOf(code);
+            if (index >= 0) {
+                mActivityIds.remove(index);
+            }
+        }
+
+        ActivityRecord[] filterExistActivities(
+                @NonNull ArraySet<ActivityRecord> pendingLoadActivity) {
+            ArrayList<ActivityRecord> matchedActivities = null;
+            for (int i = pendingLoadActivity.size() - 1; i >= 0; --i) {
+                final ActivityRecord ar = pendingLoadActivity.valueAt(i);
+                if (contains(getSystemHashCode(ar))) {
+                    if (matchedActivities == null) {
+                        matchedActivities = new ArrayList<>();
+                    }
+                    matchedActivities.add(ar);
+                }
+            }
+            if (matchedActivities == null || matchedActivities.size() != mActivityIds.size()) {
+                return null;
+            }
+            return matchedActivities.toArray(new ActivityRecord[0]);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("UserSavedFile{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" fileId=");
+            sb.append(Integer.toHexString(mFileId));
+            sb.append(", activityIds=[");
+            for (int i = mActivityIds.size() - 1; i >= 0; --i) {
+                sb.append(Integer.toHexString(mActivityIds.get(i)));
+                if (i > 0) {
+                    sb.append(',');
+                }
+            }
+            sb.append("]");
+            sb.append("}");
+            return sb.toString();
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 1872f6e..ec0e3e7 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -134,7 +134,6 @@
 import android.os.WorkSource;
 import android.provider.MediaStore;
 import android.util.ArrayMap;
-import android.util.MergedConfiguration;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
@@ -820,8 +819,6 @@
         proc.pauseConfigurationDispatch();
 
         try {
-            r.startFreezingScreenLocked(proc, 0);
-
             // schedule launch ticks to collect information about slow apps.
             r.startLaunchTickingLocked();
             r.lastLaunchTime = SystemClock.uptimeMillis();
@@ -908,13 +905,9 @@
                         r.intent.getComponent().getPackageName(), NOTIFY_PACKAGE_USE_ACTIVITY);
                 mService.getAppWarningsLocked().onStartActivity(r);
 
-                // Because we could be starting an Activity in the system process this may not go
-                // across a Binder interface which would create a new Configuration. Consequently
-                // we have to always create a new Configuration here.
                 final Configuration procConfig = proc.prepareConfigurationForLaunchingActivity();
-                final MergedConfiguration mergedConfiguration = new MergedConfiguration(
-                        procConfig, r.getMergedOverrideConfiguration());
-                r.setLastReportedConfiguration(mergedConfiguration);
+                final Configuration overrideConfig = r.getMergedOverrideConfiguration();
+                r.setLastReportedConfiguration(procConfig, overrideConfig);
 
                 logIfTransactionTooLarge(r.intent, r.getSavedState());
 
@@ -932,13 +925,10 @@
                 final int deviceId = getDeviceIdForDisplayId(r.getDisplayId());
                 final LaunchActivityItem launchActivityItem = LaunchActivityItem.obtain(r.token,
                         r.intent, System.identityHashCode(r), r.info,
-                        // TODO: Have this take the merged configuration instead of separate global
-                        // and override configs.
-                        mergedConfiguration.getGlobalConfiguration(),
-                        mergedConfiguration.getOverrideConfiguration(), deviceId,
+                        procConfig, overrideConfig, deviceId,
                         r.getFilteredReferrer(r.launchedFromPackage), task.voiceInteractor,
                         proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),
-                        results, newIntents, r.takeOptions(), isTransitionForward,
+                        results, newIntents, r.takeSceneTransitionInfo(), isTransitionForward,
                         proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,
                         r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken);
 
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 22d17b5..8aaf76a 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1311,7 +1311,7 @@
                 Rect insets;
                 if (mainWindow != null) {
                     insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets(
-                            mBounds, WindowInsets.Type.systemBars(),
+                            mBounds, WindowInsets.Type.tappableElement(),
                             false /* ignoreVisibility */).toRect();
                     InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets());
                 } else {
@@ -1327,7 +1327,8 @@
                 return mAnimationTarget;
             }
 
-            void createStartingSurface(@NonNull WindowContainer closeWindow) {
+            void createStartingSurface(@NonNull WindowContainer closeWindow,
+                    @NonNull ActivityRecord[] visibleOpenActivities) {
                 if (!mIsOpen) {
                     return;
                 }
@@ -1346,7 +1347,7 @@
                 if (mainActivity == null) {
                     return;
                 }
-                final TaskSnapshot snapshot = getSnapshot(mTarget);
+                final TaskSnapshot snapshot = getSnapshot(mTarget, visibleOpenActivities);
                 mRequestedStartingSurfaceId = openTask.mAtmService.mTaskOrganizerController
                         .addWindowlessStartingSurface(openTask, mainActivity,
                                 // Choose configuration from closeWindow, because the configuration
@@ -1489,7 +1490,8 @@
                         // Try to draw two snapshot within a WindowlessStartingWindow, or find
                         // another key for StartingWindowRecordManager.
                         && openAnimationAdaptor.length == 1) {
-                    openAnimationAdaptor[0].createStartingSurface(closeWindow);
+                    openAnimationAdaptor[0].createStartingSurface(closeWindow,
+                            visibleOpenActivities);
                 } else {
                     for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
                         setLaunchBehind(visibleOpenActivities[i]);
@@ -1594,7 +1596,9 @@
             // skip commitVisibility call in setVisibility cause the activity won't visible here.
             // Call it again to make sure the activity could be visible while handling the pending
             // animation.
-            activity.commitVisibility(true, true);
+            // Do not performLayout during prepare animation, because it could cause focus window
+            // change. Let that happen after the BackNavigationInfo has returned to shell.
+            activity.commitVisibility(true, false /* performLayout */);
             activity.mTransitionController.mSnapshotController
                     .mActivitySnapshotController.addOnBackPressedActivity(activity);
         }
@@ -1671,7 +1675,8 @@
         mPendingAnimationBuilder = null;
     }
 
-    static TaskSnapshot getSnapshot(@NonNull WindowContainer w) {
+    static TaskSnapshot getSnapshot(@NonNull WindowContainer w,
+            ActivityRecord[] visibleOpenActivities) {
         if (w.asTask() != null) {
             final Task task = w.asTask();
             return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot(
@@ -1681,7 +1686,8 @@
 
         if (w.asActivityRecord() != null) {
             final ActivityRecord ar = w.asActivityRecord();
-            return ar.mWmService.mSnapshotController.mActivitySnapshotController.getSnapshot(ar);
+            return ar.mWmService.mSnapshotController.mActivitySnapshotController
+                    .getSnapshot(visibleOpenActivities);
         }
         return null;
     }
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index fc3a338..9c9cf04 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -120,38 +120,56 @@
 
     static final int BAL_BLOCK = 0;
 
-    static final int BAL_ALLOW_DEFAULT = 1;
+    static final int BAL_ALLOW_DEFAULT =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_DEFAULT;
 
     // Following codes are in order of precedence
 
     /** Important UIDs which should be always allowed to launch activities */
-    static final int BAL_ALLOW_ALLOWLISTED_UID = 2;
+    static final int BAL_ALLOW_ALLOWLISTED_UID =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_ALLOWLISTED_UID;
 
     /** Apps that fulfill a certain role that can can always launch new tasks */
-    static final int BAL_ALLOW_ALLOWLISTED_COMPONENT = 3;
+    static final int BAL_ALLOW_ALLOWLISTED_COMPONENT =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_ALLOWLISTED_COMPONENT;
 
-    /** Apps which currently have a visible window or are bound by a service with a visible
-     * window */
-    static final int BAL_ALLOW_VISIBLE_WINDOW = 4;
+    /**
+     * Apps which currently have a visible window or are bound by a service with a visible
+     * window
+     */
+    static final int BAL_ALLOW_VISIBLE_WINDOW =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_VISIBLE_WINDOW;
 
     /** Allowed due to the PendingIntent sender */
-    static final int BAL_ALLOW_PENDING_INTENT = 5;
+    static final int BAL_ALLOW_PENDING_INTENT =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_PENDING_INTENT;
 
-    /** App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges
-     * granted to it */
-    static final int BAL_ALLOW_PERMISSION = 6;
+    /**
+     * App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges
+     * granted to it
+     */
+    static final int BAL_ALLOW_PERMISSION =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_BAL_PERMISSION;
 
     /** Process has SYSTEM_ALERT_WINDOW permission granted to it */
-    static final int BAL_ALLOW_SAW_PERMISSION = 7;
+    static final int BAL_ALLOW_SAW_PERMISSION =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_SAW_PERMISSION;
 
     /** App is in grace period after an activity was started or finished */
-    static final int BAL_ALLOW_GRACE_PERIOD = 8;
+    static final int BAL_ALLOW_GRACE_PERIOD =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_GRACE_PERIOD;
 
     /** App is in a foreground task or bound to a foreground service (but not itself visible) */
-    static final int BAL_ALLOW_FOREGROUND = 9;
+    static final int BAL_ALLOW_FOREGROUND =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_FOREGROUND;
 
     /** Process belongs to a SDK sandbox */
-    static final int BAL_ALLOW_SDK_SANDBOX = 10;
+    static final int BAL_ALLOW_SDK_SANDBOX =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_SDK_SANDBOX;
+
+    /** Process belongs to a SDK sandbox */
+    static final int BAL_ALLOW_NON_APP_VISIBLE_WINDOW =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_NON_APP_VISIBLE_WINDOW;
 
     static String balCodeToString(@BalCode int balCode) {
         return switch (balCode) {
@@ -160,6 +178,7 @@
             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_NON_APP_VISIBLE_WINDOW -> "BAL_ALLOW_NON_APP_VISIBLE_WINDOW";
             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";
@@ -788,7 +807,7 @@
                     /*background*/ false, "callingUid has visible window");
         }
         if (mService.mActiveUids.hasNonAppVisibleWindow(callingUid)) {
-            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+            return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
                     /*background*/ false, "callingUid has non-app visible window");
         }
 
@@ -884,7 +903,7 @@
                         /*background*/ false, "realCallingUid has visible window");
             }
             if (mService.mActiveUids.hasNonAppVisibleWindow(state.mRealCallingUid)) {
-                return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+                return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
                         /*background*/ false, "realCallingUid has non-app visible window");
             }
         } else {
@@ -989,7 +1008,8 @@
                     || balCode == BAL_ALLOW_PERMISSION
                     || balCode == BAL_ALLOW_PENDING_INTENT
                     || balCode == BAL_ALLOW_SAW_PERMISSION
-                    || balCode == BAL_ALLOW_VISIBLE_WINDOW) {
+                    || balCode == BAL_ALLOW_VISIBLE_WINDOW
+                    || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW) {
                 return true;
             }
         }
@@ -1501,7 +1521,8 @@
         Intent intent = state.mIntent;
 
         if (code == BAL_ALLOW_PENDING_INTENT
-                && (callingUid == Process.SYSTEM_UID || realCallingUid == Process.SYSTEM_UID)) {
+                && (callingUid < Process.FIRST_APPLICATION_UID
+                || realCallingUid < Process.FIRST_APPLICATION_UID)) {
             String activityName = intent != null
                     ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
             writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 975fdc0..0f9e5b0 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -179,6 +179,12 @@
             if (physicalDisplayUpdated) {
                 onDisplayUpdated(transition, fromRotation, startBounds);
             } else {
+                final TransitionRequestInfo.DisplayChange displayChange =
+                        getCurrentDisplayChange(fromRotation, startBounds);
+                mDisplayContent.mTransitionController.requestStartTransition(transition,
+                        /* startTask= */ null, /* remoteTransition= */ null, displayChange);
+                mDisplayContent.mTransitionController.setDisplaySyncMethod(displayChange,
+                        mDisplayContent);
                 transition.setAllReady();
             }
         });
@@ -204,15 +210,8 @@
         return new DisplayInfo(mNonOverrideDisplayInfo);
     }
 
-    /**
-     * Called when physical display is updated, this could happen e.g. on foldable
-     * devices when the physical underlying display is replaced. This method should be called
-     * when the new display info is already applied to the WM hierarchy.
-     *
-     * @param fromRotation rotation before the display change
-     * @param startBounds  display bounds before the display change
-     */
-    private void onDisplayUpdated(@NonNull Transition transition, int fromRotation,
+    @NonNull
+    private TransitionRequestInfo.DisplayChange getCurrentDisplayChange(int fromRotation,
             @NonNull Rect startBounds) {
         final Rect endBounds = new Rect(0, 0, mDisplayContent.mInitialDisplayWidth,
                 mDisplayContent.mInitialDisplayHeight);
@@ -224,6 +223,23 @@
         displayChange.setEndAbsBounds(endBounds);
         displayChange.setStartRotation(fromRotation);
         displayChange.setEndRotation(toRotation);
+        return displayChange;
+    }
+
+    /**
+     * Called when physical display is updated, this could happen e.g. on foldable
+     * devices when the physical underlying display is replaced. This method should be called
+     * when the new display info is already applied to the WM hierarchy.
+     *
+     * @param fromRotation rotation before the display change
+     * @param startBounds  display bounds before the display change
+     */
+    private void onDisplayUpdated(@NonNull Transition transition, int fromRotation,
+            @NonNull Rect startBounds) {
+        final int toRotation = mDisplayContent.getRotation();
+
+        final TransitionRequestInfo.DisplayChange displayChange =
+                getCurrentDisplayChange(fromRotation, startBounds);
         displayChange.setPhysicalDisplayChanged(true);
 
         mDisplayContent.mTransitionController.requestStartTransition(transition,
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 460a68f..63ca592 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -40,6 +40,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
@@ -959,15 +960,17 @@
 
             case TYPE_BASE_APPLICATION:
 
-                // A non-translucent main app window isn't allowed to fit insets, as it would create
-                // a hole on the display!
+                // A non-translucent main app window isn't allowed to fit insets or display cutouts,
+                // as it would create a hole on the display!
                 if (attrs.isFullscreen() && win.mActivityRecord != null
                         && win.mActivityRecord.fillsParent()
-                        && (win.mAttrs.privateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0
-                        && attrs.getFitInsetsTypes() != 0) {
+                        && (attrs.privateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0
+                        && (attrs.getFitInsetsTypes() != 0
+                                || (attrs.privateFlags & PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED) != 0
+                                        && attrs.layoutInDisplayCutoutMode
+                                                != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS)) {
                     throw new IllegalArgumentException("Illegal attributes: Main activity window"
-                            + " that isn't translucent trying to fit insets: "
-                            + attrs.getFitInsetsTypes()
+                            + " that isn't translucent trying to fit insets or display cutouts."
                             + " attrs=" + attrs);
                 }
                 break;
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index bf30af3..8035a29 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -112,7 +112,7 @@
      * z-layering reference so that we can place the recents input consumer above it.
      */
     private WeakReference<ActivityRecord> mActiveRecentsActivity = null;
-    private WeakReference<ActivityRecord> mActiveRecentsLayerRef = null;
+    private WeakReference<Task> mActiveRecentsLayerRef = null;
 
     private class UpdateInputWindows implements Runnable {
         @Override
@@ -389,9 +389,9 @@
     /**
      * Inform InputMonitor when recents is active so it can enable the recents input consumer.
      * @param activity The active recents activity. {@code null} means recents is not active.
-     * @param layer An activity whose Z-layer is used as a reference for how to sort the consumer.
+     * @param layer A task whose Z-layer is used as a reference for how to sort the consumer.
      */
-    void setActiveRecents(@Nullable ActivityRecord activity, @Nullable ActivityRecord layer) {
+    void setActiveRecents(@Nullable ActivityRecord activity, @Nullable Task layer) {
         final boolean clear = activity == null;
         final boolean wasActive = mActiveRecentsActivity != null && mActiveRecentsLayerRef != null;
         mActiveRecentsActivity = clear ? null : new WeakReference<>(activity);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 6033220..02b3f15 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -808,7 +808,6 @@
         mWmService.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
         mWmService.mAtmService.mTaskFragmentOrganizerController.dispatchPendingEvents();
         mWmService.mSyncEngine.onSurfacePlacement();
-        mWmService.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         checkAppTransitionReady(surfacePlacer);
 
diff --git a/services/core/java/com/android/server/wm/SensitiveContentPackages.java b/services/core/java/com/android/server/wm/SensitiveContentPackages.java
new file mode 100644
index 0000000..3862b82
--- /dev/null
+++ b/services/core/java/com/android/server/wm/SensitiveContentPackages.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.util.ArraySet;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Cache of distinct package/uid pairs that require being blocked from screen capture. This class is
+ * not threadsafe and any call site should hold {@link WindowManagerGlobalLock}
+ */
+public class SensitiveContentPackages {
+    private final ArraySet<PackageInfo> mProtectedPackages = new ArraySet<>();
+
+    /** Returns {@code true} if package/uid pair should be blocked from screen capture */
+    public boolean shouldBlockScreenCaptureForApp(String pkg, int uid) {
+        for (int i = 0; i < mProtectedPackages.size(); i++) {
+            PackageInfo info = mProtectedPackages.valueAt(i);
+            if (info != null && info.mPkg.equals(pkg) && info.mUid == uid) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Replaces the set of package/uid pairs to set that should be blocked from screen capture */
+    public void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos) {
+        mProtectedPackages.clear();
+        mProtectedPackages.addAll(packageInfos);
+    }
+
+    void dump(PrintWriter pw) {
+        final String innerPrefix = "  ";
+        pw.println("SensitiveContentPackages:");
+        pw.println(innerPrefix + "Packages that should block screen capture ("
+                + mProtectedPackages.size() + "):");
+        for (PackageInfo info : mProtectedPackages) {
+            pw.println(innerPrefix + "  package=" + info.mPkg + "  uid=" + info.mUid);
+        }
+    }
+
+    /** Helper class that represents a package/uid pair */
+    public static class PackageInfo {
+        private String mPkg;
+        private int mUid;
+
+        public PackageInfo(String pkg, int uid) {
+            this.mPkg = pkg;
+            this.mUid = uid;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof PackageInfo)) return false;
+            PackageInfo that = (PackageInfo) o;
+            return mUid == that.mUid && Objects.equals(mPkg, that.mPkg);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPkg, mUid);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index ed54ea8..f10a733 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -75,6 +75,7 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.View;
+import android.view.View.FocusDirection;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowManager;
@@ -1000,6 +1001,17 @@
         }
         return didTransfer;
     }
+
+    @Override
+    public boolean moveFocusToAdjacentWindow(IWindow fromWindow, @FocusDirection int direction) {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            return mService.moveFocusToAdjacentWindow(this, fromWindow, direction);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
     @Override
     public void generateDisplayHash(IWindow window, Rect boundsInWindow, String hashAlgorithm,
             RemoteCallback callback) {
diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java
index b6f040a..3014f97 100644
--- a/services/core/java/com/android/server/wm/SnapshotController.java
+++ b/services/core/java/com/android/server/wm/SnapshotController.java
@@ -160,9 +160,7 @@
                 if (!allOpensOptInOnBackInvoked() || mCloseActivities.isEmpty()) {
                     return;
                 }
-                for (int i = mCloseActivities.size() - 1; i >= 0; --i) {
-                    controller.recordSnapshot(mCloseActivities.get(i));
-                }
+                controller.recordSnapshot(mCloseActivities);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index bffdf54..e4379b5 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -274,7 +274,9 @@
 
         @Override
         void write() {
-            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "StoreWriteQueueItem");
+            if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "StoreWriteQueueItem#" + mId);
+            }
             if (!mPersistInfoProvider.createDirectory(mUserId)) {
                 Slog.e(TAG, "Unable to create snapshot directory for user dir="
                         + mPersistInfoProvider.getDirectory(mUserId));
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d556f09..a7a6bf2 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1411,12 +1411,13 @@
         return isUidPresent;
     }
 
+    WindowState topStartingWindow() {
+        return getWindow(w -> w.mAttrs.type == TYPE_APPLICATION_STARTING);
+    }
+
     ActivityRecord topActivityContainsStartingWindow() {
-        if (getParent() == null) {
-            return null;
-        }
-        return getActivity((r) -> r.getWindow(window ->
-                window.getBaseType() == TYPE_APPLICATION_STARTING) != null);
+        final WindowState startingWindow = topStartingWindow();
+        return startingWindow != null ? startingWindow.mActivityRecord : null;
     }
 
     /**
@@ -3698,6 +3699,16 @@
                 }
                 wc.assignLayer(t, layer++);
 
+                // Boost the adjacent TaskFragment for dimmer if needed.
+                final TaskFragment taskFragment = wc.asTaskFragment();
+                if (taskFragment != null && taskFragment.isEmbedded()
+                        && taskFragment.isVisibleRequested()) {
+                    final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
+                    if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) {
+                        adjacentTf.assignLayer(t, layer++);
+                    }
+                }
+
                 // Place the decor surface just above the owner TaskFragment.
                 if (mDecorSurfaceContainer != null && !decorSurfacePlaced
                         && wc == mDecorSurfaceContainer.mOwnerTaskFragment) {
@@ -4784,6 +4795,7 @@
         }
         if (top.isAttached()) {
             top.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+            top.mWaitForEnteringPinnedMode = false;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index f51bd1b..f56759f 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -39,6 +39,7 @@
 import static android.os.Process.SYSTEM_UID;
 import static android.os.UserHandle.USER_NULL;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_NONE;
@@ -2995,6 +2996,30 @@
         }, false /* traverseTopToBottom */);
     }
 
+    boolean shouldBoostDimmer() {
+        if (asTask() != null || !isDimmingOnParentTask()) {
+            // early return if not embedded or should not dim on parent Task.
+            return false;
+        }
+
+        final TaskFragment adjacentTf = getAdjacentTaskFragment();
+        if (adjacentTf == null) {
+            // early return if no adjacent TF.
+            return false;
+        }
+
+        if (getParent().mChildren.indexOf(adjacentTf) < getParent().mChildren.indexOf(this)) {
+            // early return if this TF already has higher z-ordering.
+            return false;
+        }
+
+        // boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
+        return forAllWindows(
+                (w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null
+                        && w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested()
+                        || w.mActivityRecord.isVisible()), true);
+    }
+
     @Override
     Dimmer getDimmer() {
         // If this is in an embedded TaskFragment and we want the dim applies on the TaskFragment.
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 56bef33..f620a97 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1933,29 +1933,29 @@
                 dc.getInputMonitor().getInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
         ActivityRecord recentsActivity = null;
         if (recentsAnimationInputConsumer != null) {
-            // find the top-most going-away activity and the recents activity. The top-most
+            // Find the top-most going-away task and the recents activity. The top-most
             // is used as layer reference while the recents is used for registering the consumer
             // override.
-            ActivityRecord topActivity = null;
+            Task topNonRecentsTask = null;
             for (int i = 0; i < info.getChanges().size(); ++i) {
-                final TransitionInfo.Change change = info.getChanges().get(i);
-                if (change.getTaskInfo() == null) continue;
-                final Task task = Task.fromWindowContainerToken(
-                        info.getChanges().get(i).getTaskInfo().token);
+                final ActivityManager.RunningTaskInfo taskInfo =
+                        info.getChanges().get(i).getTaskInfo();
+                if (taskInfo == null) continue;
+                final Task task = Task.fromWindowContainerToken(taskInfo.token);
                 if (task == null) continue;
-                final int activityType = change.getTaskInfo().topActivityType;
+                final int activityType = taskInfo.topActivityType;
                 final boolean isRecents = activityType == ACTIVITY_TYPE_HOME
                         || activityType == ACTIVITY_TYPE_RECENTS;
                 if (isRecents && recentsActivity == null) {
                     recentsActivity = task.getTopVisibleActivity();
-                } else if (!isRecents && topActivity == null) {
-                    topActivity = task.getTopNonFinishingActivity();
+                } else if (!isRecents && topNonRecentsTask == null) {
+                    topNonRecentsTask = task;
                 }
             }
-            if (recentsActivity != null && topActivity != null) {
+            if (recentsActivity != null && topNonRecentsTask != null) {
                 recentsAnimationInputConsumer.mWindowHandle.touchableRegion.set(
-                        topActivity.getBounds());
-                dc.getInputMonitor().setActiveRecents(recentsActivity, topActivity);
+                        topNonRecentsTask.getBounds());
+                dc.getInputMonitor().setActiveRecents(recentsActivity, topNonRecentsTask);
             }
         }
 
@@ -2020,16 +2020,17 @@
         }
         mController.mNavigationBarAttachedToApp = false;
 
-        if (mRecentsDisplayId == INVALID_DISPLAY) {
-            Slog.e(TAG, "Reparented navigation bar without a valid display");
-            mRecentsDisplayId = DEFAULT_DISPLAY;
+        int recentsDisplayId = mRecentsDisplayId;
+        if (recentsDisplayId == INVALID_DISPLAY) {
+            Slog.i(TAG, "Restore parent surface of navigation bar by another transition");
+            recentsDisplayId = DEFAULT_DISPLAY;
         }
 
         final DisplayContent dc =
-                mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId);
+                mController.mAtm.mRootWindowContainer.getDisplayContent(recentsDisplayId);
         final StatusBarManagerInternal bar = dc.getDisplayPolicy().getStatusBarManagerInternal();
         if (bar != null) {
-            bar.setNavigationBarLumaSamplingEnabled(mRecentsDisplayId, true);
+            bar.setNavigationBarLumaSamplingEnabled(recentsDisplayId, true);
         }
         final WindowState navWindow = dc.getDisplayPolicy().getNavigationBar();
         if (navWindow == null) return;
@@ -2596,6 +2597,10 @@
                 change.setBackgroundColor(ColorUtils.setAlphaComponent(backgroundColor, 255));
             }
 
+            if (activityRecord != null) {
+                change.setActivityComponent(activityRecord.mActivityComponent);
+            }
+
             change.setRotation(info.mRotation, endRotation);
             if (info.mSnapshot != null) {
                 change.setSnapshot(info.mSnapshot, info.mSnapshotLuma);
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index c3aca6f..708d63e 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -634,8 +634,8 @@
     }
 
     /** Sets the sync method for the display change. */
-    private void setDisplaySyncMethod(@NonNull TransitionRequestInfo.DisplayChange displayChange,
-            @NonNull Transition displayTransition, @NonNull DisplayContent displayContent) {
+    void setDisplaySyncMethod(@NonNull TransitionRequestInfo.DisplayChange displayChange,
+            @NonNull DisplayContent displayContent) {
         final Rect startBounds = displayChange.getStartAbsBounds();
         final Rect endBounds = displayChange.getEndAbsBounds();
         if (startBounds == null || endBounds == null) return;
@@ -686,7 +686,7 @@
                     trigger != null ? trigger.asTask() : null, remoteTransition, displayChange);
             if (newTransition != null && displayChange != null && trigger != null
                     && trigger.asDisplayContent() != null) {
-                setDisplaySyncMethod(displayChange, newTransition, trigger.asDisplayContent());
+                setDisplaySyncMethod(displayChange, trigger.asDisplayContent());
             }
         }
         if (trigger != null) {
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 33ef3c5..a9f0554 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -183,18 +183,21 @@
                 && (mWallpaperTarget == w || w.isDrawFinishedLw())) {
             if (DEBUG_WALLPAPER) Slog.v(TAG, "Found wallpaper target: " + w);
             mFindResults.setWallpaperTarget(w);
+            mFindResults.setIsWallpaperTargetForLetterbox(w.hasWallpaperForLetterboxBackground());
             if (w == mWallpaperTarget && w.isAnimating(TRANSITION | PARENTS)) {
                 // The current wallpaper target is animating, so we'll look behind it for
                 // another possible target and figure out what is going on later.
                 if (DEBUG_WALLPAPER) Slog.v(TAG,
                         "Win " + w + ": token animating, looking behind.");
             }
-            mFindResults.setIsWallpaperTargetForLetterbox(w.hasWallpaperForLetterboxBackground());
             // While the keyguard is going away, both notification shade and a normal activity such
             // as a launcher can satisfy criteria for a wallpaper target. In this case, we should
             // chose the normal activity, otherwise wallpaper becomes invisible when a new animation
             // starts before the keyguard going away animation finishes.
-            return w.mActivityRecord != null;
+            if (w.mActivityRecord == null && mDisplayContent.isKeyguardGoingAway()) {
+                return false;
+            }
+            return true;
         }
         return false;
     };
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index e28262d..bdea1bc 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3938,7 +3938,13 @@
     @Nullable
     BLASTSyncEngine.SyncGroup getSyncGroup() {
         if (mSyncGroup != null) return mSyncGroup;
-        if (mParent != null) return mParent.getSyncGroup();
+        WindowContainer<?> parent = mParent;
+        while (parent != null) {
+            if (parent.mSyncGroup != null) {
+                return parent.mSyncGroup;
+            }
+            parent = parent.mParent;
+        }
         return null;
     }
 
@@ -3972,7 +3978,7 @@
      * @param cancel If true, this is being finished because it is leaving the sync group rather
      *               than due to the sync group completing.
      */
-    void finishSync(Transaction outMergedTransaction, BLASTSyncEngine.SyncGroup group,
+    void finishSync(Transaction outMergedTransaction, @Nullable BLASTSyncEngine.SyncGroup group,
             boolean cancel) {
         if (mSyncState == SYNC_STATE_NONE) return;
         final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
@@ -4000,7 +4006,8 @@
         if (!isVisibleRequested()) {
             return true;
         }
-        if (mSyncState == SYNC_STATE_NONE) {
+        if (mSyncState == SYNC_STATE_NONE && getSyncGroup() != null) {
+            Slog.i(TAG, "prepareSync in isSyncFinished: " + this);
             prepareSync();
         }
         if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW) {
@@ -4059,16 +4066,18 @@
             }
             if (newParent == null) {
                 // This is getting removed.
+                final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
                 if (oldParent.mSyncState != SYNC_STATE_NONE) {
                     // In order to keep the transaction in sync, merge it into the parent.
-                    finishSync(oldParent.mSyncTransaction, getSyncGroup(), true /* cancel */);
-                } else if (mSyncGroup != null) {
-                    // This is watched directly by the sync-group, so merge this transaction into
-                    // into the sync-group so it isn't lost
-                    finishSync(mSyncGroup.getOrphanTransaction(), mSyncGroup, true /* cancel */);
+                    finishSync(oldParent.mSyncTransaction, syncGroup, true /* cancel */);
+                } else if (syncGroup != null) {
+                    // This is watched by the sync-group, so merge this transaction into the
+                    // sync-group for not losing the operations in the transaction.
+                    finishSync(syncGroup.getOrphanTransaction(), syncGroup, true /* cancel */);
                 } else {
-                    throw new IllegalStateException("This container is in sync mode without a sync"
-                            + " group: " + this);
+                    Slog.wtf(TAG, this + " is in sync mode without a sync group");
+                    // Make sure the removal transaction take effect.
+                    finishSync(getPendingTransaction(), null /* group */, true /* cancel */);
                 }
                 return;
             } else if (mSyncGroup == null) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 516d37c..22b690e 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -51,6 +51,7 @@
 import com.android.internal.policy.KeyInterceptionInfo;
 import com.android.server.input.InputManagerService;
 import com.android.server.policy.WindowManagerPolicy;
+import com.android.server.wm.SensitiveContentPackages.PackageInfo;
 
 import java.lang.annotation.Retention;
 import java.util.List;
@@ -1012,4 +1013,12 @@
      */
     public abstract void setOrientationRequestPolicy(boolean respected,
             int[] fromOrientations, int[] toOrientations);
+
+    /**
+     * Set whether screen capture should be disabled for all windows of a specific app windows based
+     * on sensitive content protections.
+     *
+     * @param packageInfos set of {@link PackageInfo} whose windows should be blocked from capture
+     */
+    public abstract void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos);
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 502912a..9544835 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -123,6 +123,7 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS;
+import static com.android.server.wm.SensitiveContentPackages.PackageInfo;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
@@ -286,6 +287,7 @@
 import android.view.SurfaceSession;
 import android.view.TaskTransitionSpec;
 import android.view.View;
+import android.view.View.FocusDirection;
 import android.view.ViewDebug;
 import android.view.WindowContentFrameStats;
 import android.view.WindowInsets;
@@ -312,6 +314,7 @@
 import android.window.WindowContextInfo;
 
 import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.policy.IKeyguardDismissCallback;
@@ -366,6 +369,7 @@
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
@@ -1053,6 +1057,9 @@
 
     SystemPerformanceHinter mSystemPerformanceHinter;
 
+    @GuardedBy("mGlobalLock")
+    final SensitiveContentPackages mSensitiveContentPackages = new SensitiveContentPackages();
+
     /** Listener to notify activity manager about app transitions. */
     final WindowManagerInternal.AppTransitionListener mActivityManagerAppTransitionNotifier
             = new WindowManagerInternal.AppTransitionListener() {
@@ -1797,7 +1804,12 @@
 
             // Don't do layout here, the window must call
             // relayout to be displayed, so we'll do it there.
-            win.getParent().assignChildLayers();
+            if (win.mActivityRecord != null && win.mActivityRecord.isEmbedded()) {
+                // Assign child layers from the parent Task if the Activity is embedded.
+                win.getTask().assignChildLayers();
+            } else {
+                win.getParent().assignChildLayers();
+            }
 
             if (focusChanged) {
                 displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus,
@@ -1931,12 +1943,13 @@
 
     /**
      * Set whether screen capture is disabled for all windows of a specific user from
-     * the device policy cache.
+     * the device policy cache, or specific windows based on sensitive content protections.
      */
     @Override
     public void refreshScreenCaptureDisabled() {
         int callingUid = Binder.getCallingUid();
-        if (callingUid != SYSTEM_UID) {
+        // MY_UID (Process.myUid()) should always be SYSTEM_UID here, but using MY_UID for tests
+        if (callingUid != MY_UID) {
             throw new SecurityException("Only system can call refreshScreenCaptureDisabled.");
         }
 
@@ -7169,6 +7182,7 @@
             }
             mSystemPerformanceHinter.dump(pw, "");
             mTrustedPresentationListenerController.dump(pw);
+            mSensitiveContentPackages.dump(pw);
         }
     }
 
@@ -8550,6 +8564,14 @@
             InputTarget inputTarget = WindowManagerService.this.getInputTargetFromToken(inputToken);
             return inputTarget == null ? null : inputTarget.getWindowToken();
         }
+
+        @Override
+        public void setShouldBlockScreenCaptureForApp(Set<PackageInfo> packageInfos) {
+            synchronized (mGlobalLock) {
+                mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(packageInfos);
+                WindowManagerService.this.refreshScreenCaptureDisabled();
+            }
+        }
     }
 
     private final class ImeTargetVisibilityPolicyImpl extends ImeTargetVisibilityPolicy {
@@ -9083,6 +9105,66 @@
                 win.mClient);
     }
 
+    boolean moveFocusToAdjacentWindow(Session session, IWindow fromWindow,
+            @FocusDirection int direction) {
+        synchronized (mGlobalLock) {
+            final WindowState fromWin = windowForClientLocked(session, fromWindow, false);
+            if (fromWin == null || !fromWin.isFocused()) {
+                return false;
+            }
+            final TaskFragment fromFragment = fromWin.getTaskFragment();
+            if (fromFragment == null) {
+                return false;
+            }
+            final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment();
+            if (adjacentFragment == null || adjacentFragment.asTask() != null) {
+                // Don't move the focus to another task.
+                return false;
+            }
+            final Rect fromBounds = fromFragment.getBounds();
+            final Rect adjacentBounds = adjacentFragment.getBounds();
+            switch (direction) {
+                case View.FOCUS_LEFT:
+                    if (adjacentBounds.left >= fromBounds.left) {
+                        return false;
+                    }
+                    break;
+                case View.FOCUS_UP:
+                    if (adjacentBounds.top >= fromBounds.top) {
+                        return false;
+                    }
+                    break;
+                case View.FOCUS_RIGHT:
+                    if (adjacentBounds.right <= fromBounds.right) {
+                        return false;
+                    }
+                    break;
+                case View.FOCUS_DOWN:
+                    if (adjacentBounds.bottom <= fromBounds.bottom) {
+                        return false;
+                    }
+                    break;
+                case View.FOCUS_BACKWARD:
+                case View.FOCUS_FORWARD:
+                    // These are not absolute directions. Skip checking the bounds.
+                    break;
+                default:
+                    return false;
+            }
+            final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity(
+                    true /* focusableOnly */);
+            if (topRunningActivity == null) {
+                return false;
+            }
+            moveDisplayToTopInternal(topRunningActivity.getDisplayId());
+            handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity);
+            if (fromWin.isFocused()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /** Return whether layer tracing is enabled */
     public boolean isLayerTracing() {
         if (!checkCallingPermission(
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0b43be7..56f2bc3 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1896,6 +1896,14 @@
         if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
             return true;
         }
+
+        if (com.android.server.notification.Flags.sensitiveNotificationAppProtection()) {
+            if (mWmService.mSensitiveContentPackages
+                    .shouldBlockScreenCaptureForApp(getOwningPackage(), getOwningUid())) {
+                return true;
+            }
+        }
+
         return !DevicePolicyCache.getInstance().isScreenCaptureAllowed(mShowUserId);
     }
 
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index dfa9dce..775570c 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -37,6 +37,7 @@
         "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,6 +95,16 @@
     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 cc08488..15eb7c6 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -33,3 +33,7 @@
 
 # Bug component : 158088 = per-file *AnrTimer*
 per-file *AnrTimer* = file:/PERFORMANCE_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
diff --git a/services/core/jni/com_android_server_BootReceiver.cpp b/services/core/jni/com_android_server_BootReceiver.cpp
new file mode 100644
index 0000000..3892d28
--- /dev/null
+++ b/services/core/jni/com_android_server_BootReceiver.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#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/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index afb0b20..414339d 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -299,7 +299,7 @@
     void setSystemUiLightsOut(bool lightsOut);
     void setPointerDisplayId(int32_t displayId);
     void setPointerSpeed(int32_t speed);
-    void setPointerAcceleration(float acceleration);
+    void setMousePointerAccelerationEnabled(bool enabled);
     void setTouchpadPointerSpeed(int32_t speed);
     void setTouchpadNaturalScrollingEnabled(bool enabled);
     void setTouchpadTapToClickEnabled(bool enabled);
@@ -411,8 +411,8 @@
         // Pointer speed.
         int32_t pointerSpeed{0};
 
-        // Pointer acceleration.
-        float pointerAcceleration{android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION};
+        // True if pointer acceleration is enabled for mice.
+        bool mousePointerAccelerationEnabled{true};
 
         // True if pointer gestures are enabled.
         bool pointerGesturesEnabled{true};
@@ -502,7 +502,8 @@
         dump += StringPrintf(INDENT "System UI Lights Out: %s\n",
                              toString(mLocked.systemUiLightsOut));
         dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
-        dump += StringPrintf(INDENT "Pointer Acceleration: %0.3f\n", mLocked.pointerAcceleration);
+        dump += StringPrintf(INDENT "Mouse Pointer Acceleration: %s\n",
+                             mLocked.mousePointerAccelerationEnabled ? "Enabled" : "Disabled");
         dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n",
                 toString(mLocked.pointerGesturesEnabled));
         dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches));
@@ -686,7 +687,10 @@
 
         outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed
                 * POINTER_SPEED_EXPONENT);
-        outConfig->pointerVelocityControlParameters.acceleration = mLocked.pointerAcceleration;
+        outConfig->pointerVelocityControlParameters.acceleration =
+                mLocked.mousePointerAccelerationEnabled
+                ? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION
+                : 1;
         outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;
 
         outConfig->showTouches = mLocked.showTouches;
@@ -1207,16 +1211,16 @@
             InputReaderConfiguration::Change::POINTER_SPEED);
 }
 
-void NativeInputManager::setPointerAcceleration(float acceleration) {
+void NativeInputManager::setMousePointerAccelerationEnabled(bool enabled) {
     { // acquire lock
         std::scoped_lock _l(mLock);
 
-        if (mLocked.pointerAcceleration == acceleration) {
+        if (mLocked.mousePointerAccelerationEnabled == enabled) {
             return;
         }
 
-        ALOGI("Setting pointer acceleration to %0.3f", acceleration);
-        mLocked.pointerAcceleration = acceleration;
+        ALOGI("Setting mouse pointer acceleration to %s", toString(enabled));
+        mLocked.mousePointerAccelerationEnabled = enabled;
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
@@ -2174,10 +2178,11 @@
     im->setPointerSpeed(speed);
 }
 
-static void nativeSetPointerAcceleration(JNIEnv* env, jobject nativeImplObj, jfloat acceleration) {
+static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj,
+                                                     jboolean enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setPointerAcceleration(acceleration);
+    im->setMousePointerAccelerationEnabled(enabled);
 }
 
 static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
@@ -2812,7 +2817,8 @@
          (void*)nativeTransferTouchFocus},
         {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouch},
         {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
-        {"setPointerAcceleration", "(F)V", (void*)nativeSetPointerAcceleration},
+        {"setMousePointerAccelerationEnabled", "(Z)V",
+         (void*)nativeSetMousePointerAccelerationEnabled},
         {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed},
         {"setTouchpadNaturalScrollingEnabled", "(Z)V",
          (void*)nativeSetTouchpadNaturalScrollingEnabled},
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index 97b18fa..1e48ace 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -486,9 +486,11 @@
     void remove(AnrTimerService const* service) {
         AutoMutex _l(lock_);
         timer_id_t front = headTimerId();
-        for (auto i = running_.begin(); i != running_.end(); i++) {
+        for (auto i = running_.begin(); i != running_.end(); ) {
             if (i->service == service) {
-                running_.erase(i);
+                i = running_.erase(i);
+            } else {
+                i++;
             }
         }
         if (front != headTimerId()) restartLocked();
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index f3158d1..a4b1f84 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -52,10 +52,10 @@
 int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
 int register_android_server_SyntheticPasswordManager(JNIEnv* env);
 int register_android_hardware_display_DisplayViewport(JNIEnv* env);
-int register_android_server_utils_AnrTimer(JNIEnv *env);
 int register_android_server_am_OomConnection(JNIEnv* env);
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
 int register_android_server_am_LowMemDetector(JNIEnv* env);
+int register_android_server_utils_AnrTimer(JNIEnv *env);
 int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env);
 int register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(JNIEnv* env);
 int register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(JNIEnv* env);
@@ -66,6 +66,7 @@
 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);
@@ -114,10 +115,10 @@
     register_android_server_storage_AppFuse(env);
     register_android_server_SyntheticPasswordManager(env);
     register_android_hardware_display_DisplayViewport(env);
-    register_android_server_utils_AnrTimer(env);
     register_android_server_am_OomConnection(env);
     register_android_server_am_CachedAppOptimizer(env);
     register_android_server_am_LowMemDetector(env);
+    register_android_server_utils_AnrTimer(env);
     register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env);
     register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(env);
     register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(env);
@@ -128,6 +129,7 @@
     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/lint-baseline.xml b/services/core/lint-baseline.xml
index 070bd4b..2ccd1e4 100644
--- a/services/core/lint-baseline.xml
+++ b/services/core/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 7.2.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NonUserGetterCalled"
@@ -145,4 +145,4 @@
             line="7158"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/services/core/xsd/display-layout-config/display-layout-config.xsd b/services/core/xsd/display-layout-config/display-layout-config.xsd
index 4e95465..73c13ce 100644
--- a/services/core/xsd/display-layout-config/display-layout-config.xsd
+++ b/services/core/xsd/display-layout-config/display-layout-config.xsd
@@ -49,7 +49,7 @@
 
     <xs:complexType name="display">
         <xs:sequence>
-            <xs:element name="address" type="xs:nonNegativeInteger"/>
+            <xs:group ref="displayReference" minOccurs="1" maxOccurs="1"/>
             <xs:element name="position" type="xs:string" minOccurs="0" maxOccurs="1" />
             <xs:element name="brightnessThrottlingMapId" type="xs:string" minOccurs="0" maxOccurs="1" />
             <xs:element name="powerThrottlingMapId" type="xs:string" minOccurs="0" maxOccurs="1" />
@@ -67,4 +67,11 @@
             </xs:simpleType>
         </xs:attribute>
     </xs:complexType>
+
+    <xs:group name="displayReference">
+        <xs:choice>
+            <xs:element name="address" type="xs:nonNegativeInteger" minOccurs="0"/>
+            <xs:element name="port" type="xs:nonNegativeInteger" minOccurs="0"/>
+        </xs:choice>
+    </xs:group>
 </xs:schema>
diff --git a/services/core/xsd/display-layout-config/schema/current.txt b/services/core/xsd/display-layout-config/schema/current.txt
index 195cae5..0d2f6a8 100644
--- a/services/core/xsd/display-layout-config/schema/current.txt
+++ b/services/core/xsd/display-layout-config/schema/current.txt
@@ -3,22 +3,24 @@
 
   public class Display {
     ctor public Display();
-    method public java.math.BigInteger getAddress();
+    method public java.math.BigInteger getAddress_optional();
     method public String getBrightnessThrottlingMapId();
     method public String getDisplayGroup();
     method public java.math.BigInteger getLeadDisplayAddress();
+    method public java.math.BigInteger getPort_optional();
     method public String getPosition();
     method public String getPowerThrottlingMapId();
     method public String getRefreshRateThermalThrottlingMapId();
     method public String getRefreshRateZoneId();
     method public boolean isDefaultDisplay();
     method public boolean isEnabled();
-    method public void setAddress(java.math.BigInteger);
+    method public void setAddress_optional(java.math.BigInteger);
     method public void setBrightnessThrottlingMapId(String);
     method public void setDefaultDisplay(boolean);
     method public void setDisplayGroup(String);
     method public void setEnabled(boolean);
     method public void setLeadDisplayAddress(java.math.BigInteger);
+    method public void setPort_optional(java.math.BigInteger);
     method public void setPosition(String);
     method public void setPowerThrottlingMapId(String);
     method public void setRefreshRateThermalThrottlingMapId(String);
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index dfb5a57..fb0729f 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -517,6 +517,8 @@
                                     .map(CredentialOption::getType)
                                     .collect(Collectors.toList()));
 
+            finalizeAndEmitInitialPhaseMetric(session);
+
             if (providerSessions.isEmpty()) {
                 try {
                     callback.onError(
@@ -776,6 +778,13 @@
             providerSessions.forEach(ProviderSession::invokeSession);
         }
 
+        private void finalizeAndEmitInitialPhaseMetric(GetCandidateRequestSession session) {
+            var initMetric = session.mRequestSessionMetric.getInitialPhaseMetric();
+            initMetric.setAutofillSessionId(session.getAutofillSessionId());
+            initMetric.setAutofillRequestId(session.getAutofillRequestId());
+            finalizeAndEmitInitialPhaseMetric((RequestSession) session);
+        }
+
         private void finalizeAndEmitInitialPhaseMetric(RequestSession session) {
             try {
                 var initMetric = session.mRequestSessionMetric.getInitialPhaseMetric();
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index d165171..0f914c3 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -49,7 +49,12 @@
         implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> {
     private static final String TAG = "GetCandidateRequestSession";
 
+    private static final String SESSION_ID_KEY = "autofill_session_id";
+    private static final String REQUEST_ID_KEY = "autofill_request_id";
+
     private final IAutoFillManagerClient mAutoFillCallback;
+    private final int mAutofillSessionId;
+    private final int mAutofillRequestId;
 
     public GetCandidateRequestSession(
             Context context, SessionLifetime sessionCallback,
@@ -62,6 +67,8 @@
                 RequestInfo.TYPE_GET, callingAppInfo, enabledProviders,
                 cancellationSignal, 0L);
         mAutoFillCallback = autoFillCallback;
+        mAutofillSessionId = request.getData().getInt(SESSION_ID_KEY, -1);
+        mAutofillRequestId = request.getData().getInt(REQUEST_ID_KEY, -1);
     }
 
     /**
@@ -177,4 +184,19 @@
         Slog.d(TAG, "onFinalResponseReceived");
         respondToClientWithResponseAndFinish(new GetCandidateCredentialsResponse(response));
     }
+
+    /**
+     * Returns autofill session id. Returns -1 if unavailable.
+     */
+    public int getAutofillSessionId() {
+        return mAutofillSessionId;
+    }
+
+    /**
+     * Returns autofill request id. Returns -1 if unavailable.
+     */
+    public int getAutofillRequestId() {
+        return mAutofillRequestId;
+
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index b36de0b..23aa374 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -426,7 +426,11 @@
                     /* per_classtype_counts */
                     initialPhaseMetric.getUniqueRequestCounts(),
                     /* origin_specified */
-                    initialPhaseMetric.isOriginSpecified()
+                    initialPhaseMetric.isOriginSpecified(),
+                    /* autofill_session_id */
+                    initialPhaseMetric.getAutofillSessionId(),
+                    /* autofill_request_id */
+                    initialPhaseMetric.getAutofillRequestId()
             );
         } catch (Exception e) {
             Slog.w(TAG, "Unexpected error during initial metric emit: " + e);
diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
index 8e965e3..8a4e86c 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
@@ -49,6 +49,12 @@
     // Stores the deduped request information, particularly {"req":5}
     private Map<String, Integer> mRequestCounts = new LinkedHashMap<>();
 
+    // The session id of autofill if the request is from autofill, defaults to -1
+    private int mAutofillSessionId = -1;
+
+    // The request id of autofill if the request is from autofill, defaults to -1
+    private int mAutofillRequestId = -1;
+
 
     public InitialPhaseMetric(int sessionIdTrackOne) {
         mSessionIdCaller = sessionIdTrackOne;
@@ -126,6 +132,24 @@
         return mOriginSpecified;
     }
 
+    /* ------ Autofill Integration ------ */
+
+    public void setAutofillSessionId(int autofillSessionId) {
+        mAutofillSessionId = autofillSessionId;
+    }
+
+    public int getAutofillSessionId() {
+        return mAutofillSessionId;
+    }
+
+    public void setAutofillRequestId(int autofillRequestId) {
+        mAutofillRequestId = autofillRequestId;
+    }
+
+    public int getAutofillRequestId() {
+        return mAutofillRequestId;
+    }
+
     /* ------ Unique Request Counts Map Information ------ */
 
     public void setRequestCounts(Map<String, Integer> requestCounts) {
diff --git a/services/java/com/android/server/SystemConfigService.java b/services/java/com/android/server/SystemConfigService.java
index 6e82907..fd21a32 100644
--- a/services/java/com/android/server/SystemConfigService.java
+++ b/services/java/com/android/server/SystemConfigService.java
@@ -21,6 +21,8 @@
 import android.Manifest;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
 import android.os.ISystemConfig;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -108,6 +110,15 @@
                     "Caller must hold " + Manifest.permission.QUERY_ALL_PACKAGES);
             return new ArrayList<>(SystemConfig.getInstance().getDefaultVrComponents());
         }
+
+        @Override
+        public List<String> getPreventUserDisablePackages() {
+            PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+            return SystemConfig.getInstance().getPreventUserDisablePackages().stream()
+                    .filter(preventUserDisablePackage ->
+                            pmi.canQueryPackage(Binder.getCallingUid(), preventUserDisablePackage))
+                    .collect(toList());
+        }
     };
 
     public SystemConfigService(Context context) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 1185a4e..86ad494 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2965,6 +2965,12 @@
             t.traceEnd();
         }
 
+        if (com.android.server.notification.Flags.sensitiveNotificationAppProtection()) {
+            t.traceBegin("StartSensitiveContentProtectionManager");
+            mSystemServiceManager.startService(SensitiveContentProtectionManagerService.class);
+            t.traceEnd();
+        }
+
         // These are needed to propagate to the runnable below.
         final NetworkManagementService networkManagementF = networkManagement;
         final NetworkPolicyManagerService networkPolicyF = networkPolicy;
diff --git a/services/lint-baseline.xml b/services/lint-baseline.xml
index 8489c17..a311d07 100644
--- a/services/lint-baseline.xml
+++ b/services/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="8.1.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="SimpleManualPermissionEnforcement"
@@ -56,4 +56,4 @@
             column="13"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/services/print/lint-baseline.xml b/services/print/lint-baseline.xml
index 1bf031a..11c0cc8e 100644
--- a/services/print/lint-baseline.xml
+++ b/services/print/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="8.1.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="SimpleManualPermissionEnforcement"
@@ -12,4 +12,4 @@
             column="13"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/services/robotests/Android.bp b/services/robotests/Android.bp
index 52eae21..a70802a 100644
--- a/services/robotests/Android.bp
+++ b/services/robotests/Android.bp
@@ -57,9 +57,13 @@
     ],
     static_libs: [
         "androidx.test.ext.truth",
+        "Settings-robo-testutils",
+        "SettingsLib-robo-testutils",
     ],
 
     instrumentation_for: "FrameworksServicesLib",
+
+    upstream: true,
 }
 
 filegroup {
diff --git a/services/robotests/backup/Android.bp b/services/robotests/backup/Android.bp
index 8b9efb3..569786b 100644
--- a/services/robotests/backup/Android.bp
+++ b/services/robotests/backup/Android.bp
@@ -57,6 +57,8 @@
     // Include the testing libraries
     libs: [
         "mockito-robolectric-prebuilt",
+        "Settings-robo-testutils",
+        "SettingsLib-robo-testutils",
         "platform-test-annotations",
         "testng",
         "truth",
@@ -64,4 +66,6 @@
 
     instrumentation_for: "BackupFrameworksServicesLib",
 
+    upstream: true,
+
 }
diff --git a/services/robotests/backup/config/robolectric.properties b/services/robotests/backup/config/robolectric.properties
index 850557a..1ebf6d4 100644
--- a/services/robotests/backup/config/robolectric.properties
+++ b/services/robotests/backup/config/robolectric.properties
@@ -1 +1,3 @@
-sdk=NEWEST_SDK
\ No newline at end of file
+sdk=NEWEST_SDK
+looperMode=LEGACY
+shadows=com.android.server.testing.shadows.FrameworkShadowLooper
diff --git a/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java b/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java
index ee5a534..6839a06 100644
--- a/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java
@@ -57,6 +57,7 @@
             ShadowBackupDataOutput.class,
             ShadowEnvironment.class,
             ShadowFullBackup.class,
+            ShadowSigningInfo.class,
         })
 public class AppMetadataBackupWriterTest {
     private static final String TEST_PACKAGE = "com.test.package";
diff --git a/services/robotests/backup/src/com/android/server/backup/fullbackup/ShadowSigningInfo.java b/services/robotests/backup/src/com/android/server/backup/fullbackup/ShadowSigningInfo.java
new file mode 100644
index 0000000..53d807c
--- /dev/null
+++ b/services/robotests/backup/src/com/android/server/backup/fullbackup/ShadowSigningInfo.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.fullbackup;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import android.content.pm.SigningInfo;
+
+import org.robolectric.annotation.Implements;
+
+@Implements(value = SigningInfo.class, minSdk = P)
+public class ShadowSigningInfo {
+}
diff --git a/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java b/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java
index 4949091..0092763 100644
--- a/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java
+++ b/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java
@@ -35,6 +35,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
 import org.robolectric.shadows.ShadowLooper;
 
 import java.util.concurrent.CountDownLatch;
@@ -45,6 +46,7 @@
  */
 @RunWith(RobolectricTestRunner.class)
 @Presubmit
+@LooperMode(LooperMode.Mode.LEGACY)
 public class NtpNetworkTimeHelperTest {
 
     private static final long MOCK_NTP_TIME = 1519930775453L;
diff --git a/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java b/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java
index 16d16cd..3681bd4 100644
--- a/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java
+++ b/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java
@@ -21,12 +21,15 @@
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.RealObject;
+import org.robolectric.shadows.LooperShadowPicker;
+import org.robolectric.shadows.ShadowLegacyLooper;
 import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowPausedLooper;
 
 import java.util.Optional;
 
-@Implements(value = Looper.class)
-public class FrameworkShadowLooper extends ShadowLooper {
+@Implements(value = Looper.class, shadowPicker = FrameworkShadowLooper.Picker.class)
+public class FrameworkShadowLooper extends ShadowLegacyLooper {
     @RealObject private Looper mLooper;
     private Optional<Boolean> mIsCurrentThread = Optional.empty();
 
@@ -45,4 +48,10 @@
         }
         return Thread.currentThread() == mLooper.getThread();
     }
+
+    public static class Picker extends LooperShadowPicker<ShadowLooper> {
+        public Picker() {
+            super(FrameworkShadowLooper.class, ShadowPausedLooper.class);
+        }
+    }
 }
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java
index 4a99486..1da6759 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java
@@ -95,7 +95,6 @@
         sPackageAppEnabledStates.put(packageName, Integer.valueOf(newState));  // flags unused here.
     }
 
-    @Override
     protected PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId)
             throws NameNotFoundException {
         if (!sPackageInfos.containsKey(packageName)) {
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index ffe6dc5..56423b9 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -40,6 +40,7 @@
         "frameworks-base-testutils",
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
+        "ravenwood-junit",
         "services.core",
         "service-permission.stubs.system_server",
         "servicestests-core-utils",
@@ -66,6 +67,28 @@
     },
 }
 
+android_ravenwood_test {
+    name: "FrameworksInputMethodSystemServerTests_host",
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.rules",
+        "framework",
+        "mockito_ravenwood",
+        "ravenwood-runtime",
+        "ravenwood-utils",
+        "services",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    srcs: [
+        "src/com/android/server/inputmethod/**/ClientControllerTest.java",
+    ],
+    sdk_version: "test_current",
+    auto_gen_config: true,
+}
+
 android_test {
     name: "FrameworksImeTests",
     defaults: [
@@ -88,6 +111,7 @@
         "frameworks-base-testutils",
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
+        "ravenwood-junit",
         "services.core",
         "service-permission.stubs.system_server",
         "servicestests-core-utils",
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
new file mode 100644
index 0000000..3c8f5c9
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.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 com.android.server.inputmethod;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.PackageManagerInternal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.view.Display;
+import android.view.inputmethod.InputBinding;
+
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+
+// This test is designed to run on both device and host (Ravenwood) side.
+public final class ClientControllerTest {
+    private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY;
+    private static final int ANY_CALLER_UID = 1;
+    private static final int ANY_CALLER_PID = 1;
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true).build();
+
+    @Mock
+    private PackageManagerInternal mMockPackageManagerInternal;
+
+    @Mock(extraInterfaces = IBinder.class)
+    private IInputMethodClient mClient;
+
+    @Mock
+    private IRemoteInputConnection mConnection;
+
+    @Mock
+    private IBinder.DeathRecipient mDeathRecipient;
+
+    private Handler mHandler;
+
+    private ClientController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mHandler = new Handler(Looper.getMainLooper());
+        mController = new ClientController(mMockPackageManagerInternal);
+        when(mClient.asBinder()).thenReturn((IBinder) mClient);
+    }
+
+    @Test
+    // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+    @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
+    public void testAddClient_cannotAddTheSameClientTwice() {
+        var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+
+        synchronized (ImfLock.class) {
+            mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, mDeathRecipient,
+                    ANY_CALLER_UID, ANY_CALLER_PID);
+
+            SecurityException thrown = assertThrows(SecurityException.class,
+                    () -> {
+                        synchronized (ImfLock.class) {
+                            mController.addClient(invoker, mConnection, ANY_DISPLAY_ID,
+                                    mDeathRecipient, ANY_CALLER_UID, ANY_CALLER_PID);
+                        }
+                    });
+            assertThat(thrown.getMessage()).isEqualTo(
+                    "uid=1/pid=1/displayId=0 is already registered");
+        }
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 3199e06..438bea4 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -22,6 +22,7 @@
 import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SOFT_INPUT;
 import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SWITCH_USER;
 import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_SOFT_INPUT;
+import static com.android.server.inputmethod.ClientController.ClientState;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS;
@@ -68,8 +69,7 @@
         super.setUp();
         mVisibilityApplier =
                 (DefaultImeVisibilityApplier) mInputMethodManagerService.getVisibilityApplier();
-        mInputMethodManagerService.setAttachedClientForTesting(
-                mock(InputMethodManagerService.ClientState.class));
+        mInputMethodManagerService.setAttachedClientForTesting(mock(ClientState.class));
     }
 
     @Test
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
index e5be4d9..9e11fa2 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
@@ -50,7 +50,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 
-// atest PackageManagerServiceTest:com.android.server.pm.UserDataPreparerTest
+// atest PackageManagerServiceServerTests:com.android.server.pm.UserDataPreparerTest
 @RunWith(AndroidJUnit4.class)
 @Presubmit
 @SmallTest
@@ -99,7 +99,7 @@
         systemDeDir.mkdirs();
         mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_DE);
         verify(mStorageManagerMock).prepareUserStorage(isNull(String.class), eq(TEST_USER_ID),
-                eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_DE));
+                eq(StorageManager.FLAG_STORAGE_DE));
         verify(mInstaller).createUserData(isNull(String.class), eq(TEST_USER_ID),
                 eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_DE));
         int serialNumber = UserDataPreparer.getSerialNumber(userDeDir);
@@ -116,7 +116,7 @@
         systemCeDir.mkdirs();
         mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE);
         verify(mStorageManagerMock).prepareUserStorage(isNull(String.class), eq(TEST_USER_ID),
-                eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_CE));
+                eq(StorageManager.FLAG_STORAGE_CE));
         verify(mInstaller).createUserData(isNull(String.class), eq(TEST_USER_ID),
                 eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_CE));
         int serialNumber = UserDataPreparer.getSerialNumber(userCeDir);
@@ -129,7 +129,7 @@
     public void testPrepareUserData_forNewUser_destroysOnFailure() throws Exception {
         TEST_USER.lastLoggedInTime = 0;
         doThrow(new IllegalStateException("expected exception for test")).when(mStorageManagerMock)
-                .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), eq(TEST_USER_SERIAL),
+                .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID),
                         eq(StorageManager.FLAG_STORAGE_CE));
         mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE);
         verify(mStorageManagerMock).destroyUserStorage(isNull(String.class), eq(TEST_USER_ID),
@@ -140,7 +140,7 @@
     public void testPrepareUserData_forExistingUser_doesNotDestroyOnFailure() throws Exception {
         TEST_USER.lastLoggedInTime = System.currentTimeMillis();
         doThrow(new IllegalStateException("expected exception for test")).when(mStorageManagerMock)
-                .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), eq(TEST_USER_SERIAL),
+                .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID),
                         eq(StorageManager.FLAG_STORAGE_CE));
         mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE);
         verify(mStorageManagerMock, never()).destroyUserStorage(isNull(String.class),
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java b/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
index 8cc3408..567792e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
@@ -25,6 +25,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 
@@ -45,6 +46,7 @@
     private DeviceStateToLayoutMap mDeviceStateToLayoutMap;
 
     @Mock DisplayIdProducer mDisplayIdProducerMock;
+    @Mock DisplayManagerFlags mMockFlags;
 
     @Before
     public void setUp() throws IOException {
@@ -52,7 +54,7 @@
 
         Mockito.when(mDisplayIdProducerMock.getId(false)).thenReturn(1);
 
-        setupDeviceStateToLayoutMap();
+        setupDeviceStateToLayoutMap(getContent());
     }
 
     //////////////////
@@ -268,6 +270,41 @@
                 IllegalArgumentException.class, () -> layout.postProcessLocked());
     }
 
+    @Test
+    public void testPortInLayout_disabledFlag() {
+        Mockito.when(mMockFlags.isPortInDisplayLayoutEnabled()).thenReturn(false);
+        assertThrows("Expected IllegalArgumentException when using <port>",
+                IllegalArgumentException.class,
+                () -> setupDeviceStateToLayoutMap(getPortContent()));
+    }
+
+    @Test
+    public void testPortInLayout_readLayout() throws Exception {
+        Mockito.when(mMockFlags.isPortInDisplayLayoutEnabled()).thenReturn(true);
+        setupDeviceStateToLayoutMap(getPortContent());
+
+        Layout configLayout = mDeviceStateToLayoutMap.get(0);
+
+        Layout testLayout = new Layout();
+        testLayout.createDisplayLocked(DisplayAddress.fromPortAndModel(123, null),
+                /* isDefault= */ true, /* isEnabled= */ true, /* displayGroupName= */ null,
+                mDisplayIdProducerMock,  Layout.Display.POSITION_UNKNOWN,
+                /* leadDisplayAddress= */ null, /* brightnessThrottlingMapId= */ null,
+                /* refreshRateZoneId= */ null,
+                /* refreshRateThermalThrottlingMapId= */ null,
+                /* powerThrottlingMapId= */ null);
+        testLayout.createDisplayLocked(DisplayAddress.fromPhysicalDisplayId(78910L),
+                /* isDefault= */ false, /* isEnabled= */ false, /* displayGroupName= */ null,
+                mDisplayIdProducerMock, Layout.Display.POSITION_UNKNOWN,
+                /* leadDisplayAddress= */ null, /* brightnessThrottlingMapId= */ null,
+                /* refreshRateZoneId= */ null,
+                /* refreshRateThermalThrottlingMapId= */ null,
+                /* powerThrottlingMapId= */ null);
+        testLayout.postProcessLocked();
+
+        assertEquals(testLayout, configLayout);
+    }
+
     ////////////////////
     // Helper Methods //
     ////////////////////
@@ -287,13 +324,28 @@
                 /* powerThrottlingMapId= */ null);
     }
 
-    private void setupDeviceStateToLayoutMap() throws IOException {
+    private void setupDeviceStateToLayoutMap(String content) throws IOException {
         Path tempFile = Files.createTempFile("device_state_layout_map", ".tmp");
-        Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8));
-        mDeviceStateToLayoutMap = new DeviceStateToLayoutMap(mDisplayIdProducerMock,
+        Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8));
+        mDeviceStateToLayoutMap = new DeviceStateToLayoutMap(mDisplayIdProducerMock, mMockFlags,
                 tempFile.toFile());
     }
 
+    private String getPortContent() {
+        return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                +  "<layouts>\n"
+                +    "<layout>\n"
+                +      "<state>0</state> \n"
+                +      "<display enabled=\"true\" defaultDisplay=\"true\">\n"
+                +        "<port>123</port>\n"
+                +      "</display>\n"
+                +      "<display enabled=\"false\">\n"
+                +        "<address>78910</address>\n"
+                +      "</display>\n"
+                +    "</layout>\n"
+                +  "</layouts>\n";
+    }
+
     private String getContent() {
         return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
                 +  "<layouts>\n"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
index eb6e8b4..ad4d91f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
@@ -96,6 +96,8 @@
                 .append(displayBrightnessState.isSlowChange())
                 .append("\n    maxBrightness:")
                 .append(displayBrightnessState.getMaxBrightness())
+                .append("\n    minBrightness:")
+                .append(displayBrightnessState.getMinBrightness())
                 .append("\n    customAnimationRate:")
                 .append(displayBrightnessState.getCustomAnimationRate())
                 .append("\n    shouldUpdateScreenBrightnessSetting:")
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 e370f55..c67e7c5 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -41,6 +41,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManager;
 import android.os.Temperature;
 import android.provider.Settings;
 import android.util.SparseArray;
@@ -109,6 +110,43 @@
     }
 
     @Test
+    public void testDefaultValues() {
+        when(mResources.getString(com.android.internal.R.string.config_displayLightSensorType))
+                .thenReturn("test_light_sensor");
+        when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true);
+
+        mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, /* useConfigXml= */ false,
+                mFlags);
+
+        assertEquals(DisplayDeviceConfig.BRIGHTNESS_DEFAULT,
+                mDisplayDeviceConfig.getBrightnessDefault(), ZERO_DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_MAX,
+                mDisplayDeviceConfig.getBrightnessRampFastDecrease(), ZERO_DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_MAX,
+                mDisplayDeviceConfig.getBrightnessRampFastIncrease(), ZERO_DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_MAX,
+                mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), ZERO_DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_MAX,
+                mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), ZERO_DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_MAX,
+                mDisplayDeviceConfig.getBrightnessRampSlowDecreaseIdle(), ZERO_DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_MAX,
+                mDisplayDeviceConfig.getBrightnessRampSlowIncreaseIdle(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis());
+        assertEquals(0, mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis());
+        assertEquals(0, mDisplayDeviceConfig.getBrightnessRampDecreaseMaxIdleMillis());
+        assertEquals(0, mDisplayDeviceConfig.getBrightnessRampIncreaseMaxIdleMillis());
+        assertNull(mDisplayDeviceConfig.getNits());
+        assertNull(mDisplayDeviceConfig.getBacklight());
+        assertEquals(0.3f, mDisplayDeviceConfig.getBacklightFromBrightness(0.3f), ZERO_DELTA);
+        assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type);
+        assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name);
+        assertNull(mDisplayDeviceConfig.getProximitySensor().type);
+        assertNull(mDisplayDeviceConfig.getProximitySensor().name);
+        assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable());
+    }
+
+    @Test
     public void testConfigValuesFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile();
 
@@ -681,6 +719,7 @@
 
         assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type);
         assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name);
+        assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable());
 
         assertEquals(brightnessIntToFloat(35),
                 mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
@@ -807,6 +846,24 @@
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), SMALL_DELTA);
     }
 
+    @Test
+    public void testIsAutoBrightnessAvailable_EnabledInConfigResource() throws IOException {
+        when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true);
+
+        setupDisplayDeviceConfigFromDisplayConfigFile();
+
+        assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable());
+    }
+
+    @Test
+    public void testIsAutoBrightnessAvailable_DisabledInConfigResource() throws IOException {
+        when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(false);
+
+        setupDisplayDeviceConfigFromDisplayConfigFile();
+
+        assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable());
+    }
+
     private String getValidLuxThrottling() {
         return "<luxThrottling>\n"
                 + "    <brightnessLimitMap>\n"
@@ -1176,7 +1233,7 @@
                 +           "<nits>" + NITS[2] + "</nits>\n"
                 +       "</point>\n"
                 +   "</screenBrightnessMap>\n"
-                +   "<autoBrightness>\n"
+                +   "<autoBrightness enabled=\"true\">\n"
                 +       "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n"
                 +       "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n"
                 + (includeIdleMode ? getRampSpeedsIdle() : "")
@@ -1593,6 +1650,7 @@
 
         when(mResources.getString(com.android.internal.R.string.config_displayLightSensorType))
                 .thenReturn("test_light_sensor");
+        when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true);
 
         when(mResources.getInteger(
                 R.integer.config_autoBrightnessBrighteningLightDebounce))
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
deleted file mode 100644
index 4cc68cf..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ /dev/null
@@ -1,1971 +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.server.display;
-
-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;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.res.Resources;
-import android.hardware.Sensor;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.hardware.display.BrightnessInfo;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.os.test.TestLooper;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.provider.Settings;
-import android.testing.TestableContext;
-import android.util.FloatProperty;
-import android.util.SparseArray;
-import android.view.Display;
-import android.view.DisplayInfo;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.modules.utils.testing.ExtendedMockitoRule;
-import com.android.server.LocalServices;
-import com.android.server.am.BatteryStatsService;
-import com.android.server.display.RampAnimator.DualRampAnimator;
-import com.android.server.display.brightness.BrightnessEvent;
-import com.android.server.display.brightness.clamper.BrightnessClamperController;
-import com.android.server.display.brightness.clamper.HdrClamper;
-import com.android.server.display.color.ColorDisplayService;
-import com.android.server.display.config.SensorData;
-import com.android.server.display.feature.DisplayManagerFlags;
-import com.android.server.display.feature.flags.Flags;
-import com.android.server.display.layout.Layout;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
-import com.android.server.policy.WindowManagerPolicy;
-import com.android.server.testutils.OffsettableClock;
-
-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;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.quality.Strictness;
-import org.mockito.stubbing.Answer;
-
-import java.util.List;
-
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public final class DisplayPowerController2Test {
-    private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
-    private static final String UNIQUE_ID = "unique_id_test123";
-    private static final int FOLLOWER_DISPLAY_ID = DISPLAY_ID + 1;
-    private static final String FOLLOWER_UNIQUE_ID = "unique_id_456";
-    private static final int SECOND_FOLLOWER_DISPLAY_ID = FOLLOWER_DISPLAY_ID + 1;
-    private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
-    private static final float PROX_SENSOR_MAX_RANGE = 5;
-    private static final float BRIGHTNESS_RAMP_RATE_MINIMUM = 0.0f;
-    private static final float BRIGHTNESS_RAMP_RATE_FAST_DECREASE = 0.3f;
-    private static final float BRIGHTNESS_RAMP_RATE_FAST_INCREASE = 0.4f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_DECREASE = 0.1f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_INCREASE = 0.2f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE = 0.5f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE = 0.6f;
-
-    private static final long BRIGHTNESS_RAMP_INCREASE_MAX = 1000;
-    private static final long BRIGHTNESS_RAMP_DECREASE_MAX = 2000;
-    private static final long BRIGHTNESS_RAMP_INCREASE_MAX_IDLE = 3000;
-    private static final long BRIGHTNESS_RAMP_DECREASE_MAX_IDLE = 4000;
-
-    private OffsettableClock mClock;
-    private TestLooper mTestLooper;
-    private Handler mHandler;
-    private DisplayPowerControllerHolder mHolder;
-    private Sensor mProxSensor;
-
-    @Mock
-    private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
-    @Mock
-    private SensorManager mSensorManagerMock;
-    @Mock
-    private DisplayBlanker mDisplayBlankerMock;
-    @Mock
-    private BrightnessTracker mBrightnessTrackerMock;
-    @Mock
-    private WindowManagerPolicy mWindowManagerPolicyMock;
-    @Mock
-    private PowerManager mPowerManagerMock;
-    @Mock
-    private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
-    @Mock
-    private DisplayWhiteBalanceController mDisplayWhiteBalanceControllerMock;
-    @Mock
-    private DisplayManagerFlags mDisplayManagerFlagsMock;
-    @Mock
-    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
-    @Captor
-    private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor;
-
-    @Rule
-    public final TestableContext mContext = new TestableContext(
-            InstrumentationRegistry.getInstrumentation().getContext());
-
-    @Rule
-    public final ExtendedMockitoRule mExtendedMockitoRule =
-            new ExtendedMockitoRule.Builder(this)
-                    .setStrictness(Strictness.LENIENT)
-                    .spyStatic(SystemProperties.class)
-                    .spyStatic(BatteryStatsService.class)
-                    .spyStatic(ActivityManager.class)
-                    .build();
-
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
-    @Before
-    public void setUp() throws Exception {
-        mClock = new OffsettableClock.Stopped();
-        mTestLooper = new TestLooper(mClock::now);
-        mHandler = new Handler(mTestLooper.getLooper());
-
-        // Set some settings to minimize unexpected events and have a consistent starting state
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
-        Settings.System.putFloatForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
-
-        addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
-        addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
-                mCdsiMock);
-
-        mContext.addMockSystemService(PowerManager.class, mPowerManagerMock);
-
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_displayColorFadeDisabled, false);
-
-        doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
-                SystemProperties.set(anyString(), any()));
-        doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
-        doAnswer((Answer<Boolean>) invocationOnMock -> false)
-                .when(ActivityManager::isLowRamDeviceStatic);
-
-        setUpSensors();
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-    }
-
-    @After
-    public void tearDown() {
-        LocalServices.removeServiceForTest(WindowManagerPolicy.class);
-        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
-    }
-
-    @Test
-    public void testReleaseProxSuspendBlockersOnExit() throws Exception {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState to start listener for the prox sensor
-        advanceTime(1);
-
-        SensorEventListener listener = getSensorEventListener(mProxSensor);
-        assertNotNull(listener);
-
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, /* value= */ 5));
-        advanceTime(1);
-
-        // two times, one for unfinished business and one for proximity
-        verify(mHolder.wakelockController, times(2)).acquireWakelock(
-                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        verify(mHolder.wakelockController).acquireWakelock(
-                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-
-        mHolder.dpc.stop();
-        advanceTime(1);
-        // two times, one for unfinished business and one for proximity
-        verify(mHolder.wakelockController, times(2)).acquireWakelock(
-                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        verify(mHolder.wakelockController).acquireWakelock(
-                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-    }
-
-    @Test
-    public void testScreenOffBecauseOfProximity() throws Exception {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState to start listener for the prox sensor
-        advanceTime(1);
-
-        SensorEventListener listener = getSensorEventListener(mProxSensor);
-        assertNotNull(listener);
-
-        // Send a positive proximity event
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, /* value= */ 1));
-        advanceTime(1);
-
-        // The display should have been turned off
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_OFF);
-
-        clearInvocations(mHolder.displayPowerState);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-        // Send a negative proximity event
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor,
-                (int) PROX_SENSOR_MAX_RANGE + 1));
-        // Advance time by less than PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY
-        advanceTime(1);
-
-        // The prox sensor is debounced so the display should not have been turned back on yet
-        verify(mHolder.displayPowerState, never()).setScreenState(Display.STATE_ON);
-
-        // Advance time by more than PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY
-        advanceTime(1000);
-
-        // The display should have been turned back on
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_ON);
-    }
-
-    @Test
-    public void testScreenOffBecauseOfProximity_ProxSensorGone() throws Exception {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState to start listener for the prox sensor
-        advanceTime(1);
-
-        SensorEventListener listener = getSensorEventListener(mProxSensor);
-        assertNotNull(listener);
-
-        // Send a positive proximity event
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, /* value= */ 1));
-        advanceTime(1);
-
-        // The display should have been turned off
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_OFF);
-
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-        // The display device changes and we no longer have a prox sensor
-        reset(mSensorManagerMock);
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-
-        advanceTime(1); // Run updatePowerState
-
-        // The display should have been turned back on and the listener should have been
-        // unregistered
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_ON);
-        verify(mSensorManagerMock).unregisterListener(listener);
-    }
-
-    @Test
-    public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        final DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        followerDpc.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
-                eq(mProxSensor), anyInt(), any(Handler.class));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        // Test different float scale values
-        float leadBrightness = 0.3f;
-        float followerBrightness = 0.4f;
-        float nits = 300;
-        when(mHolder.automaticBrightnessController.convertToNits(leadBrightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(followerBrightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
-        listener.onBrightnessChanged(leadBrightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(), anyFloat(), eq(false));
-        verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
-                anyFloat(), eq(false));
-
-        clearInvocations(mHolder.animator, followerDpc.animator);
-
-        // Test the same float scale value
-        float brightness = 0.6f;
-        nits = 600;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        float brightness = 0.3f;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(300f);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
-                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        float brightness = 0.3f;
-        when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        float brightness = 0.3f;
-        when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
-                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_AutomaticBrightness() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        float leadBrightness = 0.1f;
-        float rawLeadBrightness = 0.3f;
-        float followerBrightness = 0.4f;
-        float nits = 300;
-        float ambientLux = 3000;
-        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
-                .thenReturn(rawLeadBrightness);
-        when(mHolder.automaticBrightnessController
-                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
-                .thenReturn(leadBrightness);
-        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
-                .thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(followerBrightness);
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        // One triggered by handleBrightnessModeChange, another triggered by setBrightnessToFollow
-        verify(followerDpc.hbmController, times(2)).onAmbientLuxChange(ambientLux);
-        verify(followerDpc.animator, times(2)).animateTo(eq(followerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
-        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(followerBrightness);
-        clearInvocations(mHolder.animator, followerDpc.animator);
-
-        leadBrightness = 0.05f;
-        rawLeadBrightness = 0.2f;
-        followerBrightness = 0.3f;
-        nits = 200;
-        ambientLux = 2000;
-        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
-                .thenReturn(rawLeadBrightness);
-        when(mHolder.automaticBrightnessController
-                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
-                .thenReturn(leadBrightness);
-        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
-                .thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(followerBrightness);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        // The second time, the animation rate should be slow
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE), eq(false));
-        verify(followerDpc.hbmController).onAmbientLuxChange(ambientLux);
-        verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowersRemoval_RemoveSingleFollower() {
-        DisplayPowerControllerHolder followerDpc = createDisplayPowerController(FOLLOWER_DISPLAY_ID,
-                FOLLOWER_UNIQUE_ID);
-        DisplayPowerControllerHolder secondFollowerDpc = createDisplayPowerController(
-                SECOND_FOLLOWER_DISPLAY_ID, SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(secondFollowerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        secondFollowerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        // Set the initial brightness on the DPC we're going to remove so we have a fixed value for
-        // it to return to.
-        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(followerDpc.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
-        final float initialFollowerBrightness = 0.3f;
-        when(followerDpc.brightnessSetting.getBrightness()).thenReturn(initialFollowerBrightness);
-        followerListener.onBrightnessChanged(initialFollowerBrightness);
-        advanceTime(1);
-        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(followerDpc.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerDpc.dpc);
-        clearInvocations(followerDpc.animator);
-
-        // Validate both followers are correctly registered and receiving brightness updates
-        float brightness = 0.6f;
-        float nits = 600;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(secondFollowerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        clearInvocations(mHolder.animator, followerDpc.animator, secondFollowerDpc.animator);
-
-        // Remove the first follower and validate it goes back to its original brightness.
-        mHolder.dpc.removeDisplayBrightnessFollower(followerDpc.dpc);
-        advanceTime(1);
-        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), eq(false));
-
-        when(followerDpc.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-        clearInvocations(followerDpc.animator);
-
-        // Change the brightness of the lead display and validate only the second follower responds
-        brightness = 0.7f;
-        nits = 700;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator, never()).animateTo(anyFloat(), anyFloat(), anyFloat(),
-                anyBoolean());
-        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowersRemoval_RemoveAllFollowers() {
-        DisplayPowerControllerHolder followerHolder =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        DisplayPowerControllerHolder secondFollowerHolder =
-                createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
-                        SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(secondFollowerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        secondFollowerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        // Set the initial brightness on the DPCs we're going to remove so we have a fixed value for
-        // it to return to.
-        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(followerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
-        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(secondFollowerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener secondFollowerListener =
-                listenerCaptor.getValue();
-        final float initialFollowerBrightness = 0.3f;
-        when(followerHolder.brightnessSetting.getBrightness()).thenReturn(
-                initialFollowerBrightness);
-        when(secondFollowerHolder.brightnessSetting.getBrightness()).thenReturn(
-                initialFollowerBrightness);
-        followerListener.onBrightnessChanged(initialFollowerBrightness);
-        secondFollowerListener.onBrightnessChanged(initialFollowerBrightness);
-        advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(followerHolder.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-        when(secondFollowerHolder.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
-        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
-        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
-
-        // Validate both followers are correctly registered and receiving brightness updates
-        float brightness = 0.6f;
-        float nits = 600;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(secondFollowerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(followerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(secondFollowerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
-
-        // Stop the lead DPC and validate that the followers go back to their original brightness.
-        mHolder.dpc.stop();
-        advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), eq(false));
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), eq(false));
-        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-        clearInvocations(mHolder.animator);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrRemoval() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
-
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(hdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
-
-        clearInvocations(mHolder.animator);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
-    }
-
-    @Test
-    public void testDoesNotSetScreenStateForNonDefaultDisplayUntilBootCompleted() {
-        // We should still set screen state for the default display
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState, times(2)).setScreenState(anyInt());
-
-        mHolder = createDisplayPowerController(42, UNIQUE_ID);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
-
-        mHolder.dpc.onBootCompleted();
-        advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState).setScreenState(anyInt());
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorEnabled_DisplayIsOff() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(true);
-
-        // The display turns on and we use the brightness value recommended by
-        // ScreenOffBrightnessSensorController
-        clearInvocations(mHolder.screenOffBrightnessSensorController);
-        float brightness = 0.14f;
-        when(mHolder.screenOffBrightnessSensorController.getAutomaticScreenBrightness())
-                .thenReturn(brightness);
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .getAutomaticScreenBrightness();
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat(), eq(false));
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorEnabled_DisplayIsInDoze() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        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
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(true);
-
-        // The display turns on and we use the brightness value recommended by
-        // ScreenOffBrightnessSensorController
-        clearInvocations(mHolder.screenOffBrightnessSensorController);
-        float brightness = 0.14f;
-        when(mHolder.screenOffBrightnessSensorController.getAutomaticScreenBrightness())
-                .thenReturn(brightness);
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .getAutomaticScreenBrightness();
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat(), eq(false));
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_AutoBrightnessIsDisabled() {
-        // Tests are set up with manual brightness by default, so no need to set it here.
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_DisplayIsDisabled() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ false);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_DisplayIsOn() {
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_DisplayIsAFollower() {
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, /* leadDisplayId= */ 42);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testStopScreenOffBrightnessSensorControllerWhenDisplayDeviceChanges() {
-        // New display device
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController).stop();
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_DisplayIsOn() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController)
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_DisplayIsInDoze() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController)
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_ManualBrightnessMode() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        // One triggered by the test, the other by handleBrightnessModeChange
-        verify(mHolder.automaticBrightnessController, times(2)).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController, times(2))
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_DisplayIsOff() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_OFF,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController).setAutoBrightnessEnabled(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_DisplayIsInDoze() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController).setAutoBrightnessEnabled(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_FollowerDisplay() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mHolder.dpc.setBrightnessToFollow(0.3f, -1, 0, /* slowChange= */ false);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        // One triggered by the test, the other by handleBrightnessModeChange
-        verify(mHolder.automaticBrightnessController, times(2)).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
-                /* shouldResetShortTermModel= */ false
-        );
-
-        // HBM should be allowed for the follower display
-        verify(mHolder.hbmController)
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
-    }
-
-    @Test
-    public void testBrightnessNitsPersistWhenDisplayDeviceChanges() {
-        float brightness = 0.3f;
-        float nits = 500;
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay,
-                true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-
-        mHolder.dpc.setBrightness(brightness);
-        verify(mHolder.brightnessSetting).setBrightnessNitsForDefaultDisplay(nits);
-
-        float newBrightness = 0.4f;
-        when(mHolder.brightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(newBrightness);
-        // New display device
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-        // One triggered by handleBrightnessModeChange, another triggered by onDisplayChanged
-        verify(mHolder.animator, times(2)).animateTo(eq(newBrightness), anyFloat(), anyFloat(),
-                eq(false));
-    }
-
-    @Test
-    public void testShortTermModelPersistsWhenDisplayDeviceChanges() {
-        float lux = 2000;
-        float nits = 500;
-        when(mHolder.automaticBrightnessController.getUserLux()).thenReturn(lux);
-        when(mHolder.automaticBrightnessController.getUserNits()).thenReturn(nits);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1);
-        clearInvocations(mHolder.injector);
-
-        // New display device
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        advanceTime(1);
-
-        verify(mHolder.injector).getAutomaticBrightnessController(
-                any(AutomaticBrightnessController.Callbacks.class),
-                any(Looper.class),
-                eq(mSensorManagerMock),
-                /* lightSensor= */ any(),
-                /* brightnessMappingStrategyMap= */ any(SparseArray.class),
-                /* lightSensorWarmUpTime= */ anyInt(),
-                /* brightnessMin= */ anyFloat(),
-                /* brightnessMax= */ anyFloat(),
-                /* dozeScaleFactor */ anyFloat(),
-                /* lightSensorRate= */ anyInt(),
-                /* initialLightSensorRate= */ anyInt(),
-                /* brighteningLightDebounceConfig */ anyLong(),
-                /* darkeningLightDebounceConfig */ anyLong(),
-                /* brighteningLightDebounceConfigIdle= */ anyLong(),
-                /* darkeningLightDebounceConfigIdle= */ anyLong(),
-                /* resetAmbientLuxAfterWarmUpConfig= */ anyBoolean(),
-                any(HysteresisLevels.class),
-                any(HysteresisLevels.class),
-                any(HysteresisLevels.class),
-                any(HysteresisLevels.class),
-                eq(mContext),
-                any(BrightnessRangeController.class),
-                any(BrightnessThrottler.class),
-                /* ambientLightHorizonShort= */ anyInt(),
-                /* ambientLightHorizonLong= */ anyInt(),
-                eq(lux),
-                eq(nits)
-        );
-    }
-
-    @Test
-    public void testUpdateBrightnessThrottlingDataId() {
-        mHolder.display.getDisplayInfoLocked().thermalBrightnessThrottlingDataId =
-                "throttling-data-id";
-        clearInvocations(mHolder.display.getPrimaryDisplayDeviceLocked().getDisplayDeviceConfig());
-
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.display.getPrimaryDisplayDeviceLocked().getDisplayDeviceConfig())
-                .getThermalBrightnessThrottlingDataMapByThrottlingId();
-    }
-
-    @Test
-    public void testSetBrightness_BrightnessShouldBeClamped() {
-        float clampedBrightness = 0.6f;
-        when(mHolder.hbmController.getCurrentBrightnessMax()).thenReturn(clampedBrightness);
-
-        mHolder.dpc.setBrightness(PowerManager.BRIGHTNESS_MAX);
-
-        verify(mHolder.brightnessSetting).setBrightness(clampedBrightness);
-    }
-
-    @Test
-    public void testDwbcCallsHappenOnHandler() {
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-        verify(mDisplayWhiteBalanceControllerMock, never()).setStrongModeEnabled(true);
-
-        // dispatch handler looper
-        advanceTime(1);
-        verify(mDisplayWhiteBalanceControllerMock, times(1)).setStrongModeEnabled(true);
-    }
-
-    @Test
-    public void testRampRatesIdle() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        float brightness = 0.6f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(brightness);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        brightness = 0.05f;
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(brightness);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        // The second time, the animation rate should be slow
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE), eq(false));
-
-        brightness = 0.9f;
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(brightness);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-        // The third time, the animation rate should be slow
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE), eq(false));
-    }
-
-    @Test
-    public void testRampRateForHdrContent_HdrClamperOff() {
-        float hdrBrightness = 0.8f;
-        float clampedBrightness = 0.6f;
-        float transitionRate = 1.5f;
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
-        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(eq(hdrBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testRampRateForHdrContent_HdrClamperOn() {
-        float clampedBrightness = 0.6f;
-        float transitionRate = 1.5f;
-        when(mDisplayManagerFlagsMock.isHdrClamperEnabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(PowerManager.BRIGHTNESS_MAX);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
-        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(eq(clampedBrightness), anyFloat(),
-                eq(transitionRate), eq(true));
-    }
-
-    @Test
-    public void testRampRateForClampersControllerApplied() {
-        float transitionRate = 1.5f;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
-                invocation -> DisplayBrightnessState.builder()
-                        .setIsSlowChange(invocation.getArgument(2))
-                        .setBrightness(invocation.getArgument(1))
-                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
-                        .setCustomAnimationRate(transitionRate).build());
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
-                eq(transitionRate), anyBoolean());
-    }
-
-    @Test
-    public void testRampRateForClampersControllerNotApplied_ifDoze() {
-        float transitionRate = 1.5f;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        dpr.dozeScreenState = Display.STATE_UNKNOWN;
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
-                invocation -> DisplayBrightnessState.builder()
-                        .setIsSlowChange(invocation.getArgument(2))
-                        .setBrightness(invocation.getArgument(1))
-                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
-                        .setCustomAnimationRate(transitionRate).build());
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), anyBoolean());
-        verify(mHolder.animator, never()).animateTo(anyFloat(), anyFloat(),
-                eq(transitionRate), anyBoolean());
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeInteractiveThenIdle() {
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-
-        // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-        advanceTime(1);
-
-        // A second time, when switching to idle mode.
-        verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeInteractiveThenIdle_DifferentValues() {
-        when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
-
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-
-        // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-        advanceTime(1);
-
-        // A second time, when switching to idle mode.
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
-                BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeIdle() {
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-        // Run updatePowerState
-        advanceTime(1);
-        // Once on setup
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-
-        // switch to idle mode
-        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,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeIdle_DifferentValues() {
-        when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
-
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-
-        // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
-                BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
-    }
-
-    @Test
-    public void testDozeScreenStateOverride_toSupportedOffloadStateFromDoze_DisplayStateChanges() {
-        // set up.
-        int initState = Display.STATE_DOZE;
-        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        doAnswer(invocation -> {
-            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
-            return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        // start with DOZE.
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.displayPowerState).setScreenState(supportedTargetState);
-    }
-
-    @Test
-    public void testDozeScreenStateOverride_toUnSupportedOffloadStateFromDoze_stateRemains() {
-        // set up.
-        int initState = Display.STATE_DOZE;
-        int unSupportedTargetState = Display.STATE_ON;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        doAnswer(invocation -> {
-            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
-            return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        // start with DOZE.
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        mHolder.dpc.overrideDozeScreenState(unSupportedTargetState);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
-    }
-
-    @Test
-    public void testDozeScreenStateOverride_toSupportedOffloadStateFromOFF_stateRemains() {
-        // set up.
-        int initState = Display.STATE_OFF;
-        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        doAnswer(invocation -> {
-            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
-            return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        // start with OFF.
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
-    }
-
-    @Test
-    public void testBrightnessFromOffload() {
-        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        float brightness = 0.34f;
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        mHolder.dpc.setBrightnessFromOffload(brightness);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        // One triggered by handleBrightnessModeChange, another triggered by
-        // setBrightnessFromOffload
-        verify(mHolder.animator, times(2)).animateTo(eq(brightness), anyFloat(),
-                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}.
-     */
-    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
-        LocalServices.removeServiceForTest(clazz);
-        LocalServices.addService(clazz, mock);
-    }
-
-    private void advanceTime(long timeMs) {
-        mClock.fastForward(timeMs);
-        mTestLooper.dispatchAll();
-    }
-
-    private void setUpSensors() throws Exception {
-        mProxSensor = TestUtils.createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY,
-                PROX_SENSOR_MAX_RANGE);
-        Sensor screenOffBrightnessSensor = TestUtils.createSensor(
-                Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
-        when(mSensorManagerMock.getSensorList(eq(Sensor.TYPE_ALL)))
-                .thenReturn(List.of(mProxSensor, screenOffBrightnessSensor));
-    }
-
-    private SensorEventListener getSensorEventListener(Sensor sensor) {
-        verify(mSensorManagerMock).registerListener(mSensorEventListenerCaptor.capture(),
-                eq(sensor), eq(SensorManager.SENSOR_DELAY_NORMAL), isA(Handler.class));
-        return mSensorEventListenerCaptor.getValue();
-    }
-
-    private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
-            DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock,
-            boolean isEnabled) {
-        DisplayInfo info = new DisplayInfo();
-        DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo();
-        deviceInfo.uniqueId = uniqueId;
-
-        when(logicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
-        when(logicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(displayDeviceMock);
-        when(logicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
-        when(logicalDisplayMock.isEnabledLocked()).thenReturn(isEnabled);
-        when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false);
-        when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
-        when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
-        when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
-        when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
-        when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
-        when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
-        when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
-                new SensorData());
-        when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_LIGHT, null));
-        when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
-                .thenReturn(new int[0]);
-
-        when(displayDeviceConfigMock.getBrightnessRampFastDecrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_DECREASE);
-        when(displayDeviceConfigMock.getBrightnessRampFastIncrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_INCREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowDecrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowIncrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowIncreaseIdle())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowDecreaseIdle())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE);
-
-        when(displayDeviceConfigMock.getBrightnessRampIncreaseMaxMillis())
-                .thenReturn(BRIGHTNESS_RAMP_INCREASE_MAX);
-        when(displayDeviceConfigMock.getBrightnessRampDecreaseMaxMillis())
-                .thenReturn(BRIGHTNESS_RAMP_DECREASE_MAX);
-        when(displayDeviceConfigMock.getBrightnessRampIncreaseMaxIdleMillis())
-                .thenReturn(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE);
-        when(displayDeviceConfigMock.getBrightnessRampDecreaseMaxIdleMillis())
-                .thenReturn(BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
-    }
-
-    private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
-            String uniqueId) {
-        return createDisplayPowerController(displayId, uniqueId, /* isEnabled= */ true);
-    }
-
-    private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
-            String uniqueId, boolean isEnabled) {
-        final DisplayPowerState displayPowerState = mock(DisplayPowerState.class);
-        final DualRampAnimator<DisplayPowerState> animator = mock(DualRampAnimator.class);
-        final AutomaticBrightnessController automaticBrightnessController =
-                mock(AutomaticBrightnessController.class);
-        final WakelockController wakelockController = mock(WakelockController.class);
-        final BrightnessMappingStrategy brightnessMappingStrategy =
-                mock(BrightnessMappingStrategy.class);
-        final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
-        final ScreenOffBrightnessSensorController screenOffBrightnessSensorController =
-                mock(ScreenOffBrightnessSensorController.class);
-        final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class);
-        final HdrClamper hdrClamper = mock(HdrClamper.class);
-        BrightnessClamperController clamperController = mock(BrightnessClamperController.class);
-
-        when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX);
-        when(clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
-                invocation -> DisplayBrightnessState.builder()
-                        .setIsSlowChange(invocation.getArgument(2))
-                        .setBrightness(invocation.getArgument(1))
-                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
-                        .setCustomAnimationRate(-1).build());
-
-        TestInjector injector = spy(new TestInjector(displayPowerState, animator,
-                automaticBrightnessController, wakelockController, brightnessMappingStrategy,
-                hysteresisLevels, screenOffBrightnessSensorController, hbmController, hdrClamper,
-                clamperController, mDisplayManagerFlagsMock));
-
-        final LogicalDisplay display = mock(LogicalDisplay.class);
-        final DisplayDevice device = mock(DisplayDevice.class);
-        final HighBrightnessModeMetadata hbmMetadata = mock(HighBrightnessModeMetadata.class);
-        final BrightnessSetting brightnessSetting = mock(BrightnessSetting.class);
-        final DisplayDeviceConfig config = mock(DisplayDeviceConfig.class);
-
-        setUpDisplay(displayId, uniqueId, display, device, config, isEnabled);
-
-        final DisplayPowerController2 dpc = new DisplayPowerController2(
-                mContext, injector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, display,
-                mBrightnessTrackerMock, brightnessSetting, () -> {
-        },
-                hbmMetadata, /* bootCompleted= */ false, mDisplayManagerFlagsMock);
-
-        return new DisplayPowerControllerHolder(dpc, display, displayPowerState, brightnessSetting,
-                animator, automaticBrightnessController, wakelockController,
-                screenOffBrightnessSensorController, hbmController, hdrClamper, clamperController,
-                hbmMetadata, brightnessMappingStrategy, injector, config);
-    }
-
-    /**
-     * A class for holding a DisplayPowerController under test and all the mocks specifically
-     * related to it.
-     */
-    private static class DisplayPowerControllerHolder {
-        public final DisplayPowerController2 dpc;
-        public final LogicalDisplay display;
-        public final DisplayPowerState displayPowerState;
-        public final BrightnessSetting brightnessSetting;
-        public final DualRampAnimator<DisplayPowerState> animator;
-        public final AutomaticBrightnessController automaticBrightnessController;
-        public final WakelockController wakelockController;
-        public final ScreenOffBrightnessSensorController screenOffBrightnessSensorController;
-        public final HighBrightnessModeController hbmController;
-
-        public final HdrClamper hdrClamper;
-        public final BrightnessClamperController clamperController;
-        public final HighBrightnessModeMetadata hbmMetadata;
-        public final BrightnessMappingStrategy brightnessMappingStrategy;
-        public final DisplayPowerController2.Injector injector;
-        public final DisplayDeviceConfig config;
-
-        DisplayPowerControllerHolder(DisplayPowerController2 dpc, LogicalDisplay display,
-                DisplayPowerState displayPowerState, BrightnessSetting brightnessSetting,
-                DualRampAnimator<DisplayPowerState> animator,
-                AutomaticBrightnessController automaticBrightnessController,
-                WakelockController wakelockController,
-                ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
-                HighBrightnessModeController hbmController,
-                HdrClamper hdrClamper,
-                BrightnessClamperController clamperController,
-                HighBrightnessModeMetadata hbmMetadata,
-                BrightnessMappingStrategy brightnessMappingStrategy,
-                DisplayPowerController2.Injector injector,
-                DisplayDeviceConfig config) {
-            this.dpc = dpc;
-            this.display = display;
-            this.displayPowerState = displayPowerState;
-            this.brightnessSetting = brightnessSetting;
-            this.animator = animator;
-            this.automaticBrightnessController = automaticBrightnessController;
-            this.wakelockController = wakelockController;
-            this.screenOffBrightnessSensorController = screenOffBrightnessSensorController;
-            this.hbmController = hbmController;
-            this.hdrClamper = hdrClamper;
-            this.clamperController = clamperController;
-            this.hbmMetadata = hbmMetadata;
-            this.brightnessMappingStrategy = brightnessMappingStrategy;
-            this.injector = injector;
-            this.config = config;
-        }
-    }
-
-    private class TestInjector extends DisplayPowerController2.Injector {
-        private final DisplayPowerState mDisplayPowerState;
-        private final DualRampAnimator<DisplayPowerState> mAnimator;
-        private final AutomaticBrightnessController mAutomaticBrightnessController;
-        private final WakelockController mWakelockController;
-        private final BrightnessMappingStrategy mBrightnessMappingStrategy;
-        private final HysteresisLevels mHysteresisLevels;
-        private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
-        private final HighBrightnessModeController mHighBrightnessModeController;
-
-        private final HdrClamper mHdrClamper;
-
-        private final BrightnessClamperController mClamperController;
-
-        private final DisplayManagerFlags mFlags;
-
-        TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
-                AutomaticBrightnessController automaticBrightnessController,
-                WakelockController wakelockController,
-                BrightnessMappingStrategy brightnessMappingStrategy,
-                HysteresisLevels hysteresisLevels,
-                ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
-                HighBrightnessModeController highBrightnessModeController,
-                HdrClamper hdrClamper,
-                BrightnessClamperController clamperController,
-                DisplayManagerFlags flags) {
-            mDisplayPowerState = dps;
-            mAnimator = animator;
-            mAutomaticBrightnessController = automaticBrightnessController;
-            mWakelockController = wakelockController;
-            mBrightnessMappingStrategy = brightnessMappingStrategy;
-            mHysteresisLevels = hysteresisLevels;
-            mScreenOffBrightnessSensorController = screenOffBrightnessSensorController;
-            mHighBrightnessModeController = highBrightnessModeController;
-            mHdrClamper = hdrClamper;
-            mClamperController = clamperController;
-            mFlags = flags;
-        }
-
-        @Override
-        DisplayPowerController2.Clock getClock() {
-            return mClock::now;
-        }
-
-        @Override
-        DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
-                int displayId, int displayState) {
-            return mDisplayPowerState;
-        }
-
-        @Override
-        DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
-                FloatProperty<DisplayPowerState> firstProperty,
-                FloatProperty<DisplayPowerState> secondProperty) {
-            return mAnimator;
-        }
-
-        @Override
-        WakelockController getWakelockController(int displayId,
-                DisplayPowerCallbacks displayPowerCallbacks) {
-            return mWakelockController;
-        }
-
-        @Override
-        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
-                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
-                Looper looper, Runnable nudgeUpdatePowerState, int displayId,
-                SensorManager sensorManager) {
-            return new DisplayPowerProximityStateController(wakelockController,
-                    displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
-                    sensorManager,
-                    new DisplayPowerProximityStateController.Injector() {
-                        @Override
-                        DisplayPowerProximityStateController.Clock createClock() {
-                            return mClock::now;
-                        }
-                    });
-        }
-
-        @Override
-        AutomaticBrightnessController getAutomaticBrightnessController(
-                AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager, Sensor lightSensor,
-                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                boolean resetAmbientLuxAfterWarmUpConfig,
-                HysteresisLevels ambientBrightnessThresholds,
-                HysteresisLevels screenBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
-                HysteresisLevels screenBrightnessThresholdsIdle, Context context,
-                BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
-            return mAutomaticBrightnessController;
-        }
-
-        @Override
-        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
-                DisplayDeviceConfig displayDeviceConfig,
-                DisplayWhiteBalanceController displayWhiteBalanceController) {
-            return mBrightnessMappingStrategy;
-        }
-
-        @Override
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold) {
-            return mHysteresisLevels;
-        }
-
-        @Override
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
-            return mHysteresisLevels;
-        }
-
-        @Override
-        ScreenOffBrightnessSensorController getScreenOffBrightnessSensorController(
-                SensorManager sensorManager, Sensor lightSensor, Handler handler,
-                ScreenOffBrightnessSensorController.Clock clock, int[] sensorValueToLux,
-                BrightnessMappingStrategy brightnessMapper) {
-            return mScreenOffBrightnessSensorController;
-        }
-
-        @Override
-        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
-                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
-                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
-                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
-                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
-                Context context) {
-            return mHighBrightnessModeController;
-        }
-
-        @Override
-        BrightnessRangeController getBrightnessRangeController(
-                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
-                DisplayDeviceConfig displayDeviceConfig, Handler handler,
-                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
-            return new BrightnessRangeController(hbmController, modeChangeCallback,
-                    displayDeviceConfig, mHdrClamper, mFlags, displayToken, info);
-        }
-
-        @Override
-        BrightnessClamperController getBrightnessClamperController(Handler handler,
-                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
-                BrightnessClamperController.DisplayDeviceData data, Context context,
-                DisplayManagerFlags flags) {
-            return mClamperController;
-        }
-
-        @Override
-        DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
-                SensorManager sensorManager, Resources resources) {
-            return mDisplayWhiteBalanceControllerMock;
-        }
-    }
-}
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 943862f..88a9758 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,8 @@
 
 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;
@@ -67,15 +69,16 @@
 import android.view.DisplayInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.internal.util.test.LocalServiceKeeperRule;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.LocalServices;
 import com.android.server.am.BatteryStatsService;
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
+import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
 import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
@@ -85,6 +88,7 @@
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.testutils.OffsettableClock;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -147,7 +151,6 @@
     private DisplayManagerFlags mDisplayManagerFlagsMock;
     @Mock
     private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
-
     @Captor
     private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor;
 
@@ -165,9 +168,6 @@
                     .build();
 
     @Rule
-    public LocalServiceKeeperRule mLocalServiceKeeperRule = new LocalServiceKeeperRule();
-
-    @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Before
@@ -183,10 +183,9 @@
         Settings.System.putFloatForUser(mContext.getContentResolver(),
                 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
 
-        mLocalServiceKeeperRule.overrideLocalService(
-                WindowManagerPolicy.class, mWindowManagerPolicyMock);
-        mLocalServiceKeeperRule.overrideLocalService(
-                ColorDisplayService.ColorDisplayServiceInternal.class, mCdsiMock);
+        addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
+        addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
+                mCdsiMock);
 
         mContext.addMockSystemService(PowerManager.class, mPowerManagerMock);
 
@@ -203,6 +202,12 @@
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
     }
 
+    @After
+    public void tearDown() {
+        LocalServices.removeServiceForTest(WindowManagerPolicy.class);
+        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+    }
+
     @Test
     public void testReleaseProxSuspendBlockersOnExit() throws Exception {
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
@@ -222,19 +227,18 @@
         advanceTime(1);
 
         // two times, one for unfinished business and one for proximity
-        verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
-        verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+        verify(mHolder.wakelockController, times(2)).acquireWakelock(
+                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
+        verify(mHolder.wakelockController).acquireWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
 
         mHolder.dpc.stop();
         advanceTime(1);
-
         // two times, one for unfinished business and one for proximity
-        verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
-        verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+        verify(mHolder.wakelockController, times(2)).acquireWakelock(
+                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
+        verify(mHolder.wakelockController).acquireWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
     }
 
     @Test
@@ -316,14 +320,13 @@
 
     @Test
     public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
         // send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
+        final DisplayPowerControllerHolder followerDpc =
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
         followerDpc.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
 
         // Run updatePowerState
@@ -334,7 +337,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -363,13 +365,10 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
         listener.onBrightnessChanged(leadBrightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(), anyFloat(), eq(false));
         verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+                anyFloat(), eq(false));
 
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(followerBrightness);
         clearInvocations(mHolder.animator, followerDpc.animator);
 
         // Test the same float scale value
@@ -388,7 +387,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -421,7 +419,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -452,7 +449,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -485,7 +481,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_AutomaticBrightness() {
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
@@ -557,7 +552,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowersRemoval_RemoveSingleFollower() {
         DisplayPowerControllerHolder followerDpc = createDisplayPowerController(FOLLOWER_DISPLAY_ID,
                 FOLLOWER_UNIQUE_ID);
@@ -650,7 +644,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowersRemoval_RemoveAllFollowers() {
         DisplayPowerControllerHolder followerHolder =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -737,6 +730,82 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
+    public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        final float sdrBrightness = 0.1f;
+        final float hdrBrightness = 0.3f;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(sdrBrightness);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
+
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
+        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
+        clearInvocations(mHolder.animator);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
+    public void testDisplayBrightnessHdr_SkipAnimationOnHdrRemoval() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        final float sdrBrightness = 0.1f;
+        final float hdrBrightness = 0.3f;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
+
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
+        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(hdrBrightness);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
+
+        clearInvocations(mHolder.animator);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+    }
+
+    @Test
     public void testDoesNotSetScreenStateForNonDefaultDisplayUntilBootCompleted() {
         // We should still set screen state for the default display
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -761,6 +830,8 @@
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
 
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
+
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_OFF;
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -798,6 +869,7 @@
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
 
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_DOZE;
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -842,7 +914,6 @@
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ false);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1048,7 +1119,6 @@
         mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay,
                 true);
-
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
 
@@ -1199,76 +1269,98 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
+    public void testRampRateForHdrContent_HdrClamperOff() {
+        float hdrBrightness = 0.8f;
+        float clampedBrightness = 0.6f;
+        float transitionRate = 1.5f;
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
         when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
                 BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
         when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-        clearInvocations(mHolder.animator);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
+        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
 
-        mHolder.dpc.updateBrightness();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+        verify(mHolder.animator, atLeastOnce()).animateTo(eq(hdrBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrRemoval() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
-
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
+    public void testRampRateForHdrContent_HdrClamperOn() {
+        float clampedBrightness = 0.6f;
+        float transitionRate = 1.5f;
+        when(mDisplayManagerFlagsMock.isHdrClamperEnabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
+        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(PowerManager.BRIGHTNESS_MAX);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
+        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
+
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        verify(mHolder.animator, atLeastOnce()).animateTo(eq(clampedBrightness), anyFloat(),
+                eq(transitionRate), eq(true));
+    }
 
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(hdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
+    @Test
+    public void testRampRateForClampersControllerApplied() {
+        float transitionRate = 1.5f;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
+        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
+                invocation -> DisplayBrightnessState.builder()
+                        .setIsSlowChange(invocation.getArgument(2))
+                        .setBrightness(invocation.getArgument(1))
+                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .setCustomAnimationRate(transitionRate).build());
 
-        clearInvocations(mHolder.animator);
-
-        mHolder.dpc.updateBrightness();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
+                eq(transitionRate), anyBoolean());
+    }
+
+    @Test
+    public void testRampRateForClampersControllerNotApplied_ifDoze() {
+        float transitionRate = 1.5f;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        dpr.dozeScreenState = Display.STATE_UNKNOWN;
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
+        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
+                invocation -> DisplayBrightnessState.builder()
+                        .setIsSlowChange(invocation.getArgument(2))
+                        .setBrightness(invocation.getArgument(1))
+                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .setCustomAnimationRate(transitionRate).build());
+
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), anyBoolean());
+        verify(mHolder.animator, never()).animateTo(anyFloat(), anyFloat(),
+                eq(transitionRate), anyBoolean());
     }
 
     @Test
@@ -1285,14 +1377,14 @@
 
         setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
                 mHolder.config, /* isEnabled= */ true);
-
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
-        // switch to idle
+        // switch to idle mode
         mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
+        // A second time, when switching to idle mode.
         verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
     }
@@ -1301,6 +1393,8 @@
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
     public void testRampMaxTimeInteractiveThenIdle_DifferentValues() {
         when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
+
         // Send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
@@ -1312,14 +1406,14 @@
 
         setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
                 mHolder.config, /* isEnabled= */ true);
-
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
-        // switch to idle
+        // switch to idle mode
         mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
+        // A second time, when switching to idle mode.
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
     }
@@ -1332,11 +1426,9 @@
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
         mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
         // Run updatePowerState
         advanceTime(1);
-
-        // once on setup
+        // Once on setup
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
@@ -1346,7 +1438,7 @@
         // switch to idle mode
         mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
-        // second time when switching to idle screen brightness mode
+        // A second time when switching to idle mode.
         verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
     }
@@ -1355,6 +1447,7 @@
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
     public void testRampMaxTimeIdle_DifferentValues() {
         when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
 
         // Send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1374,6 +1467,7 @@
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
     }
+
     @Test
     public void testDozeScreenStateOverride_toSupportedOffloadStateFromDoze_DisplayStateChanges() {
         // set up.
@@ -1451,6 +1545,89 @@
         verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
     }
 
+    @Test
+    public void testBrightnessFromOffload() {
+        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        float brightness = 0.34f;
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        mHolder.dpc.setBrightnessFromOffload(brightness);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by handleBrightnessModeChange, another triggered by
+        // setBrightnessFromOffload
+        verify(mHolder.animator, times(2)).animateTo(eq(brightness), anyFloat(),
+                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}.
+     */
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+
     private void advanceTime(long timeMs) {
         mClock.fastForward(timeMs);
         mTestLooper.dispatchAll();
@@ -1505,10 +1682,10 @@
                 .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE);
         when(displayDeviceConfigMock.getBrightnessRampSlowIncrease())
                 .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowDecreaseIdle())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE);
         when(displayDeviceConfigMock.getBrightnessRampSlowIncreaseIdle())
                 .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE);
+        when(displayDeviceConfigMock.getBrightnessRampSlowDecreaseIdle())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE);
 
         when(displayDeviceConfigMock.getBrightnessRampIncreaseMaxMillis())
                 .thenReturn(BRIGHTNESS_RAMP_INCREASE_MAX);
@@ -1531,18 +1708,28 @@
         final DualRampAnimator<DisplayPowerState> animator = mock(DualRampAnimator.class);
         final AutomaticBrightnessController automaticBrightnessController =
                 mock(AutomaticBrightnessController.class);
+        final WakelockController wakelockController = mock(WakelockController.class);
         final BrightnessMappingStrategy brightnessMappingStrategy =
                 mock(BrightnessMappingStrategy.class);
         final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
         final ScreenOffBrightnessSensorController screenOffBrightnessSensorController =
                 mock(ScreenOffBrightnessSensorController.class);
         final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class);
+        final HdrClamper hdrClamper = mock(HdrClamper.class);
+        BrightnessClamperController clamperController = mock(BrightnessClamperController.class);
 
         when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX);
+        when(clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
+                invocation -> DisplayBrightnessState.builder()
+                        .setIsSlowChange(invocation.getArgument(2))
+                        .setBrightness(invocation.getArgument(1))
+                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .setCustomAnimationRate(-1).build());
 
-        DisplayPowerController.Injector injector = spy(new TestInjector(displayPowerState, animator,
-                automaticBrightnessController, brightnessMappingStrategy, hysteresisLevels,
-                screenOffBrightnessSensorController, hbmController));
+        TestInjector injector = spy(new TestInjector(displayPowerState, animator,
+                automaticBrightnessController, wakelockController, brightnessMappingStrategy,
+                hysteresisLevels, screenOffBrightnessSensorController, hbmController, hdrClamper,
+                clamperController, mDisplayManagerFlagsMock));
 
         final LogicalDisplay display = mock(LogicalDisplay.class);
         final DisplayDevice device = mock(DisplayDevice.class);
@@ -1555,12 +1742,14 @@
         final DisplayPowerController dpc = new DisplayPowerController(
                 mContext, injector, mDisplayPowerCallbacksMock, mHandler,
                 mSensorManagerMock, mDisplayBlankerMock, display,
-                mBrightnessTrackerMock, brightnessSetting, () -> {},
+                mBrightnessTrackerMock, brightnessSetting, () -> {
+        },
                 hbmMetadata, /* bootCompleted= */ false, mDisplayManagerFlagsMock);
 
         return new DisplayPowerControllerHolder(dpc, display, displayPowerState, brightnessSetting,
-                animator, automaticBrightnessController, screenOffBrightnessSensorController,
-                hbmController, hbmMetadata, brightnessMappingStrategy, injector, config);
+                animator, automaticBrightnessController, wakelockController,
+                screenOffBrightnessSensorController, hbmController, hdrClamper, clamperController,
+                hbmMetadata, brightnessMappingStrategy, injector, config);
     }
 
     /**
@@ -1574,8 +1763,12 @@
         public final BrightnessSetting brightnessSetting;
         public final DualRampAnimator<DisplayPowerState> animator;
         public final AutomaticBrightnessController automaticBrightnessController;
+        public final WakelockController wakelockController;
         public final ScreenOffBrightnessSensorController screenOffBrightnessSensorController;
         public final HighBrightnessModeController hbmController;
+
+        public final HdrClamper hdrClamper;
+        public final BrightnessClamperController clamperController;
         public final HighBrightnessModeMetadata hbmMetadata;
         public final BrightnessMappingStrategy brightnessMappingStrategy;
         public final DisplayPowerController.Injector injector;
@@ -1585,8 +1778,11 @@
                 DisplayPowerState displayPowerState, BrightnessSetting brightnessSetting,
                 DualRampAnimator<DisplayPowerState> animator,
                 AutomaticBrightnessController automaticBrightnessController,
+                WakelockController wakelockController,
                 ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
                 HighBrightnessModeController hbmController,
+                HdrClamper hdrClamper,
+                BrightnessClamperController clamperController,
                 HighBrightnessModeMetadata hbmMetadata,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 DisplayPowerController.Injector injector,
@@ -1597,8 +1793,11 @@
             this.brightnessSetting = brightnessSetting;
             this.animator = animator;
             this.automaticBrightnessController = automaticBrightnessController;
+            this.wakelockController = wakelockController;
             this.screenOffBrightnessSensorController = screenOffBrightnessSensorController;
             this.hbmController = hbmController;
+            this.hdrClamper = hdrClamper;
+            this.clamperController = clamperController;
             this.hbmMetadata = hbmMetadata;
             this.brightnessMappingStrategy = brightnessMappingStrategy;
             this.injector = injector;
@@ -1610,24 +1809,39 @@
         private final DisplayPowerState mDisplayPowerState;
         private final DualRampAnimator<DisplayPowerState> mAnimator;
         private final AutomaticBrightnessController mAutomaticBrightnessController;
+        private final WakelockController mWakelockController;
         private final BrightnessMappingStrategy mBrightnessMappingStrategy;
         private final HysteresisLevels mHysteresisLevels;
         private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
         private final HighBrightnessModeController mHighBrightnessModeController;
 
+        private final HdrClamper mHdrClamper;
+
+        private final BrightnessClamperController mClamperController;
+
+        private final DisplayManagerFlags mFlags;
+
         TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
                 AutomaticBrightnessController automaticBrightnessController,
+                WakelockController wakelockController,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 HysteresisLevels hysteresisLevels,
                 ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
-                HighBrightnessModeController highBrightnessModeController) {
+                HighBrightnessModeController highBrightnessModeController,
+                HdrClamper hdrClamper,
+                BrightnessClamperController clamperController,
+                DisplayManagerFlags flags) {
             mDisplayPowerState = dps;
             mAnimator = animator;
             mAutomaticBrightnessController = automaticBrightnessController;
+            mWakelockController = wakelockController;
             mBrightnessMappingStrategy = brightnessMappingStrategy;
             mHysteresisLevels = hysteresisLevels;
             mScreenOffBrightnessSensorController = screenOffBrightnessSensorController;
             mHighBrightnessModeController = highBrightnessModeController;
+            mHdrClamper = hdrClamper;
+            mClamperController = clamperController;
+            mFlags = flags;
         }
 
         @Override
@@ -1649,6 +1863,28 @@
         }
 
         @Override
+        WakelockController getWakelockController(int displayId,
+                DisplayPowerCallbacks displayPowerCallbacks) {
+            return mWakelockController;
+        }
+
+        @Override
+        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
+                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
+                Looper looper, Runnable nudgeUpdatePowerState, int displayId,
+                SensorManager sensorManager) {
+            return new DisplayPowerProximityStateController(wakelockController,
+                    displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
+                    sensorManager,
+                    new DisplayPowerProximityStateController.Injector() {
+                        @Override
+                        DisplayPowerProximityStateController.Clock createClock() {
+                            return mClock::now;
+                        }
+                    });
+        }
+
+        @Override
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
@@ -1710,6 +1946,23 @@
         }
 
         @Override
+        BrightnessRangeController getBrightnessRangeController(
+                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
+                DisplayDeviceConfig displayDeviceConfig, Handler handler,
+                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
+            return new BrightnessRangeController(hbmController, modeChangeCallback,
+                    displayDeviceConfig, mHdrClamper, mFlags, displayToken, info);
+        }
+
+        @Override
+        BrightnessClamperController getBrightnessClamperController(Handler handler,
+                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+                BrightnessClamperController.DisplayDeviceData data, Context context,
+                DisplayManagerFlags flags) {
+            return mClamperController;
+        }
+
+        @Override
         DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
                 SensorManager sensorManager, Resources resources) {
             return mDisplayWhiteBalanceControllerMock;
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 dafbbb3..33d3020 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
@@ -16,16 +16,24 @@
 
 package com.android.server.display
 
+import android.content.Context
+import android.os.Looper
 import android.view.Display
 import androidx.test.filters.SmallTest
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.junit.MockitoJUnit
 
 import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 import java.util.concurrent.Executor
 
 @SmallTest
@@ -39,11 +47,16 @@
     private val mockBlanker = mock<DisplayBlanker>()
     private val mockColorFade = mock<ColorFade>()
     private val mockExecutor = mock<Executor>()
+    private val mockContext = mock<Context>()
 
     @Before
     fun setUp() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare()
+        }
         displayPowerState = DisplayPowerState(mockBlanker, mockColorFade, 123, Display.STATE_ON,
                 mockExecutor)
+        whenever(mockColorFade.prepare(eq(mockContext), anyInt())).thenReturn(true)
     }
 
     @Test
@@ -56,4 +69,31 @@
 
         verify(mockColorFade).destroy()
     }
+
+    @Test
+    fun `GIVEN not prepared WHEN draw runnable is called THEN colorFade not drawn`() {
+        displayPowerState.mColorFadeDrawRunnable.run()
+
+        verify(mockColorFade, never()).draw(anyFloat())
+    }
+    @Test
+    fun `GIVEN prepared WHEN draw runnable is called THEN colorFade is drawn`() {
+        displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE)
+        clearInvocations(mockColorFade)
+
+        displayPowerState.mColorFadeDrawRunnable.run()
+
+        verify(mockColorFade).draw(anyFloat())
+    }
+
+    @Test
+    fun `GIVEN prepared AND stopped WHEN draw runnable is called THEN colorFade is not drawn`() {
+        displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE)
+        clearInvocations(mockColorFade)
+        displayPowerState.stop()
+
+        displayPowerState.mColorFadeDrawRunnable.run()
+
+        verify(mockColorFade, never()).draw(anyFloat())
+    }
 }
\ No newline at end of file
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 00f9892..c92ce25 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -176,6 +176,10 @@
         when(mockArray.length()).thenReturn(0);
         when(mMockedResources.obtainTypedArray(R.array.config_maskBuiltInDisplayCutoutArray))
                 .thenReturn(mockArray);
+        when(mMockedResources.obtainTypedArray(R.array.config_displayCutoutSideOverrideArray))
+                .thenReturn(mockArray);
+        when(mMockedResources.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride))
+                .thenReturn(new String[]{});
         when(mMockedResources.obtainTypedArray(R.array.config_waterfallCutoutArray))
                 .thenReturn(mockArray);
         when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerRadiusArray))
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 28ec896..bed6f92 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -49,8 +49,9 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -83,7 +84,6 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
 
 import java.io.File;
 import java.io.InputStream;
@@ -111,14 +111,14 @@
     private final DisplayIdProducer mIdProducer = (isDefault) ->
             isDefault ? DEFAULT_DISPLAY : sNextNonDefaultDisplayId++;
 
+    private DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy;
+
     @Mock LogicalDisplayMapper.Listener mListenerMock;
     @Mock Context mContextMock;
     @Mock FoldSettingProvider mFoldSettingProviderMock;
     @Mock Resources mResourcesMock;
     @Mock IPowerManager mIPowerManagerMock;
     @Mock IThermalService mIThermalServiceMock;
-    @Spy DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy =
-            new DeviceStateToLayoutMap(mIdProducer, NON_EXISTING_FILE);
     @Mock DisplayManagerFlags mFlagsMock;
     @Mock DisplayAdapter mDisplayAdapterMock;
 
@@ -131,6 +131,8 @@
         System.setProperty("dexmaker.share_classloader", "true");
         MockitoAnnotations.initMocks(this);
 
+        mDeviceStateToLayoutMapSpy =
+                spy(new DeviceStateToLayoutMap(mIdProducer, mFlagsMock, NON_EXISTING_FILE));
         mDisplayDeviceRepo = new DisplayDeviceRepository(
                 new DisplayManagerService.SyncRoot(),
                 new PersistentDataStore(new PersistentDataStore.Injector() {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
index e58b3e8..990c383 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
@@ -58,9 +58,14 @@
 
     @Test
     public void setModifierDoesntSetIfModifierIsBeyondExtremes() {
-        int extremeModifier = 0x16;
+        int extremeModifier = 0x40; // equal to BrightnessReason.MODIFIER_MASK * 2
+
+        // reset modifier
+        mBrightnessReason.setModifier(0);
+
+        // test extreme
         mBrightnessReason.setModifier(extremeModifier);
-        assertEquals(mBrightnessReason.getModifier(), BrightnessReason.MODIFIER_LOW_POWER);
+        assertEquals(0, mBrightnessReason.getModifier());
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
index 6ba7368..5294943 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
@@ -29,8 +29,10 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.provider.DeviceConfig;
+import android.testing.TestableContext;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.display.DisplayBrightnessState;
 import com.android.server.display.brightness.BrightnessReason;
@@ -39,6 +41,7 @@
 import com.android.server.testutils.TestHandler;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -52,14 +55,15 @@
 
     private final TestHandler mTestHandler = new TestHandler(null);
 
+    @Rule
+    public final TestableContext mMockContext = new TestableContext(
+            InstrumentationRegistry.getInstrumentation().getContext());
     @Mock
     private BrightnessClamperController.ClamperChangeListener mMockExternalListener;
 
     @Mock
     private BrightnessClamperController.DisplayDeviceData mMockDisplayDeviceData;
     @Mock
-    private Context mMockContext;
-    @Mock
     private DeviceConfigParameterProvider mMockDeviceConfigParameterProvider;
     @Mock
     private BrightnessClamper<BrightnessClamperController.DisplayDeviceData> mMockClamper;
@@ -231,6 +235,13 @@
         assertEquals(initialSlowChange, state.isSlowChange());
     }
 
+    @Test
+    public void testStop() {
+        mClamperController.stop();
+        verify(mMockModifier).stop();
+        verify(mMockClamper).stop();
+    }
+
     private BrightnessClamperController createBrightnessClamperController() {
         return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener,
                 mMockDisplayDeviceData, mMockContext, mFlags);
@@ -240,14 +251,14 @@
 
         private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>>
                 mClampers;
-        private final List<BrightnessModifier> mModifiers;
+        private final List<BrightnessStateModifier> mModifiers;
 
         private BrightnessClamperController.ClamperChangeListener mCapturedChangeListener;
 
         private TestInjector(
                 List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>>
                         clampers,
-                List<BrightnessModifier> modifiers) {
+                List<BrightnessStateModifier> modifiers) {
             mClampers = clampers;
             mModifiers = modifiers;
         }
@@ -268,7 +279,8 @@
         }
 
         @Override
-        List<BrightnessModifier> getModifiers(Context context) {
+        List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
+                Handler handler, BrightnessClamperController.ClamperChangeListener listener) {
             return mModifiers;
         }
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
new file mode 100644
index 0000000..ac7d1f5
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.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.server.display.brightness.clamper
+
+import android.os.PowerManager
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.TestableContext
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.display.brightness.BrightnessReason
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+
+private const val userId = UserHandle.USER_CURRENT
+
+class BrightnessLowLuxModifierTest {
+
+    private var mockClamperChangeListener =
+            mock<BrightnessClamperController.ClamperChangeListener>()
+
+    val context = TestableContext(
+            InstrumentationRegistry.getInstrumentation().getContext())
+
+    private val testHandler = TestHandler(null)
+    private lateinit var modifier: BrightnessLowLuxModifier
+
+    @Before
+    fun setUp() {
+        modifier = BrightnessLowLuxModifier(testHandler, mockClamperChangeListener, context)
+        testHandler.flush()
+    }
+
+    @Test
+    fun testThrottlingBounds() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) // true
+        Settings.Secure.putFloatForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+        modifier.recalculateLowerBound()
+        testHandler.flush()
+        assertThat(modifier.isActive).isTrue()
+
+        // TODO: code currently returns MIN/MAX; update with lux values
+        assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+    }
+
+    @Test
+    fun testGetReason_UserSet() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
+        Settings.Secure.putFloatForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+        modifier.recalculateLowerBound()
+        testHandler.flush()
+        assertThat(modifier.isActive).isTrue()
+
+        // Test restriction from user setting
+        assertThat(modifier.brightnessReason)
+                .isEqualTo(BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND)
+    }
+
+    @Test
+    fun testGetReason_Lux() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
+        Settings.Secure.putFloatForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId)
+        modifier.recalculateLowerBound()
+        testHandler.flush()
+        assertThat(modifier.isActive).isTrue()
+
+        // Test restriction from lux setting
+        assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java
new file mode 100644
index 0000000..b363fd4
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.projection.MediaProjectionInfo;
+import android.media.projection.MediaProjectionManager;
+import android.service.notification.NotificationListenerService.Ranking;
+import android.service.notification.NotificationListenerService.RankingMap;
+import android.service.notification.StatusBarNotification;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.testing.TestableLooper.RunWithLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.SensitiveContentPackages.PackageInfo;
+import com.android.server.wm.WindowManagerInternal;
+
+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;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.Set;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class SensitiveContentProtectionManagerServiceTest {
+    private static final String NOTIFICATION_KEY_1 = "com.android.server.notification.TEST_KEY_1";
+    private static final String NOTIFICATION_KEY_2 = "com.android.server.notification.TEST_KEY_2";
+
+    private static final String NOTIFICATION_PKG_1 = "com.android.server.notification.one";
+    private static final String NOTIFICATION_PKG_2 = "com.android.server.notification.two";
+
+    private static final int NOTIFICATION_UID_1 = 5;
+    private static final int NOTIFICATION_UID_2 = 6;
+
+    @Rule
+    public final TestableContext mContext =
+            new TestableContext(getInstrumentation().getTargetContext(), null);
+
+    private SensitiveContentProtectionManagerService mSensitiveContentProtectionManagerService;
+
+    @Captor
+    ArgumentCaptor<MediaProjectionManager.Callback> mMediaProjectionCallbackCaptor;
+
+    @Mock
+    private MediaProjectionManager mProjectionManager;
+
+    @Mock
+    private WindowManagerInternal mWindowManager;
+
+    @Mock
+    private StatusBarNotification mNotification1;
+
+    @Mock
+    private StatusBarNotification mNotification2;
+
+    @Mock
+    private RankingMap mRankingMap;
+
+    @Mock
+    private Ranking mSensitiveRanking;
+
+    @Mock
+    private Ranking mNonSensitiveRanking;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mSensitiveContentProtectionManagerService =
+                new SensitiveContentProtectionManagerService(mContext);
+
+        mSensitiveContentProtectionManagerService.mNotificationListener =
+                spy(mSensitiveContentProtectionManagerService.mNotificationListener);
+
+        // Setup RankingMap and two possilbe rankings
+        when(mSensitiveRanking.hasSensitiveContent()).thenReturn(true);
+        when(mNonSensitiveRanking.hasSensitiveContent()).thenReturn(false);
+        doReturn(mRankingMap)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getCurrentRanking();
+
+        setupSensitiveNotification();
+
+        mSensitiveContentProtectionManagerService.init(mProjectionManager, mWindowManager);
+
+        // Obtain useful mMediaProjectionCallback
+        verify(mProjectionManager).addCallback(mMediaProjectionCallbackCaptor.capture(), any());
+    }
+
+    @After
+    public void tearDown() {
+        mSensitiveContentProtectionManagerService.onDestroy();
+    }
+
+    private Set<PackageInfo> setupSensitiveNotification() {
+        // Setup Notification Values
+        when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1);
+        when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1);
+        when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2);
+        when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_2);
+        when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        StatusBarNotification[] mNotifications =
+                new StatusBarNotification[] {mNotification1, mNotification2};
+        doReturn(mNotifications)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getActiveNotifications();
+
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1)))
+                .thenReturn(mSensitiveRanking);
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2)))
+                .thenReturn(mNonSensitiveRanking);
+
+        return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1));
+    }
+
+    private Set<PackageInfo> setupMultipleSensitiveNotificationsFromSamePackageAndUid() {
+        // Setup Notification Values
+        when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1);
+        when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1);
+        when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2);
+        when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_1);
+        when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        StatusBarNotification[] mNotifications =
+                new StatusBarNotification[] {mNotification1, mNotification2};
+        doReturn(mNotifications)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getActiveNotifications();
+
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1)))
+                .thenReturn(mSensitiveRanking);
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2)))
+                .thenReturn(mSensitiveRanking);
+
+        return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1));
+    }
+
+    private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentPackage() {
+        // Setup Notification Values
+        when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1);
+        when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1);
+        when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2);
+        when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_2);
+        when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        StatusBarNotification[] mNotifications =
+                new StatusBarNotification[] {mNotification1, mNotification2};
+        doReturn(mNotifications)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getActiveNotifications();
+
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1)))
+                .thenReturn(mSensitiveRanking);
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2)))
+                .thenReturn(mSensitiveRanking);
+
+        return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1),
+                new PackageInfo(NOTIFICATION_PKG_2, NOTIFICATION_UID_1));
+    }
+
+    private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentUid() {
+        // Setup Notification Values
+        when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1);
+        when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1);
+        when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2);
+        when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_1);
+        when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_2);
+
+        StatusBarNotification[] mNotifications =
+                new StatusBarNotification[] {mNotification1, mNotification2};
+        doReturn(mNotifications)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getActiveNotifications();
+
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1)))
+                .thenReturn(mSensitiveRanking);
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2)))
+                .thenReturn(mSensitiveRanking);
+
+        return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1),
+                new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_2));
+    }
+
+    private void setupNoSensitiveNotifications() {
+        // Setup Notification Values
+        when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1);
+        when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1);
+        when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1);
+
+        StatusBarNotification[] mNotifications = new StatusBarNotification[] {mNotification1};
+        doReturn(mNotifications)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getActiveNotifications();
+
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1)))
+                .thenReturn(mNonSensitiveRanking);
+    }
+
+    private void setupNoNotifications() {
+        // Setup Notification Values
+        StatusBarNotification[] mNotifications = new StatusBarNotification[] {};
+        doReturn(mNotifications)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getActiveNotifications();
+    }
+
+    @Test
+    public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() {
+        Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages);
+    }
+
+    @Test
+    public void mediaProjectionOnStart_noSensitiveNotifications_noBlockedPackages() {
+        setupNoSensitiveNotifications();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    @Test
+    public void mediaProjectionOnStart_noNotifications_noBlockedPackages() {
+        setupNoNotifications();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    @Test
+    public void mediaProjectionOnStart_multipleNotifications_setWmBlockedPackages() {
+        Set<PackageInfo> expectedBlockedPackages =
+                setupMultipleSensitiveNotificationsFromSamePackageAndUid();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages);
+    }
+
+    @Test
+    public void mediaProjectionOnStart_multiplePackages_setWmBlockedPackages() {
+        Set<PackageInfo> expectedBlockedPackages =
+                setupMultipleSensitiveNotificationsFromDifferentPackage();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages);
+    }
+
+    @Test
+    public void mediaProjectionOnStart_multipleUid_setWmBlockedPackages() {
+        Set<PackageInfo> expectedBlockedPackages =
+                setupMultipleSensitiveNotificationsFromDifferentUid();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages);
+    }
+
+    @Test
+    public void mediaProjectionOnStop_onProjectionEnd_clearWmBlockedPackages() {
+        setupSensitiveNotification();
+
+        MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class);
+        mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
+        Mockito.reset(mWindowManager);
+
+        mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo);
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    @Test
+    public void mediaProjectionOnStart_afterOnStop_onProjectionStart_setWmBlockedPackages() {
+        Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
+
+        MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class);
+        mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
+        mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo);
+        Mockito.reset(mWindowManager);
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages);
+    }
+
+    @Test
+    public void mediaProjectionOnStart_getActiveNotificationsThrows_noBlockedPackages() {
+        doThrow(SecurityException.class)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getActiveNotifications();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    @Test
+    public void mediaProjectionOnStart_getCurrentRankingThrows_noBlockedPackages() {
+        doThrow(SecurityException.class)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getCurrentRanking();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    @Test
+    public void mediaProjectionOnStart_getCurrentRanking_nullRankingMap_noBlockedPackages() {
+        doReturn(null)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getCurrentRanking();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    @Test
+    public void mediaProjectionOnStart_getCurrentRanking_missingRanking_noBlockedPackages() {
+        when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null);
+
+        doReturn(mRankingMap)
+                .when(mSensitiveContentProtectionManagerService.mNotificationListener)
+                .getCurrentRanking();
+
+        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+        verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+}
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 f386c3b..d876dae 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -80,10 +80,13 @@
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
+import android.app.ApplicationExitInfo;
 import android.app.IApplicationThread;
 import android.app.IServiceConnection;
 import android.content.ComponentName;
@@ -94,9 +97,11 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.PowerManagerInternal;
+import android.os.Process;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArrayMap;
 import android.util.SparseArray;
 
@@ -107,7 +112,9 @@
 
 import org.junit.After;
 import org.junit.AfterClass;
+import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.io.File;
@@ -148,12 +155,18 @@
     private static final String MOCKAPP5_PROCESSNAME = "test #5";
     private static final String MOCKAPP5_PACKAGENAME = "com.android.test.test5";
     private static final int MOCKAPP2_UID_OTHER = MOCKAPP2_UID + UserHandle.PER_USER_RANGE;
+    private static final int MOCKAPP_ISOLATED_UID = Process.FIRST_ISOLATED_UID + 321;
+    private static final String MOCKAPP_ISOLATED_PROCESSNAME = "isolated test #1";
+
     private static int sFirstCachedAdj = ProcessList.CACHED_APP_MIN_ADJ
                                           + ProcessList.CACHED_APP_IMPORTANCE_LEVELS;
     private static Context sContext;
     private static PackageManagerInternal sPackageManagerInternal;
     private static ActivityManagerService sService;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @SuppressWarnings("GuardedBy")
     @BeforeClass
     public static void setUpOnce() {
@@ -220,6 +233,11 @@
         }
     }
 
+    @Before
+    public void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_NEW_FGS_RESTRICTION_LOGIC);
+    }
+
     @AfterClass
     public static void tearDownOnce() {
         LocalServices.removeServiceForTest(PackageManagerInternal.class);
@@ -286,6 +304,20 @@
     }
 
     /**
+     * Run updateOomAdjPendingTargetsLocked().
+     * - enqueues all provided processes to the pending list and lru before running
+     */
+    @SuppressWarnings("GuardedBy")
+    private void updateOomAdjPending(ProcessRecord... apps) {
+        setProcessesToLru(apps);
+        for (ProcessRecord app : apps) {
+            sService.mOomAdjuster.enqueueOomAdjTargetLocked(app);
+        }
+        sService.mOomAdjuster.updateOomAdjPendingTargetsLocked(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.
@@ -519,6 +551,7 @@
         sService.mConstants.mShortFgsProcStateExtraWaitDuration = 200_000;
 
         ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+        s.appInfo = new ApplicationInfo();
         s.startRequested = true;
         s.isForeground = true;
         s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
@@ -561,6 +594,7 @@
 
         // SHORT_SERVICE, timed out already.
         s = ServiceRecord.newEmptyInstanceForTest(sService);
+        s.appInfo = new ApplicationInfo();
         s.startRequested = true;
         s.isForeground = true;
         s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
@@ -1079,6 +1113,7 @@
 
         // In order to trick OomAdjuster to think it has a short-service, we need this logic.
         ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+        s.appInfo = new ApplicationInfo();
         s.startRequested = true;
         s.isForeground = true;
         s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
@@ -1109,6 +1144,7 @@
 
         // In order to trick OomAdjuster to think it has a short-service, we need this logic.
         ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+        s.appInfo = new ApplicationInfo();
         s.startRequested = true;
         s.isForeground = true;
         s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
@@ -1400,6 +1436,7 @@
 
         // In order to trick OomAdjuster to think it has a short-service, we need this logic.
         ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+        s.appInfo = new ApplicationInfo();
         s.startRequested = true;
         s.isForeground = true;
         s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
@@ -2651,6 +2688,90 @@
         assertNotEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
     }
 
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testUpdateOomAdj_DoAll_Isolated_stopService() {
+        ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID,
+                MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+
+        setProcessesToLru(app);
+        ServiceRecord s = makeServiceRecord(app);
+        s.startRequested = true;
+        s.lastActivity = SystemClock.uptimeMillis();
+        sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+        updateOomAdj();
+        assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND);
+
+        app.mServices.stopService(s);
+        updateOomAdj();
+        // isolated process should be killed immediately after service stop.
+        verify(app).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER,
+                ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testUpdateOomAdj_DoPending_Isolated_stopService() {
+        ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID,
+                MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+
+        ServiceRecord s = makeServiceRecord(app);
+        s.startRequested = true;
+        s.lastActivity = SystemClock.uptimeMillis();
+        sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+        updateOomAdjPending(app);
+        assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND);
+
+        app.mServices.stopService(s);
+        updateOomAdjPending(app);
+        // isolated process should be killed immediately after service stop.
+        verify(app).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER,
+                ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testUpdateOomAdj_DoAll_Isolated_stopServiceWithEntryPoint() {
+        ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID,
+                MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+        app.setIsolatedEntryPoint("test");
+
+        setProcessesToLru(app);
+        ServiceRecord s = makeServiceRecord(app);
+        s.startRequested = true;
+        s.lastActivity = SystemClock.uptimeMillis();
+        sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+        updateOomAdj();
+        assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND);
+
+        app.mServices.stopService(s);
+        updateOomAdj();
+        // isolated process with entry point should not be killed
+        verify(app, never()).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER,
+                ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testUpdateOomAdj_DoPending_Isolated_stopServiceWithEntryPoint() {
+        ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID,
+                MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+        app.setIsolatedEntryPoint("test");
+
+        ServiceRecord s = makeServiceRecord(app);
+        s.startRequested = true;
+        s.lastActivity = SystemClock.uptimeMillis();
+        sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+        updateOomAdjPending(app);
+        assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND);
+
+        app.mServices.stopService(s);
+        updateOomAdjPending(app);
+        // isolated process with entry point should not be killed
+        verify(app, never()).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER,
+                ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true);
+    }
+
     private ProcessRecord makeDefaultProcessRecord(int pid, int uid, String processName,
             String packageName, boolean hasShownUi) {
         long now = SystemClock.uptimeMillis();
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 fc2e5b0..0703db2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -118,7 +118,8 @@
 @Presubmit
 public class GameManagerServiceTests {
     @Mock MockContext mMockContext;
-    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+                SetFlagsRule.DefaultInitValueType.NULL_DEFAULT);
     private static final String TAG = "GameManagerServiceTests";
     private static final String PACKAGE_NAME_INVALID = "com.android.app";
     private static final int USER_ID_1 = 1001;
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index e5ecdc4..0403c64 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -210,6 +210,9 @@
                 anyInt())).thenReturn(1);
         when(mInstallerService.getExistingDraftSessionId(anyInt(), any(), anyInt())).thenReturn(
                 PackageInstaller.SessionInfo.INVALID_ID);
+        PackageInstallerSession session = mock(PackageInstallerSession.class);
+        when(mInstallerService.getSession(anyInt())).thenReturn(session);
+        when(session.getUnarchivalStatus()).thenReturn(PackageInstaller.UNARCHIVAL_STATUS_UNSET);
         doReturn(new ParceledListSlice<>(List.of(mock(ResolveInfo.class))))
                 .when(mPackageManagerService).queryIntentReceivers(any(), any(), any(), anyLong(),
                         eq(mUserId));
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
index 60cedcf..24e7242 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
@@ -108,6 +108,20 @@
     }
 
     @Test
+    public void testPackageMonitorCallback_SuspendNoAccessCallbackNotCalled() throws Exception {
+        BiFunction<Integer, Bundle, Bundle> filterExtras = (callingUid, intentExtras) -> null;
+
+        IRemoteCallback callback = createMockPackageMonitorCallback();
+        mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */,
+                Binder.getCallingUid());
+        mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGES_SUSPENDED,
+                FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0}, null /* instantUserIds */,
+                null /* broadcastAllowList */, mHandler, filterExtras);
+
+        verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
+    }
+
+    @Test
     public void testPackageMonitorCallback_SuspendCallbackCalled() throws Exception {
         Bundle result = new Bundle();
         result.putInt(Intent.EXTRA_UID, FAKE_PACKAGE_UID);
diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
index 9851bc1..37ca09d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
@@ -16,24 +16,24 @@
 
 package com.android.server.trust;
 
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.argThat;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.anyBoolean;
-
 import android.Manifest;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.app.admin.DevicePolicyManager;
 import android.app.trust.ITrustListener;
 import android.app.trust.ITrustManager;
 import android.content.BroadcastReceiver;
@@ -45,14 +45,22 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
-import android.net.Uri;
+import android.content.pm.UserInfo;
+import android.hardware.biometrics.BiometricManager;
+import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
-import android.os.test.TestLooper;
+import android.os.UserManager;
+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.security.Authorization;
+import android.security.authorization.IKeystoreAuthorization;
 import android.service.trust.TrustAgentService;
 import android.testing.TestableContext;
 import android.view.IWindowManager;
@@ -61,12 +69,11 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.internal.widget.LockPatternUtils;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
 
-import com.google.android.collect.Lists;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -74,37 +81,74 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Random;
+import java.util.Collection;
+import java.util.List;
 
 public class TrustManagerServiceTest {
 
     @Rule
-    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+            .spyStatic(ActivityManager.class)
+            .spyStatic(Authorization.class)
+            .mockStatic(ServiceManager.class)
+            .mockStatic(WindowManagerGlobal.class)
+            .build();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Rule
     public final MockContext mMockContext = new MockContext(
             ApplicationProvider.getApplicationContext());
 
     private static final String URI_SCHEME_PACKAGE = "package";
-    private static final int TEST_USER_ID = UserHandle.USER_SYSTEM;
+    private static final int TEST_USER_ID = 50;
+    private static final int PARENT_USER_ID = 60;
+    private static final int PROFILE_USER_ID = 70;
+    private static final long[] PARENT_BIOMETRIC_SIDS = new long[] { 600L, 601L };
+    private static final long[] PROFILE_BIOMETRIC_SIDS = new long[] { 700L, 701L };
 
-    private final TestLooper mLooper = new TestLooper();
     private final ArrayList<ResolveInfo> mTrustAgentResolveInfoList = new ArrayList<>();
-    private final LockPatternUtils mLockPatternUtils = new LockPatternUtils(mMockContext);
-    private final TrustManagerService mService = new TrustManagerService(mMockContext);
+    private final ArrayList<ComponentName> mKnownTrustAgents = new ArrayList<>();
+    private final ArrayList<ComponentName> mEnabledTrustAgents = new ArrayList<>();
 
-    @Mock
-    private PackageManager mPackageManagerMock;
+    private @Mock ActivityManager mActivityManager;
+    private @Mock BiometricManager mBiometricManager;
+    private @Mock DevicePolicyManager mDevicePolicyManager;
+    private @Mock IKeystoreAuthorization mKeystoreAuthorization;
+    private @Mock LockPatternUtils mLockPatternUtils;
+    private @Mock PackageManager mPackageManager;
+    private @Mock UserManager mUserManager;
+    private @Mock IWindowManager mWindowManager;
+
+    private HandlerThread mHandlerThread;
+    private TrustManagerService.Injector mInjector;
+    private TrustManagerService mService;
+    private ITrustManager mTrustManager;
 
     @Before
-    public void setUp() {
-        resetTrustAgentLockSettings();
-        LocalServices.addService(SystemServiceManager.class, mock(SystemServiceManager.class));
+    public void setUp() throws Exception {
+        when(mActivityManager.isUserRunning(TEST_USER_ID)).thenReturn(true);
+        doReturn(mock(IActivityManager.class)).when(() -> ActivityManager.getService());
+
+        doReturn(mKeystoreAuthorization).when(() -> Authorization.getService());
+
+        when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager);
+        when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true);
+        when(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).thenReturn(mKnownTrustAgents);
+        when(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).thenReturn(mEnabledTrustAgents);
+        doAnswer(invocation -> {
+            mKnownTrustAgents.clear();
+            mKnownTrustAgents.addAll((Collection<ComponentName>) invocation.getArgument(0));
+            return null;
+        }).when(mLockPatternUtils).setKnownTrustAgents(any(), eq(TEST_USER_ID));
+        doAnswer(invocation -> {
+            mEnabledTrustAgents.clear();
+            mEnabledTrustAgents.addAll((Collection<ComponentName>) invocation.getArgument(0));
+            return null;
+        }).when(mLockPatternUtils).setEnabledTrustAgents(any(), eq(TEST_USER_ID));
 
         ArgumentMatcher<Intent> trustAgentIntentMatcher = new ArgumentMatcher<Intent>() {
             @Override
@@ -112,17 +156,43 @@
                 return TrustAgentService.SERVICE_INTERFACE.equals(argument.getAction());
             }
         };
-        when(mPackageManagerMock.queryIntentServicesAsUser(argThat(trustAgentIntentMatcher),
+        when(mPackageManager.queryIntentServicesAsUser(argThat(trustAgentIntentMatcher),
                 anyInt(), anyInt())).thenReturn(mTrustAgentResolveInfoList);
-        when(mPackageManagerMock.checkPermission(any(), any())).thenReturn(
+        when(mPackageManager.checkPermission(any(), any())).thenReturn(
                 PackageManager.PERMISSION_GRANTED);
-        mMockContext.setMockPackageManager(mPackageManagerMock);
+
+        when(mUserManager.getAliveUsers()).thenReturn(
+                List.of(new UserInfo(TEST_USER_ID, "user", UserInfo.FLAG_FULL)));
+
+        when(mWindowManager.isKeyguardLocked()).thenReturn(true);
+
+        mMockContext.addMockSystemService(ActivityManager.class, mActivityManager);
+        mMockContext.addMockSystemService(BiometricManager.class, mBiometricManager);
+        mMockContext.setMockPackageManager(mPackageManager);
+        mMockContext.addMockSystemService(UserManager.class, mUserManager);
+        doReturn(mWindowManager).when(() -> WindowManagerGlobal.getWindowManagerService());
+        LocalServices.addService(SystemServiceManager.class, mock(SystemServiceManager.class));
+
+        grantPermission(Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE);
+        grantPermission(Manifest.permission.TRUST_LISTENER);
+
+        mHandlerThread = new HandlerThread("handler");
+        mHandlerThread.start();
+        mInjector = new TrustManagerService.Injector(mLockPatternUtils, mHandlerThread.getLooper());
+        mService = new TrustManagerService(mMockContext, mInjector);
+
+        // Get the ITrustManager from the new TrustManagerService.
+        mService.onStart();
+        ArgumentCaptor<IBinder> binderArgumentCaptor = ArgumentCaptor.forClass(IBinder.class);
+        verify(() -> ServiceManager.addService(eq(Context.TRUST_SERVICE),
+                    binderArgumentCaptor.capture(), anyBoolean(), anyInt()));
+        mTrustManager = ITrustManager.Stub.asInterface(binderArgumentCaptor.getValue());
     }
 
     @After
     public void tearDown() {
-        resetTrustAgentLockSettings();
         LocalServices.removeServiceForTest(SystemServiceManager.class);
+        mHandlerThread.quit();
     }
 
     @Test
@@ -142,10 +212,9 @@
 
         bootService();
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly(
-                systemTrustAgent1, systemTrustAgent2);
-        assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly(
-                systemTrustAgent1, systemTrustAgent2, userTrustAgent1, userTrustAgent2);
+        assertThat(mEnabledTrustAgents).containsExactly(systemTrustAgent1, systemTrustAgent2);
+        assertThat(mKnownTrustAgents).containsExactly(systemTrustAgent1, systemTrustAgent2,
+                    userTrustAgent1, userTrustAgent2);
     }
 
     @Test
@@ -162,10 +231,8 @@
 
         bootService();
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly(
-                defaultTrustAgent);
-        assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly(
-                systemTrustAgent, defaultTrustAgent);
+        assertThat(mEnabledTrustAgents).containsExactly(defaultTrustAgent);
+        assertThat(mKnownTrustAgents).containsExactly(systemTrustAgent, defaultTrustAgent);
     }
 
     @Test
@@ -174,16 +241,16 @@
                 "com.android/.SystemTrustAgent");
         ComponentName trustAgent2 = ComponentName.unflattenFromString(
                 "com.android/.AnotherSystemTrustAgent");
-        initializeEnabledAgents(trustAgent1);
+        mEnabledTrustAgents.add(trustAgent1);
+        Settings.Secure.putIntForUser(mMockContext.getContentResolver(),
+                Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID);
         addTrustAgent(trustAgent1, /* isSystemApp= */ true);
         addTrustAgent(trustAgent2, /* isSystemApp= */ true);
 
         bootService();
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly(
-                trustAgent1);
-        assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly(
-                trustAgent1, trustAgent2);
+        assertThat(mEnabledTrustAgents).containsExactly(trustAgent1);
+        assertThat(mKnownTrustAgents).containsExactly(trustAgent1, trustAgent2);
     }
 
     @Test
@@ -192,17 +259,17 @@
                 "com.android/.SystemTrustAgent");
         ComponentName trustAgent2 = ComponentName.unflattenFromString(
                 "com.android/.AnotherSystemTrustAgent");
-        initializeEnabledAgents(trustAgent1);
-        initializeKnownAgents(trustAgent1);
+        Settings.Secure.putIntForUser(mMockContext.getContentResolver(),
+                Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID);
+        Settings.Secure.putIntForUser(mMockContext.getContentResolver(),
+                Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID);
         addTrustAgent(trustAgent1, /* isSystemApp= */ true);
         addTrustAgent(trustAgent2, /* isSystemApp= */ true);
 
         bootService();
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly(
-                trustAgent1, trustAgent2);
-        assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly(
-                trustAgent1, trustAgent2);
+        assertThat(mEnabledTrustAgents).containsExactly(trustAgent1, trustAgent2);
+        assertThat(mKnownTrustAgents).containsExactly(trustAgent1, trustAgent2);
     }
 
     @Test
@@ -212,12 +279,10 @@
                 "com.android/.SystemTrustAgent");
         addTrustAgent(newAgentComponentName, /* isSystemApp= */ true);
 
-        mMockContext.sendPackageChangedBroadcast(newAgentComponentName);
+        notifyPackageChanged(newAgentComponentName);
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly(
-                newAgentComponentName);
-        assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly(
-                newAgentComponentName);
+        assertThat(mEnabledTrustAgents).containsExactly(newAgentComponentName);
+        assertThat(mKnownTrustAgents).containsExactly(newAgentComponentName);
     }
 
     @Test
@@ -233,12 +298,10 @@
                 "com.android/.SystemTrustAgent");
         addTrustAgent(newAgentComponentName, /* isSystemApp= */ true);
 
-        mMockContext.sendPackageChangedBroadcast(newAgentComponentName);
+        notifyPackageChanged(newAgentComponentName);
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly(
-                defaultTrustAgent);
-        assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly(
-                defaultTrustAgent, newAgentComponentName);
+        assertThat(mEnabledTrustAgents).containsExactly(defaultTrustAgent);
+        assertThat(mKnownTrustAgents).containsExactly(defaultTrustAgent, newAgentComponentName);
     }
 
     @Test
@@ -248,11 +311,10 @@
                 "com.user/.UserTrustAgent");
         addTrustAgent(newAgentComponentName, /* isSystemApp= */ false);
 
-        mMockContext.sendPackageChangedBroadcast(newAgentComponentName);
+        notifyPackageChanged(newAgentComponentName);
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).isEmpty();
-        assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly(
-                newAgentComponentName);
+        assertThat(mEnabledTrustAgents).isEmpty();
+        assertThat(mKnownTrustAgents).containsExactly(newAgentComponentName);
     }
 
     @Test
@@ -265,50 +327,88 @@
         addTrustAgent(systemTrustAgent2, /* isSystemApp= */ true);
         bootService();
         // Simulate user turning off systemTrustAgent2
-        mLockPatternUtils.setEnabledTrustAgents(Collections.singletonList(systemTrustAgent1),
-                TEST_USER_ID);
+        mLockPatternUtils.setEnabledTrustAgents(List.of(systemTrustAgent1), TEST_USER_ID);
 
-        mMockContext.sendPackageChangedBroadcast(systemTrustAgent2);
+        notifyPackageChanged(systemTrustAgent2);
 
-        assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly(
-                systemTrustAgent1);
+        assertThat(mEnabledTrustAgents).containsExactly(systemTrustAgent1);
     }
 
     @Test
     public void reportEnabledTrustAgentsChangedInformsListener() throws RemoteException {
-        final LockPatternUtils utils = mock(LockPatternUtils.class);
-        final TrustManagerService service = new TrustManagerService(mMockContext,
-                new TrustManagerService.Injector(utils, mLooper.getLooper()));
         final ITrustListener trustListener = mock(ITrustListener.class);
-        final IWindowManager windowManager = mock(IWindowManager.class);
-        final int userId = new Random().nextInt();
+        mTrustManager.registerTrustListener(trustListener);
+        mService.waitForIdle();
+        mTrustManager.reportEnabledTrustAgentsChanged(TEST_USER_ID);
+        mService.waitForIdle();
+        verify(trustListener).onEnabledTrustAgentsChanged(TEST_USER_ID);
+    }
 
-        mMockContext.getTestablePermissions().setPermission(Manifest.permission.TRUST_LISTENER,
-                PERMISSION_GRANTED);
+    // Tests that when the device is locked for a managed profile with a *unified* challenge, the
+    // device locked notification that is sent to Keystore contains the biometric SIDs of the parent
+    // user, not the profile.  This matches the authentication that is needed to unlock the device
+    // for the profile again.
+    @Test
+    @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2)
+    public void testLockDeviceForManagedProfileWithUnifiedChallenge_usesParentBiometricSids()
+            throws Exception {
+        setupMocksForProfile(/* unifiedChallenge= */ true);
 
-        when(utils.getKnownTrustAgents(anyInt())).thenReturn(new ArrayList<>());
+        when(mWindowManager.isKeyguardLocked()).thenReturn(false);
+        mTrustManager.reportKeyguardShowingChanged();
+        verify(mKeystoreAuthorization).onDeviceUnlocked(PARENT_USER_ID, null);
+        verify(mKeystoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null);
 
-        MockitoSession mockSession = mockitoSession()
-                .initMocks(this)
-                .mockStatic(ServiceManager.class)
-                .mockStatic(WindowManagerGlobal.class)
-                .startMocking();
+        when(mWindowManager.isKeyguardLocked()).thenReturn(true);
+        mTrustManager.reportKeyguardShowingChanged();
+        verify(mKeystoreAuthorization)
+                .onDeviceLocked(eq(PARENT_USER_ID), eq(PARENT_BIOMETRIC_SIDS));
+        verify(mKeystoreAuthorization)
+                .onDeviceLocked(eq(PROFILE_USER_ID), eq(PARENT_BIOMETRIC_SIDS));
+    }
 
-        doReturn(windowManager).when(() -> {
-            WindowManagerGlobal.getWindowManagerService();
-        });
+    // Tests that when the device is locked for a managed profile with a *separate* challenge, the
+    // device locked notification that is sent to Keystore contains the biometric SIDs of the
+    // profile itself.  This matches the authentication that is needed to unlock the device for the
+    // profile again.
+    @Test
+    public void testLockDeviceForManagedProfileWithSeparateChallenge_usesProfileBiometricSids()
+            throws Exception {
+        setupMocksForProfile(/* unifiedChallenge= */ false);
 
-        service.onStart();
-        ArgumentCaptor<IBinder> binderArgumentCaptor = ArgumentCaptor.forClass(IBinder.class);
-        verify(() -> ServiceManager.addService(eq(Context.TRUST_SERVICE),
-                binderArgumentCaptor.capture(), anyBoolean(), anyInt()));
-        ITrustManager manager = ITrustManager.Stub.asInterface(binderArgumentCaptor.getValue());
-        manager.registerTrustListener(trustListener);
-        mLooper.dispatchAll();
-        manager.reportEnabledTrustAgentsChanged(userId);
-        mLooper.dispatchAll();
-        verify(trustListener).onEnabledTrustAgentsChanged(eq(userId));
-        mockSession.finishMocking();
+        mTrustManager.setDeviceLockedForUser(PROFILE_USER_ID, false);
+        verify(mKeystoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null);
+
+        mTrustManager.setDeviceLockedForUser(PROFILE_USER_ID, true);
+        verify(mKeystoreAuthorization)
+                .onDeviceLocked(eq(PROFILE_USER_ID), eq(PROFILE_BIOMETRIC_SIDS));
+    }
+
+    private void setupMocksForProfile(boolean unifiedChallenge) {
+        UserInfo parent = new UserInfo(PARENT_USER_ID, "parent", UserInfo.FLAG_FULL);
+        UserInfo profile = new UserInfo(PROFILE_USER_ID, "profile", UserInfo.FLAG_MANAGED_PROFILE);
+        when(mUserManager.getAliveUsers()).thenReturn(List.of(parent, profile));
+        when(mUserManager.getUserInfo(PARENT_USER_ID)).thenReturn(parent);
+        when(mUserManager.getUserInfo(PROFILE_USER_ID)).thenReturn(profile);
+        when(mUserManager.getProfileParent(PROFILE_USER_ID)).thenReturn(parent);
+        when(mUserManager.getEnabledProfileIds(PARENT_USER_ID))
+                .thenReturn(new int[] { PROFILE_USER_ID });
+
+        when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true);
+        when(mLockPatternUtils.isProfileWithUnifiedChallenge(PROFILE_USER_ID))
+                .thenReturn(unifiedChallenge);
+        when(mLockPatternUtils.isManagedProfileWithUnifiedChallenge(PROFILE_USER_ID))
+                .thenReturn(unifiedChallenge);
+        when(mLockPatternUtils.isSeparateProfileChallengeEnabled(PROFILE_USER_ID))
+                .thenReturn(!unifiedChallenge);
+
+        when(mBiometricManager.getAuthenticatorIds(PARENT_USER_ID))
+                .thenReturn(PARENT_BIOMETRIC_SIDS);
+        when(mBiometricManager.getAuthenticatorIds(PROFILE_USER_ID))
+                .thenReturn(PROFILE_BIOMETRIC_SIDS);
+
+        bootService();
+        mService.onUserSwitching(null, new SystemService.TargetUser(parent));
     }
 
     private void addTrustAgent(ComponentName agentComponentName, boolean isSystemApp) {
@@ -327,33 +427,29 @@
         mTrustAgentResolveInfoList.add(resolveInfo);
     }
 
-    private void initializeEnabledAgents(ComponentName... enabledAgents) {
-        mLockPatternUtils.setEnabledTrustAgents(Lists.newArrayList(enabledAgents), TEST_USER_ID);
-        Settings.Secure.putIntForUser(mMockContext.getContentResolver(),
-                Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID);
-    }
-
-    private void initializeKnownAgents(ComponentName... knownAgents) {
-        mLockPatternUtils.setKnownTrustAgents(Lists.newArrayList(knownAgents), TEST_USER_ID);
-        Settings.Secure.putIntForUser(mMockContext.getContentResolver(),
-                Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID);
-    }
-
     private void bootService() {
         mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
         mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
         mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+        mMockContext.sendUserStartedBroadcast();
     }
 
-    private void resetTrustAgentLockSettings() {
-        mLockPatternUtils.setEnabledTrustAgents(Collections.emptyList(), TEST_USER_ID);
-        mLockPatternUtils.setKnownTrustAgents(Collections.emptyList(), TEST_USER_ID);
+    private void grantPermission(String permission) {
+        mMockContext.getTestablePermissions().setPermission(
+                permission, PackageManager.PERMISSION_GRANTED);
+    }
+
+    private void notifyPackageChanged(ComponentName changedComponent) {
+        mService.mPackageMonitor.onPackageChanged(
+                changedComponent.getPackageName(),
+                UserHandle.of(TEST_USER_ID).getUid(1234),
+                new String[] { changedComponent.getClassName() });
     }
 
     /** A mock Context that allows the test process to send protected broadcasts. */
     private static final class MockContext extends TestableContext {
 
-        private final ArrayList<BroadcastReceiver> mPackageChangedBroadcastReceivers =
+        private final ArrayList<BroadcastReceiver> mUserStartedBroadcastReceivers =
                 new ArrayList<>();
 
         MockContext(Context base) {
@@ -366,23 +462,22 @@
                 UserHandle user, IntentFilter filter, @Nullable String broadcastPermission,
                 @Nullable Handler scheduler) {
 
-            if (filter.hasAction(Intent.ACTION_PACKAGE_CHANGED)) {
-                mPackageChangedBroadcastReceivers.add(receiver);
+            if (filter.hasAction(Intent.ACTION_USER_STARTED)) {
+                mUserStartedBroadcastReceivers.add(receiver);
             }
             return super.registerReceiverAsUser(receiver, user, filter, broadcastPermission,
                     scheduler);
         }
 
-        void sendPackageChangedBroadcast(ComponentName changedComponent) {
-            Intent intent = new Intent(
-                    Intent.ACTION_PACKAGE_CHANGED,
-                    Uri.fromParts(URI_SCHEME_PACKAGE,
-                            changedComponent.getPackageName(), /* fragment= */ null))
-                    .putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
-                            new String[]{changedComponent.getClassName()})
-                    .putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID)
-                    .putExtra(Intent.EXTRA_UID, UserHandle.of(TEST_USER_ID).getUid(1234));
-            for (BroadcastReceiver receiver : mPackageChangedBroadcastReceivers) {
+        @Override
+        public void sendBroadcastAsUser(Intent intent, UserHandle user,
+                @Nullable String receiverPermission, @Nullable Bundle options) {
+        }
+
+        void sendUserStartedBroadcast() {
+            Intent intent = new Intent(Intent.ACTION_USER_STARTED)
+                    .putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID);
+            for (BroadcastReceiver receiver : mUserStartedBroadcastReceivers) {
                 receiver.onReceive(this, intent);
             }
         }
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index d752ae4..aec896f 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -97,6 +97,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.internal.app.IBatteryStats;
+import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -164,6 +165,7 @@
     @Mock private AttentionManagerInternal mAttentionManagerInternalMock;
     @Mock private DreamManagerInternal mDreamManagerInternalMock;
     @Mock private PowerManagerService.NativeWrapper mNativeWrapperMock;
+    @Mock private FoldGracePeriodProvider mFoldGracePeriodProvider;
     @Mock private Notifier mNotifierMock;
     @Mock private WirelessChargerDetector mWirelessChargerDetectorMock;
     @Mock private AmbientDisplayConfiguration mAmbientDisplayConfigurationMock;
@@ -359,6 +361,11 @@
             DeviceConfigParameterProvider createDeviceConfigParameterProvider() {
                 return mDeviceParameterProvider;
             }
+
+            @Override
+            FoldGracePeriodProvider createFoldGracePeriodProvider() {
+                return mFoldGracePeriodProvider;
+            }
         });
         return mService;
     }
@@ -520,6 +527,8 @@
 
     @Test
     public void testWakefulnessSleep_SoftSleepFlag_NoWakelocks() {
+        when(mFoldGracePeriodProvider.isEnabled()).thenReturn(false);
+
         createService();
         // Start with AWAKE state
         startSystem();
@@ -533,6 +542,23 @@
     }
 
     @Test
+    public void testWakefulnessAwakeShowKeyguard_SoftSleepFlag_NoWakelocks() {
+        when(mFoldGracePeriodProvider.isEnabled()).thenReturn(true);
+
+        createService();
+        // Start with AWAKE state
+        startSystem();
+        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
+
+        // Take a nap and verify we stay awake and the keyguard is requested
+        mService.getBinderServiceInstance().goToSleep(mClock.now(),
+                PowerManager.GO_TO_SLEEP_REASON_APPLICATION,
+                PowerManager.GO_TO_SLEEP_FLAG_SOFT_SLEEP);
+        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
+        verify(mNotifierMock).showDismissibleKeyguard();
+    }
+
+    @Test
     public void testWakefulnessSleep_SoftSleepFlag_WithPartialWakelock() {
         createService();
         // Start with AWAKE state
@@ -548,7 +574,7 @@
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY,
                 null /* callback */);
 
-        // Take a nap and verify we stay awake.
+        // Take a nap and verify we go to sleep.
         mService.getBinderServiceInstance().goToSleep(mClock.now(),
                 PowerManager.GO_TO_SLEEP_REASON_APPLICATION,
                 PowerManager.GO_TO_SLEEP_FLAG_SOFT_SLEEP);
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index 05e0e8f..654d7a8d 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -11,6 +11,7 @@
         "src/com/android/server/power/stats/MultiStateStatsTest.java",
         "src/com/android/server/power/stats/PowerStatsAggregatorTest.java",
         "src/com/android/server/power/stats/PowerStatsCollectorTest.java",
+        "src/com/android/server/power/stats/PowerStatsExporterTest.java",
         "src/com/android/server/power/stats/PowerStatsSchedulerTest.java",
         "src/com/android/server/power/stats/PowerStatsStoreTest.java",
         "src/com/android/server/power/stats/PowerStatsUidResolverTest.java",
@@ -83,6 +84,9 @@
     ],
     srcs: [
         ":power_stats_ravenwood_tests",
+
+        "src/com/android/server/power/stats/BatteryUsageStatsRule.java",
+        "src/com/android/server/power/stats/MockBatteryStatsImpl.java",
         "src/com/android/server/power/stats/MockClock.java",
     ],
     auto_gen_config: true,
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
index 4ea0805..3f058a2 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
@@ -37,12 +37,12 @@
     private static final double PRECISION = 0.00001;
 
     @Rule
-    public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
+    public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
+                    .setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 4000.0);
 
     @Test
     public void testDischargeTotals() {
         // Nominal battery capacity should be ignored
-        mStatsRule.setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 1234.0);
 
         final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
@@ -84,8 +84,6 @@
 
     @Test
     public void testDischargeTotals_chargeUahUnavailable() {
-        mStatsRule.setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 4000.0);
-
         final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
         batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index e61dd0b..ca162e0 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -18,11 +18,11 @@
 
 import static org.mockito.ArgumentMatchers.anyDouble;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.annotation.XmlRes;
-import android.content.Context;
 import android.net.NetworkStats;
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
@@ -54,12 +54,11 @@
                     .powerProfileModeledOnly()
                     .includePowerModels()
                     .build();
-    private final Context mContext;
 
     private final PowerProfile mPowerProfile;
     private final MockClock mMockClock = new MockClock();
     private final MockBatteryStatsImpl mBatteryStats;
-    private final Handler mHandler;
+    private Handler mHandler;
 
     private BatteryUsageStats mBatteryUsageStats;
     private boolean mScreenOn;
@@ -76,11 +75,8 @@
     }
 
     public BatteryUsageStatsRule(long currentTime, File historyDir) {
-        HandlerThread bgThread = new HandlerThread("bg thread");
-        bgThread.start();
-        mHandler = new Handler(bgThread.getLooper());
-        mContext = InstrumentationRegistry.getContext();
-        mPowerProfile = spy(new PowerProfile(mContext, true /* forTest */));
+        mHandler = mock(Handler.class);
+        mPowerProfile = spy(new PowerProfile());
         mMockClock.currentTime = currentTime;
         mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir, mHandler);
         mBatteryStats.setPowerProfile(mPowerProfile);
@@ -103,7 +99,7 @@
     }
 
     public BatteryUsageStatsRule setTestPowerProfile(@XmlRes int xmlId) {
-        mPowerProfile.forceInitForTesting(mContext, xmlId);
+        mPowerProfile.forceInitForTesting(InstrumentationRegistry.getContext(), xmlId);
         return this;
     }
 
@@ -222,13 +218,17 @@
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
-                noteOnBattery();
+                before();
                 base.evaluate();
             }
         };
     }
 
-    private void noteOnBattery() {
+    private void before() {
+        HandlerThread bgThread = new HandlerThread("bg thread");
+        bgThread.start();
+        mHandler = new Handler(bgThread.getLooper());
+        mBatteryStats.setHandler(mHandler);
         mBatteryStats.setOnBatteryInternal(true);
         mBatteryStats.getOnBatteryTimeBase().setRunning(true, 0, 0);
         mBatteryStats.getOnBatteryScreenOffTimeBase().setRunning(!mScreenOn, 0, 0);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index fb71ac8..78c4bac 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -271,6 +271,10 @@
     public void writeSyncLocked() {
     }
 
+    public void setHandler(Handler handler) {
+        mHandler = handler;
+    }
+
     public static class DummyExternalStatsSync implements ExternalStatsSync {
         public int flags = 0;
 
@@ -315,4 +319,3 @@
         }
     }
 }
-
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
index 3c48262..3560a26 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.power.stats;
 
-import static androidx.test.InstrumentationRegistry.getContext;
-
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -34,6 +32,7 @@
 import android.os.Parcel;
 import android.os.PersistableBundle;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -48,6 +47,8 @@
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
@@ -57,7 +58,12 @@
     private static final int APP_UID2 = 84;
     private static final double TOLERANCE = 0.01;
 
-    @Rule
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720)
             .setCpuScalingPolicy(0, new int[]{0}, new int[]{100})
@@ -75,8 +81,8 @@
     private PowerStats.Descriptor mPowerStatsDescriptor;
 
     @Before
-    public void setup() {
-        File storeDirectory = new File(getContext().getCacheDir(), getClass().getSimpleName());
+    public void setup() throws IOException {
+        File storeDirectory = Files.createTempDirectory("PowerStatsExporterTest").toFile();
         clearDirectory(storeDirectory);
 
         AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig();
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 9b84190..0045026 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -215,6 +215,16 @@
 }
 
 java_library {
+    name: "mockito-test-utils",
+    srcs: [
+        "utils-mockito/**/*.kt",
+    ],
+    static_libs: [
+        "mockito-target-minus-junit4",
+    ],
+}
+
+java_library {
     name: "servicestests-utils-mockito-extended",
     srcs: [
         "utils/**/*.java",
diff --git a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java b/services/tests/servicestests/src/com/android/server/BootReceiverTest.java
new file mode 100644
index 0000000..523c5c0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/BootReceiverTest.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 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/am/AnrHelperTest.java b/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
index c0051c6..eee3752 100644
--- a/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
@@ -133,7 +133,8 @@
         verify(mAnrApp.mErrorState, timeout(TIMEOUT_MS)).appNotResponding(
                 eq(activityShortComponentName), eq(appInfo), eq(parentShortComponentName),
                 eq(parentProcess), eq(aboveSystem), eq(timeoutRecord), eq(mAuxExecutorService),
-                eq(false) /* onlyDumpSelf */, eq(false) /*isContinuousAnr*/, eq(mEarlyDumpFuture));
+                anyBoolean() /* onlyDumpSelf */, eq(false) /*isContinuousAnr*/,
+                eq(mEarlyDumpFuture));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 77b1455..26934d8 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -763,8 +763,7 @@
 
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_BACKGROUND);
 
-        verify(mInjector.mStorageManagerMock, never())
-                .unlockCeStorage(eq(TEST_USER_ID), anyInt(), any());
+        verify(mInjector.mStorageManagerMock, never()).unlockCeStorage(eq(TEST_USER_ID), any());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index 8929900..f7480de 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -44,12 +44,18 @@
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.fingerprint.Fingerprint;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.UserHandle;
 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.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
 import android.testing.TestableLooper;
@@ -60,6 +66,7 @@
 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.log.BiometricLogger;
 import com.android.server.biometrics.nano.BiometricSchedulerProto;
@@ -95,14 +102,39 @@
     @Rule
     public final TestableContext mContext = new TestableContext(
             InstrumentationRegistry.getContext(), null);
-    private BiometricScheduler mScheduler;
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+    private BiometricScheduler<IFingerprint, ISession> mScheduler;
     private IBinder mToken;
+    private int mCurrentUserId = UserHandle.USER_SYSTEM;
+    private boolean mShouldFailStopUser = false;
+    private final List<Integer> mStartedUsers = new ArrayList<>();
+    private final StartUserClient.UserStartedCallback<ISession> mUserStartedCallback =
+            (newUserId, newUser, halInterfaceVersion) -> {
+                mStartedUsers.add(newUserId);
+                mCurrentUserId = newUserId;
+            };
+    private int mUsersStoppedCount = 0;
+    private final StopUserClient.UserStoppedCallback mUserStoppedCallback =
+            () -> {
+                mUsersStoppedCount++;
+                mCurrentUserId = UserHandle.USER_NULL;
+            };
+    private boolean mStartOperationsFinish = true;
+    private int mStartUserClientCount = 0;
     @Mock
     private IBiometricService mBiometricService;
     @Mock
     private BiometricContext mBiometricContext;
     @Mock
     private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private BiometricLogger mBiometricLogger;
+    @Mock
+    private ISession mSession;
+    @Mock
+    private IFingerprint mFingerprint;
 
     @Before
     public void setUp() {
@@ -111,9 +143,39 @@
         when(mAuthSessionCoordinator.getLockoutStateFor(anyInt(), anyInt())).thenReturn(
                 BIOMETRIC_SUCCESS);
         when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
-        mScheduler = new BiometricScheduler(TAG, new Handler(TestableLooper.get(this).getLooper()),
-                BiometricScheduler.SENSOR_TYPE_UNKNOWN, null /* gestureAvailabilityTracker */,
-                mBiometricService, LOG_NUM_RECENT_OPERATIONS);
+        if (Flags.deHidl()) {
+            mScheduler = new BiometricScheduler<>(
+                    new Handler(TestableLooper.get(this).getLooper()),
+                    BiometricScheduler.SENSOR_TYPE_UNKNOWN,
+                    null /* gestureAvailabilityDispatcher */,
+                    mBiometricService,
+                    LOG_NUM_RECENT_OPERATIONS,
+                    () -> mCurrentUserId,
+                    new UserSwitchProvider<IFingerprint, ISession>() {
+                        @NonNull
+                        @Override
+                        public StopUserClient<ISession> getStopUserClient(int userId) {
+                            return new TestStopUserClient(mContext, () -> mSession, mToken, userId,
+                                    TEST_SENSOR_ID, mBiometricLogger, mBiometricContext,
+                                    mUserStoppedCallback, () -> mShouldFailStopUser);
+                        }
+
+                        @NonNull
+                        @Override
+                        public StartUserClient<IFingerprint, ISession> getStartUserClient(
+                                int newUserId) {
+                            mStartUserClientCount++;
+                            return new TestStartUserClient(mContext, () -> mFingerprint, mToken,
+                                    newUserId, TEST_SENSOR_ID, mBiometricLogger, mBiometricContext,
+                                    mUserStartedCallback, mStartOperationsFinish);
+                        }
+                    });
+        } else {
+            mScheduler = new BiometricScheduler<>(
+                    new Handler(TestableLooper.get(this).getLooper()),
+                    BiometricScheduler.SENSOR_TYPE_UNKNOWN, null /* gestureAvailabilityTracker */,
+                    mBiometricService, LOG_NUM_RECENT_OPERATIONS);
+        }
     }
 
     @Test
@@ -479,6 +541,7 @@
         final boolean isEnroll = client instanceof TestEnrollClient;
 
         mScheduler.scheduleClientMonitor(client);
+        waitForIdle();
         if (started) {
             mScheduler.startPreparedClient(client.getCookie());
         }
@@ -789,6 +852,172 @@
         assertEquals(1,  client1.getFingerprints().size());
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testScheduleOperation_whenNoUser() {
+        mCurrentUserId = UserHandle.USER_NULL;
+
+        final BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        when(nextClient.getTargetUserId()).thenReturn(0);
+
+        mScheduler.scheduleClientMonitor(nextClient);
+        waitForIdle();
+
+        assertThat(mUsersStoppedCount).isEqualTo(0);
+        assertThat(mStartedUsers).containsExactly(0);
+        verify(nextClient).start(any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testScheduleOperation_whenNoUser_notStarted() {
+        mCurrentUserId = UserHandle.USER_NULL;
+        mStartOperationsFinish = false;
+
+        final BaseClientMonitor[] nextClients = new BaseClientMonitor[]{
+                mock(BaseClientMonitor.class),
+                mock(BaseClientMonitor.class),
+                mock(BaseClientMonitor.class)
+        };
+        for (BaseClientMonitor client : nextClients) {
+            when(client.getTargetUserId()).thenReturn(5);
+            mScheduler.scheduleClientMonitor(client);
+            waitForIdle();
+        }
+
+        assertThat(mUsersStoppedCount).isEqualTo(0);
+        assertThat(mStartedUsers).isEmpty();
+        assertThat(mStartUserClientCount).isEqualTo(1);
+        for (BaseClientMonitor client : nextClients) {
+            verify(client, never()).start(any());
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testScheduleOperation_whenNoUser_notStarted_andReset() {
+        mCurrentUserId = UserHandle.USER_NULL;
+        mStartOperationsFinish = false;
+
+        final BaseClientMonitor client = mock(BaseClientMonitor.class);
+
+        when(client.getTargetUserId()).thenReturn(5);
+
+        mScheduler.scheduleClientMonitor(client);
+        waitForIdle();
+
+        final TestStartUserClient startUserClient =
+                (TestStartUserClient) mScheduler.mCurrentOperation.getClientMonitor();
+        mScheduler.reset();
+
+        assertThat(mScheduler.mCurrentOperation).isNull();
+
+        final BiometricSchedulerOperation fakeOperation = new BiometricSchedulerOperation(
+                mock(BaseClientMonitor.class), new ClientMonitorCallback() {});
+        mScheduler.mCurrentOperation = fakeOperation;
+        startUserClient.mCallback.onClientFinished(startUserClient, true);
+
+        assertThat(fakeOperation).isSameInstanceAs(mScheduler.mCurrentOperation);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testScheduleOperation_whenSameUser() {
+        mCurrentUserId = 10;
+
+        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        when(nextClient.getTargetUserId()).thenReturn(mCurrentUserId);
+
+        mScheduler.scheduleClientMonitor(nextClient);
+
+        waitForIdle();
+
+        verify(nextClient).start(any());
+        assertThat(mUsersStoppedCount).isEqualTo(0);
+        assertThat(mStartedUsers).isEmpty();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testScheduleOperation_whenDifferentUser() {
+        mCurrentUserId = 10;
+
+        final int nextUserId = 11;
+        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        when(nextClient.getTargetUserId()).thenReturn(nextUserId);
+
+        mScheduler.scheduleClientMonitor(nextClient);
+
+        waitForIdle();
+        assertThat(mUsersStoppedCount).isEqualTo(1);
+
+        waitForIdle();
+        assertThat(mStartedUsers).containsExactly(nextUserId);
+
+        waitForIdle();
+        verify(nextClient).start(any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testStartUser_alwaysStartsNextOperation() {
+        mCurrentUserId = UserHandle.USER_NULL;
+
+        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        when(nextClient.getTargetUserId()).thenReturn(10);
+
+        mScheduler.scheduleClientMonitor(nextClient);
+
+        waitForIdle();
+        verify(nextClient).start(any());
+
+        // finish first operation
+        mScheduler.getInternalCallback().onClientFinished(nextClient, true /* success */);
+        waitForIdle();
+
+        // schedule second operation but swap out the current operation
+        // before it runs so that it's not current when it's completion callback runs
+        nextClient = mock(BaseClientMonitor.class);
+        when(nextClient.getTargetUserId()).thenReturn(11);
+        mScheduler.scheduleClientMonitor(nextClient);
+
+        waitForIdle();
+        verify(nextClient).start(any());
+        assertThat(mStartedUsers).containsExactly(10, 11).inOrder();
+        assertThat(mUsersStoppedCount).isEqualTo(1);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testStartUser_failsClearsStopUserClient() {
+        mCurrentUserId = UserHandle.USER_NULL;
+
+        // When a stop user client fails, check that mStopUserClient
+        // is set to null to prevent the scheduler from getting stuck.
+        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        when(nextClient.getTargetUserId()).thenReturn(10);
+
+        mScheduler.scheduleClientMonitor(nextClient);
+
+        waitForIdle();
+        verify(nextClient).start(any());
+
+        // finish first operation
+        mScheduler.getInternalCallback().onClientFinished(nextClient, true /* success */);
+        waitForIdle();
+
+        // schedule second operation but swap out the current operation
+        // before it runs so that it's not current when it's completion callback runs
+        nextClient = mock(BaseClientMonitor.class);
+        when(nextClient.getTargetUserId()).thenReturn(11);
+        mShouldFailStopUser = true;
+        mScheduler.scheduleClientMonitor(nextClient);
+
+        waitForIdle();
+        assertThat(mStartedUsers).containsExactly(10, 11).inOrder();
+        assertThat(mUsersStoppedCount).isEqualTo(0);
+        assertThat(mScheduler.getStopUserClient()).isEqualTo(null);
+    }
 
     private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception {
         return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer));
@@ -1069,4 +1298,82 @@
             return mFingerprints;
         }
     }
+
+    private interface StopUserClientShouldFail {
+        boolean shouldFail();
+    }
+
+    private class TestStopUserClient extends StopUserClient<ISession> {
+        private StopUserClientShouldFail mShouldFailClient;
+        TestStopUserClient(@NonNull Context context,
+                @NonNull Supplier<ISession> lazyDaemon, @Nullable IBinder token, int userId,
+                int sensorId, @NonNull BiometricLogger logger,
+                @NonNull BiometricContext biometricContext,
+                @NonNull UserStoppedCallback callback, StopUserClientShouldFail shouldFail) {
+            super(context, lazyDaemon, token, userId, sensorId, logger, biometricContext, callback);
+            mShouldFailClient = shouldFail;
+        }
+
+        @Override
+        protected void startHalOperation() {
+
+        }
+
+        @Override
+        public void start(@NonNull ClientMonitorCallback callback) {
+            super.start(callback);
+            if (mShouldFailClient.shouldFail()) {
+                getCallback().onClientFinished(this, false /* success */);
+                // When the above fails, it means that the HAL has died, in this case we
+                // need to ensure the UserSwitchCallback correctly returns the NULL user handle.
+                mCurrentUserId = UserHandle.USER_NULL;
+            } else {
+                onUserStopped();
+            }
+        }
+
+        @Override
+        public void unableToStart() {
+
+        }
+    }
+
+    private static class TestStartUserClient extends StartUserClient<IFingerprint, ISession> {
+
+        @Mock
+        private ISession mSession;
+        private final boolean mShouldFinish;
+        ClientMonitorCallback mCallback;
+
+        TestStartUserClient(@NonNull Context context,
+                @NonNull Supplier<IFingerprint> lazyDaemon, @Nullable IBinder token, int userId,
+                int sensorId, @NonNull BiometricLogger logger,
+                @NonNull BiometricContext biometricContext,
+                @NonNull UserStartedCallback<ISession> callback, boolean shouldFinish) {
+            super(context, lazyDaemon, token, userId, sensorId, logger, biometricContext, callback);
+            mShouldFinish = shouldFinish;
+        }
+
+        @Override
+        protected void startHalOperation() {
+
+        }
+
+        @Override
+        public void start(@NonNull ClientMonitorCallback callback) {
+            super.start(callback);
+
+            mCallback = callback;
+            if (mShouldFinish) {
+                mUserStartedCallback.onUserStarted(
+                        getTargetUserId(), mSession, 1 /* halInterfaceVersion */);
+                callback.onClientFinished(this, true /* success */);
+            }
+        }
+
+        @Override
+        public void unableToStart() {
+
+        }
+    }
 }
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 8b1a291..772ec8b 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
@@ -36,8 +36,10 @@
 import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.SensorProps;
 import android.hardware.face.HidlFaceSensorConfig;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserManager;
+import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
@@ -88,14 +90,11 @@
     @Mock
     private BiometricStateCallback mBiometricStateCallback;
 
+    private final TestLooper mLooper = new TestLooper();
     private SensorProps[] mSensorProps;
     private LockoutResetDispatcher mLockoutResetDispatcher;
     private FaceProvider mFaceProvider;
 
-    private static void waitForIdle() {
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-    }
-
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
@@ -121,7 +120,8 @@
 
         mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback,
                 mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext,
-                mDaemon, false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */);
+                mDaemon, new Handler(mLooper.getLooper()),
+                false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */);
     }
 
     @Test
@@ -156,6 +156,7 @@
         mFaceProvider = new FaceProvider(mContext,
                 mBiometricStateCallback, hidlFaceSensorConfig, TAG,
                 mLockoutResetDispatcher, mBiometricContext, mDaemon,
+                new Handler(mLooper.getLooper()),
                 true /* resetLockoutRequiresChallenge */,
                 true /* testHalEnabled */);
 
@@ -210,4 +211,12 @@
             assertEquals(0, scheduler.getCurrentPendingCount());
         }
     }
+
+    private void waitForIdle() {
+        if (Flags.deHidl()) {
+            mLooper.dispatchAll();
+        } else {
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        }
+    }
 }
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 e7f7195..fe9cd43 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
@@ -31,6 +31,7 @@
 import android.content.Context;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.SensorProps;
 import android.hardware.face.FaceSensorPropertiesInternal;
@@ -40,6 +41,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
@@ -49,6 +51,7 @@
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.UserSwitchProvider;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -74,6 +77,8 @@
     @Mock
     private UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback;
     @Mock
+    private UserSwitchProvider<IFace, ISession> mUserSwitchProvider;
+    @Mock
     private AidlResponseHandler.HardwareUnavailableCallback mHardwareUnavailableCallback;
     @Mock
     private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -84,16 +89,16 @@
     @Mock
     private AuthSessionCoordinator mAuthSessionCoordinator;
     @Mock
-    FaceProvider mFaceProvider;
+    private FaceProvider mFaceProvider;
     @Mock
-    BaseClientMonitor mClientMonitor;
+    private BaseClientMonitor mClientMonitor;
     @Mock
-    AidlSession mCurrentSession;
+    private AidlSession mCurrentSession;
 
     private final TestLooper mLooper = new TestLooper();
     private final LockoutCache mLockoutCache = new LockoutCache();
 
-    private UserAwareBiometricScheduler mScheduler;
+    private BiometricScheduler<IFace, ISession> mScheduler;
     private AidlResponseHandler mHalCallback;
 
     @Before
@@ -101,16 +106,26 @@
         MockitoAnnotations.initMocks(this);
 
         when(mContext.getSystemService(Context.BIOMETRIC_SERVICE)).thenReturn(mBiometricService);
-
         when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
 
-        mScheduler = new UserAwareBiometricScheduler(TAG,
-                new Handler(mLooper.getLooper()),
-                BiometricScheduler.SENSOR_TYPE_FACE,
-                null /* gestureAvailabilityDispatcher */,
-                mBiometricService,
-                () -> USER_ID,
-                mUserSwitchCallback);
+        if (Flags.deHidl()) {
+            mScheduler = new BiometricScheduler<>(
+                    new Handler(mLooper.getLooper()),
+                    BiometricScheduler.SENSOR_TYPE_FACE,
+                    null /* gestureAvailabilityDispatcher */,
+                    mBiometricService,
+                    2 /* recentOperationsLimit */,
+                    () -> USER_ID,
+                    mUserSwitchProvider);
+        } else {
+            mScheduler = new UserAwareBiometricScheduler<>(TAG,
+                    new Handler(mLooper.getLooper()),
+                    BiometricScheduler.SENSOR_TYPE_FACE,
+                    null /* gestureAvailabilityDispatcher */,
+                    mBiometricService,
+                    () -> USER_ID,
+                    mUserSwitchCallback);
+        }
         mHalCallback = new AidlResponseHandler(mContext, mScheduler, SENSOR_ID, USER_ID,
                 mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator,
                 mHardwareUnavailableCallback);
@@ -146,18 +161,8 @@
     @Test
     public void onBinderDied_noErrorOnNullClient() {
         mLooper.dispatchAll();
-
-        final SensorProps sensorProps = new SensorProps();
-        sensorProps.commonProps = new CommonProps();
-        sensorProps.commonProps.sensorId = 1;
-        final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
-                sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
-                sensorProps.commonProps.maxEnrollmentsPerUser, null /* componentInfo */,
-                sensorProps.sensorType, sensorProps.supportsDetectInteraction,
-                sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
-        final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext,
-                null /* handler */, internalProp, mLockoutResetDispatcher, mBiometricContext);
-        sensor.init(mLockoutResetDispatcher, mFaceProvider);
+        final Sensor sensor = getSensor();
+        mScheduler = sensor.getScheduler();
         mScheduler.reset();
 
         assertNull(mScheduler.getCurrentClient());
@@ -175,18 +180,8 @@
         when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID);
         when(mClientMonitor.isInterruptable()).thenReturn(false);
 
-        final SensorProps sensorProps = new SensorProps();
-        sensorProps.commonProps = new CommonProps();
-        sensorProps.commonProps.sensorId = 1;
-        final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
-                sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
-                sensorProps.commonProps.maxEnrollmentsPerUser, null /* componentInfo */,
-                sensorProps.sensorType, sensorProps.supportsDetectInteraction,
-                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();
+        final Sensor sensor = getSensor();
+        mScheduler = sensor.getScheduler();
         sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
                 USER_ID, mHalCallback);
 
@@ -206,4 +201,20 @@
         verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID));
         verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
     }
+
+    private Sensor getSensor() {
+        final SensorProps sensorProps = new SensorProps();
+        sensorProps.commonProps = new CommonProps();
+        sensorProps.commonProps.sensorId = 1;
+        final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+                sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
+                sensorProps.commonProps.maxEnrollmentsPerUser, null /* componentInfo */,
+                sensorProps.sensorType, sensorProps.supportsDetectInteraction,
+                sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
+        final Sensor sensor = new Sensor(mFaceProvider, mContext,
+                null /* handler */, internalProp, mBiometricContext);
+        sensor.init(mLockoutResetDispatcher, mFaceProvider);
+
+        return sensor;
+    }
 }
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
index 4e43332..940fe69 100644
--- 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
@@ -45,7 +45,6 @@
 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;
@@ -88,9 +87,9 @@
     @Mock
     private IBiometricsFace mDaemon;
     @Mock
-    AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    private AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
     @Mock
-    BiometricUtils<Face> mBiometricUtils;
+    private BiometricUtils<Face> mBiometricUtils;
 
     private final TestLooper mLooper = new TestLooper();
     private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter;
@@ -118,20 +117,14 @@
         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,
+        mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(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);
     }
 
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 bf5986c..258be57 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
@@ -38,8 +38,10 @@
 import android.hardware.biometrics.fingerprint.SensorLocation;
 import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.fingerprint.HidlFingerprintSensorConfig;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserManager;
+import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
@@ -92,14 +94,12 @@
     @Mock
     private BiometricContext mBiometricContext;
 
+    private final TestLooper mLooper = new TestLooper();
+
     private SensorProps[] mSensorProps;
     private LockoutResetDispatcher mLockoutResetDispatcher;
     private FingerprintProvider mFingerprintProvider;
 
-    private static void waitForIdle() {
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-    }
-
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
@@ -126,7 +126,8 @@
         mFingerprintProvider = new FingerprintProvider(mContext,
                 mBiometricStateCallback, mAuthenticationStateListeners, mSensorProps, TAG,
                 mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext,
-                mDaemon, false /* resetLockoutRequiresHardwareAuthToken */,
+                mDaemon, new Handler(mLooper.getLooper()),
+                false /* resetLockoutRequiresHardwareAuthToken */,
                 true /* testHalEnabled */);
     }
 
@@ -159,6 +160,7 @@
                 mBiometricStateCallback, mAuthenticationStateListeners,
                 hidlFingerprintSensorConfigs, TAG, mLockoutResetDispatcher,
                 mGestureAvailabilityDispatcher, mBiometricContext, mDaemon,
+                new Handler(mLooper.getLooper()),
                 false /* resetLockoutRequiresHardwareAuthToken */,
                 true /* testHalEnabled */);
 
@@ -215,4 +217,12 @@
             assertEquals(0, scheduler.getCurrentPendingCount());
         }
     }
+
+    private void waitForIdle() {
+        if (Flags.deHidl()) {
+            mLooper.dispatchAll();
+        } else {
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        }
+    }
 }
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 126a05e..b4c2ee8 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
@@ -32,14 +32,17 @@
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.common.CommonProps;
 import android.hardware.biometrics.face.SensorProps;
+import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
@@ -49,6 +52,7 @@
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.UserSwitchProvider;
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
 import org.junit.Before;
@@ -75,6 +79,8 @@
     @Mock
     private UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback;
     @Mock
+    private UserSwitchProvider<IFingerprint, ISession> mUserSwitchProvider;
+    @Mock
     private AidlResponseHandler.HardwareUnavailableCallback mHardwareUnavailableCallback;
     @Mock
     private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -92,11 +98,13 @@
     private AidlSession mCurrentSession;
     @Mock
     private BaseClientMonitor mClientMonitor;
+    @Mock
+    private HandlerThread mThread;
 
     private final TestLooper mLooper = new TestLooper();
     private final LockoutCache mLockoutCache = new LockoutCache();
 
-    private BiometricScheduler mScheduler;
+    private BiometricScheduler<IFingerprint, ISession> mScheduler;
     private AidlResponseHandler mHalCallback;
 
     @Before
@@ -105,14 +113,26 @@
 
         when(mContext.getSystemService(Context.BIOMETRIC_SERVICE)).thenReturn(mBiometricService);
         when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
+        when(mThread.getLooper()).thenReturn(mLooper.getLooper());
 
-        mScheduler = new UserAwareBiometricScheduler(TAG,
-                new Handler(mLooper.getLooper()),
-                BiometricScheduler.SENSOR_TYPE_FP_OTHER,
-                null /* gestureAvailabilityDispatcher */,
-                mBiometricService,
-                () -> USER_ID,
-                mUserSwitchCallback);
+        if (Flags.deHidl()) {
+            mScheduler = new BiometricScheduler<>(
+                    new Handler(mLooper.getLooper()),
+                    BiometricScheduler.SENSOR_TYPE_FP_OTHER,
+                    null /* gestureAvailabilityDispatcher */,
+                    mBiometricService,
+                    2 /* recentOperationsLimit */,
+                    () -> USER_ID,
+                    mUserSwitchProvider);
+        } else {
+            mScheduler = new UserAwareBiometricScheduler<>(TAG,
+                    new Handler(mLooper.getLooper()),
+                    BiometricScheduler.SENSOR_TYPE_FP_OTHER,
+                    null /* gestureAvailabilityDispatcher */,
+                    mBiometricService,
+                    () -> USER_ID,
+                    mUserSwitchCallback);
+        }
         mHalCallback = new AidlResponseHandler(mContext, mScheduler, SENSOR_ID, USER_ID,
                 mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator,
                 mHardwareUnavailableCallback);
@@ -153,18 +173,7 @@
         when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID);
         when(mClientMonitor.isInterruptable()).thenReturn(false);
 
-        final SensorProps sensorProps = new SensorProps();
-        sensorProps.commonProps = new CommonProps();
-        sensorProps.commonProps.sensorId = 1;
-        final FingerprintSensorPropertiesInternal internalProp = new
-                FingerprintSensorPropertiesInternal(
-                        sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
-                        sensorProps.commonProps.maxEnrollmentsPerUser, null,
-                        sensorProps.sensorType, false /* resetLockoutRequiresHardwareAuthToken */);
-        final Sensor sensor = new Sensor("SensorTest", mFingerprintProvider, mContext,
-                null /* handler */, internalProp, mLockoutResetDispatcher,
-                mGestureAvailabilityDispatcher, mBiometricContext, mCurrentSession);
-        sensor.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcher);
+        final Sensor sensor = getSensor();
         mScheduler = sensor.getScheduler();
         sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
                 USER_ID, mHalCallback);
@@ -185,4 +194,21 @@
         verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID));
         verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
     }
+
+    private Sensor getSensor() {
+        final SensorProps sensorProps = new SensorProps();
+        sensorProps.commonProps = new CommonProps();
+        sensorProps.commonProps.sensorId = 1;
+        final FingerprintSensorPropertiesInternal internalProp = new
+                FingerprintSensorPropertiesInternal(
+                sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
+                sensorProps.commonProps.maxEnrollmentsPerUser, null,
+                sensorProps.sensorType, false /* resetLockoutRequiresHardwareAuthToken */);
+        final Sensor sensor = new Sensor(mFingerprintProvider, mContext,
+                null /* handler */, internalProp,
+                mBiometricContext, mCurrentSession);
+        sensor.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcher);
+
+        return sensor;
+    }
 }
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
index 89a4961..cbbc545 100644
--- 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
@@ -36,12 +36,12 @@
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.HidlFingerprintSensorConfig;
 import android.os.Handler;
+import android.os.HandlerThread;
 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;
 
@@ -49,17 +49,11 @@
 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;
@@ -111,60 +105,18 @@
     private BiometricUtils<Fingerprint> mBiometricUtils;
     @Mock
     private AuthenticationStateListeners mAuthenticationStateListeners;
+    @Mock
+    private HandlerThread mThread;
 
     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);
+        when(mThread.getLooper()).thenReturn(mLooper.getLooper());
         doAnswer((answer) -> {
             mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
                     .onEnrollmentProgress(1 /* enrollmentId */, 0 /* remaining */);
@@ -175,26 +127,18 @@
         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,
+        mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(
                 mFingerprintProvider, mContext, new Handler(mLooper.getLooper()),
                 fingerprintSensorConfig, mLockoutResetDispatcherForSensor,
-                mGestureAvailabilityDispatcher, mBiometricContext,
-                false /* resetLockoutRequiresHardwareAuthToken */,
+                mBiometricContext, false /* resetLockoutRequiresHardwareAuthToken */,
                 mInternalCleanupRunnable, mAuthSessionCoordinator, mDaemon,
                 mAidlResponseHandlerCallback);
         mHidlToAidlSensorAdapter.init(mGestureAvailabilityDispatcher,
                 mLockoutResetDispatcherForSensor);
-        mHidlToAidlSensorAdapter.setScheduler(scheduler);
+
         mHidlToAidlSensorAdapter.handleUserChanged(USER_ID);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 995d1f4..e1f490a 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -341,7 +341,8 @@
         mSetFlagsRule.initAllFlagsToReleaseConfigDefault();
 
         doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
-        doNothing().when(mInputManagerInternalMock).setPointerAcceleration(anyFloat(), anyInt());
+        doNothing().when(mInputManagerInternalMock)
+                .setMousePointerAccelerationEnabled(anyBoolean(), anyInt());
         doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
         LocalServices.removeServiceForTest(InputManagerInternal.class);
         LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock);
@@ -1982,8 +1983,8 @@
         return new AssociationInfo(associationId, /* userId= */ 0, /* packageName=*/ null,
                 /* tag= */ null, MacAddress.BROADCAST_ADDRESS, /* displayName= */ "", deviceProfile,
                 /* associatedDevice= */ null, /* selfManaged= */ true,
-                /* notifyOnDeviceNearby= */ false, /* revoked= */false,  /* timeApprovedMs= */0,
-                /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1);
+                /* notifyOnDeviceNearby= */ false, /* revoked= */ false, /* pending= */ false,
+                /* timeApprovedMs= */0, /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1);
     }
 
     /** Helper class to drop permissions temporarily and restore them at the end of a test. */
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
index a2f8c8b..6b85a32 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
@@ -25,10 +25,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.ContentResolver;
@@ -1223,35 +1221,6 @@
                 StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR));
     }
 
-    @Test
-    public void testInputMethodSettings_SwitchCurrentUser() {
-        TestContext ownerUserContext = createMockContext(0 /* userId */);
-        final InputMethodInfo systemIme = createFakeInputMethodInfo(
-                "SystemIme", "fake.latin", true /* isSystem */);
-        final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme",
-                "fake.voice0", false /* isSystem */);
-        final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
-        methodMap.put(systemIme.getId(), systemIme);
-
-        // Init InputMethodSettings for the owner user (userId=0), verify calls can get the
-        // corresponding user's context, contentResolver and the resources configuration.
-        InputMethodUtils.InputMethodSettings settings = new InputMethodUtils.InputMethodSettings(
-                methodMap, 0 /* userId */);
-        assertEquals(0, settings.getCurrentUserId());
-
-        settings.getEnabledInputMethodSubtypeListLocked(nonSystemIme, true);
-        verify(ownerUserContext.getResources(), atLeastOnce()).getConfiguration();
-
-        // Calling switchCurrentUser to the secondary user (userId=10), verify calls can get the
-        // corresponding user's context, contentResolver and the resources configuration.
-        settings.switchCurrentUser(10 /* userId */);
-        assertEquals(10, settings.getCurrentUserId());
-
-        settings.getEnabledInputMethodSubtypeListLocked(nonSystemIme, true);
-        verify(TestContext.getSecondaryUserContext().getResources(),
-                atLeastOnce()).getConfiguration();
-    }
-
     private static IntArray createSubtypeHashCodeArrayFromStr(String subtypeHashCodesStr) {
         final IntArray subtypes = new IntArray();
         final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index f5d50d1..6986cab 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -305,9 +305,9 @@
         doAnswer(invocation -> {
             Object[] args = invocation.getArguments();
             mStorageManager.unlockCeStorage(/* userId= */ (int) args[0],
-                    /* secret= */ (byte[]) args[2]);
+                    /* secret= */ (byte[]) args[1]);
             return null;
-        }).when(sm).unlockCeStorage(anyInt(), anyInt(), any());
+        }).when(sm).unlockCeStorage(anyInt(), any());
 
         doAnswer(invocation -> {
             Object[] args = invocation.getArguments();
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 6a088d9..757abde 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
@@ -157,7 +157,7 @@
         validateTagCount("uses-library", 1000, tag)
         validateTagCount("activity-alias", 4000, tag)
         validateTagCount("provider", 8000, tag)
-        validateTagCount("activity", 40000, tag)
+        validateTagCount("activity", 30000, tag)
     }
 
     @Test
@@ -465,7 +465,8 @@
             R.styleable.AndroidManifestData_pathAdvancedPattern,
             4000
         )
-        validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 512)
+        validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 255)
+        validateTagAttr(tag, "mimeGroup", R.styleable.AndroidManifestData_mimeGroup, 1024)
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 42ad73a..8622488 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -158,6 +158,9 @@
         when(mAudioManager.isAudioFocusExclusive()).thenReturn(false);
         when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10);
+        // consistent with focus not exclusive and volume not muted
+        when(mAudioManager.shouldNotificationSoundPlay(any(AudioAttributes.class)))
+                .thenReturn(true);
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
         when(mAudioManager.getFocusRampTimeMs(anyInt(), any(AudioAttributes.class))).thenReturn(50);
         when(mUsageStats.isAlertRateLimited(any())).thenReturn(false);
@@ -869,6 +872,7 @@
         // the phone is quiet
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mService.buzzBeepBlinkLocked(r);
 
@@ -886,6 +890,8 @@
         // the phone is quiet
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(1);
+        // all streams at 1 means no muting from audio framework
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(true);
 
         mService.buzzBeepBlinkLocked(r);
 
@@ -904,6 +910,7 @@
         // the phone is quiet
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mService.buzzBeepBlinkLocked(r);
 
@@ -924,6 +931,7 @@
         // the phone is quiet
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mService.buzzBeepBlinkLocked(r);
 
@@ -1195,6 +1203,7 @@
         // the phone is quiet
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mService.buzzBeepBlinkLocked(r);
         verifyDelayedVibrate(mService.getVibratorHelper().createFallbackVibration(false));
@@ -1923,6 +1932,7 @@
         NotificationRecord r = getBuzzyBeepyNotification();
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mService.buzzBeepBlinkLocked(r);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index cf8548c..bfd2df2d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -19,17 +19,21 @@
 import static android.app.Notification.GROUP_ALERT_ALL;
 import static android.app.Notification.GROUP_ALERT_CHILDREN;
 import static android.app.Notification.GROUP_ALERT_SUMMARY;
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_MIN;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
+
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -178,6 +182,8 @@
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10);
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
         when(mAudioManager.getFocusRampTimeMs(anyInt(), any(AudioAttributes.class))).thenReturn(50);
+        when(mAudioManager.shouldNotificationSoundPlay(any(AudioAttributes.class)))
+                .thenReturn(true);
         when(mUsageStats.isAlertRateLimited(any())).thenReturn(false);
         when(mVibrator.hasFrequencyControl()).thenReturn(false);
         when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(false);
@@ -195,7 +201,8 @@
         // TODO (b/291907312): remove feature flag
         mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER);
         // Disable feature flags by default. Tests should enable as needed.
-        mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_EXPIRE_BITMAPS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS,
+                Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED);
 
         mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger,
             mNotificationInstanceIdSequence));
@@ -364,10 +371,20 @@
     }
 
     private NotificationRecord getNotificationRecord(int id,
-        boolean insistent, boolean once,
-        boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration,
-        boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior,
-        boolean isLeanback, UserHandle userHandle) {
+            boolean insistent, boolean once,
+            boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration,
+            boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior,
+            boolean isLeanback, UserHandle userHandle) {
+        return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration,
+                defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, userHandle,
+                mPkg);
+    }
+
+    private NotificationRecord getNotificationRecord(int id,
+            boolean insistent, boolean once,
+            boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration,
+            boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior,
+            boolean isLeanback, UserHandle userHandle, String packageName) {
 
         final Builder builder = new Builder(getContext())
             .setContentTitle("foo")
@@ -427,8 +444,8 @@
         when(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
             .thenReturn(isLeanback);
 
-        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, mTag, mUid,
-            mPid, n, userHandle, null, System.currentTimeMillis());
+        StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, id, mTag,
+                mUid, mPid, n, userHandle, null, System.currentTimeMillis());
         NotificationRecord r = new NotificationRecord(context, sbn, mChannel);
         mService.addNotification(r);
         return r;
@@ -915,6 +932,8 @@
         // the phone is quiet
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+        when(mAudioManager.shouldNotificationSoundPlay(any(AudioAttributes.class)))
+                .thenReturn(false);
 
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
 
@@ -932,6 +951,8 @@
         // the phone is quiet
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(1);
+        // all streams at 1 means no muting from audio framework
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(true);
 
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
 
@@ -950,6 +971,7 @@
         // the phone is quiet
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
 
@@ -971,6 +993,7 @@
         // the phone is quiet
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
 
@@ -1243,6 +1266,7 @@
         // the phone is quiet
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
         verifyDelayedVibrate(mAttentionHelper.getVibratorHelper().createFallbackVibration(false));
@@ -1973,6 +1997,7 @@
         NotificationRecord r = getBuzzyBeepyNotification();
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0);
+        when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false);
 
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
 
@@ -1990,7 +2015,6 @@
     public void testBeepVolume_politeNotif() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         TestableFlagResolver flagResolver = new TestableFlagResolver();
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1");
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
         initAttentionHelper(flagResolver);
@@ -2015,13 +2039,11 @@
         assertNotEquals(-1, r.getLastAudiblyAlertedMs());
     }
 
-    // TODO b/270456865: Only one of the two strategies will be released.
-    //  The other one need to be removed
     @Test
-    public void testBeepVolume_politeNotif_Strategy2() throws Exception {
+    public void testBeepVolume_politeNotif_GlobalStrategy() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
         TestableFlagResolver flagResolver = new TestableFlagResolver();
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule2");
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
         initAttentionHelper(flagResolver);
@@ -2032,14 +2054,58 @@
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
         Mockito.reset(mRingtonePlayer);
 
-        // update should beep at 0% volume
-        r.isUpdate = true;
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        // Use different package for next notifications
+        NotificationRecord r2 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg");
+
+        // update should beep at 50% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(0.5f);
+
+        // Use different package for next notifications
+        NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg");
+
+        // 2nd update should beep at 0% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
         verifyBeepVolume(0.0f);
 
+        verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+    }
+
+    @Test
+    public void testBeepVolume_politeNotif_GlobalStrategy_ChannelHasUserSound() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        initAttentionHelper(flagResolver);
+
+        NotificationRecord r = getBeepyNotification();
+
+        // set up internal state
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // Use package with user-set sounds for next notifications
+        mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT);
+        mChannel.lockFields(NotificationChannel.USER_LOCKED_SOUND);
+        NotificationRecord r2 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg");
+
+        // update should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+
         // 2nd update should beep at 50% volume
         Mockito.reset(mRingtonePlayer);
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
         verifyBeepVolume(0.5f);
 
         verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
@@ -2047,10 +2113,101 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_applyPerApp() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        // NOTIFICATION_COOLDOWN_ALL setting is enabled
+        Settings.System.putInt(getContext().getContentResolver(),
+                Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+        initAttentionHelper(flagResolver);
+
+        NotificationRecord r = getBeepyNotification();
+
+        // set up internal state
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // Use different channel for next notifications
+        mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT);
+
+        // update should beep at 50% volume
+        NotificationRecord r2 = getBeepyNotification();
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(0.5f);
+
+        // 2nd update should beep at 0% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(0.0f);
+
+        // Use different package for next notifications
+        NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg");
+
+        // Update from new package should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+
+        verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+    }
+
+    @Test
+    public void testBeepVolume_politeNotif_applyPerApp_ChannelHasUserSound() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        // NOTIFICATION_COOLDOWN_ALL setting is enabled
+        Settings.System.putInt(getContext().getContentResolver(),
+                Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+        initAttentionHelper(flagResolver);
+
+        NotificationRecord r = getBeepyNotification();
+
+        // set up internal state
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // Use different channel for next notifications
+        mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT);
+        mChannel.lockFields(NotificationChannel.USER_LOCKED_SOUND);
+
+        // update should beep at 100% volume
+        NotificationRecord r2 = getBeepyNotification();
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+
+        // 2nd update should beep at 50% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(0.5f);
+
+        // Use different package for next notifications
+        mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT);
+        NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg");
+
+        // Update from new package should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+
+        verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+    }
+
+    @Test
     public void testVibrationIntensity_politeNotif() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         TestableFlagResolver flagResolver = new TestableFlagResolver();
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1");
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
         initAttentionHelper(flagResolver);
@@ -2076,37 +2233,9 @@
     }
 
     @Test
-    public void testVibrationIntensity_politeNotif_Strategy2() throws Exception {
-        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
-        TestableFlagResolver flagResolver = new TestableFlagResolver();
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule2");
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
-        initAttentionHelper(flagResolver);
-
-        NotificationRecord r = getBuzzyBeepyNotification();
-
-        // set up internal state
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
-
-        VibratorHelper vibratorHelper = mAttentionHelper.getVibratorHelper();
-        Mockito.reset(vibratorHelper);
-
-        // update should buzz at 0% intensity
-        r.isUpdate = true;
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
-
-        verify(vibratorHelper, times(1)).scale(any(), eq(0.0f));
-        Mockito.reset(vibratorHelper);
-
-        // 2nd update should buzz at 50% intensity
-        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
-        verify(vibratorHelper, times(1)).scale(any(), eq(0.5f));
-    }
-
-    @Test
     public void testBuzzOnlyOnScreenUnlock_politeNotif() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_VIBRATE_WHILE_UNLOCKED);
         TestableFlagResolver flagResolver = new TestableFlagResolver();
 
         // When NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED setting is enabled
@@ -2161,7 +2290,6 @@
     public void testBeepVolume_politeNotif_workProfile() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         TestableFlagResolver flagResolver = new TestableFlagResolver();
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1");
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
 
@@ -2202,7 +2330,6 @@
     public void testBeepVolume_politeNotif_workProfile_disabled() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         TestableFlagResolver flagResolver = new TestableFlagResolver();
-        flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1");
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
         flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
index 5892793..9b25f58 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
@@ -170,6 +170,10 @@
         Settings.Secure.putIntForUser(getContext().getContentResolver(),
                 Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, USER_SYSTEM);
         mHistoryManager.mSettingsObserver.update(null, USER_SYSTEM);
+        // fake cloned settings to profile
+        Settings.Secure.putIntForUser(getContext().getContentResolver(),
+                Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, mProfileId);
+        mHistoryManager.mSettingsObserver.update(null, mProfileId);
 
         // unlock user, verify that history is disabled for self and profile
         mHistoryManager.onUserUnlocked(USER_SYSTEM);
@@ -179,6 +183,36 @@
         assertThat(mHistoryManager.doesHistoryExistForUser(mProfileId)).isFalse();
         verify(mDb, times(2)).disableHistory();
     }
+    @Test
+    public void testAddProfile_historyEnabledInPrimary() {
+        // create a history
+        mHistoryManager.onUserUnlocked(MIN_SECONDARY_USER_ID);
+        assertThat(mHistoryManager.doesHistoryExistForUser(MIN_SECONDARY_USER_ID)).isTrue();
+
+        // fake Settings#CLONE_TO_MANAGED_PROFILE
+        int newProfileId = 99;
+        Settings.Secure.putIntForUser(getContext().getContentResolver(),
+                Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1, newProfileId);
+        mUsers = new ArrayList<>();
+        UserInfo userFullSecondary = new UserInfo();
+        userFullSecondary.id = MIN_SECONDARY_USER_ID;
+        mUsers.add(userFullSecondary);
+        UserInfo userProfile = new UserInfo();
+        userProfile.id = newProfileId;
+        mUsers.add(userProfile);
+        when(mUserManager.getUsers()).thenReturn(mUsers);
+
+        mProfiles = new int[] {MIN_SECONDARY_USER_ID, userProfile.id};
+        when(mUserManager.getProfileIds(MIN_SECONDARY_USER_ID, true)).thenReturn(mProfiles);
+        when(mUserManager.getProfileIds(userProfile.id, true))
+                .thenReturn(new int[] {userProfile.id});
+        when(mUserManager.getProfileParent(userProfile.id)).thenReturn(userFullSecondary);
+
+        // add profile
+        mHistoryManager.onUserAdded(newProfileId);
+        mHistoryManager.onUserUnlocked(newProfileId);
+        assertThat(mHistoryManager.doesHistoryExistForUser(newProfileId)).isTrue();
+    }
 
     @Test
     public void testOnUserUnlocked_historyDisabledThenEnabled() {
@@ -575,4 +609,14 @@
 
         assertThat(mHistoryManager.isHistoryEnabled(USER_SYSTEM)).isFalse();
     }
+    @Test
+    public void testDelayedPackageRemoval_userLocked() {
+        String pkg = "pkg";
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.onUserStopped(USER_SYSTEM);
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+        // no exception, yay
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
index 53ca704..bf850cf 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
@@ -44,8 +44,10 @@
 import android.content.pm.ShortcutInfo;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
+import android.os.BadParcelableException;
 import android.os.Binder;
 import android.os.Build;
+import android.os.DeadObjectException;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -99,6 +101,20 @@
     }
 
     @Test
+    public void testGetActiveNotifications_handlesBinderErrors() throws RemoteException {
+        TestListenerService service = new TestListenerService();
+        INotificationManager noMan = service.getNoMan();
+        when(noMan.getActiveNotificationsFromListener(any(), any(), anyInt()))
+                .thenThrow(new BadParcelableException("oops", new DeadObjectException("")));
+
+        assertNotNull(service.getActiveNotifications());
+        assertNotNull(service.getActiveNotifications(NotificationListenerService.TRIM_FULL));
+        assertNotNull(service.getActiveNotifications(new String[0]));
+        assertNull(service.getActiveNotifications(
+                new String[0], NotificationListenerService.TRIM_LIGHT));
+    }
+
+    @Test
     public void testGetActiveNotifications_preP_mapsExtraPeople() throws RemoteException {
         TestListenerService service = new TestListenerService();
         service.attachBaseContext(mContext);
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 f1edd9a..c1f35cc 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -958,22 +958,24 @@
         mTestNotificationChannel.setAllowBubbles(channelEnabled);
     }
 
-    private void setUpPrefsForHistory(int uid, boolean globalEnabled) throws Exception {
+    private void setUpPrefsForHistory(@UserIdInt int userId, boolean globalEnabled)
+            throws Exception {
         initNMS(SystemService.PHASE_ACTIVITY_MANAGER_READY);
 
         // Sets NOTIFICATION_HISTORY_ENABLED setting for calling process uid
         Settings.Secure.putIntForUser(mContext.getContentResolver(),
-                Settings.Secure.NOTIFICATION_HISTORY_ENABLED, globalEnabled ? 1 : 0, uid);
+                Settings.Secure.NOTIFICATION_HISTORY_ENABLED, globalEnabled ? 1 : 0, userId);
         // Sets NOTIFICATION_HISTORY_ENABLED setting for uid 0
         Settings.Secure.putInt(mContext.getContentResolver(),
                 Settings.Secure.NOTIFICATION_HISTORY_ENABLED, globalEnabled ? 1 : 0);
+        setUsers(new int[] {0, userId});
 
         // Forces an update by calling observe on mSettingsObserver, which picks up the settings
         // changes above.
         mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START, mMainLooper);
 
         assertEquals(globalEnabled, Settings.Secure.getIntForUser(mContext.getContentResolver(),
-                Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0 /* =def */, uid) != 0);
+                Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0 /* =def */, userId) != 0);
     }
 
     private StatusBarNotification generateSbn(String pkg, int uid, long postTime, int userId) {
@@ -10443,7 +10445,7 @@
     @Test
     public void testHandleOnPackageRemoved_ClearsHistory() throws Exception {
         // Enables Notification History setting
-        setUpPrefsForHistory(mUid, true /* =enabled */);
+        setUpPrefsForHistory(mUserId, true /* =enabled */);
 
         // Posts a notification to the mTestNotificationChannel.
         final NotificationRecord notif = generateNotificationRecord(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
index 999e33c..3d8ec2e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
@@ -18,19 +18,31 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.Flags;
 import android.os.Parcel;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenDeviceEffects;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.UiServiceTestCase;
 
+import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(AndroidJUnit4.class)
 public class ZenDeviceEffectsTest extends UiServiceTestCase {
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public final void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+    }
+
     @Test
     public void builder() {
         ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
@@ -40,6 +52,7 @@
                 .setShouldMaximizeDoze(true)
                 .setShouldUseNightMode(false)
                 .setShouldSuppressAmbientDisplay(false).setShouldSuppressAmbientDisplay(true)
+                .setUserModifiedFields(8)
                 .build();
 
         assertThat(deviceEffects.shouldDimWallpaper()).isTrue();
@@ -52,6 +65,7 @@
         assertThat(deviceEffects.shouldMinimizeRadioUsage()).isFalse();
         assertThat(deviceEffects.shouldUseNightMode()).isFalse();
         assertThat(deviceEffects.shouldSuppressAmbientDisplay()).isTrue();
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(8);
     }
 
     @Test
@@ -83,6 +97,7 @@
                 .setShouldMinimizeRadioUsage(true)
                 .setShouldUseNightMode(true)
                 .setShouldSuppressAmbientDisplay(true)
+                .setUserModifiedFields(6)
                 .build();
 
         Parcel parcel = Parcel.obtain();
@@ -101,6 +116,7 @@
         assertThat(copy.shouldUseNightMode()).isTrue();
         assertThat(copy.shouldSuppressAmbientDisplay()).isTrue();
         assertThat(copy.shouldDisplayGrayscale()).isFalse();
+        assertThat(copy.getUserModifiedFields()).isEqualTo(6);
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 3185c50..dd252f3 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -19,12 +19,15 @@
 import static android.app.AutomaticZenRule.TYPE_BEDTIME;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertNotNull;
 import static junit.framework.TestCase.assertNull;
 import static junit.framework.TestCase.assertTrue;
 
+import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.app.NotificationManager.Policy;
 import android.content.ComponentName;
@@ -46,6 +49,7 @@
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -56,6 +60,7 @@
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.time.Instant;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -83,6 +88,11 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    @Before
+    public final void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+    }
+
     @Test
     public void testPriorityOnlyMutingAllNotifications() {
         ZenModeConfig config = getMutedRingerConfig();
@@ -275,6 +285,7 @@
         assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders());
         assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders());
         assertEquals(expected.getAllowedChannels(), actual.getAllowedChannels());
+        assertEquals(expected.getUserModifiedFields(), actual.getUserModifiedFields());
     }
 
     @Test
@@ -327,6 +338,53 @@
     }
 
     @Test
+    public void testCanBeUpdatedByApp_nullPolicyAndDeviceEffects() throws Exception {
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.zenPolicy = null;
+        rule.zenDeviceEffects = null;
+
+        assertThat(rule.canBeUpdatedByApp()).isTrue();
+
+        rule.userModifiedFields = 1;
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
+    public void testCanBeUpdatedByApp_policyModified() throws Exception {
+        ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0);
+        ZenPolicy policy = policyBuilder.build();
+
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.zenPolicy = policy;
+
+        assertThat(rule.userModifiedFields).isEqualTo(0);
+        assertThat(rule.canBeUpdatedByApp()).isTrue();
+
+        policy = policyBuilder.setUserModifiedFields(1).build();
+        assertThat(policy.getUserModifiedFields()).isEqualTo(1);
+        rule.zenPolicy = policy;
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
+    public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception {
+        ZenDeviceEffects.Builder deviceEffectsBuilder =
+                new ZenDeviceEffects.Builder().setUserModifiedFields(0);
+        ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
+
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.zenDeviceEffects = deviceEffects;
+
+        assertThat(rule.userModifiedFields).isEqualTo(0);
+        assertThat(rule.canBeUpdatedByApp()).isTrue();
+
+        deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build();
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1);
+        rule.zenDeviceEffects = deviceEffects;
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
     public void testWriteToParcel() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
 
@@ -347,8 +405,10 @@
 
         rule.allowManualInvocation = ALLOW_MANUAL;
         rule.type = TYPE;
+        rule.userModifiedFields = 16;
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
+        rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
 
         Parcel parcel = Parcel.obtain();
         rule.writeToParcel(parcel, 0);
@@ -371,11 +431,13 @@
         assertEquals(rule.allowManualInvocation, parceled.allowManualInvocation);
         assertEquals(rule.iconResName, parceled.iconResName);
         assertEquals(rule.type, parceled.type);
+        assertEquals(rule.userModifiedFields, parceled.userModifiedFields);
         assertEquals(rule.triggerDescription, parceled.triggerDescription);
         assertEquals(rule.zenPolicy, parceled.zenPolicy);
+        assertEquals(rule.deletionInstant, parceled.deletionInstant);
+
         assertEquals(rule, parceled);
         assertEquals(rule.hashCode(), parceled.hashCode());
-
     }
 
     @Test
@@ -448,8 +510,10 @@
 
         rule.allowManualInvocation = ALLOW_MANUAL;
         rule.type = TYPE;
+        rule.userModifiedFields = 4;
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
+        rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         writeRuleXml(rule, baos);
@@ -476,8 +540,10 @@
 
         assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation);
         assertEquals(rule.type, fromXml.type);
+        assertEquals(rule.userModifiedFields, fromXml.userModifiedFields);
         assertEquals(rule.triggerDescription, fromXml.triggerDescription);
         assertEquals(rule.iconResName, fromXml.iconResName);
+        assertEquals(rule.deletionInstant, fromXml.deletionInstant);
     }
 
     @Test
@@ -550,6 +616,22 @@
     }
 
     @Test
+    public void testRuleXml_userModifiedField() throws Exception {
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
+        assertThat(rule.userModifiedFields).isEqualTo(1);
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
+
+        assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields);
+        assertThat(fromXml.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
     public void testZenPolicyXml_classic() throws Exception {
         ZenPolicy policy = new ZenPolicy.Builder()
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
@@ -615,6 +697,7 @@
                 .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
                 .hideAllVisualEffects()
                 .showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true)
+                .setUserModifiedFields(4)
                 .build();
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -649,6 +732,7 @@
         assertEquals(policy.getVisualEffectAmbient(), fromXml.getVisualEffectAmbient());
         assertEquals(policy.getVisualEffectNotificationList(),
                 fromXml.getVisualEffectNotificationList());
+        assertEquals(policy.getUserModifiedFields(), fromXml.getUserModifiedFields());
     }
 
     private ZenModeConfig getMutedRingerConfig() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 93cd44e..9d7cf53 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -65,18 +65,22 @@
 @TestableLooper.RunWithLooper
 public class ZenModeDiffTest extends UiServiceTestCase {
     // Base set of exempt fields independent of fields that are enabled/disabled via flags.
-    // version is not included in the diff; manual & automatic rules have special handling
+    // version is not included in the diff; manual & automatic rules have special handling;
+    // deleted rules are not included in the diff.
     public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS =
-            Set.of("version", "manualRule", "automaticRules");
+            android.app.Flags.modesApi()
+                    ? Set.of("version", "manualRule", "automaticRules", "deletedRules")
+                    : Set.of("version", "manualRule", "automaticRules");
 
     // Differences for flagged fields are only generated if the flag is enabled.
-    // TODO: b/310620812 - Remove this exempt list when flag is inlined.
+    // "Metadata" fields (userModifiedFields, deletionInstant) are not compared.
     private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
             android.app.Flags.modesApi()
-                    ? Set.of()
+                    ? Set.of("userModifiedFields", "deletionInstant")
                     : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
                             RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
-                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS);
+                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields",
+                            "deletionInstant");
 
     // allowPriorityChannels is flagged by android.app.modes_api
     public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
@@ -304,6 +308,7 @@
             rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
                     .setShouldDimWallpaper(true)
                     .build();
+            rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
         }
         return rule;
     }
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 f84d8e9..9e3e336 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -21,6 +21,9 @@
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ENABLED;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS;
@@ -40,6 +43,7 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 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;
@@ -89,6 +93,7 @@
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
 
+import android.Manifest;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.AppGlobals;
@@ -101,6 +106,7 @@
 import android.content.ContentResolver;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
@@ -113,6 +119,7 @@
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Process;
+import android.os.SimpleClock;
 import android.os.UserHandle;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -169,12 +176,16 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 @SmallTest
 @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
@@ -182,8 +193,9 @@
 @TestableLooper.RunWithLooper
 public class ZenModeHelperTest extends UiServiceTestCase {
 
-    private static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE";
-    private static final String SCHEDULE_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE";
+    private static final String EVENTS_DEFAULT_RULE_ID = ZenModeConfig.EVENTS_DEFAULT_RULE_ID;
+    private static final String SCHEDULE_DEFAULT_RULE_ID =
+            ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID;
     private static final String CUSTOM_PKG_NAME = "not.android";
     private static final String CUSTOM_APP_LABEL = "This is not Android";
     private static final int CUSTOM_PKG_UID = 1;
@@ -228,6 +240,7 @@
     @Mock PackageManager mPackageManager;
     private Resources mResources;
     private TestableLooper mTestableLooper;
+    private final TestClock mTestClock = new TestClock();
     private ZenModeHelper mZenModeHelper;
     private ContentResolver mContentResolver;
     @Mock DeviceEffectsApplier mDeviceEffectsApplier;
@@ -265,7 +278,7 @@
         mConditionProviders.addSystemProvider(new CountdownConditionProvider());
         mConditionProviders.addSystemProvider(new ScheduleConditionProvider());
         mZenModeEventLogger = new ZenModeEventLoggerFake(mPackageManager);
-        mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(),
+        mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(), mTestClock,
                 mConditionProviders, mTestFlagResolver, mZenModeEventLogger);
 
         ResolveInfo ri = new ResolveInfo();
@@ -1194,7 +1207,7 @@
     @Test
     public void ruleUidAutomaticZenRuleRemovedUpdatesCache() throws Exception {
         when(mContext.checkCallingPermission(anyString()))
-                .thenReturn(PackageManager.PERMISSION_GRANTED);
+                .thenReturn(PERMISSION_GRANTED);
 
         setupZenConfig();
         // one enabled automatic rule
@@ -1776,7 +1789,7 @@
     public void testDoNotUpdateModifiedDefaultAutoRule() {
         // mDefaultConfig is set to default config in setup by getDefaultConfigParser
         when(mContext.checkCallingPermission(anyString()))
-                .thenReturn(PackageManager.PERMISSION_GRANTED);
+                .thenReturn(PERMISSION_GRANTED);
 
         // shouldn't update rule that's been modified
         ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule();
@@ -1802,7 +1815,7 @@
     public void testDoNotUpdateEnabledDefaultAutoRule() {
         // mDefaultConfig is set to default config in setup by getDefaultConfigParser
         when(mContext.checkCallingPermission(anyString()))
-                .thenReturn(PackageManager.PERMISSION_GRANTED);
+                .thenReturn(PERMISSION_GRANTED);
 
         // shouldn't update the rule that's enabled
         ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule();
@@ -1829,7 +1842,7 @@
         // mDefaultConfig is set to default config in setup by getDefaultConfigParser
         final String defaultRuleName = "rule name test";
         when(mContext.checkCallingPermission(anyString()))
-                .thenReturn(PackageManager.PERMISSION_GRANTED);
+                .thenReturn(PERMISSION_GRANTED);
 
         // will update rule that is not enabled and modified
         ZenModeConfig.ZenRule customDefaultRule = new ZenModeConfig.ZenRule();
@@ -2197,7 +2210,7 @@
     }
 
     @Test
-    public void addAutomaticZenRule_fromUser_respectsHiddenEffects() {
+    public void addAutomaticZenRule_fromUser_respectsHiddenEffects() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
 
         ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
@@ -2222,7 +2235,13 @@
                 "reasons", 0);
 
         AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
-        assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
+
+        // savedRule.getDeviceEffects() is equal to zde, except for the userModifiedFields.
+        // So we clear before comparing.
+        ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects())
+                .setUserModifiedFields(0).build();
+
+        assertThat(savedEffects).isEqualTo(zde);
     }
 
     @Test
@@ -2298,8 +2317,11 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0);
 
         ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder()
-                .setShouldUseNightMode(true) // Good
-                .setShouldMaximizeDoze(true) // Also good
+                .setShouldUseNightMode(true)
+                .setShouldMaximizeDoze(true)
+                // Just to emphasize that unset values default to false;
+                // even with this line removed, tap to wake would be set to false.
+                .setShouldDisableTapToWake(false)
                 .build();
         mZenModeHelper.updateAutomaticZenRule(ruleId,
                 new AutomaticZenRule.Builder("Rule", CONDITION_ID)
@@ -2308,7 +2330,75 @@
                 UPDATE_ORIGIN_USER, "reasons", 0);
 
         AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
-        assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser);
+
+        // savedRule.getDeviceEffects() is equal to updateFromUser, except for the
+        // userModifiedFields, so we clear before comparing.
+        ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects())
+                .setUserModifiedFields(0).build();
+
+        assertThat(savedEffects).isEqualTo(updateFromUser);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
+        ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
+        sleepingRule.enabled = false;
+        sleepingRule.userModifiedFields = 0;
+        sleepingRule.name = "ZZZZZZZ...";
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.mConfig.automaticRules.put(sleepingRule.id, sleepingRule);
+
+        AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID)
+                .setType(TYPE_BEDTIME)
+                .build();
+        String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP,
+                "reason", CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(bedtimeRuleId);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_withTypeBedtime_keepsEnabledSleeping() {
+        ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
+        sleepingRule.enabled = true;
+        sleepingRule.userModifiedFields = 0;
+        sleepingRule.name = "ZZZZZZZ...";
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.mConfig.automaticRules.put(sleepingRule.id, sleepingRule);
+
+        AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID)
+                .setType(TYPE_BEDTIME)
+                .build();
+        String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP,
+                "reason", CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+                ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, bedtimeRuleId);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_withTypeBedtime_keepsCustomizedSleeping() {
+        ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
+        sleepingRule.enabled = false;
+        sleepingRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+        sleepingRule.name = "ZZZZZZZ...";
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.mConfig.automaticRules.put(sleepingRule.id, sleepingRule);
+
+        AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID)
+                .setType(TYPE_BEDTIME)
+                .build();
+        String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP,
+                "reason", CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+                ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, bedtimeRuleId);
     }
 
     @Test
@@ -3321,6 +3411,7 @@
 
         rule.allowManualInvocation = ALLOW_MANUAL;
         rule.type = TYPE;
+        rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
 
@@ -3335,6 +3426,7 @@
         assertEquals(POLICY, actual.getZenPolicy());
         assertEquals(CONFIG_ACTIVITY, actual.getConfigurationActivity());
         assertEquals(TYPE, actual.getType());
+        assertEquals(AutomaticZenRule.FIELD_NAME, actual.getUserModifiedFields());
         assertEquals(ALLOW_MANUAL, actual.isManualInvocationAllowed());
         assertEquals(CREATION_TIME, actual.getCreationTime());
         assertEquals(OWNER.getPackageName(), actual.getPackageName());
@@ -3376,10 +3468,480 @@
         assertEquals(ALLOW_MANUAL, rule.allowManualInvocation);
         assertEquals(OWNER.getPackageName(), rule.getPkg());
         assertEquals(ICON_RES_NAME, rule.iconResName);
+        // Because the origin of the update is the app, we don't expect the bitmask to change.
+        assertEquals(0, rule.userModifiedFields);
         assertEquals(TRIGGER_DESC, rule.triggerDescription);
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesNameUnlessUserModified() {
+        // Add a starting rule with the name OriginalName.
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // Checks the name can be changed by the app because the user has not modified it.
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("NewName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("NewName");
+        assertThat(rule.canUpdate()).isTrue();
+
+        // The user modifies some other field in the rule, which makes the rule as a whole not
+        // app modifiable.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getUserModifiedFields())
+                .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+        assertThat(rule.canUpdate()).isFalse();
+
+        // ...but the app can still modify the name, because the name itself hasn't been modified
+        // by the user.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("NewAppName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("NewAppName");
+
+        // The user modifies the name.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("UserProvidedName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("UserProvidedName");
+        assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME
+                | AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+
+        // The app is no longer able to modify the name.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("NewAppName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("UserProvidedName");
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesBitmaskAndValueForUserOrigin() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setZenPolicy(new ZenPolicy.Builder().build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // Modifies the zen policy and device effects
+        ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
+                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .build();
+        ZenDeviceEffects deviceEffects =
+                new ZenDeviceEffects.Builder(rule.getDeviceEffects())
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Update the rule with the AZR from origin user.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // UPDATE_ORIGIN_USER should change the bitmask and change the values.
+        assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
+        assertThat(rule.getUserModifiedFields())
+                .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+        assertThat(rule.getZenPolicy().getUserModifiedFields())
+                .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS);
+        assertThat(rule.getZenPolicy().getAllowedChannels())
+                .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(rule.getDeviceEffects().getUserModifiedFields())
+                .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE);
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_doesNotUpdateValuesForInitUserOrigin() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL) // Already the default, no change
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(false)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(false)
+                        .build())
+                .build();
+        // Adds the rule using the user, to set user-modified bits.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isFalse();
+        assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME);
+
+        ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
+                .allowReminders(true)
+                .build();
+        ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects())
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Attempts to update the rule with the AZR from origin init user.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason",
+                Process.SYSTEM_UID);
+        AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // UPDATE_ORIGIN_INIT_USER does not change the bitmask or values if rule is user modified.
+        // TODO: b/318506692 - Remove once we check that INIT origins can't call add/updateAZR.
+        assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields());
+        assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL);
+        assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+                rule.getZenPolicy().getUserModifiedFields());
+        assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders()).isEqualTo(
+                ZenPolicy.STATE_DISALLOW);
+        assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
+                rule.getDeviceEffects().getUserModifiedFields());
+        assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isFalse();
+
+        // Creates a new rule with the AZR from origin init user.
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule newRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+
+        // UPDATE_ORIGIN_INIT_USER does change the values if the rule is new,
+        // but does not update the bitmask.
+        assertThat(newRule.getUserModifiedFields()).isEqualTo(0);
+        assertThat(newRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
+        assertThat(newRule.getZenPolicy().getUserModifiedFields()).isEqualTo(0);
+        assertThat(newRule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(newRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0);
+        assertThat(newRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesValuesForSystemUiOrigin() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(false)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(false)
+                        .build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // Modifies the zen policy and device effects
+        ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
+                .allowReminders(true)
+                .build();
+        ZenDeviceEffects deviceEffects =
+                new ZenDeviceEffects.Builder(rule.getDeviceEffects())
+                        .setShouldDisplayGrayscale(true)
+                        .build();
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Update the rule with the AZR from origin systemUI.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+                "reason", Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask.
+        assertThat(rule.getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesValuesIfRuleNotUserModified() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(false)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(false)
+                        .build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        ZenPolicy policy = new ZenPolicy.Builder()
+                .allowReminders(true)
+                .build();
+        ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azrUpdate =  new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Since the rule is not already user modified, UPDATE_ORIGIN_UNKNOWN can modify the rule.
+        // The bitmask is not modified.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_UNKNOWN, "reason",
+                Process.SYSTEM_UID);
+        AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields());
+        assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+        assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+                rule.getZenPolicy().getUserModifiedFields());
+        assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
+                rule.getDeviceEffects().getUserModifiedFields());
+        assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+
+        // Creates another rule, this time from user. This will have user modified bits set.
+        String ruleIdUser = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser);
+        assertThat(ruleUser.canUpdate()).isFalse();
+
+        // Zen rule update coming from unknown origin. This cannot fully update the rule, because
+        // the rule is already considered user modified.
+        mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_UNKNOWN,
+                "reason", Process.SYSTEM_UID);
+        ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser);
+
+        // UPDATE_ORIGIN_UNKNOWN can only change the value if the rule is not already user modified,
+        // so the rule is not changed, and neither is the bitmask.
+        assertThat(ruleUser.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL);
+        // Interruption Filter All is the default value, so it's not included as a modified field.
+        assertThat(ruleUser.getUserModifiedFields() | AutomaticZenRule.FIELD_NAME).isGreaterThan(0);
+        assertThat(ruleUser.getZenPolicy().getUserModifiedFields()
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS).isGreaterThan(0);
+        assertThat(ruleUser.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(ruleUser.getDeviceEffects().getUserModifiedFields()
+                | ZenDeviceEffects.FIELD_GRAYSCALE).isGreaterThan(0);
+        assertThat(ruleUser.getDeviceEffects().shouldDisplayGrayscale()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesValuesIfRuleNew() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(true)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(true)
+                        .build())
+                .build();
+        // Adds the rule using origin unknown, to show that a new rule is always allowed.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_UNKNOWN, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // The values are modified but the bitmask is not.
+        assertThat(rule.canUpdate()).isTrue();
+        assertThat(rule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullDeviceEffectsUpdate() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setDeviceEffects(new ZenDeviceEffects.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
+                // Sets Device Effects to null
+                .setDeviceEffects(null)
+                .build();
+
+        // Zen rule update coming from unknown origin, but since the rule isn't already
+        // user modified, it can be updated.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // When AZR's ZenDeviceEffects is null, the updated rule's device effects will be null.
+        assertThat(rule.getDeviceEffects()).isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullPolicyUpdate() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setZenPolicy(new ZenPolicy.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
+                // Set zen policy to null
+                .setZenPolicy(null)
+                .build();
+
+        // Zen rule update coming from unknown origin, but since the rule isn't already
+        // user modified, it can be updated.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // When AZR's ZenPolicy is null, we expect the updated rule's policy to be null.
+        assertThat(rule.getZenPolicy()).isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullToNonNullPolicyUpdate() {
+        when(mContext.checkCallingPermission(anyString()))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setZenPolicy(null)
+                // .setDeviceEffects(new ZenDeviceEffects.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        // Create a fully populated ZenPolicy.
+        ZenPolicy policy = new ZenPolicy.Builder()
+                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) // Differs from the default
+                .allowReminders(true) // Differs from the default
+                .allowEvents(true) // Differs from the default
+                .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT)
+                .allowMessages(PEOPLE_TYPE_STARRED)
+                .allowCalls(PEOPLE_TYPE_STARRED)
+                .allowRepeatCallers(true)
+                .allowAlarms(true)
+                .allowMedia(true)
+                .allowSystem(true) // Differs from the default
+                .showFullScreenIntent(true) // Differs from the default
+                .showLights(true) // Differs from the default
+                .showPeeking(true) // Differs from the default
+                .showStatusBarIcons(true)
+                .showBadges(true)
+                .showInAmbientDisplay(true) // Differs from the default
+                .showInNotificationList(true)
+                .build();
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
+                .setZenPolicy(policy)
+                .build();
+
+        // Applies the update to the rule.
+        // Default config defined in getDefaultConfigParser() is used as the original rule.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // New ZenPolicy differs from the default config
+        assertThat(rule.getZenPolicy()).isNotNull();
+        assertThat(rule.getZenPolicy().getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+        assertThat(rule.canUpdate()).isFalse();
+        assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+                ZenPolicy.FIELD_ALLOW_CHANNELS
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM
+                | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT
+                | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS
+                | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK
+                | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT
+        );
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullToNonNullDeviceEffectsUpdate() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setDeviceEffects(null)
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(rule)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Applies the update to the rule.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // New ZenDeviceEffects is used; all fields considered set, since previously were null.
+        assertThat(rule.getDeviceEffects()).isNotNull();
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+        assertThat(rule.canUpdate()).isFalse();
+        assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
+                ZenDeviceEffects.FIELD_GRAYSCALE);
+    }
+
+    @Test
     public void testUpdateAutomaticRule_disabled_triggersBroadcast() throws Exception {
         setupZenConfig();
 
@@ -3765,6 +4327,324 @@
     }
 
     @Test
+    public void removeAndAddAutomaticZenRule_wasCustomized_isRestored() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        // Start with a rule.
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mTestClock.setNowMillis(1000);
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+        assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).canUpdate()).isTrue();
+
+        // User customizes it.
+        AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+                Process.SYSTEM_UID);
+
+        // App deletes it.
+        mTestClock.advanceByMillis(1000);
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+
+        // App adds it again.
+        mTestClock.advanceByMillis(1000);
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+        // Verify that the rule was restored:
+        // - id and creation time is the same as the original one.
+        // - ZenPolicy is the one that the user had set.
+        // - rule still has the user-modified fields.
+        AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+        assertThat(finalRule.getCreationTime()).isEqualTo(1000); // And not 3000.
+        assertThat(newRuleId).isEqualTo(ruleId);
+        assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+        assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo(
+                ZenPolicy.STATE_ALLOW);
+        assertThat(finalRule.getUserModifiedFields()).isEqualTo(
+                AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+        assertThat(finalRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+                ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS);
+
+        // Also, we discarded the "deleted rule" since we already used it for restoration.
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+    }
+
+    @Test
+    public void removeAndAddAutomaticZenRule_wasNotCustomized_isNotRestored() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        // Start with a single rule.
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mTestClock.setNowMillis(1000);
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+
+        // App deletes it.
+        mTestClock.advanceByMillis(1000);
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+
+        // App adds it again.
+        mTestClock.advanceByMillis(1000);
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+        // Verify that the rule was recreated. This means id and creation time are new.
+        AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+        assertThat(finalRule.getCreationTime()).isEqualTo(3000);
+        assertThat(newRuleId).isNotEqualTo(ruleId);
+    }
+
+    @Test
+    public void removeAndAddAutomaticZenRule_recreatedButNotByApp_isNotRestored() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        // Start with a single rule.
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mTestClock.setNowMillis(1000);
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+
+        // User customizes it.
+        mTestClock.advanceByMillis(1000);
+        AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+                Process.SYSTEM_UID);
+
+        // App deletes it.
+        mTestClock.advanceByMillis(1000);
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+
+        // User creates it again (unusual case, but ok).
+        mTestClock.advanceByMillis(1000);
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_USER, "add it anew", CUSTOM_PKG_UID);
+
+        // Verify that the rule was recreated. This means id and creation time are new, and the rule
+        // matches the latest data supplied to addAZR.
+        AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+        assertThat(finalRule.getCreationTime()).isEqualTo(4000);
+        assertThat(newRuleId).isNotEqualTo(ruleId);
+        assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
+        assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo(
+                ZenPolicy.STATE_DISALLOW);
+
+        // Also, we discarded the "deleted rule" since we're not interested in recreating it.
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+    }
+
+    @Test
+    public void removeAndAddAutomaticZenRule_removedByUser_isNotRestored() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        // Start with a single rule.
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mTestClock.setNowMillis(1000);
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+
+        // User customizes it.
+        mTestClock.advanceByMillis(1000);
+        AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+                Process.SYSTEM_UID);
+
+        // User deletes it.
+        mTestClock.advanceByMillis(1000);
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "delete it",
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+
+        // App creates it again.
+        mTestClock.advanceByMillis(1000);
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+        // Verify that the rule was recreated. This means id and creation time are new.
+        AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+        assertThat(finalRule.getCreationTime()).isEqualTo(4000);
+        assertThat(newRuleId).isNotEqualTo(ruleId);
+    }
+
+    @Test
+    public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
+                PERMISSION_GRANTED); // So that canManageAZR passes although packages don't match.
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        // Start with a bunch of customized rules where conditionUris are not unique.
+        String id1 = mZenModeHelper.addAutomaticZenRule("pkg1",
+                new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+                "add it", CUSTOM_PKG_UID);
+        String id2 = mZenModeHelper.addAutomaticZenRule("pkg1",
+                new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP,
+                "add it", CUSTOM_PKG_UID);
+        String id3 = mZenModeHelper.addAutomaticZenRule("pkg1",
+                new AutomaticZenRule.Builder("Test3", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP,
+                "add it", CUSTOM_PKG_UID);
+        String id4 = mZenModeHelper.addAutomaticZenRule("pkg2",
+                new AutomaticZenRule.Builder("Test4", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+                "add it", CUSTOM_PKG_UID);
+        String id5 = mZenModeHelper.addAutomaticZenRule("pkg2",
+                new AutomaticZenRule.Builder("Test5", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+                "add it", CUSTOM_PKG_UID);
+        for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) {
+            zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+        }
+
+        mZenModeHelper.removeAutomaticZenRule(id1, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+        mZenModeHelper.removeAutomaticZenRule(id2, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+        mZenModeHelper.removeAutomaticZenRule(id3, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+        mZenModeHelper.removeAutomaticZenRule(id4, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+        mZenModeHelper.removeAutomaticZenRule(id5, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.mConfig.deletedRules.keySet())
+                .containsExactly("pkg1|uri1", "pkg1|uri2", "pkg2|uri1");
+        assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(zr -> zr.name)
+                .collect(Collectors.toList()))
+                .containsExactly("Test1", "Test3", "Test5");
+    }
+
+    @Test
+    public void removeAllZenRules_preservedForRestoring() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+                "add it", CUSTOM_PKG_UID);
+        mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP,
+                "add it", CUSTOM_PKG_UID);
+
+        for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) {
+            zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+        }
+
+        mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), UPDATE_ORIGIN_APP,
+                "begone", CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(2);
+    }
+
+    @Test
+    public void removeAllZenRules_fromSystem_deletesPreservedRulesToo() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        // Start with deleted rules from 2 different packages.
+        Instant now = Instant.ofEpochMilli(1701796461000L);
+        ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now);
+        ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now);
+        mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule);
+        mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule);
+
+        mZenModeHelper.removeAutomaticZenRules("pkg1",
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "goodbye pkg1", Process.SYSTEM_UID);
+
+        // Preserved rules from pkg1 are gone; those from pkg2 are still there.
+        assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(r -> r.pkg)
+                .collect(Collectors.toSet())).containsExactly("pkg2");
+    }
+
+    @Test
+    public void testRuleCleanup() throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        Instant now = Instant.ofEpochMilli(1701796461000L);
+        Instant yesterday = now.minus(1, ChronoUnit.DAYS);
+        Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS);
+        Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS);
+        mTestClock.setNowMillis(now.toEpochMilli());
+
+        when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt()))
+                .thenReturn(new PackageInfo());
+        when(mPackageManager.getPackageInfo(eq("bad_pkg"), anyInt()))
+                .thenThrow(new PackageManager.NameNotFoundException("bad_pkg is not here"));
+
+        // Set up a config for another user containing:
+        ZenModeConfig config = new ZenModeConfig();
+        config.user = 42;
+        mZenModeHelper.mConfigs.put(42, config);
+        // okay rules (not deleted, package exists, with a range of creation dates).
+        config.automaticRules.put("ar1", newZenRule("good_pkg", now, null));
+        config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null));
+        config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null));
+        // newish rules for a missing package
+        config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null));
+        // oldish rules belonging to a missing package
+        config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null));
+        // rules deleted recently
+        config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday));
+        config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo));
+        // rules deleted a long time ago
+        config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo));
+        // rules for a missing package, created recently and deleted recently
+        config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now));
+        // rules for a missing package, created a long time ago and deleted recently
+        config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now));
+        // rules for a missing package, created a long time ago and deleted a long time ago
+        config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo));
+
+        mZenModeHelper.onUserUnlocked(42); // copies config and cleans it up.
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.keySet())
+                .containsExactly("ar1", "ar2", "ar3", "ar4");
+        assertThat(mZenModeHelper.mConfig.deletedRules.keySet())
+                .containsExactly("del1", "del2", "del4");
+    }
+
+    private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) {
+        ZenRule rule = new ZenRule();
+        rule.pkg = pkg;
+        rule.creationTime = createdAt.toEpochMilli();
+        rule.deletionInstant = deletedAt;
+        // Plus stuff so that isValidAutomaticRule() passes
+        rule.name = "A rule from " + pkg + " created on " + createdAt;
+        rule.conditionId = Uri.parse(rule.name);
+        return rule;
+    }
+
+    @Test
     public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -4366,4 +5246,25 @@
             return parser.nextTag();
         }
     }
+
+    private static class TestClock extends SimpleClock {
+        private long mNowMillis = 441644400000L;
+
+        private TestClock() {
+            super(ZoneOffset.UTC);
+        }
+
+        @Override
+        public long millis() {
+            return mNowMillis;
+        }
+
+        private void setNowMillis(long millis) {
+            mNowMillis = millis;
+        }
+
+        private void advanceByMillis(long millis) {
+            mNowMillis += millis;
+        }
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index 2f4f891c..21c96d6 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -34,6 +34,7 @@
 
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -49,6 +50,11 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    @Before
+    public final void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+    }
+
     @Test
     public void testZenPolicyApplyAllowedToDisallowed() {
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
@@ -640,6 +646,54 @@
     }
 
     @Test
+    public void testFromParcel() {
+        ZenPolicy.Builder builder = new ZenPolicy.Builder();
+        builder.setUserModifiedFields(10);
+
+        ZenPolicy policy = builder.build();
+        assertThat(policy.getUserModifiedFields()).isEqualTo(10);
+
+        Parcel parcel = Parcel.obtain();
+        policy.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel);
+        assertThat(fromParcel.getUserModifiedFields()).isEqualTo(10);
+    }
+
+    @Test
+    public void testPolicy_userModifiedFields() {
+        ZenPolicy.Builder builder = new ZenPolicy.Builder();
+        builder.setUserModifiedFields(10);
+        assertThat(builder.build().getUserModifiedFields()).isEqualTo(10);
+
+        builder.setUserModifiedFields(0);
+        assertThat(builder.build().getUserModifiedFields()).isEqualTo(0);
+    }
+
+    @Test
+    public void testPolicyBuilder_constructFromPolicy() {
+        ZenPolicy.Builder builder = new ZenPolicy.Builder();
+        ZenPolicy policy = builder.allowRepeatCallers(true).allowAlarms(false)
+                .showLights(true).showBadges(false)
+                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .setUserModifiedFields(20).build();
+
+        ZenPolicy newPolicy = new ZenPolicy.Builder(policy).build();
+
+        assertThat(newPolicy.getPriorityCategoryAlarms()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(newPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(newPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(ZenPolicy.STATE_ALLOW);
+
+        assertThat(newPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
+
+        assertThat(newPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(newPolicy.getUserModifiedFields()).isEqualTo(20);
+    }
+
+    @Test
     public void testTooLongLists_fromParcel() {
         ArrayList<Integer> longList = new ArrayList<Integer>(50);
         for (int i = 0; i < 50; i++) {
diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
new file mode 100644
index 0000000..c3da903
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static android.os.PowerManager.WAKE_REASON_CAMERA_LAUNCH;
+import static android.os.PowerManager.WAKE_REASON_LID;
+import static android.os.PowerManager.WAKE_REASON_GESTURE;
+import static android.os.PowerManager.WAKE_REASON_POWER_BUTTON;
+import static android.os.PowerManager.WAKE_REASON_WAKE_KEY;
+import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION;
+import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
+import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.KeyEvent.KEYCODE_HOME;
+import static android.view.KeyEvent.KEYCODE_POWER;
+import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY;
+
+import static com.android.internal.R.bool.config_allowTheaterModeWakeFromKey;
+import static com.android.internal.R.bool.config_allowTheaterModeWakeFromPowerKey;
+import static com.android.internal.R.bool.config_allowTheaterModeWakeFromMotion;
+import static com.android.internal.R.bool.config_allowTheaterModeWakeFromCameraLens;
+import static com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch;
+import static com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture;
+import static com.android.server.policy.Flags.FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Resources;
+import android.os.PowerManager;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.os.Clock;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
+import com.android.server.LocalServices;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.function.BooleanSupplier;
+/**
+ * Test class for {@link WindowWakeUpPolicy}.
+ *
+ * <p>Build/Install/Run: atest WmTests:WindowWakeUpPolicyTests
+ */
+public final class WindowWakeUpPolicyTests {
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Mock PowerManager mPowerManager;
+    @Mock Clock mClock;
+    @Mock WindowWakeUpPolicyInternal.InputWakeUpDelegate mInputWakeUpDelegate;
+
+    private Context mContextSpy;
+    private Resources mResourcesSpy;
+
+    private WindowWakeUpPolicy mPolicy;
+
+    @Before
+    public void setUp() {
+        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+        mResourcesSpy = spy(mContextSpy.getResources());
+        when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
+        when(mContextSpy.getSystemService(PowerManager.class)).thenReturn(mPowerManager);
+        LocalServices.removeServiceForTest(WindowWakeUpPolicyInternal.class);
+    }
+
+    @Test
+    public void testSupportsInputWakeDelegatse_publishesLocalService() {
+        mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
+
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+
+        assertThat(LocalServices.getService(WindowWakeUpPolicyInternal.class)).isNotNull();
+    }
+
+    @Test
+    public void testDoesNotSupportInputWakeDelegatse_doesNotPublishLocalService() {
+        mSetFlagsRule.disableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
+
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+
+        assertThat(LocalServices.getService(WindowWakeUpPolicyInternal.class)).isNull();
+    }
+
+    @Test
+    public void testMotionWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
+        setTheaterModeEnabled(false);
+        mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        LocalServices.getService(WindowWakeUpPolicyInternal.class)
+                .setInputWakeUpDelegate(mInputWakeUpDelegate);
+
+        setDelegatedMotionWakeUpResult(true);
+
+        // Verify the policy wake up call succeeds because of the call on the delegate, and not
+        // because of a PowerManager wake up.
+        assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isTrue();
+        verify(mInputWakeUpDelegate).wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true);
+        verifyNoPowerManagerWakeUp();
+
+        setDelegatedMotionWakeUpResult(false);
+
+        // Verify the policy wake up call succeeds because of the PowerManager wake up, since the
+        // delegate would not handle the wake up request.
+        assertThat(mPolicy.wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false)).isTrue();
+        verify(mInputWakeUpDelegate).wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false);
+        verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
+    }
+
+    @Test
+    public void testKeyWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
+        setTheaterModeEnabled(false);
+        mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        LocalServices.getService(WindowWakeUpPolicyInternal.class)
+                .setInputWakeUpDelegate(mInputWakeUpDelegate);
+
+        setDelegatedKeyWakeUpResult(true);
+
+        // Verify the policy wake up call succeeds because of the call on the delegate, and not
+        // because of a PowerManager wake up.
+        assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isTrue();
+        verify(mInputWakeUpDelegate).wakeUpFromKey(200, KEYCODE_POWER, true);
+        verifyNoPowerManagerWakeUp();
+
+        setDelegatedKeyWakeUpResult(false);
+
+        // Verify the policy wake up call succeeds because of the PowerManager wake up, since the
+        // delegate would not handle the wake up request.
+        assertThat(mPolicy.wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false)).isTrue();
+        verify(mInputWakeUpDelegate).wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false);
+        verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_KEY, "android.policy:KEY");
+    }
+
+    @Test
+    public void testDelegatedKeyWakeIsSubjectToPolicyChecks() {
+        mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
+        setDelegatedKeyWakeUpResult(true);
+        setTheaterModeEnabled(true);
+        setBooleanRes(config_allowTheaterModeWakeFromKey, false);
+        setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        LocalServices.getService(WindowWakeUpPolicyInternal.class)
+                .setInputWakeUpDelegate(mInputWakeUpDelegate);
+
+        // Check that the wake up does not happen because the theater mode policy check fails.
+        assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isFalse();
+        verify(mInputWakeUpDelegate, never()).wakeUpFromKey(anyLong(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void testDelegatedMotionWakeIsSubjectToPolicyChecks() {
+        mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
+        setDelegatedMotionWakeUpResult(true);
+        setTheaterModeEnabled(true);
+        setBooleanRes(config_allowTheaterModeWakeFromMotion, false);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        LocalServices.getService(WindowWakeUpPolicyInternal.class)
+                .setInputWakeUpDelegate(mInputWakeUpDelegate);
+
+        // Check that the wake up does not happen because the theater mode policy check fails.
+        assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isFalse();
+        verify(mInputWakeUpDelegate, never()).wakeUpFromMotion(anyLong(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void testWakeUpFromMotion() {
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromMotion(mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true),
+                config_allowTheaterModeWakeFromMotion,
+                WAKE_REASON_WAKE_MOTION,
+                "android.policy:MOTION");
+    }
+
+    @Test
+    public void testWakeUpFromKey_nonPowerKey() {
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_HOME, true),
+                config_allowTheaterModeWakeFromKey,
+                WAKE_REASON_WAKE_KEY,
+                "android.policy:KEY");
+    }
+
+    @Test
+    public void testWakeUpFromKey_powerKey() {
+        // Disable the resource affecting all wake keys because it affects power key as well.
+        // That way, power key wake during theater mode will solely be controlled by
+        // `config_allowTheaterModeWakeFromPowerKey` in the checks.
+        setBooleanRes(config_allowTheaterModeWakeFromKey, false);
+
+        // Test with power key
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, true),
+                config_allowTheaterModeWakeFromPowerKey,
+                WAKE_REASON_POWER_BUTTON,
+                "android.policy:POWER");
+
+        // Test that power key wake ups happen during theater mode as long as wake-keys are allowed
+        // even if the power-key specific theater mode config is disabled.
+        setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false);
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, false),
+                config_allowTheaterModeWakeFromKey,
+                WAKE_REASON_POWER_BUTTON,
+                "android.policy:POWER");
+    }
+
+    @Test
+    public void testWakeUpFromLid() {
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromLid(),
+                config_allowTheaterModeWakeFromLidSwitch,
+                WAKE_REASON_LID,
+                "android.policy:LID");
+    }
+
+    @Test
+    public void testWakeUpFromWakeGesture() {
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromWakeGesture(),
+                config_allowTheaterModeWakeFromGesture,
+                WAKE_REASON_GESTURE,
+                "android.policy:GESTURE");
+    }
+
+    @Test
+    public void testwakeUpFromCameraCover() {
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromCameraCover(mClock.uptimeMillis()),
+                config_allowTheaterModeWakeFromCameraLens,
+                WAKE_REASON_CAMERA_LAUNCH,
+                "android.policy:CAMERA_COVER");
+    }
+
+    @Test
+    public void testWakeUpFromPowerKeyCameraGesture() {
+        // Disable the resource affecting all wake keys because it affects power key as well.
+        // That way, power key wake during theater mode will solely be controlled by
+        // `config_allowTheaterModeWakeFromPowerKey` in the checks.
+        setBooleanRes(config_allowTheaterModeWakeFromKey, false);
+
+        runPowerManagerUpChecks(
+                () -> mPolicy.wakeUpFromPowerKeyCameraGesture(),
+                config_allowTheaterModeWakeFromPowerKey,
+                WAKE_REASON_CAMERA_LAUNCH,
+                "android.policy:CAMERA_GESTURE_PREVENT_LOCK");
+    }
+
+    private void runPowerManagerUpChecks(
+            BooleanSupplier wakeUpCall,
+            int theatherModeWakeResId,
+            int expectedWakeReason,
+            String expectedWakeDetails) {
+        // Test under theater mode enabled.
+        setTheaterModeEnabled(true);
+
+        Mockito.reset(mPowerManager);
+        setBooleanRes(theatherModeWakeResId, true);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        setUptimeMillis(200);
+        assertWithMessage("Wake should happen in theater mode when config allows it.")
+                .that(wakeUpCall.getAsBoolean()).isTrue();
+        verify(mPowerManager).wakeUp(200L, expectedWakeReason, expectedWakeDetails);
+
+        Mockito.reset(mPowerManager);
+        setBooleanRes(theatherModeWakeResId, false);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        setUptimeMillis(250);
+        assertWithMessage("Wake should not happen in theater mode when config disallows it.")
+                .that(wakeUpCall.getAsBoolean()).isFalse();
+        verifyNoPowerManagerWakeUp();
+
+        // Cases when theater mode is disabled.
+        setTheaterModeEnabled(false);
+
+        Mockito.reset(mPowerManager);
+        setBooleanRes(theatherModeWakeResId, true);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        setUptimeMillis(300);
+        assertWithMessage("Wake should happen when not in theater mode.")
+                .that(wakeUpCall.getAsBoolean()).isTrue();
+        verify(mPowerManager).wakeUp(300L, expectedWakeReason, expectedWakeDetails);
+
+        Mockito.reset(mPowerManager);
+        setBooleanRes(theatherModeWakeResId, false);
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+        setUptimeMillis(350);
+        assertWithMessage("Wake should happen when not in theater mode.")
+                .that(wakeUpCall.getAsBoolean()).isTrue();
+        verify(mPowerManager).wakeUp(350L, expectedWakeReason, expectedWakeDetails);
+    }
+
+    private void verifyNoPowerManagerWakeUp() {
+        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+    }
+
+    private void setBooleanRes(int resId, boolean val) {
+        when(mResourcesSpy.getBoolean(resId)).thenReturn(val);
+    }
+
+    private void setUptimeMillis(long uptimeMillis) {
+        when(mClock.uptimeMillis()).thenReturn(uptimeMillis);
+    }
+
+    private void setTheaterModeEnabled(boolean enabled) {
+        Settings.Global.putInt(
+                mContextSpy.getContentResolver(), Settings.Global.THEATER_MODE_ON, enabled ? 1 : 0);
+    }
+
+    private void setDelegatedMotionWakeUpResult(boolean result) {
+        when(mInputWakeUpDelegate.wakeUpFromMotion(anyLong(), anyInt(), anyBoolean()))
+                .thenReturn(result);
+    }
+
+    private void setDelegatedKeyWakeUpResult(boolean result) {
+        when(mInputWakeUpDelegate.wakeUpFromKey(anyLong(), anyInt(), anyBoolean()))
+                .thenReturn(result);
+    }
+}
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 f049b33..9bb2da0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -143,7 +143,6 @@
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
-import android.util.MergedConfiguration;
 import android.util.MutableBoolean;
 import android.view.DisplayInfo;
 import android.view.IRemoteAnimationFinishedCallback;
@@ -395,8 +394,7 @@
         activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
-        activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
-                activity.getConfiguration()));
+        activity.setLastReportedConfiguration(new Configuration(), activity.getConfiguration());
 
         activity.info.configChanges &= ~CONFIG_ORIENTATION;
         final Configuration newConfig = new Configuration(task.getConfiguration());
@@ -420,8 +418,7 @@
         activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
-        activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
-                activity.getConfiguration()));
+        activity.setLastReportedConfiguration(new Configuration(), activity.getConfiguration());
 
         activity.info.configChanges &= ~CONFIG_ORIENTATION;
         final Configuration newConfig = new Configuration(task.getConfiguration());
@@ -447,8 +444,7 @@
         activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
-        activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
-                activity.getConfiguration()));
+        activity.setLastReportedConfiguration(new Configuration(), activity.getConfiguration());
 
         activity.info.configChanges &= ~CONFIG_ORIENTATION;
         final Configuration newConfig = new Configuration(task.getConfiguration());
@@ -468,8 +464,7 @@
         activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
-        activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
-                activity.getConfiguration()));
+        activity.setLastReportedConfiguration(new Configuration(), activity.getConfiguration());
 
         activity.info.configChanges &= ~ActivityInfo.CONFIG_FONT_SCALE;
         final Configuration newConfig = new Configuration(task.getConfiguration());
@@ -571,8 +566,7 @@
                 .build();
         activity.setState(RESUMED, "Testing");
 
-        activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
-                activity.getConfiguration()));
+        activity.setLastReportedConfiguration(new Configuration(), activity.getConfiguration());
 
         clearInvocations(mClientLifecycleManager);
 
@@ -799,8 +793,7 @@
             doReturn(false).when(stack).isTranslucent(any());
             assertTrue(task.shouldBeVisible(null /* starting */));
 
-            activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
-                    activity.getConfiguration()));
+            activity.setLastReportedConfiguration(new Configuration(), activity.getConfiguration());
 
             final Configuration newConfig = new Configuration(activity.getConfiguration());
             final int shortSide = newConfig.screenWidthDp == newConfig.screenHeightDp
@@ -846,7 +839,7 @@
     }
 
     @Test
-    public void testTakeOptions() {
+    public void testTakeSceneTransitionInfo() {
         final ActivityRecord activity = createActivityWithTask();
         ActivityOptions opts = ActivityOptions.makeRemoteAnimation(
                 new RemoteAnimationAdapter(new Stub() {
@@ -864,7 +857,9 @@
                     }
                 }, 0, 0));
         activity.updateOptionsLocked(opts);
-        assertNotNull(activity.takeOptions());
+        // Ensure the SceneTransitionInfo is null (since the ActivityOptions is for remote
+        // animation and AR#takeSceneTransitionInfo also clear the AR#mPendingOptions
+        assertNull(activity.takeSceneTransitionInfo());
         assertNull(activity.getOptions());
 
         final AppTransition appTransition = activity.mDisplayContent.mAppTransition;
@@ -3314,7 +3309,7 @@
         // keyguard to back to the app, expect IME insets is not frozen
         app.mActivityRecord.commitVisibility(true, false);
         mDisplayContent.updateImeInputAndControlTarget(app);
-        mDisplayContent.mWmService.mRoot.performSurfacePlacement();
+        performSurfacePlacementAndWaitForWindowAnimator();
 
         assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
 
@@ -3363,7 +3358,7 @@
         mDisplayContent.setImeLayeringTarget(app2);
         app2.mActivityRecord.commitVisibility(true, false);
         mDisplayContent.updateImeInputAndControlTarget(app2);
-        mDisplayContent.mWmService.mRoot.performSurfacePlacement();
+        performSurfacePlacementAndWaitForWindowAnimator();
 
         // Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets
         // to client if the app didn't request IME visible.
@@ -3417,7 +3412,7 @@
         // frozen until the input started.
         mDisplayContent.setImeLayeringTarget(app1);
         mDisplayContent.updateImeInputAndControlTarget(app1);
-        mDisplayContent.mWmService.mRoot.performSurfacePlacement();
+        performSurfacePlacementAndWaitForWindowAnimator();
 
         assertEquals(app1, mDisplayContent.getImeInputTarget());
         assertFalse(activity1.mImeInsetsFrozenUntilStartInput);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
index 98f1843..03d3029 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
@@ -17,13 +17,31 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 
+import android.content.ComponentName;
+import android.graphics.ColorSpace;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
 import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+import android.view.Surface;
+import android.window.TaskSnapshot;
 
 import androidx.test.filters.SmallTest;
 
@@ -32,6 +50,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * Test class for {@link ActivitySnapshotController}.
@@ -45,6 +64,7 @@
 public class ActivitySnapshotControllerTests extends WindowTestsBase {
 
     private ActivitySnapshotController mActivitySnapshotController;
+
     @Before
     public void setUp() throws Exception {
         spyOn(mWm.mSnapshotController.mActivitySnapshotController);
@@ -154,4 +174,90 @@
         assertEquals(openingWindowBelow.mActivityRecord,
                 mActivitySnapshotController.mPendingLoadActivity.valueAt(0));
     }
+
+    /**
+     * Simulate multiple TaskFragments inside a task.
+     */
+    @Test
+    public void testMultipleActivitiesLoadSnapshot() {
+        final Task testTask = createTask(mDisplayContent);
+        final ActivityRecord activityA = createActivityRecord(testTask);
+        final ActivityRecord activityB = createActivityRecord(testTask);
+        final ActivityRecord activityC = createActivityRecord(testTask);
+        final TaskSnapshot taskSnapshot = createSnapshot();
+
+        final int[] mixedCode = new int[3];
+        mixedCode[0] = ActivitySnapshotController.getSystemHashCode(activityA);
+        mixedCode[1] = ActivitySnapshotController.getSystemHashCode(activityB);
+        mixedCode[2] = ActivitySnapshotController.getSystemHashCode(activityC);
+
+        mActivitySnapshotController.addUserSavedFile(testTask.mUserId, taskSnapshot, mixedCode);
+        mActivitySnapshotController.mCache.putSnapshot(activityA, taskSnapshot);
+        mActivitySnapshotController.mCache.putSnapshot(activityB, taskSnapshot);
+        mActivitySnapshotController.mCache.putSnapshot(activityC, taskSnapshot);
+
+        assertTrue(mActivitySnapshotController.hasRecord(activityA));
+        assertTrue(mActivitySnapshotController.hasRecord(activityB));
+
+        // If A is removed, B and C should also be removed because they share the same snapshot.
+        mActivitySnapshotController.onAppRemoved(activityA);
+        assertFalse(mActivitySnapshotController.hasRecord(activityA));
+        assertFalse(mActivitySnapshotController.hasRecord(activityB));
+        final ActivityRecord[] singleActivityList = new ActivityRecord[1];
+        singleActivityList[0] = activityA;
+        assertNull(mActivitySnapshotController.getSnapshot(singleActivityList));
+        singleActivityList[0] = activityB;
+        assertNull(mActivitySnapshotController.getSnapshot(singleActivityList));
+        final ActivityRecord[] activities = new ActivityRecord[3];
+        activities[0] = activityA;
+        activities[1] = activityB;
+        activities[2] = activityC;
+        assertNull(mActivitySnapshotController.getSnapshot(activities));
+
+        // Reset and test load snapshot
+        mActivitySnapshotController.addUserSavedFile(testTask.mUserId, taskSnapshot, mixedCode);
+        // Request to load by B, nothing will be loaded because the snapshot was [A,B,C].
+        mActivitySnapshotController.mPendingLoadActivity.add(activityB);
+        mActivitySnapshotController.loadActivitySnapshot();
+        verify(mActivitySnapshotController, never()).loadSnapshotInner(any(), any());
+
+        // Able to load snapshot when requesting for all A, B, C
+        mActivitySnapshotController.mPendingLoadActivity.clear();
+        mActivitySnapshotController.mPendingLoadActivity.add(activityA);
+        mActivitySnapshotController.mPendingLoadActivity.add(activityB);
+        mActivitySnapshotController.mPendingLoadActivity.add(activityC);
+        final ArraySet<ActivityRecord> verifyList = new ArraySet<>();
+        verifyList.add(activityA);
+        verifyList.add(activityB);
+        verifyList.add(activityC);
+        mActivitySnapshotController.loadActivitySnapshot();
+        verify(mActivitySnapshotController).loadSnapshotInner(argThat(
+                argument -> {
+                    final ArrayList<ActivityRecord> argumentList = new ArrayList<>(
+                            Arrays.asList(argument));
+                    return verifyList.containsAll(argumentList)
+                            && argumentList.containsAll(verifyList);
+                }),
+                any());
+
+        for (int i = activities.length - 1; i >= 0; --i) {
+            mActivitySnapshotController.mCache.putSnapshot(activities[i], taskSnapshot);
+        }
+        // The loaded snapshot can be retrieved only if the activities match exactly.
+        singleActivityList[0] = activityB;
+        assertNull(mActivitySnapshotController.getSnapshot(singleActivityList));
+        assertEquals(taskSnapshot, mActivitySnapshotController.getSnapshot(activities));
+    }
+
+    private TaskSnapshot createSnapshot() {
+        HardwareBuffer buffer = mock(HardwareBuffer.class);
+        doReturn(100).when(buffer).getWidth();
+        doReturn(100).when(buffer).getHeight();
+        return new TaskSnapshot(1, 0 /* captureTime */, new ComponentName("", ""), buffer,
+                ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
+                Surface.ROTATION_0, new Point(100, 100), new Rect() /* contentInsets */,
+                new Rect() /* letterboxInsets*/, false /* isLowResolution */,
+                true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, 0 /* mSystemUiVisibility */,
+                false /* isTranslucent */, false /* hasImeSurface */);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index ac18f80..402cbcc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -464,9 +464,10 @@
         // Simulate ActivityOptions#makeSceneTransitionAnimation
         final Bundle myBundle = new Bundle();
         myBundle.putInt(ActivityOptions.KEY_ANIM_TYPE, ANIM_SCENE_TRANSITION);
-        myBundle.putParcelable("android:activity.transitionCompleteListener",
-                mock(android.os.ResultReceiver.class));
         final ActivityOptions options = new ActivityOptions(myBundle);
+        final ActivityOptions.SceneTransitionInfo info = new ActivityOptions.SceneTransitionInfo();
+        info.setResultReceiver(mock(android.os.ResultReceiver.class));
+        options.setSceneTransitionInfo(info);
 
         final ActivityRecord testActivity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
@@ -741,7 +742,7 @@
 
         MockitoSession mockitoSession = mockitoSession().mockStatic(BackNavigationController.class)
                 .strictness(Strictness.LENIENT).startMocking();
-        doReturn(taskSnapshot).when(() -> BackNavigationController.getSnapshot(any()));
+        doReturn(taskSnapshot).when(() -> BackNavigationController.getSnapshot(any(), any()));
         when(resourcesSpy.getBoolean(
                 com.android.internal.R.bool.config_predictShowStartingSurface))
                 .thenReturn(preferWindowlessSurface);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index dfa595c..99d354a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -55,6 +55,11 @@
 @RunWith(WindowTestRunner.class)
 public class DisplayContentDeferredUpdateTests extends WindowTestsBase {
 
+    // The fields to override the current DisplayInfo.
+    private String mUniqueId;
+    private int mColorMode;
+    private int mLogicalDensityDpi;
+
     @Override
     protected void onBeforeSystemServicesCreated() {
         // Set other flags to their default values
@@ -73,7 +78,7 @@
     public void testUpdate_deferrableFieldChangedTransitionStarted_deferrableFieldUpdated() {
         performInitialDisplayUpdate();
 
-        givenDisplayInfo(/* uniqueId= */ "old");
+        mUniqueId = "old";
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
 
@@ -82,11 +87,21 @@
         verify(onUpdated).run();
         clearInvocations(mDisplayContent.mTransitionController, onUpdated);
 
-        givenDisplayInfo(/* uniqueId= */ "new");
+        mUniqueId = "new";
         mDisplayContent.requestDisplayUpdate(onUpdated);
         captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true);
         verify(onUpdated).run();
+        verify(mDisplayContent.mTransitionController).requestStartTransition(
+                any(), any(), any(), any());
         assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("new");
+        clearInvocations(mDisplayContent.mTransitionController, onUpdated);
+
+        mLogicalDensityDpi += 100;
+        mDisplayContent.requestDisplayUpdate(onUpdated);
+        captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true);
+        verify(onUpdated).run();
+        verify(mDisplayContent.mTransitionController).requestStartTransition(
+                any(), any(), any(), any());
     }
 
     @Test
@@ -94,7 +109,8 @@
         performInitialDisplayUpdate();
 
         // Update only color mode (non-deferrable field) and keep the same unique id
-        givenDisplayInfo(/* uniqueId= */ "initial_unique_id", /* colorMode= */ 123);
+        mUniqueId = "initial_unique_id";
+        mColorMode = 123;
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
 
@@ -107,7 +123,8 @@
         performInitialDisplayUpdate();
 
         // Update only color mode (non-deferrable field) and keep the same unique id
-        givenDisplayInfo(/* uniqueId= */ "initial_unique_id", /* colorMode= */ 123);
+        mUniqueId = "initial_unique_id";
+        mColorMode = 123;
         mDisplayContent.requestDisplayUpdate(mock(Runnable.class));
 
         assertThat(mDisplayContent.getDisplayInfo().colorMode).isEqualTo(123);
@@ -116,7 +133,7 @@
 
         // Update unique id (deferrable field), keep the same color mode,
         // this update should be deferred
-        givenDisplayInfo(/* uniqueId= */ "new_unique_id", /* colorMode= */ 123);
+        mUniqueId = "new_unique_id";
         mDisplayContent.requestDisplayUpdate(mock(Runnable.class));
 
         assertThat(mDisplayContent.getDisplayInfo().colorMode).isEqualTo(123);
@@ -126,7 +143,7 @@
         // Update color mode again and keep the same unique id, color mode update
         // should not be deferred, unique id update is still deferred as transition
         // has not started collecting yet
-        givenDisplayInfo(/* uniqueId= */ "new_unique_id", /* colorMode= */ 456);
+        mColorMode = 456;
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
 
@@ -146,14 +163,14 @@
     @Test
     public void testUpdate_deferrableFieldUpdatedTransitionPending_fieldNotUpdated() {
         performInitialDisplayUpdate();
-        givenDisplayInfo(/* uniqueId= */ "old");
+        mUniqueId = "old";
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
         captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true);
         verify(onUpdated).run();
         clearInvocations(mDisplayContent.mTransitionController, onUpdated);
 
-        givenDisplayInfo(/* uniqueId= */ "new");
+        mUniqueId = "new";
         mDisplayContent.requestDisplayUpdate(onUpdated);
 
         captureStartTransitionCollection(); // do not continue by not starting the collection
@@ -164,7 +181,7 @@
     @Test
     public void testTwoDisplayUpdates_transitionStarted_displayUpdated() {
         performInitialDisplayUpdate();
-        givenDisplayInfo(/* uniqueId= */ "old");
+        mUniqueId = "old";
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
         captureStartTransitionCollection().getValue()
@@ -173,10 +190,10 @@
         clearInvocations(mDisplayContent.mTransitionController, onUpdated);
 
         // Perform two display updates while WM is 'busy'
-        givenDisplayInfo(/* uniqueId= */ "new1");
+        mUniqueId = "new1";
         Runnable onUpdated1 = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated1);
-        givenDisplayInfo(/* uniqueId= */ "new2");
+        mUniqueId = "new2";
         Runnable onUpdated2 = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated2);
 
@@ -215,22 +232,19 @@
         return callbackCaptor;
     }
 
-    private void givenDisplayInfo(String uniqueId) {
-        givenDisplayInfo(uniqueId, /* colorMode= */ 0);
-    }
+    private void performInitialDisplayUpdate() {
+        mUniqueId = "initial_unique_id";
+        mColorMode = 0;
+        mLogicalDensityDpi = 400;
 
-    private void givenDisplayInfo(String uniqueId, int colorMode) {
         spyOn(mDisplayContent.mDisplay);
         doAnswer(invocation -> {
             DisplayInfo info = invocation.getArgument(0);
-            info.uniqueId = uniqueId;
-            info.colorMode = colorMode;
+            info.uniqueId = mUniqueId;
+            info.colorMode = mColorMode;
+            info.logicalDensityDpi = mLogicalDensityDpi;
             return null;
         }).when(mDisplayContent.mDisplay).getDisplayInfo(any());
-    }
-
-    private void performInitialDisplayUpdate() {
-        givenDisplayInfo(/* uniqueId= */ "initial_unique_id", /* colorMode= */ 0);
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 28fecd6..6013063 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -76,7 +76,6 @@
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
-import android.util.MergedConfiguration;
 import android.util.Pair;
 
 import androidx.test.filters.MediumTest;
@@ -545,8 +544,7 @@
         assertNotEquals(activity.getConfiguration().orientation, rotatedConfig.orientation);
         // Assume the activity was shown in different orientation. For example, the top activity is
         // landscape and the portrait lockscreen is shown.
-        activity.setLastReportedConfiguration(
-                new MergedConfiguration(mAtm.getGlobalConfiguration(), rotatedConfig));
+        activity.setLastReportedConfiguration(mAtm.getGlobalConfiguration(), rotatedConfig);
         activity.setState(STOPPED, "sleep");
 
         display.setIsSleeping(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java
new file mode 100644
index 0000000..71dbc57
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.SensitiveContentPackages.PackageInfo;
+
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Build/Install/Run:
+ *  atest WmTests:SensitiveContentPackagesTest
+ */
+@SmallTest
+@Presubmit
+public class SensitiveContentPackagesTest {
+    private static final String APP_PKG_1 = "com.android.server.wm.one";
+    private static final String APP_PKG_2 = "com.android.server.wm.two";
+    private static final String APP_PKG_3 = "com.android.server.wm.three";
+
+    private static final int APP_UID_1 = 5;
+    private static final int APP_UID_2 = 6;
+    private static final int APP_UID_3 = 7;
+
+
+    private final SensitiveContentPackages mSensitiveContentPackages =
+            new SensitiveContentPackages();
+
+    @After
+    public void tearDown() {
+        mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
+    @Test
+    public void setShouldBlockScreenCaptureForApp() {
+        Set<PackageInfo> blockedApps =
+                Set.of(new PackageInfo(APP_PKG_1, APP_UID_1),
+                        new PackageInfo(APP_PKG_1, APP_UID_2),
+                        new PackageInfo(APP_PKG_2, APP_UID_1),
+                        new PackageInfo(APP_PKG_2, APP_UID_2));
+
+        mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedApps);
+
+        assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1));
+        assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3));
+
+        assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1));
+        assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3));
+
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3));
+    }
+
+    @Test
+    public void setShouldBlockScreenCaptureForApp_empty() {
+        Set<PackageInfo> blockedApps =
+                Set.of(new PackageInfo(APP_PKG_1, APP_UID_1),
+                        new PackageInfo(APP_PKG_1, APP_UID_2),
+                        new PackageInfo(APP_PKG_2, APP_UID_1),
+                        new PackageInfo(APP_PKG_2, APP_UID_2));
+
+        mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedApps);
+        mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet());
+
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3));
+
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3));
+
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2));
+        assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3));
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
index 8bd5473..2d3c4bb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
@@ -28,6 +28,7 @@
 import android.os.Looper;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
+import android.util.Log;
 
 import androidx.test.filters.MediumTest;
 
@@ -147,6 +148,8 @@
     private void setExceptionListAndWaitForCallback(String commaSeparatedList) {
         CountDownLatch latch = new CountDownLatch(1);
         mOnUpdateDeviceConfig = rawList -> {
+            Log.i(getClass().getSimpleName(), "updateDeviceConfig expected="
+                    + commaSeparatedList + " actual=" + rawList);
             if (commaSeparatedList.equals(rawList)) {
                 latch.countDown();
             }
@@ -155,7 +158,7 @@
                 KEY_SPLASH_SCREEN_EXCEPTION_LIST, commaSeparatedList, false);
         try {
             assertTrue("Timed out waiting for DeviceConfig to be updated.",
-                    latch.await(1, TimeUnit.SECONDS));
+                    latch.await(5, TimeUnit.SECONDS));
         } catch (InterruptedException e) {
             Assert.fail(e.getMessage());
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index 810cbe8..6c5f975 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -134,6 +135,33 @@
         assertFalse(r.isSyncFinished(r.getSyncGroup()));
         r.finishRelaunching();
         assertTrue(r.isSyncFinished(r.getSyncGroup()));
+        assertEquals(SYNC_STATE_READY, r.mSyncState);
+
+        // If the container has finished the sync, isSyncFinished should not change the sync state.
+        final BLASTSyncEngine.SyncGroup syncGroup = r.getSyncGroup();
+        r.finishSync(mTransaction, syncGroup, false /* cancel */);
+        assertEquals(SYNC_STATE_NONE, r.mSyncState);
+        assertTrue(r.isSyncFinished(syncGroup));
+        assertEquals(SYNC_STATE_NONE, r.mSyncState);
+    }
+
+    @Test
+    public void testFinishSyncByStartingWindow() {
+        final ActivityRecord taskRoot = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Task task = taskRoot.getTask();
+        final ActivityRecord translucentTop = new ActivityBuilder(mAtm).setTask(task)
+                .setActivityTheme(android.R.style.Theme_Translucent).build();
+        createWindow(null, TYPE_BASE_APPLICATION, taskRoot, "win");
+        final WindowState startingWindow = createWindow(null, TYPE_APPLICATION_STARTING,
+                translucentTop, "starting");
+        startingWindow.mStartingData = new SnapshotStartingData(mWm, null, 0);
+        task.mSharedStartingData = startingWindow.mStartingData;
+        task.prepareSync();
+
+        final BLASTSyncEngine.SyncGroup group = mock(BLASTSyncEngine.SyncGroup.class);
+        assertFalse(task.isSyncFinished(group));
+        startingWindow.onSyncFinishedDrawing();
+        assertTrue(task.isSyncFinished(group));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index e9fe4bb..22ddf84 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -53,6 +53,7 @@
 import android.os.Binder;
 import android.platform.test.annotations.Presubmit;
 import android.view.SurfaceControl;
+import android.view.View;
 import android.window.ITaskFragmentOrganizer;
 import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentInfo;
@@ -695,4 +696,75 @@
         mTaskFragment.getDimBounds(dimBounds);
         assertEquals(taskFragmentBounds, dimBounds);
     }
+
+    @Test
+    public void testMoveFocusToAdjacentWindow() {
+        // Setup two activities in ActivityEmbedding split.
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment taskFragmentLeft = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(2)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+        final TaskFragment taskFragmentRight = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(1)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+        taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight);
+        taskFragmentLeft.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+        taskFragmentRight.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+        task.setBounds(0, 0, 1200, 1000);
+        taskFragmentLeft.setBounds(0, 0, 600, 1000);
+        taskFragmentRight.setBounds(600, 0, 1200, 1000);
+        final ActivityRecord appLeftTop = taskFragmentLeft.getTopMostActivity();
+        final ActivityRecord appLeftBottom = taskFragmentLeft.getBottomMostActivity();
+        final ActivityRecord appRightTop = taskFragmentRight.getTopMostActivity();
+        appLeftTop.setVisibleRequested(true);
+        appRightTop.setVisibleRequested(true);
+        final WindowState winLeftTop = createAppWindow(appLeftTop, "winLeftTop");
+        final WindowState winLeftBottom = createAppWindow(appLeftBottom, "winLeftBottom");
+        final WindowState winRightTop = createAppWindow(appRightTop, "winRightTop");
+        winLeftTop.setHasSurface(true);
+        winRightTop.setHasSurface(true);
+
+        taskFragmentLeft.setResumedActivity(appLeftTop, "test");
+        taskFragmentRight.setResumedActivity(appRightTop, "test");
+        appLeftTop.setState(RESUMED, "test");
+        appRightTop.setState(RESUMED, "test");
+        mDisplayContent.mFocusedApp = appRightTop;
+
+        // Make the appLeftTop be the focused activity and ensure the focused app is updated.
+        appLeftTop.moveFocusableActivityToTop("test");
+        assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
+
+        // Send request from a non-focused window with valid direction.
+        assertFalse(mWm.moveFocusToAdjacentWindow(null, winLeftBottom.mClient, View.FOCUS_RIGHT));
+        // The focus should remain the same.
+        assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
+
+        // Send request from the focused window with valid direction.
+        assertTrue(mWm.moveFocusToAdjacentWindow(null, winLeftTop.mClient, View.FOCUS_RIGHT));
+        // The focus should change.
+        assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+
+        // Send request from the focused window with invalid direction.
+        assertFalse(mWm.moveFocusToAdjacentWindow(null, winRightTop.mClient, View.FOCUS_UP));
+        // The focus should remain the same.
+        assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+
+        // Send request from the focused window with valid direction.
+        assertTrue(mWm.moveFocusToAdjacentWindow(null, winRightTop.mClient, View.FOCUS_BACKWARD));
+        // The focus should change.
+        assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
+    }
+
+    private WindowState createAppWindow(ActivityRecord app, String name) {
+        final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, app, name,
+                0 /* ownerId */, false /* ownerCanAddInternalSystemWindow */, new TestIWindow());
+        mWm.mWindowMap.put(win.mClient.asBinder(), win);
+        return win;
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 45e1e95..b360800 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -43,6 +43,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.policy.WindowManagerPolicy.USER_ROTATION_FREE;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
+import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_PARENT_TASK;
 import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -1620,6 +1621,29 @@
     }
 
     @Test
+    public void testBoostDimmingTaskFragmentOnTask() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment primary = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final TaskFragment secondary = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+
+        primary.mVisibleRequested = true;
+        secondary.mVisibleRequested = true;
+        primary.setAdjacentTaskFragment(secondary);
+        secondary.setAdjacentTaskFragment(primary);
+        primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK);
+        doReturn(true).when(primary).shouldBoostDimmer();
+        task.assignChildLayers(t);
+
+        // The layers are initially assigned via the hierarchy, but the primary will be boosted and
+        // assigned again to above of the secondary.
+        verify(primary).assignLayer(t, 0);
+        verify(secondary).assignLayer(t, 1);
+        verify(primary).assignLayer(t, 2);
+    }
+
+    @Test
     public void testMoveOrCreateDecorSurface() {
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final Task task =  new TaskBuilder(mSupervisor).setCreateActivity(true).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 52e2d8a..7551b165 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -330,6 +330,10 @@
     }
 
     @Override
+    public void showDismissibleKeyguard() {
+    }
+
+    @Override
     public void setPipVisibilityLw(boolean visible) {
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 0514943..51df1d4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -195,6 +195,36 @@
     }
 
     @Test
+    public void testCreateInfo_Activity() {
+        final Transition transition = createTestTransition(TRANSIT_OPEN);
+        ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+        ArraySet<WindowContainer> participants = transition.mParticipants;
+
+        final Task theTask = createTask(mDisplayContent);
+        final ActivityRecord closing = createActivityRecord(theTask);
+        final ActivityRecord opening = createActivityRecord(theTask);
+        // Start states.
+        changes.put(theTask, new Transition.ChangeInfo(theTask, true /* vis */, false /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, false /* exChg */));
+        fillChangeMap(changes, theTask);
+        // End states.
+        closing.setVisibleRequested(false);
+        opening.setVisibleRequested(true);
+
+        final int transit = transition.mType;
+        int flags = 0;
+
+        participants.add(opening);
+        participants.add(closing);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
+        assertEquals(2, info.getChanges().size());
+        assertEquals(info.getChanges().get(1).getActivityComponent(), closing.mActivityComponent);
+    }
+
+    @Test
     public void testCreateInfo_NestedTasks() {
         final Transition transition = createTestTransition(TRANSIT_OPEN);
         ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
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 21fee72..a1cc8d5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -80,6 +80,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
 import android.util.MergedConfiguration;
 import android.view.ContentRecordingSession;
 import android.view.IWindow;
@@ -102,16 +103,19 @@
 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
 import com.android.internal.os.IResultReceiver;
 import com.android.server.LocalServices;
+import com.android.server.wm.SensitiveContentPackages.PackageInfo;
 import com.android.server.wm.WindowManagerService.WindowContainerInfo;
 
 import com.google.common.truth.Expect;
 
+import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 
 import java.util.ArrayList;
+import java.util.Collections;
 
 /**
  * Build/Install/Run:
@@ -133,6 +137,11 @@
     @Rule
     public Expect mExpect = Expect.create();
 
+    @After
+    public void tearDown() {
+        mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
     @Test
     public void testIsRequestedOrientationMapped() {
         mWm.setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled*/ true,
@@ -815,6 +824,42 @@
     }
 
     @Test
+    public void setShouldBlockScreenCaptureForApp() {
+        String testPackage = "test";
+        int ownerId1 = 20;
+        int ownerId2 = 21;
+        PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1);
+        ArraySet<PackageInfo> blockedPackages = new ArraySet();
+        blockedPackages.add(blockedPackage);
+
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        wmInternal.setShouldBlockScreenCaptureForApp(blockedPackages);
+
+        assertTrue(mWm.mSensitiveContentPackages
+                .shouldBlockScreenCaptureForApp(testPackage, ownerId1));
+        assertFalse(mWm.mSensitiveContentPackages
+                .shouldBlockScreenCaptureForApp(testPackage, ownerId2));
+        verify(mWm).refreshScreenCaptureDisabled();
+    }
+
+    @Test
+    public void setShouldBlockScreenCaptureForApp_emptySet_clearsCache() {
+        String testPackage = "test";
+        int ownerId1 = 20;
+        PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1);
+        ArraySet<PackageInfo> blockedPackages = new ArraySet();
+        blockedPackages.add(blockedPackage);
+
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        wmInternal.setShouldBlockScreenCaptureForApp(blockedPackages);
+        wmInternal.setShouldBlockScreenCaptureForApp(Collections.emptySet());
+
+        assertFalse(mWm.mSensitiveContentPackages
+                .shouldBlockScreenCaptureForApp(testPackage, ownerId1));
+        verify(mWm, times(2)).refreshScreenCaptureDisabled();
+    }
+
+    @Test
     public void testisLetterboxBackgroundMultiColored() {
         assertThat(setupLetterboxConfigurationWithBackgroundType(
                 LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING)).isTrue();
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 f24baba..fb4edfa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -55,6 +55,7 @@
 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;
+import static com.android.server.notification.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION;
 import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
 import static com.android.server.wm.WindowContainer.SYNC_STATE_WAITING_FOR_DRAW;
@@ -90,6 +91,7 @@
 import android.os.InputConfig;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.util.ArraySet;
 import android.util.MergedConfiguration;
 import android.view.Gravity;
@@ -109,7 +111,9 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.server.testutils.StubTransaction;
+import com.android.server.wm.SensitiveContentPackages.PackageInfo;
 
+import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -130,6 +134,11 @@
 @RunWith(WindowTestRunner.class)
 public class WindowStateTests extends WindowTestsBase {
 
+    @After
+    public void tearDown() {
+        mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet());
+    }
+
     @Test
     public void testIsParentWindowHidden() {
         final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
@@ -1373,6 +1382,28 @@
         assertThat(listener.mIsVisibleForImeTargetOverlay).isFalse();
     }
 
+    @Test
+    @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+    public void testIsSecureLocked_sensitiveContentProtectionManagerEnabled() {
+        String testPackage = "test";
+        int ownerId1 = 20;
+        int ownerId2 = 21;
+        final WindowState window1 = createWindow(null, TYPE_APPLICATION, "window1", ownerId1);
+        final WindowState window2 = createWindow(null, TYPE_APPLICATION, "window2", ownerId2);
+
+        // Setting packagename for targeted feature
+        window1.mAttrs.packageName = testPackage;
+        window2.mAttrs.packageName = testPackage;
+
+        PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1);
+        ArraySet<PackageInfo> blockedPackages = new ArraySet();
+        blockedPackages.add(blockedPackage);
+        mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedPackages);
+
+        assertTrue(window1.isSecureLocked());
+        assertFalse(window2.isSecureLocked());
+    }
+
     private static class TestImeTargetChangeListener implements ImeTargetChangeListener {
         private IBinder mImeTargetToken;
         private boolean mIsRemoved;
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 60e84b0..9c421ba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1054,6 +1054,19 @@
     }
 
     /**
+     * Performs surface placement and waits for WindowAnimator to complete the frame. It is used
+     * to execute the callbacks if the surface placement is expected to add some callbacks via
+     * {@link WindowAnimator#addAfterPrepareSurfacesRunnable}.
+     */
+    void performSurfacePlacementAndWaitForWindowAnimator() {
+        mWm.mAnimator.ready();
+        if (!mWm.mWindowPlacerLocked.isTraversalScheduled()) {
+            mRootWindowContainer.performSurfacePlacement();
+        }
+        waitUntilWindowAnimatorIdle();
+    }
+
+    /**
      * Avoids rotating screen disturbed by some conditions. It is usually used for the default
      * display that is not the instance of {@link TestDisplayContent} (it bypasses the conditions).
      *
diff --git a/services/usb/lint-baseline.xml b/services/usb/lint-baseline.xml
index c2c0a35..62a2ee5 100644
--- a/services/usb/lint-baseline.xml
+++ b/services/usb/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="7.1.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NonUserGetterCalled"
@@ -12,4 +12,4 @@
             column="42"/>
     </issue>
 
-</issues>
+</issues>
\ No newline at end of file
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java
index 8773cab..1df7012 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java
@@ -24,6 +24,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.telephony.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -117,12 +118,28 @@
     private boolean checkCallStatus() {
         List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList();
         if (infoList == null) return false;
-        return infoList.stream()
-                .filter(s -> (s.getSubscriptionId() != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
-                .anyMatch(s -> isCallOngoingFromState(
-                                        mTelephonyManager
-                                                .createForSubscriptionId(s.getSubscriptionId())
-                                                .getCallStateForSubscription()));
+        if (!Flags.enforceTelephonyFeatureMapping()) {
+            return infoList.stream()
+                    .filter(s -> (s.getSubscriptionId()
+                            != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
+                    .anyMatch(s -> isCallOngoingFromState(
+                            mTelephonyManager
+                                    .createForSubscriptionId(s.getSubscriptionId())
+                                    .getCallStateForSubscription()));
+        } else {
+            return infoList.stream()
+                    .filter(s -> (s.getSubscriptionId()
+                            != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
+                    .anyMatch(s -> {
+                        try {
+                            return isCallOngoingFromState(mTelephonyManager
+                                    .createForSubscriptionId(s.getSubscriptionId())
+                                    .getCallStateForSubscription());
+                        } catch (UnsupportedOperationException e) {
+                            return false;
+                        }
+                    });
+        }
     }
 
     private void updateTelephonyListeners() {
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index d05eb5c..57b13e9 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -422,8 +422,8 @@
             "android.telecom.extra.CALL_CREATED_TIME_MILLIS";
 
     /**
-     * The extra for call log uri that was used to mark missed calls as read when dialer gets the
-     * notification on reboot.
+     * Extra URI that is used by a dialer to query the {@link android.provider.CallLog} content
+     * provider and associate a missed call notification with a call log entry.
      */
     @FlaggedApi(Flags.FLAG_ADD_CALL_URI_FOR_MISSED_CALLS)
     public static final String EXTRA_CALL_LOG_URI =
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index 250c3a5..2a6ac98 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -196,7 +196,7 @@
         // We have READ_PHONE_STATE permission, so return true as long as the AppOps bit hasn't been
         // revoked.
         AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        return appOps.noteOp(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
+        return appOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
                 callingFeatureId, null) == AppOpsManager.MODE_ALLOWED;
     }
 
@@ -249,7 +249,7 @@
         // We have READ_PHONE_STATE permission, so return true as long as the AppOps bit hasn't been
         // revoked.
         AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        return appOps.noteOp(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
+        return appOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
                 callingFeatureId, null) == AppOpsManager.MODE_ALLOWED;
     }
 
@@ -521,7 +521,7 @@
         // We have READ_CALL_LOG permission, so return true as long as the AppOps bit hasn't been
         // revoked.
         AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        return appOps.noteOp(AppOpsManager.OPSTR_READ_CALL_LOG, uid, callingPackage,
+        return appOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_CALL_LOG, uid, callingPackage,
                 callingPackageName, null) == AppOpsManager.MODE_ALLOWED;
     }
 
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index c7b84a3..79e4ad0 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -10955,6 +10955,9 @@
      * @return A {@link PersistableBundle} containing the config for the given subId, or default
      *         values for an invalid subId.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
+     *
      * @deprecated Use {@link #getConfigForSubId(int, String...)} instead.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@@ -11002,6 +11005,9 @@
      * @return A {@link PersistableBundle} with key/value mapping for the specified configuration
      * on success, or an empty (but never null) bundle on failure (for example, when the calling app
      * has no permission).
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PHONE_STATE,
@@ -11047,6 +11053,9 @@
      * @param overrideValues Key-value pairs of the values that are to be overridden. If set to
      *                       {@code null}, this will remove all previous overrides and set the
      *                       carrier configuration back to production values.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
      */
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@@ -11104,6 +11113,10 @@
      *
      * @see #getConfigForSubId
      * @see #getConfig(String...)
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
+     *
      * @deprecated use {@link #getConfig(String...)} instead.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@@ -11138,6 +11151,9 @@
      * configs on success, or an empty (but never null) bundle on failure.
      * @see #getConfigForSubId(int, String...)
      * @see SubscriptionManager#getDefaultSubscriptionId()
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PHONE_STATE,
@@ -11189,6 +11205,9 @@
      *
      * <p>This method returns before the reload has completed, and {@link
      * android.service.carrier.CarrierService#onLoadConfig} will be called from an arbitrary thread.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -11212,6 +11231,8 @@
      * <p>Depending on simState, the config may be cleared or loaded from config app. This is only
      * used by SubscriptionInfoUpdater.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
      */
     @SystemApi
@@ -11234,6 +11255,8 @@
      * Gets the package name for a default carrier service.
      * @return the package name for a default carrier service; empty string if not available.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
      */
     @NonNull
@@ -11287,6 +11310,9 @@
      * @param subId the subscription ID, normally obtained from {@link SubscriptionManager}.
      *
      * @see #getConfigForSubId
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
diff --git a/telephony/java/android/telephony/SecurityAlgorithmUpdate.java b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java
index 61d7ead..c902016 100644
--- a/telephony/java/android/telephony/SecurityAlgorithmUpdate.java
+++ b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java
@@ -169,6 +169,7 @@
     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_IMS_NULL = 67;
     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;
@@ -176,9 +177,8 @@
     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_HMAC_MD5_96 = 75;
+    public static final int SECURITY_ALGORITHM_SRTP_NULL = 86;
     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;
@@ -199,15 +199,15 @@
             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_IMS_NULL, 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})
+            SECURITY_ALGORITHM_HMAC_SHA1_96, SECURITY_ALGORITHM_HMAC_MD5_96,
+            SECURITY_ALGORITHM_SRTP_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/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 5615602..a5c6d57 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1315,7 +1315,7 @@
      * A source of phone number: the EF-MSISDN (see 3GPP TS 31.102),
      * or EF-MDN for CDMA (see 3GPP2 C.P0065-B), from UICC application.
      *
-     * <p>The availability and a of the number depends on the carrier.
+     * <p>The availability and accuracy of the number depends on the carrier.
      * The number may be updated by over-the-air update to UICC applications
      * from the carrier, or by other means with physical access to the SIM.
      */
@@ -1557,12 +1557,21 @@
      * caller can see all subscription across user profiles as it does today today even if it's
      * {@code false}.
      */
-    private boolean mIsForAllUserProfiles = false;
+    private final boolean mIsForAllUserProfiles;
 
     /** @hide */
     @UnsupportedAppUsage
     public SubscriptionManager(Context context) {
-        if (DBG) logd("SubscriptionManager created");
+        this(context, false /*isForAllUserProfiles*/);
+    }
+
+    /**  Constructor */
+    private SubscriptionManager(Context context, boolean isForAllUserProfiles) {
+        if (DBG) {
+            logd("SubscriptionManager created "
+                    + (isForAllUserProfiles ? "for all user profile" : ""));
+        }
+        mIsForAllUserProfiles = isForAllUserProfiles;
         mContext = context;
     }
 
@@ -1998,7 +2007,7 @@
     }
 
     /**
-     * Convert this subscription manager instance into one that can see all subscriptions across
+     * Create a new subscription manager instance that can see all subscriptions across
      * user profiles.
      *
      * @return a SubscriptionManager that can see all subscriptions regardless its user profile
@@ -2008,13 +2017,12 @@
      * @see #getActiveSubscriptionInfoCount
      * @see UserHandle
      */
-    @FlaggedApi(Flags.FLAG_WORK_PROFILE_API_SPLIT)
+    @FlaggedApi(Flags.FLAG_ENFORCE_SUBSCRIPTION_USER_FILTER)
     // @RequiresPermission(TODO(b/308809058))
     // The permission check for accessing all subscriptions will be enforced upon calling the
     // individual APIs linked above.
     @NonNull public SubscriptionManager createForAllUserProfiles() {
-        mIsForAllUserProfiles = true;
-        return this;
+        return new SubscriptionManager(mContext, true/*isForAllUserProfiles*/);
     }
 
     /**
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 1b47dfe..0dce084 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -18923,6 +18923,62 @@
     }
 
     /**
+     * Enables or disables notifications sent when cellular null cipher or integrity algorithms
+     * are in use by the cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
+    @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
+    @SystemApi
+    public void setEnableNullCipherNotifications(boolean enable) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                telephony.setEnableNullCipherNotifications(enable);
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "setEnableNullCipherNotifications RemoteException", ex);
+            ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get whether notifications are enabled for null cipher or integrity algorithms in use by the
+     * cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
+    @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @SystemApi
+    public boolean isNullCipherNotificationsEnabled() {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.isNullCipherNotificationsEnabled();
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "isNullCipherNotificationsEnabled RemoteException", ex);
+            ex.rethrowFromSystemServer();
+        }
+        return false;
+    }
+
+
+    /**
      * Get current cell broadcast message identifier ranges.
      *
      * @throws SecurityException if the caller does not have the required permission
diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java
index 9dd83d1..b6f9e1f 100644
--- a/telephony/java/android/telephony/data/DataCallResponse.java
+++ b/telephony/java/android/telephony/data/DataCallResponse.java
@@ -451,7 +451,7 @@
 
     /**
      * Return the network validation status that was initiated by {@link
-     * DataService.DataServiceProvider#requestValidation}
+     * DataService.DataServiceProvider#requestNetworkValidation}
      *
      * @return The network validation status of data connection.
      */
@@ -931,7 +931,7 @@
 
         /**
          * Set the network validation status that corresponds to the state of the network validation
-         * request started by {@link DataService.DataServiceProvider#requestValidation}
+         * request started by {@link DataService.DataServiceProvider#requestNetworkValidation}
          *
          * @param status The network validation status.
          * @return The same instance of the builder.
diff --git a/telephony/java/android/telephony/data/DataService.java b/telephony/java/android/telephony/data/DataService.java
index 80e91a3..f04e1c9 100644
--- a/telephony/java/android/telephony/data/DataService.java
+++ b/telephony/java/android/telephony/data/DataService.java
@@ -415,13 +415,13 @@
          *     request validation to the DataService and checks if the request has been submitted.
          */
         @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
-        public void requestValidation(int cid,
+        public void requestNetworkValidation(int cid,
                 @NonNull @CallbackExecutor Executor executor,
                 @NonNull @DataServiceCallback.ResultCode Consumer<Integer> resultCodeCallback) {
             Objects.requireNonNull(executor, "executor cannot be null");
             Objects.requireNonNull(resultCodeCallback, "resultCodeCallback cannot be null");
 
-            Log.d(TAG, "requestValidation: " + cid);
+            Log.d(TAG, "requestNetworkValidation: " + cid);
 
             // The default implementation is to return unsupported.
             executor.execute(() -> resultCodeCallback
@@ -741,7 +741,7 @@
                 case DATA_SERVICE_REQUEST_VALIDATION:
                     if (serviceProvider == null) break;
                     ValidationRequest validationRequest = (ValidationRequest) message.obj;
-                    serviceProvider.requestValidation(
+                    serviceProvider.requestNetworkValidation(
                             validationRequest.cid,
                             validationRequest.executor,
                             FunctionalUtils
@@ -924,9 +924,10 @@
         }
 
         @Override
-        public void requestValidation(int slotIndex, int cid, IIntegerConsumer resultCodeCallback) {
+        public void requestNetworkValidation(int slotIndex, int cid,
+                IIntegerConsumer resultCodeCallback) {
             if (resultCodeCallback == null) {
-                loge("requestValidation: resultCodeCallback is null");
+                loge("requestNetworkValidation: resultCodeCallback is null");
                 return;
             }
             ValidationRequest validationRequest =
diff --git a/telephony/java/android/telephony/data/IDataService.aidl b/telephony/java/android/telephony/data/IDataService.aidl
index 15f8881..c36c302 100644
--- a/telephony/java/android/telephony/data/IDataService.aidl
+++ b/telephony/java/android/telephony/data/IDataService.aidl
@@ -48,5 +48,5 @@
     void cancelHandover(int slotId, int cid, IDataServiceCallback callback);
     void registerForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
     void unregisterForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
-    void requestValidation(int slotId, int cid, IIntegerConsumer callback);
+    void requestNetworkValidation(int slotId, int cid, IIntegerConsumer callback);
 }
diff --git a/telephony/java/android/telephony/euicc/EuiccCardManager.java b/telephony/java/android/telephony/euicc/EuiccCardManager.java
index e981e1f..69594f2 100644
--- a/telephony/java/android/telephony/euicc/EuiccCardManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccCardManager.java
@@ -183,6 +183,9 @@
      * @param cardId   The Id of the eUICC.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code and all the profiles.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestAllProfiles(String cardId, @CallbackExecutor Executor executor,
             ResultCallback<EuiccProfileInfo[]> callback) {
@@ -212,6 +215,9 @@
      * @param iccid    The iccid of the profile.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code and profile.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestProfile(String cardId, String iccid, @CallbackExecutor Executor executor,
             ResultCallback<EuiccProfileInfo> callback) {
@@ -244,6 +250,9 @@
      *                  ICCID is known, an APDU will be sent through to read the enabled profile.
      * @param executor  The executor through which the callback should be invoked.
      * @param callback  The callback to get the result code and the profile.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestEnabledProfileForPort(@NonNull String cardId, int portIndex,
             @NonNull @CallbackExecutor Executor executor,
@@ -276,6 +285,9 @@
      * @param refresh  Whether sending the REFRESH command to modem.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void disableProfile(String cardId, String iccid, boolean refresh,
             @CallbackExecutor Executor executor, ResultCallback<Void> callback) {
@@ -307,6 +319,9 @@
      * @param refresh  Whether sending the REFRESH command to modem.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code and the EuiccProfileInfo enabled.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @deprecated instead use {@link #switchToProfile(String, String, int, boolean, Executor,
      * ResultCallback)}
      */
@@ -344,6 +359,9 @@
      * @param refresh   Whether sending the REFRESH command to modem.
      * @param executor  The executor through which the callback should be invoked.
      * @param callback  The callback to get the result code and the EuiccProfileInfo enabled.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void switchToProfile(@Nullable String cardId, @Nullable String iccid, int portIndex,
             boolean refresh, @NonNull @CallbackExecutor Executor executor,
@@ -375,6 +393,9 @@
      * @param nickname The nickname of the profile.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void setNickname(String cardId, String iccid, String nickname,
             @CallbackExecutor Executor executor, ResultCallback<Void> callback) {
@@ -404,6 +425,9 @@
      * @param iccid The iccid of the profile.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void deleteProfile(String cardId, String iccid, @CallbackExecutor Executor executor,
             ResultCallback<Void> callback) {
@@ -434,6 +458,9 @@
      *     EuiccCard for details.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void resetMemory(String cardId, @ResetOption int options,
             @CallbackExecutor Executor executor, ResultCallback<Void> callback) {
@@ -462,6 +489,9 @@
      * @param cardId The Id of the eUICC.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code and the default SM-DP+ address.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestDefaultSmdpAddress(String cardId, @CallbackExecutor Executor executor,
             ResultCallback<String> callback) {
@@ -490,6 +520,9 @@
      * @param cardId The Id of the eUICC.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code and the SM-DS address.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestSmdsAddress(String cardId, @CallbackExecutor Executor executor,
             ResultCallback<String> callback) {
@@ -519,6 +552,9 @@
      * @param defaultSmdpAddress The default SM-DP+ address to set.
      * @param executor The executor through which the callback should be invoked.
      * @param callback The callback to get the result code.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void setDefaultSmdpAddress(String cardId, String defaultSmdpAddress,
             @CallbackExecutor Executor executor, ResultCallback<Void> callback) {
@@ -548,6 +584,9 @@
      * @param cardId The Id of the eUICC.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and the rule authorisation table.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestRulesAuthTable(String cardId, @CallbackExecutor Executor executor,
             ResultCallback<EuiccRulesAuthTable> callback) {
@@ -576,6 +615,9 @@
      * @param cardId The Id of the eUICC.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and the challenge.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestEuiccChallenge(String cardId, @CallbackExecutor Executor executor,
             ResultCallback<byte[]> callback) {
@@ -604,6 +646,9 @@
      * @param cardId The Id of the eUICC.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and the info1.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestEuiccInfo1(String cardId, @CallbackExecutor Executor executor,
             ResultCallback<byte[]> callback) {
@@ -632,6 +677,9 @@
      * @param cardId The Id of the eUICC.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and the info2.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void requestEuiccInfo2(String cardId, @CallbackExecutor Executor executor,
             ResultCallback<byte[]> callback) {
@@ -671,6 +719,9 @@
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and a byte array which represents a
      *     {@code AuthenticateServerResponse} defined in GSMA RSP v2.0+.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void authenticateServer(String cardId, String matchingId, byte[] serverSigned1,
             byte[] serverSignature1, byte[] euiccCiPkIdToBeUsed, byte[] serverCertificate,
@@ -716,6 +767,9 @@
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and a byte array which represents a
      *     {@code PrepareDownloadResponse} defined in GSMA RSP v2.0+
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void prepareDownload(String cardId, @Nullable byte[] hashCc, byte[] smdpSigned2,
             byte[] smdpSignature2, byte[] smdpCertificate, @CallbackExecutor Executor executor,
@@ -753,6 +807,9 @@
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and a byte array which represents a
      *     {@code LoadBoundProfilePackageResponse} defined in GSMA RSP v2.0+.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void loadBoundProfilePackage(String cardId, byte[] boundProfilePackage,
             @CallbackExecutor Executor executor, ResultCallback<byte[]> callback) {
@@ -787,6 +844,9 @@
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and an byte[] which represents a
      *     {@code CancelSessionResponse} defined in GSMA RSP v2.0+.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void cancelSession(String cardId, byte[] transactionId, @CancelReason int reason,
             @CallbackExecutor Executor executor, ResultCallback<byte[]> callback) {
@@ -820,6 +880,9 @@
      * @param events bits of the event types ({@link EuiccNotification.Event}) to list.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and the list of notifications.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void listNotifications(String cardId, @EuiccNotification.Event int events,
             @CallbackExecutor Executor executor, ResultCallback<EuiccNotification[]> callback) {
@@ -850,6 +913,9 @@
      * @param events bits of the event types ({@link EuiccNotification.Event}) to list.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and the list of notifications.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void retrieveNotificationList(String cardId, @EuiccNotification.Event int events,
             @CallbackExecutor Executor executor, ResultCallback<EuiccNotification[]> callback) {
@@ -880,6 +946,9 @@
      * @param seqNumber the sequence number of the notification.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code and the notification.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void retrieveNotification(String cardId, int seqNumber,
             @CallbackExecutor Executor executor, ResultCallback<EuiccNotification> callback) {
@@ -910,6 +979,9 @@
      * @param seqNumber the sequence number of the notification.
      * @param executor The executor through which the callback should be invoked.
      * @param callback the callback to get the result code.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public void removeNotificationFromList(String cardId, int seqNumber,
             @CallbackExecutor Executor executor, ResultCallback<Void> callback) {
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index 86fbb04..09d2108 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -927,6 +927,9 @@
      * subscription APIs.
      *
      * @return true if embedded subscriptions are currently enabled.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public boolean isEnabled() {
         // In the future, this may reach out to IEuiccController (if non-null) to check any dynamic
@@ -942,6 +945,9 @@
      * access to the EID of another eUICC.
      *
      * @return the EID. May be null if the eUICC is not ready.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     @Nullable
     public String getEid() {
@@ -963,6 +969,8 @@
      * @return the status of eUICC OTA. If the eUICC is not ready,
      *         {@link OtaStatus#EUICC_OTA_STATUS_UNAVAILABLE} will be returned.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1014,6 +1022,9 @@
      * @param subscription the subscription to download.
      * @param switchAfterDownload if true, the profile will be activated upon successful download.
      * @param callbackIntent a PendingIntent to launch when the operation completes.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
     public void downloadSubscription(DownloadableSubscription subscription,
@@ -1075,6 +1086,9 @@
      * @param resolutionExtras Resolution-specific extras depending on the result of the resolution.
      *     For example, this may indicate whether the user has consented or may include the input
      *     they provided.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1111,6 +1125,9 @@
      *
      * @param subscription the subscription which needs metadata filled in
      * @param callbackIntent a PendingIntent to launch when the operation completes.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1142,6 +1159,9 @@
      * internal system use only.
      *
      * @param callbackIntent a PendingIntent to launch when the operation completes.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1163,6 +1183,9 @@
      * Returns information about the eUICC chip/device.
      *
      * @return the {@link EuiccInfo}. May be null if the eUICC is not ready.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     @Nullable
     public EuiccInfo getEuiccInfo() {
@@ -1188,6 +1211,9 @@
      *
      * @param subscriptionId the ID of the subscription to delete.
      * @param callbackIntent a PendingIntent to launch when the operation completes.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
     public void deleteSubscription(int subscriptionId, PendingIntent callbackIntent) {
@@ -1251,6 +1277,9 @@
      *     {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission, or the
      *     calling app must be authorized to manage the active subscription on the target eUICC.
      * @param callbackIntent a PendingIntent to launch when the operation completes.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
     public void switchToSubscription(int subscriptionId, PendingIntent callbackIntent) {
@@ -1312,6 +1341,9 @@
      *     {@link SubscriptionInfo#getPortIndex()}.
      * @param portIndex the index of the port to target for the enabled subscription
      * @param callbackIntent a PendingIntent to launch when the operation completes.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
     public void switchToSubscription(int subscriptionId, int portIndex,
@@ -1349,6 +1381,9 @@
      * @param subscriptionId the ID of the subscription to update.
      * @param nickname the new nickname to apply.
      * @param callbackIntent a PendingIntent to launch when the operation completes.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
     public void updateSubscriptionNickname(
@@ -1376,6 +1411,8 @@
      * @deprecated From R, callers should specify a flag for specific set of subscriptions to erase
      * and use {@link #eraseSubscriptions(int, PendingIntent)} instead
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1402,6 +1439,8 @@
      * @param options flag indicating specific set of subscriptions to erase
      * @param callbackIntent a PendingIntent to launch when the operation completes.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1459,6 +1498,9 @@
      * determine whether a country is supported please check {@link #isSupportedCountry}.
      *
      * @param supportedCountries is a list of strings contains country ISO codes in uppercase.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1487,6 +1529,9 @@
      * determine whether a country is supported please check {@link #isSupportedCountry}.
      *
      * @param unsupportedCountries is a list of strings contains country ISO codes in uppercase.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1512,6 +1557,9 @@
      * {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
      *
      * @return list of strings contains country ISO codes in uppercase.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1535,6 +1583,9 @@
      * {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
      *
      * @return list of strings contains country ISO codes in uppercase.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1566,6 +1617,9 @@
      * @param countryIso should be the ISO-3166 country code is provided in uppercase 2 character
      * format.
      * @return whether the given country supports eUICC or not.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      * @hide
      */
     @SystemApi
@@ -1630,6 +1684,9 @@
      *
      * @param portIndex is an enumeration of the ports available on the UICC.
      * @return {@code true} if port is available
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
      */
     public boolean isSimPortAvailable(int portIndex) {
         try {
diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java
index 71bb329..551057f 100644
--- a/telephony/java/android/telephony/ims/ImsMmTelManager.java
+++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java
@@ -779,6 +779,8 @@
      * @see android.telephony.CarrierConfigManager#KEY_CARRIER_VOLTE_AVAILABLE_BOOL
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @return true if the user's setting for advanced calling is enabled, false otherwise.
      */
     @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
@@ -827,6 +829,8 @@
      * @see #isAdvancedCallingSettingEnabled()
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@@ -865,6 +869,8 @@
      * @param capability The IMS MmTel capability to query.
      * @return {@code true} if the MmTel IMS capability is capable for this subscription, false
      *         otherwise.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@@ -893,6 +899,8 @@
      * @param capability The IMS MmTel capability to query.
      * @return {@code true} if the MmTel IMS capability is available for this subscription, false
      *         otherwise.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -986,6 +994,8 @@
      *
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @return true if the user’s “Video Calling” setting is currently enabled.
      */
     @RequiresPermission(anyOf = {
@@ -1017,6 +1027,8 @@
      *
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @see #isVtSettingEnabled()
      * @hide
      */
@@ -1060,6 +1072,8 @@
      *
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
     @RequiresPermission(anyOf = {
@@ -1090,6 +1104,8 @@
      *
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @param isEnabled true if the user's setting for Voice over WiFi is enabled, false otherwise=
      * @see #isVoWiFiSettingEnabled()
      * @hide
@@ -1148,6 +1164,8 @@
      *
      * @throws ImsException if the IMS service associated with this subscription is not available or
      * the IMS service is not available.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @return true if the user's setting for Voice over Cross SIM is enabled and false if it is not
      */
     @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
@@ -1192,6 +1210,8 @@
      * </ul>
      * @throws ImsException if the IMS service associated with this subscription is not available or
      * the IMS service is not available.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @param isEnabled true if the user's setting for Voice over Cross SIM is enabled,
      *                 false otherwise
      * @see #isCrossSimCallingEnabled()
@@ -1233,6 +1253,8 @@
      *
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @return true if the user's setting for Voice over WiFi while roaming is enabled, false
      * if disabled.
      */
@@ -1267,6 +1289,8 @@
      *     false otherwise.
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @see #isVoWiFiRoamingSettingEnabled()
      * @hide
      */
@@ -1304,6 +1328,8 @@
      * - {@link #WIFI_MODE_WIFI_PREFERRED}
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @see #setVoWiFiSettingEnabled(boolean)
      * @hide
      */
@@ -1347,6 +1373,8 @@
      *
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @return The Voice over WiFi Mode preference set by the user, which can be one of the
      * following:
      * - {@link #WIFI_MODE_WIFI_ONLY}
@@ -1386,6 +1414,8 @@
      * - {@link #WIFI_MODE_WIFI_PREFERRED}
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @see #getVoWiFiModeSetting()
      * @hide
      */
@@ -1422,6 +1452,8 @@
      *     - {@link #WIFI_MODE_WIFI_PREFERRED}
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @see #setVoWiFiRoamingSettingEnabled(boolean)
      * @hide
      */
@@ -1458,6 +1490,8 @@
      *     - {@link #WIFI_MODE_WIFI_PREFERRED}
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @see #getVoWiFiRoamingModeSetting()
      * @hide
      */
@@ -1492,6 +1526,8 @@
      * settings.
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @param isEnabled if true RTT should be enabled during calls made on this subscription.
      * @hide
      */
@@ -1535,6 +1571,8 @@
      *
      * @throws IllegalArgumentException if the subscription associated with this operation is not
      * active (SIM is not inserted, ESIM inactive) or invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @see android.telephony.CarrierConfigManager#KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL
      */
     @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
diff --git a/telephony/java/android/telephony/ims/ImsRcsManager.java b/telephony/java/android/telephony/ims/ImsRcsManager.java
index 2b49bcd..62d4263 100644
--- a/telephony/java/android/telephony/ims/ImsRcsManager.java
+++ b/telephony/java/android/telephony/ims/ImsRcsManager.java
@@ -250,6 +250,8 @@
      * the {@code 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.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public void registerImsRegistrationCallback(
@@ -294,6 +296,8 @@
      * @param c The {@link RegistrationManager.RegistrationCallback} to be removed.
      * @see android.telephony.SubscriptionManager.OnSubscriptionsChangedListener
      * @see #registerImsRegistrationCallback(Executor, RegistrationCallback)
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public void unregisterImsRegistrationCallback(
@@ -329,6 +333,8 @@
      * following: {@link RegistrationManager#REGISTRATION_STATE_NOT_REGISTERED},
      * {@link RegistrationManager#REGISTRATION_STATE_REGISTERING}, or
      * {@link RegistrationManager#REGISTRATION_STATE_REGISTERED}.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public void getRegistrationState(@NonNull @CallbackExecutor Executor executor,
@@ -378,6 +384,8 @@
      * {@see AccessNetworkConstants#TRANSPORT_TYPE_WWAN},
      * {@see AccessNetworkConstants#TRANSPORT_TYPE_WLAN}, or
      * {@see AccessNetworkConstants#TRANSPORT_TYPE_INVALID}.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public void getRegistrationTransportType(@NonNull @CallbackExecutor Executor executor,
@@ -435,6 +443,8 @@
      * {@link ImsRcsManager} is valid, but the ImsService associated with the subscription is not
      * available. This can happen if the ImsService has crashed, for example, or if the subscription
      * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -479,6 +489,8 @@
      * @see #addOnAvailabilityChangedListener(Executor, OnAvailabilityChangedListener)
      * @throws ImsException if the IMS service is not available when calling this method.
      * See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -525,6 +537,8 @@
      * @see android.telephony.CarrierConfigManager.Ims#KEY_ENABLE_PRESENCE_CAPABILITY_EXCHANGE_BOOL
      * @throws ImsException if the IMS service is not available when calling this method.
      * See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -563,6 +577,8 @@
      * @see #isCapable(int, int)
      * @throws ImsException if the IMS service is not available when calling this method.
      * See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
diff --git a/telephony/java/android/telephony/ims/ProvisioningManager.java b/telephony/java/android/telephony/ims/ProvisioningManager.java
index 1c5d1e9..62b8420 100644
--- a/telephony/java/android/telephony/ims/ProvisioningManager.java
+++ b/telephony/java/android/telephony/ims/ProvisioningManager.java
@@ -1300,8 +1300,10 @@
      * @param executor The executor that the callback methods will be called on.
      * @param callback The callback instance being registered.
      * @throws ImsException if the subscription associated with this callback is
-     * valid, but the {@link ImsService the service crashed, for example. See
+     * valid, but the service crashed, for example. See
      * {@link ImsException#getCode()} for a more detailed reason.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public void registerFeatureProvisioningChangedCallback(
@@ -1327,6 +1329,8 @@
      *
      * @param callback The existing {@link FeatureProvisioningCallback} to be removed.
      * @see #registerFeatureProvisioningChangedCallback(Executor, FeatureProvisioningCallback)
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     public void unregisterFeatureProvisioningChangedCallback(
             @NonNull FeatureProvisioningCallback callback) {
@@ -1347,6 +1351,8 @@
      * @return an integer value for the provided key, or
      * {@link ImsConfigImplBase#CONFIG_RESULT_UNKNOWN} if the key doesn't exist.
      * @throws IllegalArgumentException if the key provided was invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -1369,6 +1375,8 @@
      * @return a String value for the provided key, {@code null} if the key doesn't exist, or
      * {@link StringResultError} if there was an error getting the value for the provided key.
      * @throws IllegalArgumentException if the key provided was invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -1392,6 +1400,8 @@
      * @param key An integer that represents the provisioning key, which is defined by the OEM.
      * @param value a integer value for the provided key.
      * @return the result of setting the configuration value.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      *
      * Note: For compatibility purposes, the integer values [0 - 99] used in
@@ -1420,6 +1430,8 @@
      *     should be appropriately namespaced to avoid collision.
      * @param value a String value for the provided key.
      * @return the result of setting the configuration value.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -1451,6 +1463,9 @@
      *
      * @see CarrierConfigManager.Ims#KEY_MMTEL_REQUIRES_PROVISIONING_BUNDLE
      * @param isProvisioned true if the device is provisioned for UT over IMS, false otherwise.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @WorkerThread
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@@ -1485,6 +1500,9 @@
      * @return true if the device is provisioned for the capability or does not require
      * provisioning, false if the capability does require provisioning and has not been
      * provisioned yet.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @WorkerThread
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
@@ -1509,6 +1527,9 @@
      * @return true if the device is provisioned for the capability or does not require
      * provisioning, false if the capability does require provisioning and has not been
      * provisioned yet.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
+     *
      * @deprecated Use {@link #getRcsProvisioningStatusForCapability(int, int)} instead,
      * as this only retrieves provisioning information for
      * {@link ImsRegistrationImplBase#REGISTRATION_TECH_LTE}
@@ -1546,6 +1567,9 @@
      * @return true if the device is provisioned for the capability or does not require
      * provisioning, false if the capability does require provisioning and has not been
      * provisioned yet.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @WorkerThread
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
@@ -1577,6 +1601,9 @@
      * @see CarrierConfigManager#KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL
      * @param isProvisioned true if the device is provisioned for the RCS capability specified,
      *                      false otherwise.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
+     *
      * @deprecated Use {@link #setRcsProvisioningStatusForCapability(int, int, boolean)} instead,
      * as this method only sets provisioning information for
      * {@link ImsRegistrationImplBase#REGISTRATION_TECH_LTE}
@@ -1615,6 +1642,9 @@
      * @see CarrierConfigManager.Ims#KEY_RCS_REQUIRES_PROVISIONING_BUNDLE
      * @param isProvisioned true if the device is provisioned for the RCS capability specified,
      *                      false otherwise.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @WorkerThread
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@@ -1644,6 +1674,9 @@
      * @return true if provisioning is required for the MMTEL capability and IMS
      * registration technology specified, false if it is not required or if the device does not
      * support IMS.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public boolean isProvisioningRequiredForCapability(
@@ -1672,6 +1705,9 @@
      * @return true if provisioning is required for the RCS capability and IMS
      * registration technology specified, false if it is not required or if the device does not
      * support IMS.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public boolean isRcsProvisioningRequiredForCapability(
@@ -1700,10 +1736,14 @@
      * @param config The XML file to be read. ASCII/UTF8 encoded text if not compressed.
      * @param isCompressed The XML file is compressed in gzip format and must be decompressed
      *         before being read.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}.
      * @hide
      */
     @SystemApi
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION)
     public void notifyRcsAutoConfigurationReceived(@NonNull byte[] config, boolean isCompressed) {
         if (config == null) {
             throw new IllegalArgumentException("Must include a non-null config XML file.");
@@ -1714,7 +1754,6 @@
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
-
     }
 
     /**
@@ -1787,10 +1826,14 @@
      * When the IMS/RCS service receives the RCS client configuration, it will detect
      * the change in the configuration, and trigger the auto-configuration as needed.
      * @param rcc RCS client configuration {@link RcsClientConfiguration}
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}.
      * @hide
      */
     @SystemApi
     @RequiresPermission(Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION)
     public void setRcsClientConfiguration(
             @NonNull RcsClientConfiguration rcc) throws ImsException {
         try {
@@ -1826,6 +1869,7 @@
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION})
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION)
     public boolean isRcsVolteSingleRegistrationCapable() throws ImsException {
         try {
             return getITelephony().isRcsVolteSingleRegistrationCapable(mSubId);
@@ -1870,12 +1914,15 @@
     * params (See {@link #setRcsClientConfiguration}) and re register the
     * callback.
     * See {@link ImsException#getCode()} for a more detailed reason.
+    * @throws UnsupportedOperationException If the device does not have
+    *          {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}.
     * @hide
     */
     @SystemApi
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION})
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION)
     public void registerRcsProvisioningCallback(
             @NonNull @CallbackExecutor Executor executor,
             @NonNull RcsProvisioningCallback callback) throws ImsException {
@@ -1908,12 +1955,15 @@
      * @see #registerRcsProvisioningCallback(Executor, RcsProvisioningCallback)
      * @throws IllegalArgumentException if the subscription associated with
      * this callback is invalid.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}.
      * @hide
      */
     @SystemApi
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION})
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION)
     public void unregisterRcsProvisioningCallback(
             @NonNull RcsProvisioningCallback callback) {
         try {
@@ -1935,10 +1985,14 @@
      * {@link RcsProvisioningCallback#onConfigurationReset}, then
      * {@link RcsProvisioningCallback#onConfigurationChanged} when the new
      * RCS configuration is received and notified by {@link #notifyRcsAutoConfigurationReceived}
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}.
      * @hide
      */
     @SystemApi
     @RequiresPermission(Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION)
     public void triggerRcsReconfiguration() {
         try {
             getITelephony().triggerRcsReconfiguration(mSubId);
diff --git a/telephony/java/android/telephony/ims/RcsUceAdapter.java b/telephony/java/android/telephony/ims/RcsUceAdapter.java
index 3bb9be0..8925a9e 100644
--- a/telephony/java/android/telephony/ims/RcsUceAdapter.java
+++ b/telephony/java/android/telephony/ims/RcsUceAdapter.java
@@ -21,9 +21,11 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.IBinder;
@@ -49,6 +51,7 @@
  *
  * @see ImsRcsManager#getUceAdapter() for information on creating an instance of this class.
  */
+@RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS)
 public class RcsUceAdapter {
     private static final String TAG = "RcsUceAdapter";
 
@@ -585,6 +588,8 @@
      * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not
      * available. This can happen if the ImsService has crashed, for example, or if the subscription
      * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -682,6 +687,8 @@
      * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not
      * available. This can happen if the ImsService has crashed, for example, or if the subscription
      * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -759,6 +766,8 @@
      * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not
      * available. This can happen if the ImsService has crashed, for example, or if the subscription
      * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -800,6 +809,8 @@
      * 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.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -845,6 +856,8 @@
      * 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.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
@@ -901,6 +914,8 @@
      * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not
      * available. This can happen if the ImsService has crashed, for example, or if the subscription
      * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      */
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
     public boolean isUceSettingEnabled() throws ImsException {
@@ -954,6 +969,8 @@
      * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not
      * available. This can happen if the ImsService has crashed, for example, or if the subscription
      * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS}.
      * @hide
      */
     @SystemApi
diff --git a/telephony/java/android/telephony/ims/RegistrationManager.java b/telephony/java/android/telephony/ims/RegistrationManager.java
index 54ceaed..9f83da9 100644
--- a/telephony/java/android/telephony/ims/RegistrationManager.java
+++ b/telephony/java/android/telephony/ims/RegistrationManager.java
@@ -84,7 +84,7 @@
                 SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK,
                 SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK_WITH_TIMEOUT,
                 SUGGESTED_ACTION_TRIGGER_RAT_BLOCK,
-                SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCK
+                SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCKS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SuggestedAction {}
@@ -116,9 +116,10 @@
 
     /**
      * Indicates that the IMS registration on current RAT failed multiple times.
-     * The radio shall block the current RAT and search for other available RATs in the
-     * background. If no other RAT is available that meets the carrier requirements, the
-     * radio may remain on the current RAT for internet service. The radio clears all
+     * The radio shall block the {@link ImsRegistrationImplBase.ImsRegistrationTech}
+     * included with this and search for other available RATs in the background.
+     * If no other RAT is available that meets the carrier requirements, the
+     * radio may remain on the blocked RAT for internet service. The radio clears all
      * RATs marked as unavailable if the IMS service is registered to the carrier network.
      * @hide
      */
@@ -133,7 +134,7 @@
      */
     @SystemApi
     @FlaggedApi(Flags.FLAG_ADD_RAT_RELATED_SUGGESTED_ACTION_TO_IMS_REGISTRATION)
-    int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCK = 4;
+    int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCKS = 4;
 
     /**@hide*/
     // Translate ImsRegistrationImplBase API to new AccessNetworkConstant because WLAN
diff --git a/telephony/java/android/telephony/ims/SipDelegateManager.java b/telephony/java/android/telephony/ims/SipDelegateManager.java
index 25ebdd0..abf2105 100644
--- a/telephony/java/android/telephony/ims/SipDelegateManager.java
+++ b/telephony/java/android/telephony/ims/SipDelegateManager.java
@@ -525,6 +525,8 @@
      * @param callback The callback instance being registered.
      * @throws ImsException in the case that the callback can not be registered.
      * See {@link ImsException#getCode} for more information on when this is called.
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}.
      */
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public void registerSipDialogStateCallback(@NonNull Executor executor,
@@ -557,6 +559,9 @@
      * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE}
      *
      * @param callback The callback instance to be unregistered.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}.
      */
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public void unregisterSipDialogStateCallback(@NonNull SipDialogStateCallback callback)
diff --git a/telephony/java/android/telephony/satellite/AntennaPosition.java b/telephony/java/android/telephony/satellite/AntennaPosition.java
index 8842886..d6440fc 100644
--- a/telephony/java/android/telephony/satellite/AntennaPosition.java
+++ b/telephony/java/android/telephony/satellite/AntennaPosition.java
@@ -35,10 +35,10 @@
 @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
 public final class AntennaPosition implements Parcelable {
     /** Antenna direction used for satellite communication. */
-    @NonNull AntennaDirection mAntennaDirection;
+    @NonNull private AntennaDirection mAntennaDirection;
 
     /** Enum corresponding to device hold position to be used by the end user. */
-    @SatelliteManager.DeviceHoldPosition int mSuggestedHoldPosition;
+    @SatelliteManager.DeviceHoldPosition private int mSuggestedHoldPosition;
 
     /**
      * @hide
diff --git a/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
similarity index 94%
rename from telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl
rename to telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
index cd9d81e..9ff73e2 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
@@ -20,7 +20,7 @@
  * Interface for satellite state change callback.
  * @hide
  */
-oneway interface ISatelliteStateCallback {
+oneway interface ISatelliteModemStateCallback {
     /**
      * Indicates that the satellite modem state has changed.
      *
diff --git a/telephony/java/android/telephony/satellite/PointingInfo.java b/telephony/java/android/telephony/satellite/PointingInfo.java
index 022a856..9440b65 100644
--- a/telephony/java/android/telephony/satellite/PointingInfo.java
+++ b/telephony/java/android/telephony/satellite/PointingInfo.java
@@ -17,6 +17,7 @@
 package android.telephony.satellite;
 
 import android.annotation.FlaggedApi;
+import android.annotation.FloatRange;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.os.Parcel;
@@ -108,11 +109,19 @@
         return sb.toString();
     }
 
+    /**
+     * Returns the azimuth of the satellite, in degrees.
+     */
+    @FloatRange(from = -180, to = 180)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public float getSatelliteAzimuthDegrees() {
         return mSatelliteAzimuthDegrees;
     }
 
+    /**
+     * Returns the elevation of the satellite, in degrees.
+     */
+    @FloatRange(from = -90, to = 90)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public float getSatelliteElevationDegrees() {
         return mSatelliteElevationDegrees;
diff --git a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
index b5763c3..8e79ca5 100644
--- a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
@@ -22,10 +22,15 @@
 
 import com.android.internal.telephony.flags.Flags;
 
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
  * A callback class for listening to satellite datagrams.
+ * {@link SatelliteDatagramCallback} is registered to telephony when an app which invokes
+ * {@link SatelliteManager#registerForSatelliteDatagram(Executor, SatelliteDatagramCallback)},
+ * and {@link #onSatelliteDatagramReceived(long, SatelliteDatagram, int, Consumer)} will be invoked
+ * when a new datagram is received from satellite.
  *
  * @hide
  */
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index e09bd20..2a69703 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -75,8 +75,9 @@
     private static final ConcurrentHashMap<SatelliteProvisionStateCallback,
             ISatelliteProvisionStateCallback> sSatelliteProvisionStateCallbackMap =
             new ConcurrentHashMap<>();
-    private static final ConcurrentHashMap<SatelliteStateCallback, ISatelliteStateCallback>
-            sSatelliteStateCallbackMap = new ConcurrentHashMap<>();
+    private static final ConcurrentHashMap<SatelliteModemStateCallback,
+            ISatelliteModemStateCallback>
+            sSatelliteModemStateCallbackMap = new ConcurrentHashMap<>();
     private static final ConcurrentHashMap<SatelliteTransmissionUpdateCallback,
             ISatelliteTransmissionUpdateCallback> sSatelliteTransmissionUpdateCallbackMap =
             new ConcurrentHashMap<>();
@@ -624,6 +625,11 @@
     /**
      * Request to get whether the satellite service is supported on the device.
      *
+     * <p>
+     * Note: This API only checks whether the device supports the satellite feature. The result will
+     * not be affected by whether the device is provisioned.
+     * </p>
+     *
      * @param executor The executor on which the callback will be called.
      * @param callback The callback object to which the result will be delivered.
      *                 If the request is successful, {@link OutcomeReceiver#onResult(Object)}
@@ -1301,21 +1307,22 @@
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     @SatelliteResult public int registerForSatelliteModemStateChanged(
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull SatelliteStateCallback callback) {
+            @NonNull SatelliteModemStateCallback callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
 
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                ISatelliteStateCallback internalCallback = new ISatelliteStateCallback.Stub() {
+                ISatelliteModemStateCallback internalCallback =
+                        new ISatelliteModemStateCallback.Stub() {
                     @Override
                     public void onSatelliteModemStateChanged(int state) {
                         executor.execute(() -> Binder.withCleanCallingIdentity(() ->
                                 callback.onSatelliteModemStateChanged(state)));
                     }
                 };
-                sSatelliteStateCallbackMap.put(callback, internalCallback);
+                sSatelliteModemStateCallbackMap.put(callback, internalCallback);
                 return telephony.registerForSatelliteModemStateChanged(mSubId, internalCallback);
             } else {
                 throw new IllegalStateException("telephony service is null.");
@@ -1332,16 +1339,18 @@
      * If callback was not registered before, the request will be ignored.
      *
      * @param callback The callback that was passed to
-     * {@link #registerForSatelliteModemStateChanged(Executor, SatelliteStateCallback)}.
+     * {@link #registerForSatelliteModemStateChanged(Executor, SatelliteModemStateCallback)}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
      */
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
-    public void unregisterForSatelliteModemStateChanged(@NonNull SatelliteStateCallback callback) {
+    public void unregisterForSatelliteModemStateChanged(
+            @NonNull SatelliteModemStateCallback callback) {
         Objects.requireNonNull(callback);
-        ISatelliteStateCallback internalCallback = sSatelliteStateCallbackMap.remove(callback);
+        ISatelliteModemStateCallback internalCallback = sSatelliteModemStateCallbackMap.remove(
+                callback);
 
         try {
             ITelephony telephony = getITelephony();
diff --git a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
similarity index 96%
rename from telephony/java/android/telephony/satellite/SatelliteStateCallback.java
rename to telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
index bfe6e10..8d33c88 100644
--- a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
@@ -28,7 +28,7 @@
  */
 @SystemApi
 @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
-public interface SatelliteStateCallback {
+public interface SatelliteModemStateCallback {
     /**
      * Called when satellite modem state changes.
      * @param state The new satellite modem state.
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 84777c9..3ea86c7 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -72,7 +72,7 @@
 import android.telephony.satellite.ISatelliteDatagramCallback;
 import android.telephony.satellite.ISatelliteTransmissionUpdateCallback;
 import android.telephony.satellite.ISatelliteProvisionStateCallback;
-import android.telephony.satellite.ISatelliteStateCallback;
+import android.telephony.satellite.ISatelliteModemStateCallback;
 import android.telephony.satellite.NtnSignalStrength;
 import android.telephony.satellite.SatelliteCapabilities;
 import android.telephony.satellite.SatelliteDatagram;
@@ -2896,7 +2896,7 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
-    int registerForSatelliteModemStateChanged(int subId, ISatelliteStateCallback callback);
+    int registerForSatelliteModemStateChanged(int subId, ISatelliteModemStateCallback callback);
 
     /**
      * Unregisters for modem state changed from satellite modem.
@@ -2907,7 +2907,7 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
-    void unregisterForSatelliteModemStateChanged(int subId, ISatelliteStateCallback callback);
+    void unregisterForSatelliteModemStateChanged(int subId, ISatelliteModemStateCallback callback);
 
    /**
      * Register to receive incoming datagrams over satellite.
@@ -3055,6 +3055,29 @@
     boolean setEmergencyCallToSatelliteHandoverType(int handoverType, int delaySeconds);
 
     /**
+     * This API should be used by only CTS tests to forcefully set the country codes.
+     *
+     * @param reset {@code true} mean the overridden country codes should not be used, {@code false}
+     *              otherwise.
+     * @return {@code true} if the country code is set successfully, {@code false} otherwise.
+     */
+    boolean setCountryCodes(in boolean reset, in List<String> currentNetworkCountryCodes,
+            in Map cachedNetworkCountryCodes, in String locationCountryCode,
+            in long locationCountryCodeTimestampNanos);
+
+    /**
+     * This API should be used by only CTS tests to override the overlay configs of satellite
+     * access controller.
+     *
+     * @param reset {@code true} mean the overridden configs should not be used, {@code false}
+     *              otherwise.
+     * @return {@code true} if the overlay configs are set successfully, {@code false} otherwise.
+     */
+    boolean setSatelliteAccessControlOverlayConfigs(in boolean reset, in boolean isAllowed,
+            in String s2CellFile, in long locationFreshDurationNanos,
+            in List<String> satelliteCountryCodes);
+
+    /**
      * Test method to confirm the file contents are not altered.
      */
      @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
@@ -3207,4 +3230,32 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
         + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
     boolean isCellularIdentifierDisclosureNotificationsEnabled();
+
+    /**
+     * Enables or disables notifications sent when cellular null cipher or integrity algorithms
+     * are in use by the cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+        + "android.Manifest.permission.MODIFY_PHONE_STATE)")
+    void setEnableNullCipherNotifications(boolean enable);
+
+    /**
+     * Get whether notifications are enabled for null cipher or integrity algorithms in use by the
+     * cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+        + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
+    boolean isNullCipherNotificationsEnabled();
 }
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
index 80c1e5be..217659e 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
@@ -43,9 +43,7 @@
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Optional;
 import java.util.stream.Collectors;
 
 /**
@@ -178,8 +176,14 @@
             this.deviceFrameRate = deviceFrameRate;
         }
 
+        FrameRateTimeoutException(List<Float> expectedFrameRates, float deviceFrameRate) {
+            this.expectedFrameRates = expectedFrameRates;
+            this.deviceFrameRate = deviceFrameRate;
+        }
+
         public float expectedFrameRate;
         public float deviceFrameRate;
+        public List<Float> expectedFrameRates;
     }
 
     public enum Api {
@@ -502,31 +506,37 @@
     }
 
     private void waitForStableFrameRate(TestSurface... surfaces) throws InterruptedException {
-        verifyCompatibleAndStableFrameRate(0, surfaces);
+        verifyFrameRates(List.of(), surfaces);
     }
 
     private void verifyExactAndStableFrameRate(
             float expectedFrameRate,
             TestSurface... surfaces) throws InterruptedException {
-        verifyFrameRate(expectedFrameRate, false, surfaces);
+        verifyFrameRate(List.of(expectedFrameRate), false, surfaces);
     }
 
     private void verifyCompatibleAndStableFrameRate(
             float expectedFrameRate,
             TestSurface... surfaces) throws InterruptedException {
-        verifyFrameRate(expectedFrameRate, true, surfaces);
+        verifyFrameRate(List.of(expectedFrameRate), true, surfaces);
     }
 
-    // Set expectedFrameRate to 0.0 to verify only stable frame rate.
-    private void verifyFrameRate(
-            float expectedFrameRate, boolean multiplesAllowed,
+    /** Verify stable frame rate at one of the expectedFrameRates. */
+    private void verifyFrameRates(List<Float> expectedFrameRates, TestSurface... surfaces)
+            throws InterruptedException {
+        verifyFrameRate(expectedFrameRates, true, surfaces);
+    }
+
+    // Set expectedFrameRates to empty to verify only stable frame rate.
+    private void verifyFrameRate(List<Float> expectedFrameRates, boolean multiplesAllowed,
             TestSurface... surfaces) throws InterruptedException {
         Log.i(TAG, "Verifying compatible and stable frame rate");
         long nowNanos = System.nanoTime();
         long gracePeriodEndTimeNanos =
                 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS * 1_000_000_000L;
         while (true) {
-            if (expectedFrameRate > FRAME_RATE_TOLERANCE) { // expectedFrameRate > 0
+            if (expectedFrameRates.size() == 1) {
+                float expectedFrameRate = expectedFrameRates.get(0);
                 // Wait until we switch to a compatible frame rate.
                 Log.i(TAG,
                         String.format(
@@ -557,11 +567,25 @@
             while (endTimeNanos > nowNanos) {
                 int numModeChangedEvents = mModeChangedEvents.size();
                 if (waitForEvents(endTimeNanos, surfaces)) {
-                    Log.i(TAG,
-                            String.format("Stable frame rate %.2f verified",
-                                    multiplesAllowed ? mDisplayModeRefreshRate
-                                                     : mDisplayRefreshRate));
-                    return;
+                    // Verify any expected frame rate since there are multiple that will suffice.
+                    // Mainly to account for running tests on real devices, where other non-test
+                    // layers may affect the outcome.
+                    if (expectedFrameRates.size() > 1) {
+                        for (float expectedFrameRate : expectedFrameRates) {
+                            if (isFrameRateMultiple(mDisplayModeRefreshRate, expectedFrameRate)) {
+                                return;
+                            }
+                        }
+                        // The frame rate is stable but it is not one of the expected frame rates.
+                        throw new FrameRateTimeoutException(
+                                expectedFrameRates, mDisplayModeRefreshRate);
+                    } else {
+                        Log.i(TAG,
+                                String.format("Stable frame rate %.2f verified",
+                                        multiplesAllowed ? mDisplayModeRefreshRate
+                                                         : mDisplayRefreshRate));
+                        return;
+                    }
                 }
                 nowNanos = System.nanoTime();
                 if (mModeChangedEvents.size() > numModeChangedEvents) {
@@ -623,12 +647,28 @@
                             // caused the timeout failure. Wait for a bit to see if we get notified
                             // of a precondition violation, and if so, retry the test. Otherwise
                             // fail.
-                            assertTrue(String.format(
-                                               "Timed out waiting for a stable and compatible frame"
-                                                       + " rate. expected=%.2f received=%.2f."
-                                                       + " Stack trace: " + stackTrace,
-                                               exc.expectedFrameRate, exc.deviceFrameRate),
-                                    waitForPreconditionViolation());
+                            if (exc.expectedFrameRates.isEmpty()) {
+                                assertTrue(
+                                        String.format(
+                                                "Timed out waiting for a stable and compatible"
+                                                        + " frame rate."
+                                                        + " expected=%.2f received=%.2f."
+                                                        + " Stack trace: " + stackTrace,
+                                                exc.expectedFrameRate, exc.deviceFrameRate),
+                                        waitForPreconditionViolation());
+                            } else {
+                                assertTrue(
+                                        String.format(
+                                                "Timed out waiting for a stable and compatible"
+                                                        + " frame rate."
+                                                        + " expected={%.2f} received=%.2f."
+                                                        + " Stack trace: " + stackTrace,
+                                                exc.expectedFrameRates.stream()
+                                                        .map(Object::toString)
+                                                        .collect(Collectors.joining(", ")),
+                                                exc.deviceFrameRate),
+                                        waitForPreconditionViolation());
+                            }
                         }
 
                         if (!testPassed) {
@@ -679,10 +719,15 @@
                     "**** Running testSurfaceControlFrameRateCompatibility with compatibility "
                             + compatibility);
 
-            float expectedFrameRate = getExpectedFrameRateForCompatibility(compatibility);
+            List<Float> expectedFrameRates = getExpectedFrameRateForCompatibility(compatibility);
+            Log.i(TAG,
+                    "Expected frame rates: "
+                            + expectedFrameRates.stream()
+                                      .map(Object::toString)
+                                      .collect(Collectors.joining(", ")));
             int initialNumEvents = mModeChangedEvents.size();
             surface.setFrameRate(30.f, compatibility);
-            verifyExactAndStableFrameRate(expectedFrameRate, surface);
+            verifyFrameRates(expectedFrameRates, surface);
             verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size());
         });
     }
@@ -699,10 +744,10 @@
         runOneSurfaceTest((TestSurface surface) -> {
             Log.i(TAG, "**** Running testSurfaceControlFrameRateCategory for category " + category);
 
-            float expectedFrameRate = getExpectedFrameRateForCategory(category);
+            List<Float> expectedFrameRates = getExpectedFrameRateForCategory(category);
             int initialNumEvents = mModeChangedEvents.size();
             surface.setFrameRateCategory(category);
-            verifyCompatibleAndStableFrameRate(expectedFrameRate, surface);
+            verifyFrameRates(expectedFrameRates, surface);
             verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size());
         });
     }
@@ -771,41 +816,41 @@
                 "frame rate strategy=" + parentStrategy);
     }
 
-    private float getExpectedFrameRateForCompatibility(int compatibility) {
+    private List<Float> getExpectedFrameRateForCompatibility(int compatibility) {
         assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED for compatibility "
                         + compatibility,
                 compatibility == Surface.FRAME_RATE_COMPATIBILITY_GTE);
 
         Display display = getDisplay();
-        Optional<Float> expectedFrameRate = getRefreshRates(display.getMode(), display)
-                                                    .stream()
-                                                    .filter(rate -> rate >= 30.f)
-                                                    .min(Comparator.naturalOrder());
+        List<Float> expectedFrameRates = getRefreshRates(display.getMode(), display)
+                                                 .stream()
+                                                 .filter(rate -> rate >= 30.f)
+                                                 .collect(Collectors.toList());
 
         assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED because no refresh rate "
                         + "is >= 30",
-                expectedFrameRate.isPresent());
-        return expectedFrameRate.get();
+                !expectedFrameRates.isEmpty());
+        return expectedFrameRates;
     }
 
-    private float getExpectedFrameRateForCategory(int category) {
+    private List<Float> getExpectedFrameRateForCategory(int category) {
         Display display = getDisplay();
         List<Float> frameRates = getRefreshRates(display.getMode(), display);
 
         if (category == Surface.FRAME_RATE_CATEGORY_DEFAULT) {
             // Max due to default vote and no other frame rate specifications.
-            return Collections.max(frameRates);
+            return List.of(Collections.max(frameRates));
         } else if (category == Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE) {
-            return Collections.min(frameRates);
+            return frameRates;
         }
 
         FpsRange categoryRange = convertCategory(category);
-        Optional<Float> expectedFrameRate = frameRates.stream()
-                                                    .filter(fps -> categoryRange.includes(fps))
-                                                    .min(Comparator.naturalOrder());
+        List<Float> expectedFrameRates = frameRates.stream()
+                                                 .filter(fps -> categoryRange.includes(fps))
+                                                 .collect(Collectors.toList());
         assumeTrue("**** testSurfaceControlFrameRateCategory SKIPPED for category " + category,
-                expectedFrameRate.isPresent());
-        return expectedFrameRate.get();
+                !expectedFrameRates.isEmpty());
+        return expectedFrameRates;
     }
 
     private FpsRange convertCategory(int category) {
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index 29cbf01..e3988cd 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -24,8 +24,8 @@
 import android.view.View;
 import android.widget.ToggleButton;
 
+import androidx.window.embedding.ActivityEmbeddingController;
 import androidx.window.embedding.SplitAttributes;
-import androidx.window.embedding.SplitAttributesCalculatorParams;
 import androidx.window.embedding.SplitController;
 
 /**
@@ -66,7 +66,9 @@
                     @Override
                     public void onClick(View v) {
                         // This triggers a recalcuation of splitatributes.
-                        mSplitController.invalidateTopVisibleSplitAttributes();
+                        ActivityEmbeddingController
+                                .getInstance(ActivityEmbeddingSecondaryActivity.this)
+                                .invalidateTopVisibleActivityStacks();
                     }
         });
         findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
diff --git a/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java b/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java
index be479f2..1b02792 100644
--- a/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java
+++ b/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java
@@ -25,6 +25,7 @@
 import android.security.Flags;
 
 import com.android.blockdevicewriter.BlockDeviceWriter;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
@@ -52,7 +53,6 @@
     private static final String TARGET_PACKAGE = "com.android.fsverity";
 
     private static final String BASENAME = "test.file";
-    private static final String TARGET_PATH = "/data/data/" + TARGET_PACKAGE + "/files/" + BASENAME;
 
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
@@ -63,11 +63,11 @@
         prepareTest(10000);
 
         ITestDevice device = getDevice();
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 0);
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 8192);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 0);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 8192);
         BlockDeviceWriter.dropCaches(device);
 
-        verifyRead(TARGET_PATH, "0,2");
+        verifyRead(getTargetFilePath(), "0,2");
     }
 
     @Test
@@ -75,12 +75,17 @@
         prepareTest(128 * 4096 + 1);
 
         ITestDevice device = getDevice();
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 4096);
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 100 * 4096);
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 128 * 4096 + 1);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 4096);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 100 * 4096);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 128 * 4096 + 1);
         BlockDeviceWriter.dropCaches(device);
 
-        verifyRead(TARGET_PATH, "1,100,128");
+        verifyRead(getTargetFilePath(), "1,100,128");
+    }
+
+    private String getTargetFilePath() throws DeviceNotAvailableException {
+        return "/data/user/" + getDevice().getCurrentUser() + "/" + TARGET_PACKAGE + "/files/"
+                + BASENAME;
     }
 
     private void prepareTest(int fileSize) throws Exception {
@@ -97,7 +102,7 @@
         options.setTestClassName(TARGET_PACKAGE + ".Helper");
         options.setTestMethodName("verifyFileRead");
         options.addInstrumentationArg("brokenBlockIndicesCsv", indicesCsv);
-        options.addInstrumentationArg("filePath", TARGET_PATH);
+        options.addInstrumentationArg("filePath", getTargetFilePath());
         assertThat(runDeviceTests(options)).isTrue();
     }
 }
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index c1784f3..7b191f8 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -22,7 +22,6 @@
 import android.hardware.display.DisplayViewport
 import android.hardware.input.InputManager
 import android.hardware.input.InputManagerGlobal
-import android.os.IInputConstants
 import android.os.test.TestLooper
 import android.platform.test.annotations.Presubmit
 import android.provider.Settings
@@ -295,14 +294,13 @@
 
         localService.setPointerIconVisible(false, 10)
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        localService.setPointerAcceleration(5f, 10)
-        verify(native).setPointerAcceleration(eq(5f))
+        localService.setMousePointerAccelerationEnabled(false, 10)
+        verify(native).setMousePointerAccelerationEnabled(eq(false))
 
         service.onDisplayRemoved(10)
         verify(native).displayRemoved(eq(10))
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
-        verify(native).setPointerAcceleration(
-            eq(IInputConstants.DEFAULT_POINTER_ACCELERATION.toFloat()))
+        verify(native).setMousePointerAccelerationEnabled(true)
         verifyNoMoreInteractions(native)
 
         // This call should not block because the virtual mouse pointer override was never removed.
@@ -318,38 +316,38 @@
 
         localService.setPointerIconVisible(false, 10)
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        localService.setPointerAcceleration(5f, 10)
-        verify(native).setPointerAcceleration(eq(5f))
+        localService.setMousePointerAccelerationEnabled(false, 10)
+        verify(native).setMousePointerAccelerationEnabled(eq(false))
 
         localService.setPointerIconVisible(true, 10)
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
-        localService.setPointerAcceleration(1f, 10)
-        verify(native).setPointerAcceleration(eq(1f))
+        localService.setMousePointerAccelerationEnabled(true, 10)
+        verify(native).setMousePointerAccelerationEnabled(eq(true))
 
         // Verify that setting properties on a different display is not propagated until the
         // pointer is moved to that display.
         localService.setPointerIconVisible(false, 20)
-        localService.setPointerAcceleration(6f, 20)
+        localService.setMousePointerAccelerationEnabled(false, 20)
         verifyNoMoreInteractions(native)
 
         clearInvocations(native)
         setVirtualMousePointerDisplayIdAndVerify(20)
 
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        verify(native).setPointerAcceleration(eq(6f))
+        verify(native).setMousePointerAccelerationEnabled(eq(false))
     }
 
     @Test
     fun setAdditionalInputPropertiesBeforeOverride() {
         localService.setPointerIconVisible(false, 10)
-        localService.setPointerAcceleration(5f, 10)
+        localService.setMousePointerAccelerationEnabled(false, 10)
 
         verifyNoMoreInteractions(native)
 
         setVirtualMousePointerDisplayIdAndVerify(10)
 
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        verify(native).setPointerAcceleration(eq(5f))
+        verify(native).setMousePointerAccelerationEnabled(eq(false))
     }
 
     @Test
diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
index ba9e4a8..f82d9ca 100644
--- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
+++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
@@ -130,14 +130,13 @@
     private static final Pattern PATTERN_SYSTEM_FONT_FILES =
             Pattern.compile("^/(system|product)/fonts/");
 
-    private String mKeyId;
     private FontManager mFontManager;
     private UiDevice mUiDevice;
 
     @Before
     public void setUp() throws Exception {
         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        mKeyId = insertCert(CERT_PATH);
+        insertCert(CERT_PATH);
         mFontManager = context.getSystemService(FontManager.class);
         expectCommandToSucceed("cmd font clear");
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
@@ -147,9 +146,6 @@
     public void tearDown() throws Exception {
         // Ignore errors because this may fail if updatable system font is not enabled.
         runShellCommand("cmd font clear", null);
-        if (mKeyId != null) {
-            expectCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
-        }
     }
 
     @Test
@@ -369,20 +365,11 @@
         assertThat(isFileOpenedBy(fontPath, EMOJI_RENDERING_TEST_APP_ID)).isFalse();
     }
 
-    private static String insertCert(String certPath) throws Exception {
-        Pair<String, String> result;
-        try (InputStream is = new FileInputStream(certPath)) {
-            result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is);
-        }
+    private static void insertCert(String certPath) throws Exception {
         // /data/local/tmp is not readable by system server. Copy a cert file to /data/fonts
         final String copiedCert = "/data/fonts/debug_cert.der";
         runShellCommand("cp " + certPath + " " + copiedCert, null);
         runShellCommand("cmd font install-debug-cert " + copiedCert, null);
-        // Assert that there are no errors.
-        assertThat(result.second).isEmpty();
-        String keyId = result.first.trim();
-        assertThat(keyId).matches("^\\d+$");
-        return keyId;
     }
 
     private int updateFontFile(String fontPath, String signaturePath) throws IOException {
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index 692c8a8..49665f7 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -27,15 +27,18 @@
 import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY;
 
 import static com.android.server.vcn.VcnGatewayConnection.DUMMY_ADDR;
+import static com.android.server.vcn.VcnGatewayConnection.SAFEMODE_TIMEOUT_SECONDS;
 import static com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration;
 import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
 import static com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 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.CALLS_REAL_METHODS;
@@ -55,6 +58,7 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
+import android.net.vcn.VcnManager;
 import android.net.vcn.VcnTransportInfo;
 import android.net.wifi.WifiInfo;
 import android.os.ParcelUuid;
@@ -81,6 +85,7 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 
 /** Tests for TelephonySubscriptionTracker */
 @RunWith(AndroidJUnit4.class)
@@ -352,4 +357,71 @@
                         any(Executor.class),
                         any(ConnectivityDiagnosticsCallback.class));
     }
+
+    private void verifyGetSafeModeTimeoutMs(
+            boolean isInTestMode,
+            boolean isConfigTimeoutSupported,
+            PersistableBundleWrapper carrierConfig,
+            long expectedTimeoutMs)
+            throws Exception {
+        doReturn(isInTestMode).when(mVcnContext).isInTestMode();
+        doReturn(isConfigTimeoutSupported).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
+
+        final TelephonySubscriptionSnapshot snapshot = mock(TelephonySubscriptionSnapshot.class);
+        doReturn(carrierConfig).when(snapshot).getCarrierConfigForSubGrp(TEST_SUB_GRP);
+
+        final long result =
+                VcnGatewayConnection.getSafeModeTimeoutMs(mVcnContext, snapshot, TEST_SUB_GRP);
+
+        assertEquals(expectedTimeoutMs, result);
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutUnsupported() throws Exception {
+        verifyGetSafeModeTimeoutMs(
+                false /* isInTestMode */,
+                false /* isConfigTimeoutSupported */,
+                null /* carrierConfig */,
+                TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutSupported() throws Exception {
+        final int carrierConfigTimeoutSeconds = 20;
+        final PersistableBundleWrapper carrierConfig = mock(PersistableBundleWrapper.class);
+        doReturn(carrierConfigTimeoutSeconds)
+                .when(carrierConfig)
+                .getInt(eq(VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY), anyInt());
+
+        verifyGetSafeModeTimeoutMs(
+                false /* isInTestMode */,
+                true /* isConfigTimeoutSupported */,
+                carrierConfig,
+                TimeUnit.SECONDS.toMillis(carrierConfigTimeoutSeconds));
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutSupported_carrierConfigNull()
+            throws Exception {
+        verifyGetSafeModeTimeoutMs(
+                false /* isInTestMode */,
+                true /* isConfigTimeoutSupported */,
+                null /* carrierConfig */,
+                TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutOverrideTestModeDefault() throws Exception {
+        final int carrierConfigTimeoutSeconds = 20;
+        final PersistableBundleWrapper carrierConfig = mock(PersistableBundleWrapper.class);
+        doReturn(carrierConfigTimeoutSeconds)
+                .when(carrierConfig)
+                .getInt(eq(VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY), anyInt());
+
+        verifyGetSafeModeTimeoutMs(
+                true /* isInTestMode */,
+                true /* isConfigTimeoutSupported */,
+                carrierConfig,
+                TimeUnit.SECONDS.toMillis(carrierConfigTimeoutSeconds));
+    }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index edced87..4c7b25a 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -67,6 +67,8 @@
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.Vcn.VcnGatewayStatusCallback;
 import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
+import com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
+import com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
 import com.android.server.vcn.VcnGatewayConnection.VcnWakeLock;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController;
 import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
@@ -118,13 +120,7 @@
             NetworkCapabilities networkCapabilities,
             LinkProperties linkProperties,
             boolean isBlocked) {
-        return new UnderlyingNetworkRecord(
-                network,
-                networkCapabilities,
-                linkProperties,
-                isBlocked,
-                false /* isSelected */,
-                0 /* priorityClass */);
+        return new UnderlyingNetworkRecord(network, networkCapabilities, linkProperties, isBlocked);
     }
 
     protected static final String TEST_TCP_BUFFER_SIZES_1 = "1,2,3,4";
@@ -226,6 +222,7 @@
         doReturn(mTestLooper.getLooper()).when(mVcnContext).getLooper();
         doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
         doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
+        doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
new file mode 100644
index 0000000..9daba6a
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.vcn.routeselection;
+
+import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY;
+import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY;
+
+import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.PACKET_LOSS_UNAVALAIBLE;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.net.IpSecTransformState;
+import android.os.OutcomeReceiver;
+import android.os.PowerManager;
+
+import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculator;
+import com.android.server.vcn.routeselection.NetworkMetricMonitor.IpSecTransformWrapper;
+import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.concurrent.TimeUnit;
+
+public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase {
+    private static final String TAG = IpSecPacketLossDetectorTest.class.getSimpleName();
+
+    private static final int REPLAY_BITMAP_LEN_BYTE = 512;
+    private static final int REPLAY_BITMAP_LEN_BIT = REPLAY_BITMAP_LEN_BYTE * 8;
+    private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD = 5;
+    private static final long POLL_IPSEC_STATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30L);
+
+    @Mock private IpSecTransformWrapper mIpSecTransform;
+    @Mock private NetworkMetricMonitorCallback mMetricMonitorCallback;
+    @Mock private PersistableBundleWrapper mCarrierConfig;
+    @Mock private IpSecPacketLossDetector.Dependencies mDependencies;
+    @Spy private PacketLossCalculator mPacketLossCalculator = new PacketLossCalculator();
+
+    @Captor private ArgumentCaptor<OutcomeReceiver> mTransformStateReceiverCaptor;
+    @Captor private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
+
+    private IpSecPacketLossDetector mIpSecPacketLossDetector;
+    private IpSecTransformState mTransformStateInitial;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mTransformStateInitial = newTransformState(0, 0, newReplayBitmap(0));
+
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY), anyInt()))
+                .thenReturn((int) TimeUnit.MILLISECONDS.toSeconds(POLL_IPSEC_STATE_INTERVAL_MS));
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY),
+                        anyInt()))
+                .thenReturn(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD);
+
+        when(mDependencies.getPacketLossCalculator()).thenReturn(mPacketLossCalculator);
+
+        mIpSecPacketLossDetector =
+                new IpSecPacketLossDetector(
+                        mVcnContext,
+                        mNetwork,
+                        mCarrierConfig,
+                        mMetricMonitorCallback,
+                        mDependencies);
+    }
+
+    private static IpSecTransformState newTransformState(
+            long rxSeqNo, long packtCount, byte[] replayBitmap) {
+        return new IpSecTransformState.Builder()
+                .setRxHighestSequenceNumber(rxSeqNo)
+                .setPacketCount(packtCount)
+                .setReplayBitmap(replayBitmap)
+                .build();
+    }
+
+    private static byte[] newReplayBitmap(int receivedPktCnt) {
+        final BitSet bitSet = new BitSet(REPLAY_BITMAP_LEN_BIT);
+        for (int i = 0; i < receivedPktCnt; i++) {
+            bitSet.set(i);
+        }
+        return Arrays.copyOf(bitSet.toByteArray(), REPLAY_BITMAP_LEN_BYTE);
+    }
+
+    private void verifyStopped() {
+        assertFalse(mIpSecPacketLossDetector.isStarted());
+        assertFalse(mIpSecPacketLossDetector.isValidationFailed());
+        assertNull(mIpSecPacketLossDetector.getLastTransformState());
+
+        // No event scheduled
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testInitialization() throws Exception {
+        assertFalse(mIpSecPacketLossDetector.isSelectedUnderlyingNetwork());
+        verifyStopped();
+    }
+
+    private OutcomeReceiver<IpSecTransformState, RuntimeException>
+            startMonitorAndCaptureStateReceiver() {
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
+        mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
+
+        // Trigger the runnable
+        mTestLooper.dispatchAll();
+
+        verify(mIpSecTransform)
+                .getIpSecTransformState(any(), mTransformStateReceiverCaptor.capture());
+        return mTransformStateReceiverCaptor.getValue();
+    }
+
+    @Test
+    public void testStartMonitor() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+
+        assertTrue(mIpSecPacketLossDetector.isStarted());
+        assertFalse(mIpSecPacketLossDetector.isValidationFailed());
+        assertTrue(mIpSecPacketLossDetector.isSelectedUnderlyingNetwork());
+        assertEquals(mIpSecTransform, mIpSecPacketLossDetector.getInboundTransformInternal());
+
+        // Mock receiving a state
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+
+        // Verify the first polled state is stored
+        assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+        verify(mPacketLossCalculator, never())
+                .getPacketLossRatePercentage(any(), any(), anyString());
+
+        // Verify next poll is scheduled
+        assertNull(mTestLooper.nextMessage());
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        assertNotNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testStartedMonitor_enterDozeMoze() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+
+        // Mock receiving a state
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+        assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+
+        // Mock entering doze mode
+        final Intent intent = mock(Intent.class);
+        when(intent.getAction()).thenReturn(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+        when(mPowerManagerService.isDeviceIdleMode()).thenReturn(true);
+
+        verify(mContext).registerReceiver(mBroadcastReceiverCaptor.capture(), any(), any(), any());
+        final BroadcastReceiver broadcastReceiver = mBroadcastReceiverCaptor.getValue();
+        broadcastReceiver.onReceive(mContext, intent);
+
+        assertNull(mIpSecPacketLossDetector.getLastTransformState());
+    }
+
+    @Test
+    public void testStartedMonitor_updateInboundTransform() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+
+        // Mock receiving a state
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+        assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+
+        // Update the inbound transform
+        final IpSecTransformWrapper newTransform = mock(IpSecTransformWrapper.class);
+        mIpSecPacketLossDetector.setInboundTransformInternal(newTransform);
+
+        // Verifications
+        assertNull(mIpSecPacketLossDetector.getLastTransformState());
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        mTestLooper.dispatchAll();
+        verify(newTransform).getIpSecTransformState(any(), any());
+    }
+
+    @Test
+    public void testStartedMonitor_updateCarrierConfig() throws Exception {
+        startMonitorAndCaptureStateReceiver();
+
+        final int additionalPollIntervalMs = (int) TimeUnit.SECONDS.toMillis(10L);
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY), anyInt()))
+                .thenReturn(
+                        (int)
+                                TimeUnit.MILLISECONDS.toSeconds(
+                                        POLL_IPSEC_STATE_INTERVAL_MS + additionalPollIntervalMs));
+        mIpSecPacketLossDetector.setCarrierConfig(mCarrierConfig);
+        mTestLooper.dispatchAll();
+
+        // The already scheduled event is still fired with the old timeout
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        mTestLooper.dispatchAll();
+
+        // The next scheduled event will take 10 more seconds to fire
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        assertNull(mTestLooper.nextMessage());
+        mTestLooper.moveTimeForward(additionalPollIntervalMs);
+        assertNotNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testStopMonitor() throws Exception {
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
+        mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
+
+        assertTrue(mIpSecPacketLossDetector.isStarted());
+        assertNotNull(mTestLooper.nextMessage());
+
+        // Unselect the monitor
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(false /* setIsSelected */);
+        verifyStopped();
+    }
+
+    @Test
+    public void testClose() throws Exception {
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
+        mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
+
+        assertTrue(mIpSecPacketLossDetector.isStarted());
+        assertNotNull(mTestLooper.nextMessage());
+
+        // Stop the monitor
+        mIpSecPacketLossDetector.close();
+        verifyStopped();
+        verify(mIpSecTransform).close();
+    }
+
+    @Test
+    public void testTransformStateReceiverOnResultWhenStopped() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+
+        // Unselect the monitor
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(false /* setIsSelected */);
+        verifyStopped();
+
+        xfrmStateReceiver.onResult(newTransformState(1, 1, newReplayBitmap(1)));
+        verify(mPacketLossCalculator, never())
+                .getPacketLossRatePercentage(any(), any(), anyString());
+    }
+
+    @Test
+    public void testTransformStateReceiverOnError() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+
+        xfrmStateReceiver.onError(new RuntimeException("Test"));
+        verify(mPacketLossCalculator, never())
+                .getPacketLossRatePercentage(any(), any(), anyString());
+    }
+
+    private void checkHandleLossRate(
+            int mockPacketLossRate, boolean isLastStateExpectedToUpdate, boolean isCallbackExpected)
+            throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+        doReturn(mockPacketLossRate)
+                .when(mPacketLossCalculator)
+                .getPacketLossRatePercentage(any(), any(), anyString());
+
+        // Mock receiving two states with mTransformStateInitial and an arbitrary transformNew
+        final IpSecTransformState transformNew = newTransformState(1, 1, newReplayBitmap(1));
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+        xfrmStateReceiver.onResult(transformNew);
+
+        // Verifications
+        verify(mPacketLossCalculator)
+                .getPacketLossRatePercentage(
+                        eq(mTransformStateInitial), eq(transformNew), anyString());
+
+        if (isLastStateExpectedToUpdate) {
+            assertEquals(transformNew, mIpSecPacketLossDetector.getLastTransformState());
+        } else {
+            assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+        }
+
+        if (isCallbackExpected) {
+            verify(mMetricMonitorCallback).onValidationResultReceived();
+        } else {
+            verify(mMetricMonitorCallback, never()).onValidationResultReceived();
+        }
+    }
+
+    @Test
+    public void testHandleLossRate_validationPass() throws Exception {
+        checkHandleLossRate(
+                2, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */);
+    }
+
+    @Test
+    public void testHandleLossRate_validationFail() throws Exception {
+        checkHandleLossRate(
+                22, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */);
+    }
+
+    @Test
+    public void testHandleLossRate_resultUnavalaible() throws Exception {
+        checkHandleLossRate(
+                PACKET_LOSS_UNAVALAIBLE,
+                false /* isLastStateExpectedToUpdate */,
+                false /* isCallbackExpected */);
+    }
+
+    private void checkGetPacketLossRate(
+            IpSecTransformState oldState, IpSecTransformState newState, int expectedLossRate)
+            throws Exception {
+        assertEquals(
+                expectedLossRate,
+                mPacketLossCalculator.getPacketLossRatePercentage(oldState, newState, TAG));
+    }
+
+    private void checkGetPacketLossRate(
+            IpSecTransformState oldState,
+            int rxSeqNo,
+            int packetCount,
+            int packetInWin,
+            int expectedDataLossRate)
+            throws Exception {
+        final IpSecTransformState newState =
+                newTransformState(rxSeqNo, packetCount, newReplayBitmap(packetInWin));
+        checkGetPacketLossRate(oldState, newState, expectedDataLossRate);
+    }
+
+    @Test
+    public void testGetPacketLossRate_replayWindowUnchanged() throws Exception {
+        checkGetPacketLossRate(
+                mTransformStateInitial, mTransformStateInitial, PACKET_LOSS_UNAVALAIBLE);
+        checkGetPacketLossRate(mTransformStateInitial, 3000, 2000, 2000, PACKET_LOSS_UNAVALAIBLE);
+    }
+
+    @Test
+    public void testGetPacketLossRate_againstInitialState() throws Exception {
+        checkGetPacketLossRate(mTransformStateInitial, 7000, 7001, 4096, 0);
+        checkGetPacketLossRate(mTransformStateInitial, 7000, 6000, 4096, 15);
+        checkGetPacketLossRate(mTransformStateInitial, 7000, 6000, 4000, 14);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqSmallerThanWinSize_overlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(2000, 1500, newReplayBitmap(1500));
+
+        checkGetPacketLossRate(oldState, 5000, 5001, 4096, 0);
+        checkGetPacketLossRate(oldState, 5000, 4000, 4096, 29);
+        checkGetPacketLossRate(oldState, 5000, 4000, 4000, 27);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqSmallerThanWinSize_notOverlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(2000, 1500, newReplayBitmap(1500));
+
+        checkGetPacketLossRate(oldState, 7000, 7001, 4096, 0);
+        checkGetPacketLossRate(oldState, 7000, 5000, 4096, 37);
+        checkGetPacketLossRate(oldState, 7000, 5000, 3000, 21);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqLargerThanWinSize_overlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(10000, 5000, newReplayBitmap(3000));
+
+        checkGetPacketLossRate(oldState, 12000, 8096, 4096, 0);
+        checkGetPacketLossRate(oldState, 12000, 7000, 4096, 36);
+        checkGetPacketLossRate(oldState, 12000, 7000, 3000, 0);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqLargerThanWinSize_notOverlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(10000, 5000, newReplayBitmap(3000));
+
+        checkGetPacketLossRate(oldState, 20000, 16096, 4096, 0);
+        checkGetPacketLossRate(oldState, 20000, 14000, 4096, 19);
+        checkGetPacketLossRate(oldState, 20000, 14000, 3000, 10);
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
new file mode 100644
index 0000000..355c221
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.vcn.routeselection;
+
+import static com.android.server.vcn.VcnTestUtils.setupSystemService;
+import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName;
+
+import static org.mockito.Mockito.doNothing;
+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.content.Context;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.FeatureFlags;
+import android.os.Handler;
+import android.os.IPowerManager;
+import android.os.IThermalService;
+import android.os.ParcelUuid;
+import android.os.PowerManager;
+import android.os.test.TestLooper;
+import android.telephony.TelephonyManager;
+
+import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.VcnNetworkProvider;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Set;
+import java.util.UUID;
+
+public abstract class NetworkEvaluationTestBase {
+    protected static final String SSID = "TestWifi";
+    protected static final String SSID_OTHER = "TestWifiOther";
+    protected static final String PLMN_ID = "123456";
+    protected static final String PLMN_ID_OTHER = "234567";
+
+    protected static final int SUB_ID = 1;
+    protected static final int WIFI_RSSI = -60;
+    protected static final int WIFI_RSSI_HIGH = -50;
+    protected static final int WIFI_RSSI_LOW = -80;
+    protected static final int CARRIER_ID = 1;
+    protected static final int CARRIER_ID_OTHER = 2;
+
+    protected static final int LINK_UPSTREAM_BANDWIDTH_KBPS = 1024;
+    protected static final int LINK_DOWNSTREAM_BANDWIDTH_KBPS = 2048;
+
+    protected static final int TEST_MIN_UPSTREAM_BANDWIDTH_KBPS = 100;
+    protected static final int TEST_MIN_DOWNSTREAM_BANDWIDTH_KBPS = 200;
+
+    protected static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
+
+    protected static final NetworkCapabilities WIFI_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .setSignalStrength(WIFI_RSSI)
+                    .setSsid(SSID)
+                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
+                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
+                    .build();
+
+    protected static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
+            new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
+    protected static final NetworkCapabilities CELL_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                    .setSubscriptionIds(Set.of(SUB_ID))
+                    .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
+                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
+                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
+                    .build();
+
+    protected static final LinkProperties LINK_PROPERTIES = getLinkPropertiesWithName("test_iface");
+
+    @Mock protected Context mContext;
+    @Mock protected Network mNetwork;
+    @Mock protected FeatureFlags mFeatureFlags;
+    @Mock protected com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
+    @Mock protected TelephonySubscriptionSnapshot mSubscriptionSnapshot;
+    @Mock protected TelephonyManager mTelephonyManager;
+    @Mock protected IPowerManager mPowerManagerService;
+
+    protected TestLooper mTestLooper;
+    protected VcnContext mVcnContext;
+    protected PowerManager mPowerManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mNetwork.getNetId()).thenReturn(-1);
+
+        mTestLooper = new TestLooper();
+        mVcnContext =
+                spy(
+                        new VcnContext(
+                                mContext,
+                                mTestLooper.getLooper(),
+                                mock(VcnNetworkProvider.class),
+                                false /* isInTestMode */));
+        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+
+        doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
+        doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
+
+        setupSystemService(
+                mContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
+        when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
+        when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
+        when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
+
+        mPowerManager =
+                new PowerManager(
+                        mContext,
+                        mPowerManagerService,
+                        mock(IThermalService.class),
+                        mock(Handler.class));
+        setupSystemService(mContext, mPowerManager, Context.POWER_SERVICE, PowerManager.class);
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
index 2266041..d85c515 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -24,152 +24,48 @@
 import static android.net.vcn.VcnUnderlyingNetworkTemplateTestBase.TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS;
 import static android.net.vcn.VcnUnderlyingNetworkTemplateTestBase.TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS;
 
-import static com.android.server.vcn.VcnTestUtils.setupSystemService;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_FALLBACK;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesCellPriorityRule;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesPriorityRule;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesWifiPriorityRule;
-import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName;
 import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
-import android.content.Context;
-import android.net.LinkProperties;
-import android.net.Network;
 import android.net.NetworkCapabilities;
-import android.net.TelephonyNetworkSpecifier;
 import android.net.vcn.VcnCellUnderlyingNetworkTemplate;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnManager;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.net.vcn.VcnWifiUnderlyingNetworkTemplate;
-import android.os.ParcelUuid;
 import android.os.PersistableBundle;
-import android.os.test.TestLooper;
-import android.telephony.TelephonyManager;
 import android.util.ArraySet;
 
-import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
-import com.android.server.vcn.VcnContext;
-import com.android.server.vcn.VcnNetworkProvider;
-
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
-import java.util.UUID;
 
-public class NetworkPriorityClassifierTest {
-    private static final String SSID = "TestWifi";
-    private static final String SSID_OTHER = "TestWifiOther";
-    private static final String PLMN_ID = "123456";
-    private static final String PLMN_ID_OTHER = "234567";
-
-    private static final int SUB_ID = 1;
-    private static final int WIFI_RSSI = -60;
-    private static final int WIFI_RSSI_HIGH = -50;
-    private static final int WIFI_RSSI_LOW = -80;
-    private static final int CARRIER_ID = 1;
-    private static final int CARRIER_ID_OTHER = 2;
-
-    private static final int LINK_UPSTREAM_BANDWIDTH_KBPS = 1024;
-    private static final int LINK_DOWNSTREAM_BANDWIDTH_KBPS = 2048;
-
-    private static final int TEST_MIN_UPSTREAM_BANDWIDTH_KBPS = 100;
-    private static final int TEST_MIN_DOWNSTREAM_BANDWIDTH_KBPS = 200;
-
-    private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
-
-    private static final NetworkCapabilities WIFI_NETWORK_CAPABILITIES =
-            new NetworkCapabilities.Builder()
-                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                    .setSignalStrength(WIFI_RSSI)
-                    .setSsid(SSID)
-                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
-                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
-                    .build();
-
-    private static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
-            new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
-    private static final NetworkCapabilities CELL_NETWORK_CAPABILITIES =
-            new NetworkCapabilities.Builder()
-                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                    .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
-                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-                    .setSubscriptionIds(Set.of(SUB_ID))
-                    .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
-                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
-                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
-                    .build();
-
-    private static final LinkProperties LINK_PROPERTIES = getLinkPropertiesWithName("test_iface");
-
-    @Mock private Network mNetwork;
-    @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
-    @Mock private TelephonyManager mTelephonyManager;
-
-    private TestLooper mTestLooper;
-    private VcnContext mVcnContext;
+public class NetworkPriorityClassifierTest extends NetworkEvaluationTestBase {
     private UnderlyingNetworkRecord mWifiNetworkRecord;
     private UnderlyingNetworkRecord mCellNetworkRecord;
 
     @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
+    public void setUp() throws Exception {
+        super.setUp();
 
-        final Context mockContext = mock(Context.class);
-        mTestLooper = new TestLooper();
-        mVcnContext =
-                spy(
-                        new VcnContext(
-                                mockContext,
-                                mTestLooper.getLooper(),
-                                mock(VcnNetworkProvider.class),
-                                false /* isInTestMode */));
-        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
-
-        setupSystemService(
-                mockContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
-        when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
-        when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
-        when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
-
-        mWifiNetworkRecord =
-                getTestNetworkRecord(
-                        WIFI_NETWORK_CAPABILITIES,
-                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES);
-        mCellNetworkRecord =
-                getTestNetworkRecord(
-                        CELL_NETWORK_CAPABILITIES,
-                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES);
+        mWifiNetworkRecord = getTestNetworkRecord(WIFI_NETWORK_CAPABILITIES);
+        mCellNetworkRecord = getTestNetworkRecord(CELL_NETWORK_CAPABILITIES);
     }
 
-    private UnderlyingNetworkRecord getTestNetworkRecord(
-            NetworkCapabilities nc, List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates) {
-        return new UnderlyingNetworkRecord(
-                mNetwork,
-                nc,
-                LINK_PROPERTIES,
-                false /* isBlocked */,
-                mVcnContext,
-                underlyingNetworkTemplates,
-                SUB_GROUP,
-                mSubscriptionSnapshot,
-                null /* currentlySelected */,
-                null /* carrierConfig */);
+    private UnderlyingNetworkRecord getTestNetworkRecord(NetworkCapabilities nc) {
+        return new UnderlyingNetworkRecord(mNetwork, nc, LINK_PROPERTIES, false /* isBlocked */);
     }
 
     @Test
@@ -186,14 +82,14 @@
                         mWifiNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        null /* currentlySelecetd */,
+                        false /* isSelected */,
                         null /* carrierConfig */));
     }
 
     private void verifyMatchesPriorityRuleForUpstreamBandwidth(
             int entryUpstreamBandwidth,
             int exitUpstreamBandwidth,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             boolean expectMatch) {
         final VcnWifiUnderlyingNetworkTemplate wifiNetworkPriority =
                 new VcnWifiUnderlyingNetworkTemplate.Builder()
@@ -208,14 +104,14 @@
                         mWifiNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        currentlySelected,
+                        isSelected,
                         null /* carrierConfig */));
     }
 
     private void verifyMatchesPriorityRuleForDownstreamBandwidth(
             int entryDownstreamBandwidth,
             int exitDownstreamBandwidth,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             boolean expectMatch) {
         final VcnWifiUnderlyingNetworkTemplate wifiNetworkPriority =
                 new VcnWifiUnderlyingNetworkTemplate.Builder()
@@ -231,7 +127,7 @@
                         mWifiNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        currentlySelected,
+                        isSelected,
                         null /* carrierConfig */));
     }
 
@@ -240,7 +136,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 TEST_MIN_ENTRY_UPSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 true);
     }
 
@@ -249,7 +145,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 false);
     }
 
@@ -258,7 +154,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 TEST_MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 true);
     }
 
@@ -267,7 +163,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 false);
     }
 
@@ -276,7 +172,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 true);
     }
 
@@ -285,7 +181,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 false);
     }
 
@@ -294,7 +190,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 true);
     }
 
@@ -303,7 +199,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 false);
     }
 
@@ -318,14 +214,12 @@
                                 TEST_MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS,
                                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS)
                         .build();
-        final UnderlyingNetworkRecord selectedNetworkRecord =
-                isSelectedNetwork ? mWifiNetworkRecord : null;
         assertEquals(
                 expectMatch,
                 checkMatchesWifiPriorityRule(
                         wifiNetworkPriority,
                         mWifiNetworkRecord,
-                        selectedNetworkRecord,
+                        isSelectedNetwork,
                         carrierConfig == null
                                 ? null
                                 : new PersistableBundleWrapper(carrierConfig)));
@@ -381,7 +275,7 @@
                 checkMatchesWifiPriorityRule(
                         wifiNetworkPriority,
                         mWifiNetworkRecord,
-                        null /* currentlySelecetd */,
+                        false /* isSelected */,
                         null /* carrierConfig */));
     }
 
@@ -516,7 +410,7 @@
                         mCellNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        null /* currentlySelected */,
+                        false /* isSelected */,
                         null /* carrierConfig */));
     }
 
@@ -543,7 +437,16 @@
 
     @Test
     public void testCalculatePriorityClass() throws Exception {
-        assertEquals(2, mCellNetworkRecord.priorityClass);
+        final int priorityClass =
+                NetworkPriorityClassifier.calculatePriorityClass(
+                        mVcnContext,
+                        mCellNetworkRecord,
+                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        false /* isSelected */,
+                        null /* carrierConfig */);
+        assertEquals(2, priorityClass);
     }
 
     private void checkCalculatePriorityClassFailToMatchAny(
@@ -561,10 +464,19 @@
             ncBuilder.addCapability(NET_CAPABILITY_INTERNET);
         }
 
-        final UnderlyingNetworkRecord nonDunNetworkRecord =
-                getTestNetworkRecord(ncBuilder.build(), templatesRequireDun);
+        final UnderlyingNetworkRecord nonDunNetworkRecord = getTestNetworkRecord(ncBuilder.build());
 
-        assertEquals(expectedPriorityClass, nonDunNetworkRecord.priorityClass);
+        final int priorityClass =
+                NetworkPriorityClassifier.calculatePriorityClass(
+                        mVcnContext,
+                        nonDunNetworkRecord,
+                        templatesRequireDun,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        false /* isSelected */,
+                        null /* carrierConfig */);
+
+        assertEquals(expectedPriorityClass, priorityClass);
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index 2941fde..992f102 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -29,13 +29,12 @@
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -67,6 +66,7 @@
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
 import com.android.server.vcn.VcnNetworkProvider;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController.Dependencies;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.NetworkBringupCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener;
@@ -77,6 +77,7 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -154,10 +155,13 @@
     @Mock private UnderlyingNetworkControllerCallback mNetworkControllerCb;
     @Mock private Network mNetwork;
 
+    @Spy private Dependencies mDependencies = new Dependencies();
+
     @Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor;
 
     private TestLooper mTestLooper;
     private VcnContext mVcnContext;
+    private UnderlyingNetworkEvaluator mNetworkEvaluator;
     private UnderlyingNetworkController mUnderlyingNetworkController;
 
     @Before
@@ -189,13 +193,28 @@
 
         when(mSubscriptionSnapshot.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(INITIAL_SUB_IDS);
 
+        mNetworkEvaluator =
+                spy(
+                        new UnderlyingNetworkEvaluator(
+                                mVcnContext,
+                                mNetwork,
+                                VcnGatewayConnectionConfigTest.buildTestConfig()
+                                        .getVcnUnderlyingNetworkPriorities(),
+                                SUB_GROUP,
+                                mSubscriptionSnapshot,
+                                null));
+        doReturn(mNetworkEvaluator)
+                .when(mDependencies)
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
+
         mUnderlyingNetworkController =
                 new UnderlyingNetworkController(
                         mVcnContext,
                         VcnGatewayConnectionConfigTest.buildTestConfig(),
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        mNetworkControllerCb);
+                        mNetworkControllerCb,
+                        mDependencies);
     }
 
     private void resetVcnContext() {
@@ -489,13 +508,7 @@
             NetworkCapabilities networkCapabilities,
             LinkProperties linkProperties,
             boolean isBlocked) {
-        return new UnderlyingNetworkRecord(
-                network,
-                networkCapabilities,
-                linkProperties,
-                isBlocked,
-                false /* isSelected */,
-                0 /* priorityClass */);
+        return new UnderlyingNetworkRecord(network, networkCapabilities, linkProperties, isBlocked);
     }
 
     @Test
@@ -515,24 +528,12 @@
         UnderlyingNetworkRecord recordC =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
-                        INITIAL_LINK_PROPERTIES,
-                        false /* isBlocked */,
-                        true /* isSelected */,
-                        -1 /* priorityClass */);
-        UnderlyingNetworkRecord recordD =
-                getTestNetworkRecord(
-                        mNetwork,
                         UPDATED_NETWORK_CAPABILITIES,
                         UPDATED_LINK_PROPERTIES,
                         false /* isBlocked */);
 
         assertEquals(recordA, recordB);
-        assertEquals(recordA, recordC);
-        assertNotEquals(recordA, recordD);
-
-        assertTrue(UnderlyingNetworkRecord.isEqualIncludingPriorities(recordA, recordB));
-        assertFalse(UnderlyingNetworkRecord.isEqualIncludingPriorities(recordA, recordC));
+        assertNotEquals(recordA, recordC);
     }
 
     @Test
@@ -540,6 +541,19 @@
         verifyRegistrationOnAvailableAndGetCallback();
     }
 
+    @Test
+    public void testUpdateSubscriptionSnapshotAndCarrierConfig() {
+        verifyRegistrationOnAvailableAndGetCallback();
+
+        TelephonySubscriptionSnapshot subscriptionUpdate =
+                mock(TelephonySubscriptionSnapshot.class);
+        when(subscriptionUpdate.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(UPDATED_SUB_IDS);
+
+        mUnderlyingNetworkController.updateSubscriptionSnapshot(subscriptionUpdate);
+
+        verify(mNetworkEvaluator).reevaluate(any(), any(), any(), any());
+    }
+
     private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback() {
         return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
     }
@@ -583,6 +597,7 @@
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
+        verify(mNetworkEvaluator).setIsSelected(eq(true), any(), any(), any(), any());
         return cb;
     }
 
@@ -713,7 +728,8 @@
                 VcnGatewayConnectionConfigTest.buildTestConfig(networkTemplates),
                 SUB_GROUP,
                 mSubscriptionSnapshot,
-                mNetworkControllerCb);
+                mNetworkControllerCb,
+                mDependencies);
 
         verify(cm)
                 .registerNetworkCallback(
@@ -724,30 +740,43 @@
         return mUnderlyingNetworkListenerCaptor.getValue();
     }
 
-    private UnderlyingNetworkRecord bringupNetworkAndGetRecord(
+    private UnderlyingNetworkEvaluator bringupNetworkAndGetEvaluator(
             UnderlyingNetworkListener cb,
             NetworkCapabilities requestNetworkCaps,
-            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-            UnderlyingNetworkRecord currentlySelected) {
+            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates) {
         final Network network = mock(Network.class);
         final NetworkCapabilities responseNetworkCaps =
                 buildResponseNwCaps(requestNetworkCaps, INITIAL_SUB_IDS);
+        final UnderlyingNetworkEvaluator evaluator =
+                spy(
+                        new UnderlyingNetworkEvaluator(
+                                mVcnContext,
+                                network,
+                                underlyingNetworkTemplates,
+                                SUB_GROUP,
+                                mSubscriptionSnapshot,
+                                null));
+        doReturn(evaluator)
+                .when(mDependencies)
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
 
         cb.onAvailable(network);
         cb.onCapabilitiesChanged(network, responseNetworkCaps);
         cb.onLinkPropertiesChanged(network, INITIAL_LINK_PROPERTIES);
         cb.onBlockedStatusChanged(network, false /* isFalse */);
-        return new UnderlyingNetworkRecord(
-                network,
-                responseNetworkCaps,
-                INITIAL_LINK_PROPERTIES,
-                false /* isBlocked */,
-                mVcnContext,
-                underlyingNetworkTemplates,
-                SUB_GROUP,
-                mSubscriptionSnapshot,
-                currentlySelected,
-                null /* carrierConfig */);
+
+        return evaluator;
+    }
+
+    private void verifySelectNetwork(UnderlyingNetworkEvaluator expectedEvaluator) {
+        verifyOnSelectedUnderlyingNetworkChanged(expectedEvaluator.getNetworkRecord());
+        verify(expectedEvaluator).setIsSelected(eq(true), any(), any(), any(), any());
+    }
+
+    private void verifyNeverSelectNetwork(UnderlyingNetworkEvaluator expectedEvaluator) {
+        verify(mNetworkControllerCb, never())
+                .onSelectedUnderlyingNetworkChanged(eq(expectedEvaluator.getNetworkRecord()));
+        verify(expectedEvaluator, never()).setIsSelected(eq(true), any(), any(), any(), any());
     }
 
     @Test
@@ -759,19 +788,15 @@
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
         // Bring up CBS network
-        final UnderlyingNetworkRecord cbsNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb,
-                        CBS_NETWORK_CAPABILITIES,
-                        networkTemplates,
-                        null /* currentlySelected */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(cbsNetworkRecord));
+        final UnderlyingNetworkEvaluator cbsNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, CBS_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(cbsNetworkEvaluator);
 
         // Bring up DUN network
-        final UnderlyingNetworkRecord dunNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb, DUN_NETWORK_CAPABILITIES, networkTemplates, cbsNetworkRecord);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(dunNetworkRecord));
+        final UnderlyingNetworkEvaluator dunNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, DUN_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(dunNetworkEvaluator);
+        verify(cbsNetworkEvaluator).setIsSelected(eq(false), any(), any(), any(), any());
     }
 
     @Test
@@ -783,20 +808,14 @@
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
         // Bring up DUN network
-        final UnderlyingNetworkRecord dunNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb,
-                        DUN_NETWORK_CAPABILITIES,
-                        networkTemplates,
-                        null /* currentlySelected */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(dunNetworkRecord));
+        final UnderlyingNetworkEvaluator dunNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, DUN_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(dunNetworkEvaluator);
 
         // Bring up CBS network
-        final UnderlyingNetworkRecord cbsNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb, CBS_NETWORK_CAPABILITIES, networkTemplates, dunNetworkRecord);
-        verify(mNetworkControllerCb, never())
-                .onSelectedUnderlyingNetworkChanged(eq(cbsNetworkRecord));
+        final UnderlyingNetworkEvaluator cbsNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, CBS_NETWORK_CAPABILITIES, networkTemplates);
+        verifyNeverSelectNetwork(cbsNetworkEvaluator);
     }
 
     @Test
@@ -808,13 +827,9 @@
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
         // Bring up an Internet network without DUN capability
-        final UnderlyingNetworkRecord networkRecord =
-                bringupNetworkAndGetRecord(
-                        cb,
-                        INITIAL_NETWORK_CAPABILITIES,
-                        networkTemplates,
-                        null /* currentlySelected */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(networkRecord));
+        final UnderlyingNetworkEvaluator evaluator =
+                bringupNetworkAndGetEvaluator(cb, INITIAL_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(evaluator);
     }
 
     @Test
@@ -825,10 +840,8 @@
                 new VcnCellUnderlyingNetworkTemplate.Builder().setDun(MATCH_REQUIRED).build());
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
-        bringupNetworkAndGetRecord(
-                cb, CBS_NETWORK_CAPABILITIES, networkTemplates, null /* currentlySelected */);
-
-        verify(mNetworkControllerCb, never())
-                .onSelectedUnderlyingNetworkChanged(any(UnderlyingNetworkRecord.class));
+        final UnderlyingNetworkEvaluator evaluator =
+                bringupNetworkAndGetEvaluator(cb, CBS_NETWORK_CAPABILITIES, networkTemplates);
+        verifyNeverSelectNetwork(evaluator);
     }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
new file mode 100644
index 0000000..985e70c
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.vcn.routeselection;
+
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.vcn.VcnGatewayConnectionConfig;
+import android.os.PersistableBundle;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase {
+    private PersistableBundleWrapper mCarrierConfig;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCarrierConfig = new PersistableBundleWrapper(new PersistableBundle());
+    }
+
+    private UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator() {
+        return new UnderlyingNetworkEvaluator(
+                mVcnContext,
+                mNetwork,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+    }
+
+    @Test
+    public void testInitializedEvaluator() throws Exception {
+        final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator();
+
+        assertFalse(evaluator.isValid());
+        assertEquals(mNetwork, evaluator.getNetwork());
+        assertEquals(PRIORITY_INVALID, evaluator.getPriorityClass());
+
+        try {
+            evaluator.getNetworkRecord();
+            fail("Expected to fail because evaluator is not valid");
+        } catch (Exception expected) {
+        }
+    }
+
+    @Test
+    public void testValidEvaluator() {
+        final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator();
+        evaluator.setNetworkCapabilities(
+                CELL_NETWORK_CAPABILITIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        evaluator.setLinkProperties(
+                LINK_PROPERTIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        evaluator.setIsBlocked(
+                false /* isBlocked */,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+
+        final UnderlyingNetworkRecord expectedRecord =
+                new UnderlyingNetworkRecord(
+                        mNetwork,
+                        CELL_NETWORK_CAPABILITIES,
+                        LINK_PROPERTIES,
+                        false /* isBlocked */);
+
+        assertTrue(evaluator.isValid());
+        assertEquals(mNetwork, evaluator.getNetwork());
+        assertEquals(2, evaluator.getPriorityClass());
+        assertEquals(expectedRecord, evaluator.getNetworkRecord());
+    }
+}