Merge "[Output Switcher] Add new fields in metrics event" into udc-qpr-dev
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 41c58ef..1571fdd 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -34,7 +34,6 @@
 import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
 import static android.window.ConfigurationHelper.isDifferentDisplay;
 import static android.window.ConfigurationHelper.shouldUpdateResources;
-import static android.window.ConfigurationHelper.shouldUpdateWindowMetricsBounds;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL;
@@ -6118,11 +6117,6 @@
     public static boolean shouldReportChange(@Nullable Configuration currentConfig,
             @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets sizeBuckets,
             int handledConfigChanges, boolean alwaysReportChange) {
-        // Always report changes in window configuration bounds
-        if (shouldUpdateWindowMetricsBounds(currentConfig, newConfig)) {
-            return true;
-        }
-
         final int publicDiff = currentConfig.diffPublicOnly(newConfig);
         // Don't report the change if there's no public diff between current and new config.
         if (publicDiff == 0) {
diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index 3ffbe1d..2ea6513 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -101,20 +101,6 @@
     ],
     "presubmit-large":[
         {
-            "name":"CtsContentTestCases",
-            "options":[
-                {
-                    "exclude-annotation":"androidx.test.filters.FlakyTest"
-                },
-                {
-                    "exclude-annotation":"org.junit.Ignore"
-                },
-                {
-                    "include-filter":"android.content.pm.cts"
-                }
-            ]
-        },
-        {
             "name":"CtsUsesNativeLibraryTest",
             "options":[
                 {
@@ -156,6 +142,20 @@
     ],
     "postsubmit":[
         {
+            "name":"CtsContentTestCases",
+            "options":[
+                {
+                    "exclude-annotation":"androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation":"org.junit.Ignore"
+                },
+                {
+                    "include-filter":"android.content.pm.cts"
+                }
+            ]
+        },
+        {
             "name":"CtsAppSecurityHostTestCases",
             "options":[
                 {
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index 1d6778b..55b2251 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -1498,6 +1498,11 @@
      * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume.
      */
     private int consumeFlingInStretch(int unconsumed) {
+        int scrollX = getScrollX();
+        if (scrollX < 0 || scrollX > getScrollRange()) {
+            // We've overscrolled, so don't stretch
+            return unconsumed;
+        }
         if (unconsumed > 0 && mEdgeGlowLeft != null && mEdgeGlowLeft.getDistance() != 0f) {
             int size = getWidth();
             float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size;
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index cb5dbe6..e591e9e 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -1545,6 +1545,11 @@
      * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume.
      */
     private int consumeFlingInStretch(int unconsumed) {
+        int scrollY = getScrollY();
+        if (scrollY < 0 || scrollY > getScrollRange()) {
+            // We've overscrolled, so don't stretch
+            return unconsumed;
+        }
         if (unconsumed > 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) {
             int size = getHeight();
             float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size;
diff --git a/core/java/android/window/ConfigurationHelper.java b/core/java/android/window/ConfigurationHelper.java
index 269ce08..e32adcf 100644
--- a/core/java/android/window/ConfigurationHelper.java
+++ b/core/java/android/window/ConfigurationHelper.java
@@ -106,7 +106,7 @@
      * @see WindowManager#getCurrentWindowMetrics()
      * @see WindowManager#getMaximumWindowMetrics()
      */
-    public static boolean shouldUpdateWindowMetricsBounds(@NonNull Configuration currentConfig,
+    private static boolean shouldUpdateWindowMetricsBounds(@NonNull Configuration currentConfig,
             @NonNull Configuration newConfig) {
         final Rect currentBounds = currentConfig.windowConfiguration.getBounds();
         final Rect newBounds = newConfig.windowConfiguration.getBounds();
diff --git a/core/java/com/android/internal/app/AssistUtils.java b/core/java/com/android/internal/app/AssistUtils.java
index 0ea8014..4261a0f 100644
--- a/core/java/com/android/internal/app/AssistUtils.java
+++ b/core/java/com/android/internal/app/AssistUtils.java
@@ -234,6 +234,23 @@
     }
 
     /**
+     * Allows subscription to {@link android.service.voice.VisualQueryDetectionService} service
+     * status.
+     *
+     * @param listener to receive visual service start/stop events.
+     */
+    public void subscribeVisualQueryRecognitionStatus(IVisualQueryRecognitionStatusListener
+            listener) {
+        try {
+            if (mVoiceInteractionManagerService != null) {
+                mVoiceInteractionManagerService.subscribeVisualQueryRecognitionStatus(listener);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to register visual query detection start listener", e);
+        }
+    }
+
+    /**
      * Enables visual detection service.
      *
      * @param listener to receive visual attention gained/lost events.
diff --git a/core/java/com/android/internal/app/IVisualQueryRecognitionStatusListener.aidl b/core/java/com/android/internal/app/IVisualQueryRecognitionStatusListener.aidl
new file mode 100644
index 0000000..cc49a75
--- /dev/null
+++ b/core/java/com/android/internal/app/IVisualQueryRecognitionStatusListener.aidl
@@ -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.internal.app;
+
+
+ oneway interface IVisualQueryRecognitionStatusListener {
+    /**
+     * Called when {@link VisualQueryDetectionService#onStartDetection} is scheduled from the system
+     * server via {@link VoiceInteractionManagerService#StartPerceiving}.
+     */
+    void onStartPerceiving();
+
+    /**
+     * Called when {@link VisualQueryDetectionService#onStopDetection} is scheduled from the system
+     * server via {@link VoiceInteractionManagerService#StopPerceiving}.
+     */
+    void onStopPerceiving();
+ }
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 24d5afc..314ed69 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -40,6 +40,7 @@
 import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.internal.app.IVisualQueryRecognitionStatusListener;
 
 interface IVoiceInteractionManagerService {
     void showSession(in Bundle sessionArgs, int flags, String attributionTag);
@@ -325,6 +326,9 @@
     void shutdownHotwordDetectionService();
 
     @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE")
+    void subscribeVisualQueryRecognitionStatus(in IVisualQueryRecognitionStatusListener listener);
+
+    @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE")
     void enableVisualQueryDetection(in IVisualQueryDetectionAttentionListener Listener);
 
     @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE")
diff --git a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp
index a95b6e3..76f5c10 100644
--- a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp
+++ b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp
@@ -127,16 +127,17 @@
     }
 }
 
-static void throwReadRE(JNIEnv *env, binder_status_t status) {
+static void throwReadException(JNIEnv *env, binder_status_t status) {
     ALOGE("Could not read LongArrayMultiStateCounter from Parcel, status = %d", status);
-    jniThrowRuntimeException(env, "Could not read LongArrayMultiStateCounter from Parcel");
+    jniThrowException(env, "android.os.BadParcelableException",
+                      "Could not read LongArrayMultiStateCounter from Parcel");
 }
 
 #define THROW_AND_RETURN_ON_READ_ERROR(expr) \
     {                                        \
         binder_status_t status = expr;       \
         if (status != STATUS_OK) {           \
-            throwReadRE(env, status);        \
+            throwReadException(env, status); \
             return 0L;                       \
         }                                    \
     }
@@ -147,6 +148,11 @@
     int32_t stateCount;
     THROW_AND_RETURN_ON_READ_ERROR(AParcel_readInt32(parcel.get(), &stateCount));
 
+    if (stateCount < 0 || stateCount > 0xEFFF) {
+        throwReadException(env, STATUS_INVALID_OPERATION);
+        return 0L;
+    }
+
     int32_t arrayLength;
     THROW_AND_RETURN_ON_READ_ERROR(AParcel_readInt32(parcel.get(), &arrayLength));
 
diff --git a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
index 1712b3a8..ddf7a67 100644
--- a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
+++ b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
@@ -131,16 +131,17 @@
     }
 }
 
-static void throwReadRE(JNIEnv *env, binder_status_t status) {
+static void throwReadException(JNIEnv *env, binder_status_t status) {
     ALOGE("Could not read LongMultiStateCounter from Parcel, status = %d", status);
-    jniThrowRuntimeException(env, "Could not read LongMultiStateCounter from Parcel");
+    jniThrowException(env, "android.os.BadParcelableException",
+                      "Could not read LongMultiStateCounter from Parcel");
 }
 
 #define THROW_AND_RETURN_ON_READ_ERROR(expr) \
     {                                        \
         binder_status_t status = expr;       \
         if (status != STATUS_OK) {           \
-            throwReadRE(env, status);        \
+            throwReadException(env, status); \
             return 0L;                       \
         }                                    \
     }
@@ -151,6 +152,11 @@
     int32_t stateCount;
     THROW_AND_RETURN_ON_READ_ERROR(AParcel_readInt32(parcel.get(), &stateCount));
 
+    if (stateCount < 0 || stateCount > 0xEFFF) {
+        throwReadException(env, STATUS_INVALID_OPERATION);
+        return 0L;
+    }
+
     auto counter = std::make_unique<battery::LongMultiStateCounter>(stateCount, 0);
 
     for (battery::state_t state = 0; state < stateCount; state++) {
diff --git a/core/proto/android/server/windowmanagertransitiontrace.proto b/core/proto/android/server/windowmanagertransitiontrace.proto
index a950a79..34ccb48 100644
--- a/core/proto/android/server/windowmanagertransitiontrace.proto
+++ b/core/proto/android/server/windowmanagertransitiontrace.proto
@@ -56,6 +56,7 @@
   repeated Target targets = 8;
   optional int32 flags = 9;
   optional int64 abort_time_ns = 10;
+  optional int64 starting_window_remove_time_ns = 11;
 }
 
 message Target {
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 130648c..cbc6c2a 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5336,6 +5336,12 @@
     <!-- Default value for performant auth feature. -->
     <bool name="config_performantAuthDefault">false</bool>
 
+    <!-- Threshold for false rejection rate (FRR) of biometric authentication. Applies for both
+         fingerprint and face. If a dual-modality device only enrolled a single biometric and
+         experiences high FRR (above threshold), system notification will be sent to encourage user
+         to enroll the other eligible biometric. -->
+    <fraction name="config_biometricNotificationFrrThreshold">30%</fraction>
+
     <!-- The component name for the default profile supervisor, which can be set as a profile owner
     even after user setup is complete. The defined component should be used for supervision purposes
     only. The component must be part of a system app. -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f795bd7..33c18c2 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1831,6 +1831,8 @@
     <string name="fingerprint_error_not_match">Fingerprint not recognized</string>
     <!-- Message shown when UDFPS fails to match -->
     <string name="fingerprint_udfps_error_not_match">Fingerprint not recognized</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>
 
     <!-- Accessibility message announced when a fingerprint has been authenticated [CHAR LIMIT=NONE] -->
     <string name="fingerprint_authenticated">Fingerprint authenticated</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3610ead..8de1304 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2581,6 +2581,9 @@
   <java-symbol type="string" name="biometric_error_device_not_secured" />
   <java-symbol type="string" name="biometric_error_generic" />
 
+  <!-- Biometric FRR config -->
+  <java-symbol type="fraction" name="config_biometricNotificationFrrThreshold" />
+
   <!-- Device credential strings for BiometricManager -->
   <java-symbol type="string" name="screen_lock_app_setting_name" />
   <java-symbol type="string" name="screen_lock_dialog_default_subtitle" />
@@ -2594,6 +2597,7 @@
   <java-symbol type="string" name="fingerprint_error_vendor_unknown" />
   <java-symbol type="string" name="fingerprint_error_not_match" />
   <java-symbol type="string" name="fingerprint_udfps_error_not_match" />
+  <java-symbol type="string" name="fingerprint_dialog_use_fingerprint_instead" />
   <java-symbol type="string" name="fingerprint_acquired_partial" />
   <java-symbol type="string" name="fingerprint_acquired_insufficient" />
   <java-symbol type="string" name="fingerprint_acquired_imager_dirty" />
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 129de64..31755ef 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -231,6 +231,28 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="android.widget.HorizontalScrollViewActivity"
+                android:label="HorizontalScrollViewActivity"
+                android:screenOrientation="portrait"
+                android:exported="true"
+                android:theme="@android:style/Theme.Material.Light">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name="android.widget.ScrollViewActivity"
+                android:label="ScrollViewActivity"
+                android:screenOrientation="portrait"
+                android:exported="true"
+                android:theme="@android:style/Theme.Material.Light">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
+
         <activity android:name="android.widget.DatePickerActivity"
                 android:label="DatePickerActivity"
                 android:screenOrientation="portrait"
diff --git a/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml
new file mode 100644
index 0000000..866e1a9
--- /dev/null
+++ b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/horizontal_scroll_view">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <View
+            android:background="#F00"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#880"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#0F0"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#088"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#00F"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#808"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#F00"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#880"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#0F0"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#088"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#00F"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#808"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#F00"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#880"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#0F0"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#088"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#00F"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#808"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+    </LinearLayout>
+</HorizontalScrollView>
diff --git a/core/tests/coretests/res/layout/activity_scroll_view.xml b/core/tests/coretests/res/layout/activity_scroll_view.xml
new file mode 100644
index 0000000..61fabf8
--- /dev/null
+++ b/core/tests/coretests/res/layout/activity_scroll_view.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/scroll_view">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <View
+            android:background="#F00"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#880"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#0F0"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#088"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#00F"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#808"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#F00"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#880"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#0F0"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#088"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#00F"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#808"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#F00"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#880"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#0F0"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#088"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#00F"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+        <View
+            android:background="#808"
+            android:layout_width="100dp"
+            android:layout_height="100dp" />
+
+    </LinearLayout>
+</ScrollView>
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index 4857741..f9a7148 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -451,8 +451,10 @@
         final Rect bounds = activity.getWindowManager().getCurrentWindowMetrics().getBounds();
         assertEquals(activityConfigPortrait.windowConfiguration.getBounds(), bounds);
 
-        // Ensure changes in window configuration bounds are reported
-        assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges);
+        // Ensure that Activity#onConfigurationChanged() not be called because the changes in
+        // WindowConfiguration shouldn't be reported, and we only apply the latest Configuration
+        // update in transaction.
+        assertEquals(numOfConfig, activity.mNumOfConfigChanges);
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/widget/HorizontalScrollViewActivity.java b/core/tests/coretests/src/android/widget/HorizontalScrollViewActivity.java
new file mode 100644
index 0000000..2101354
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/HorizontalScrollViewActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+/**
+ * An activity for testing the TextView widget.
+ */
+public class HorizontalScrollViewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_horizontal_scroll_view);
+    }
+}
diff --git a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java
new file mode 100644
index 0000000..86f26e5
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.widget;
+
+import static org.junit.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+import android.util.PollingCheck;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+@Presubmit
+public class HorizontalScrollViewFunctionalTest {
+    private HorizontalScrollViewActivity mActivity;
+    private HorizontalScrollView mHorizontalScrollView;
+    @Rule
+    public ActivityTestRule<HorizontalScrollViewActivity> mActivityRule = new ActivityTestRule<>(
+            HorizontalScrollViewActivity.class);
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = mActivityRule.getActivity();
+        mHorizontalScrollView = mActivity.findViewById(R.id.horizontal_scroll_view);
+    }
+
+    @Test
+    public void testScrollAfterFlingTop() {
+        mHorizontalScrollView.scrollTo(100, 0);
+        mHorizontalScrollView.fling(-10000);
+        PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowLeft.getDistance() > 0);
+        PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowLeft.getDistance() == 0f);
+        assertEquals(0, mHorizontalScrollView.getScrollX());
+    }
+
+    @Test
+    public void testScrollAfterFlingBottom() {
+        int childWidth = mHorizontalScrollView.getChildAt(0).getWidth();
+        int maxScroll = childWidth - mHorizontalScrollView.getWidth();
+        mHorizontalScrollView.scrollTo(maxScroll - 100, 0);
+        mHorizontalScrollView.fling(10000);
+        PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowRight.getDistance() > 0);
+        PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowRight.getDistance() == 0f);
+        assertEquals(maxScroll, mHorizontalScrollView.getScrollX());
+    }
+}
+
diff --git a/core/tests/coretests/src/android/widget/ScrollViewActivity.java b/core/tests/coretests/src/android/widget/ScrollViewActivity.java
new file mode 100644
index 0000000..899d631
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/ScrollViewActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+/**
+ * An activity for testing the TextView widget.
+ */
+public class ScrollViewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_scroll_view);
+    }
+}
diff --git a/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java
new file mode 100644
index 0000000..a49bb6a
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.widget;
+
+import static org.junit.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+import android.util.PollingCheck;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+@Presubmit
+public class ScrollViewFunctionalTest {
+    private ScrollViewActivity mActivity;
+    private ScrollView mScrollView;
+    @Rule
+    public ActivityTestRule<ScrollViewActivity> mActivityRule = new ActivityTestRule<>(
+            ScrollViewActivity.class);
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = mActivityRule.getActivity();
+        mScrollView = mActivity.findViewById(R.id.scroll_view);
+    }
+
+    @Test
+    public void testScrollAfterFlingTop() {
+        mScrollView.scrollTo(0, 100);
+        mScrollView.fling(-10000);
+        PollingCheck.waitFor(() -> mScrollView.mEdgeGlowTop.getDistance() > 0);
+        PollingCheck.waitFor(() -> mScrollView.mEdgeGlowTop.getDistance() == 0f);
+        assertEquals(0, mScrollView.getScrollY());
+    }
+
+    @Test
+    public void testScrollAfterFlingBottom() {
+        int childHeight = mScrollView.getChildAt(0).getHeight();
+        int maxScroll = childHeight - mScrollView.getHeight();
+        mScrollView.scrollTo(0, maxScroll - 100);
+        mScrollView.fling(10000);
+        PollingCheck.waitFor(() -> mScrollView.mEdgeGlowBottom.getDistance() > 0);
+        PollingCheck.waitFor(() -> mScrollView.mEdgeGlowBottom.getDistance() == 0f);
+        assertEquals(maxScroll, mScrollView.getScrollY());
+    }
+}
+
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 516dee7..faccf1a 100644
--- a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.BadParcelableException;
 import android.os.Parcel;
 
 import androidx.test.filters.SmallTest;
@@ -163,6 +164,45 @@
     }
 
     @Test
+    public void createFromBadBundle() {
+        Parcel data = Parcel.obtain();
+        int bundleLenPos = data.dataPosition();
+        data.writeInt(0);
+        data.writeInt(0x4C444E42);      // BaseBundle.BUNDLE_MAGIC
+
+        int bundleStart = data.dataPosition();
+
+        data.writeInt(1);
+        data.writeString("key");
+        data.writeInt(4);
+        int lazyValueLenPos = data.dataPosition();
+        data.writeInt(0);
+        int lazyValueStart = data.dataPosition();
+        data.writeString("com.android.internal.os.LongArrayMultiStateCounter");
+
+        // Invalid int16 value
+        data.writeInt(0x10000);     // stateCount
+        data.writeInt(10);          // arrayLength
+        for (int i = 0; i < 0x10000; ++i) {
+            data.writeLong(0);
+        }
+
+        backPatchLength(data, lazyValueLenPos, lazyValueStart);
+        backPatchLength(data, bundleLenPos, bundleStart);
+        data.setDataPosition(0);
+
+        assertThrows(BadParcelableException.class,
+                () -> data.readBundle().getParcelable("key", LongArrayMultiStateCounter.class));
+    }
+
+    private static void backPatchLength(Parcel parcel, int lengthPos, int startPos) {
+        int endPos = parcel.dataPosition();
+        parcel.setDataPosition(lengthPos);
+        parcel.writeInt(endPos - startPos);
+        parcel.setDataPosition(endPos);
+    }
+
+    @Test
     public void combineValues() {
         long[] values = new long[] {0, 1, 2, 3, 42};
         LongArrayMultiStateCounter.LongArrayContainer container =
diff --git a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
index fc86ebe..3413753 100644
--- a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.BadParcelableException;
 import android.os.Parcel;
 
 import androidx.test.filters.SmallTest;
@@ -210,4 +211,42 @@
         assertThrows(RuntimeException.class,
                 () -> LongMultiStateCounter.CREATOR.createFromParcel(parcel));
     }
+
+    @Test
+    public void createFromBadBundle() {
+        Parcel data = Parcel.obtain();
+        int bundleLenPos = data.dataPosition();
+        data.writeInt(0);
+        data.writeInt(0x4C444E42);      // BaseBundle.BUNDLE_MAGIC
+
+        int bundleStart = data.dataPosition();
+
+        data.writeInt(1);
+        data.writeString("key");
+        data.writeInt(4);
+        int lazyValueLenPos = data.dataPosition();
+        data.writeInt(0);
+        int lazyValueStart = data.dataPosition();
+        data.writeString("com.android.internal.os.LongMultiStateCounter");
+
+        // Invalid int16 value
+        data.writeInt(0x10000);     // stateCount
+        for (int i = 0; i < 0x10000; ++i) {
+            data.writeLong(0);
+        }
+
+        backPatchLength(data, lazyValueLenPos, lazyValueStart);
+        backPatchLength(data, bundleLenPos, bundleStart);
+        data.setDataPosition(0);
+
+        assertThrows(BadParcelableException.class,
+                () -> data.readBundle().getParcelable("key", LongMultiStateCounter.class));
+    }
+
+    private static void backPatchLength(Parcel parcel, int lengthPos, int startPos) {
+        int endPos = parcel.dataPosition();
+        parcel.setDataPosition(lengthPos);
+        parcel.writeInt(endPos - startPos);
+        parcel.setDataPosition(endPos);
+    }
 }
diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml
index e278c52..dcc9686 100644
--- a/data/etc/com.android.settings.xml
+++ b/data/etc/com.android.settings.xml
@@ -37,6 +37,7 @@
         <permission name="android.permission.MANAGE_USER_OEM_UNLOCK_STATE" />
         <permission name="android.permission.MASTER_CLEAR"/>
         <permission name="android.permission.MEDIA_CONTENT_CONTROL"/>
+        <permission name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" />
         <permission name="android.permission.MODIFY_PHONE_STATE"/>
         <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
         <permission name="android.permission.MOVE_PACKAGE"/>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index d5f4d6c..bcbf728 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -242,9 +242,20 @@
                 return false;
             }
 
+            // Abort if no space to split.
+            final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes(
+                    task.getTaskProperties(), splitPinRule,
+                    splitPinRule.getDefaultSplitAttributes(),
+                    getActivitiesMinDimensionsPair(primaryContainer.getTopNonFinishingActivity(),
+                            topContainer.getTopNonFinishingActivity()));
+            if (!SplitPresenter.shouldShowSplit(calculatedSplitAttributes)) {
+                Log.w(TAG, "No space to split, abort pinning top ActivityStack.");
+                return false;
+            }
+
             // Registers a Split
             final SplitPinContainer splitPinContainer = new SplitPinContainer(primaryContainer,
-                    topContainer, splitPinRule, splitPinRule.getDefaultSplitAttributes());
+                    topContainer, splitPinRule, calculatedSplitAttributes);
             task.addSplitContainer(splitPinContainer);
 
             // Updates the Split
@@ -263,7 +274,33 @@
 
     @Override
     public void unpinTopActivityStack(int taskId){
-        // TODO
+        synchronized (mLock) {
+            final TaskContainer task = getTaskContainer(taskId);
+            if (task == null) {
+                Log.e(TAG, "Cannot find the task to unpin, id: " + taskId);
+                return;
+            }
+
+            final SplitPinContainer splitPinContainer = task.getSplitPinContainer();
+            if (splitPinContainer == null) {
+                Log.e(TAG, "No ActivityStack is pinned.");
+                return;
+            }
+
+            // Remove the SplitPinContainer from the task.
+            final TaskFragmentContainer containerToUnpin =
+                    splitPinContainer.getSecondaryContainer();
+            task.removeSplitPinContainer();
+
+            // Resets the isolated navigation and updates the container.
+            final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+            final WindowContainerTransaction wct = transactionRecord.getTransaction();
+            mPresenter.setTaskFragmentIsolatedNavigation(wct,
+                    containerToUnpin.getTaskFragmentToken(), false /* isolated */);
+            updateContainer(wct, containerToUnpin);
+            transactionRecord.apply(false /* shouldApplyIndependently */);
+            updateCallbackIfNecessary();
+        }
     }
 
     @Override
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index d2d63bd..463c8ce 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -265,8 +265,25 @@
     }
 
     void removeSplitPinContainer() {
+        if (mSplitPinContainer == null) {
+            return;
+        }
+
+        final TaskFragmentContainer primaryContainer = mSplitPinContainer.getPrimaryContainer();
+        final TaskFragmentContainer secondaryContainer = mSplitPinContainer.getSecondaryContainer();
         mSplitContainers.remove(mSplitPinContainer);
         mSplitPinContainer = null;
+
+        // Remove the other SplitContainers that contains the unpinned container (unless it
+        // is the current top-most split-pair), since the state are no longer valid.
+        final List<SplitContainer> splitsToRemove = new ArrayList<>();
+        for (SplitContainer splitContainer : mSplitContainers) {
+            if (splitContainer.getSecondaryContainer().equals(secondaryContainer)
+                    && !splitContainer.getPrimaryContainer().equals(primaryContainer)) {
+                splitsToRemove.add(splitContainer);
+            }
+        }
+        removeSplitContainers(splitsToRemove);
     }
 
     @Nullable
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
index edefe9e..74a243d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
@@ -371,7 +371,15 @@
 
         @Override
         public void onBackCancelled() {
-            mProgressAnimator.onBackCancelled(CrossActivityAnimation.this::finishAnimation);
+            mProgressAnimator.onBackCancelled(() -> {
+                // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring,
+                // and if we release all animation leash first, the leavingProgressSpring won't
+                // able to update the animation anymore, which cause flicker.
+                // Here should force update the closing animation target to the final stage before
+                // release it.
+                setLeavingProgress(0);
+                finishAnimation();
+            });
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 99c8acd..a70cf00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -25,7 +25,6 @@
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED;
@@ -37,6 +36,7 @@
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
 
 import android.annotation.BinderThread;
@@ -85,6 +85,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.launcher3.icons.BubbleIconFactory;
 import com.android.wm.shell.R;
@@ -1004,9 +1005,7 @@
     }
 
     private void onNotificationPanelExpandedChanged(boolean expanded) {
-        if (DEBUG_BUBBLE_GESTURE) {
-            Log.d(TAG, "onNotificationPanelExpandedChanged: expanded=" + expanded);
-        }
+        ProtoLog.d(WM_SHELL_BUBBLES, "onNotificationPanelExpandedChanged: expanded=%b", expanded);
         if (mStackView != null && mStackView.isExpanded()) {
             if (expanded) {
                 mStackView.stopMonitoringSwipeUpGesture();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
index dce6b56..250e010 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
@@ -47,7 +47,6 @@
     static final boolean DEBUG_USER_EDUCATION = false;
     static final boolean DEBUG_POSITIONER = false;
     public static final boolean DEBUG_COLLAPSE_ANIMATOR = false;
-    static final boolean DEBUG_BUBBLE_GESTURE = false;
     public static boolean DEBUG_EXPANDED_VIEW_DRAGGING = false;
 
     private static final boolean FORCE_SHOW_USER_EDUCATION = false;
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 f58b121..da5974f 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
@@ -21,11 +21,11 @@
 
 import static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
 import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -75,6 +75,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
@@ -2024,9 +2025,7 @@
      * Monitor for swipe up gesture that is used to collapse expanded view
      */
     void startMonitoringSwipeUpGesture() {
-        if (DEBUG_BUBBLE_GESTURE) {
-            Log.d(TAG, "startMonitoringSwipeUpGesture");
-        }
+        ProtoLog.d(WM_SHELL_BUBBLES, "startMonitoringSwipeUpGesture");
         stopMonitoringSwipeUpGestureInternal();
 
         if (isGestureNavEnabled()) {
@@ -2046,9 +2045,7 @@
      * Stop monitoring for swipe up gesture
      */
     void stopMonitoringSwipeUpGesture() {
-        if (DEBUG_BUBBLE_GESTURE) {
-            Log.d(TAG, "stopMonitoringSwipeUpGesture");
-        }
+        ProtoLog.d(WM_SHELL_BUBBLES, "stopMonitoringSwipeUpGesture");
         stopMonitoringSwipeUpGestureInternal();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
index 3a3a378..1375684 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
@@ -18,10 +18,10 @@
 
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
 
 import android.content.Context;
 import android.hardware.input.InputManager;
-import android.util.Log;
 import android.view.Choreographer;
 import android.view.InputChannel;
 import android.view.InputEventReceiver;
@@ -29,6 +29,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
 
 /**
@@ -58,9 +59,7 @@
      * @param listener listener that is notified of touch events
      */
     void start(MotionEventListener listener) {
-        if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
-            Log.d(TAG, "start monitoring bubbles swipe up gesture");
-        }
+        ProtoLog.d(WM_SHELL_BUBBLES, "start monitoring bubbles swipe up gesture");
 
         stopInternal();
 
@@ -76,9 +75,7 @@
     }
 
     void stop() {
-        if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
-            Log.d(TAG, "stop monitoring bubbles swipe up gesture");
-        }
+        ProtoLog.d(WM_SHELL_BUBBLES, "stop monitoring bubbles swipe up gesture");
         stopInternal();
     }
 
@@ -94,9 +91,7 @@
     }
 
     private void onInterceptTouch() {
-        if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
-            Log.d(TAG, "intercept touch event");
-        }
+        ProtoLog.d(WM_SHELL_BUBBLES, "intercept touch event");
         if (mInputMonitor != null) {
             mInputMonitor.pilferPointers();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java
index 844526c..b7107f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java
@@ -16,19 +16,20 @@
 
 package com.android.wm.shell.bubbles;
 
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
 
 import android.content.Context;
 import android.graphics.PointF;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.protolog.common.ProtoLog;
+
 /**
  * Handles {@link MotionEvent}s for bubbles that begin in the nav bar area
  */
@@ -112,10 +113,8 @@
     private boolean isInGestureRegion(MotionEvent ev) {
         // Only handles touch events beginning in navigation bar system gesture zone
         if (mPositioner.getNavBarGestureZone().contains((int) ev.getX(), (int) ev.getY())) {
-            if (DEBUG_BUBBLE_GESTURE) {
-                Log.d(TAG, "handling touch y=" + ev.getY()
-                        + " navBarGestureZone=" + mPositioner.getNavBarGestureZone());
-            }
+            ProtoLog.d(WM_SHELL_BUBBLES, "handling touch x=%d y=%d navBarGestureZone=%s",
+                    (int) ev.getX(), (int) ev.getY(), mPositioner.getNavBarGestureZone());
             return true;
         }
         return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
index e95e8e5..1b41f79 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
@@ -41,9 +41,9 @@
     private val ANIMATE_DURATION: Long = 200
 
     private val positioner: BubblePositioner = positioner
-    private val manageView by lazy { findViewById<ViewGroup>(R.id.manage_education_view) }
-    private val manageButton by lazy { findViewById<Button>(R.id.manage_button) }
-    private val gotItButton by lazy { findViewById<Button>(R.id.got_it) }
+    private val manageView by lazy { requireViewById<ViewGroup>(R.id.manage_education_view) }
+    private val manageButton by lazy { requireViewById<Button>(R.id.manage_button) }
+    private val gotItButton by lazy { requireViewById<Button>(R.id.got_it) }
 
     private var isHiding = false
     private var realManageButtonRect = Rect()
@@ -122,7 +122,7 @@
             manageButton
                 .setOnClickListener {
                     hide()
-                    expandedView.findViewById<View>(R.id.manage_button).performClick()
+                    expandedView.requireViewById<View>(R.id.manage_button).performClick()
                 }
             gotItButton.setOnClickListener { hide() }
             setOnClickListener { hide() }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index d0598cd..5e3a077 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -48,9 +48,9 @@
     private val positioner: BubblePositioner = positioner
     private val controller: BubbleController = controller
 
-    private val view by lazy { findViewById<View>(R.id.stack_education_layout) }
-    private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) }
-    private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) }
+    private val view by lazy { requireViewById<View>(R.id.stack_education_layout) }
+    private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) }
+    private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) }
 
     var isHiding = false
         private set
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
new file mode 100644
index 0000000..a141ff9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.pip
+
+import android.app.AppOpsManager
+import android.content.Context
+import android.content.pm.PackageManager
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.pip.PipUtils
+
+class PipAppOpsListener(
+    private val mContext: Context,
+    private val mCallback: Callback,
+    private val mMainExecutor: ShellExecutor
+) {
+    private val mAppOpsManager: AppOpsManager = checkNotNull(
+        mContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager)
+    private val mAppOpsChangedListener = AppOpsManager.OnOpChangedListener { _, packageName ->
+        try {
+            // Dismiss the PiP once the user disables the app ops setting for that package
+            val topPipActivityInfo = PipUtils.getTopPipActivity(mContext)
+            val componentName = topPipActivityInfo.first ?: return@OnOpChangedListener
+            val userId = topPipActivityInfo.second
+            val appInfo = mContext.packageManager
+                .getApplicationInfoAsUser(packageName, 0, userId)
+            if (appInfo.packageName == componentName.packageName &&
+                mAppOpsManager.checkOpNoThrow(
+                    AppOpsManager.OP_PICTURE_IN_PICTURE, appInfo.uid,
+                    packageName
+                ) != AppOpsManager.MODE_ALLOWED
+            ) {
+                mMainExecutor.execute { mCallback.dismissPip() }
+            }
+        } catch (e: PackageManager.NameNotFoundException) {
+            // Unregister the listener if the package can't be found
+            unregisterAppOpsListener()
+        }
+    }
+
+    fun onActivityPinned(packageName: String) {
+        // Register for changes to the app ops setting for this package while it is in PiP
+        registerAppOpsListener(packageName)
+    }
+
+    fun onActivityUnpinned() {
+        // Unregister for changes to the previously PiP'ed package
+        unregisterAppOpsListener()
+    }
+
+    private fun registerAppOpsListener(packageName: String) {
+        mAppOpsManager.startWatchingMode(
+            AppOpsManager.OP_PICTURE_IN_PICTURE, packageName,
+            mAppOpsChangedListener
+        )
+    }
+
+    private fun unregisterAppOpsListener() {
+        mAppOpsManager.stopWatchingMode(mAppOpsChangedListener)
+    }
+
+    /** Callback for PipAppOpsListener to request changes to the PIP window.  */
+    interface Callback {
+        /** Dismisses the PIP window.  */
+        fun dismissPip()
+    }
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
similarity index 98%
rename from core/java/com/android/internal/policy/DividerSnapAlgorithm.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index a065e2b..1901e0b 100644
--- a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.internal.policy;
+package com.android.wm.shell.common.split;
 
 import static android.view.WindowManager.DOCKED_INVALID;
 import static android.view.WindowManager.DOCKED_LEFT;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 2dbc444..0b0c693 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -53,7 +53,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.DividerSnapAlgorithm;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
diff --git a/core/java/com/android/internal/policy/DockedDividerUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DockedDividerUtils.java
similarity index 97%
rename from core/java/com/android/internal/policy/DockedDividerUtils.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DockedDividerUtils.java
index b61b9de..f25dfea 100644
--- a/core/java/com/android/internal/policy/DockedDividerUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DockedDividerUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.internal.policy;
+package com.android.wm.shell.common.split;
 
 import static android.view.WindowManager.DOCKED_BOTTOM;
 import static android.view.WindowManager.DOCKED_INVALID;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index d3fada3..5d7e532 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -25,8 +25,8 @@
 import static android.view.WindowManager.DOCKED_TOP;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE;
-import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
-import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;
+import static com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
+import static com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;
 import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR;
 import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -58,8 +58,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.DividerSnapAlgorithm;
-import com.android.internal.policy.DockedDividerUtils;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.animation.Interpolators;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 422e3b0..1a84f4b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -44,6 +44,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.DockStateReader;
+import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -73,7 +74,6 @@
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.onehanded.OneHandedController;
-import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
@@ -122,6 +122,12 @@
 
     @WMSingleton
     @Provides
+    static FloatingContentCoordinator provideFloatingContentCoordinator() {
+        return new FloatingContentCoordinator();
+    }
+
+    @WMSingleton
+    @Provides
     static DisplayController provideDisplayController(Context context,
             IWindowManager wmService,
             ShellInit shellInit,
@@ -797,7 +803,6 @@
             ShellTaskOrganizer shellTaskOrganizer,
             Optional<BubbleController> bubblesOptional,
             Optional<SplitScreenController> splitScreenOptional,
-            Optional<Pip> pipOptional,
             FullscreenTaskListener fullscreenTaskListener,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 54be901..9bf973f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -31,13 +31,13 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
+import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java
index f29b3a3..e8fae24 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java
@@ -21,7 +21,6 @@
 import android.os.Handler;
 
 import com.android.internal.logging.UiEventLogger;
-import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.pip.PipMediaController;
@@ -37,12 +36,6 @@
  */
 @Module
 public abstract class Pip1SharedModule {
-    @WMSingleton
-    @Provides
-    static FloatingContentCoordinator provideFloatingContentCoordinator() {
-        return new FloatingContentCoordinator();
-    }
-
     // Needs handler for registering broadcast receivers
     @WMSingleton
     @Provides
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
index 52c6d20..80ffbb0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
@@ -29,12 +29,12 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.LegacySizeSpecSource;
+import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
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 a4c086b..1d46e75 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
@@ -275,7 +275,7 @@
      */
     fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
         val wct = WindowContainerTransaction()
-        wct.setBounds(taskInfo.token, null)
+        wct.setBounds(taskInfo.token, Rect())
         shellTaskOrganizer.applyTransaction(wct)
     }
 
@@ -313,7 +313,7 @@
             task.taskId
         )
         val wct = WindowContainerTransaction()
-        wct.setBounds(task.token, null)
+        wct.setBounds(task.token, Rect())
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(wct,
@@ -550,6 +550,7 @@
         )
         // Check if we should skip handling this transition
         var reason = ""
+        val triggerTask = request.triggerTask
         val shouldHandleRequest =
             when {
                 // Only handle open or to front transitions
@@ -558,19 +559,19 @@
                     false
                 }
                 // Only handle when it is a task transition
-                request.triggerTask == null -> {
+                triggerTask == null -> {
                     reason = "triggerTask is null"
                     false
                 }
                 // Only handle standard type tasks
-                request.triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> {
-                    reason = "activityType not handled (${request.triggerTask.activityType})"
+                triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> {
+                    reason = "activityType not handled (${triggerTask.activityType})"
                     false
                 }
                 // Only handle fullscreen or freeform tasks
-                request.triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN &&
-                        request.triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
-                    reason = "windowingMode not handled (${request.triggerTask.windowingMode})"
+                triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN &&
+                        triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
+                    reason = "windowingMode not handled (${triggerTask.windowingMode})"
                     false
                 }
                 // Otherwise process it
@@ -586,17 +587,17 @@
             return null
         }
 
-        val task: RunningTaskInfo = request.triggerTask
-
-        val result = when {
-            // If display has tasks stashed, handle as stashed launch
-            desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task)
-            // Check if fullscreen task should be updated
-            task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task)
-            // Check if freeform task should be updated
-            task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task)
-            else -> {
-                null
+        val result = triggerTask?.let { task ->
+            when {
+                // If display has tasks stashed, handle as stashed launch
+                desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task)
+                // Check if fullscreen task should be updated
+                task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task)
+                // Check if freeform task should be updated
+                task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task)
+                else -> {
+                    null
+                }
             }
         }
         KtProtoLog.v(
@@ -703,7 +704,7 @@
             WINDOWING_MODE_FULLSCREEN
         }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
-        wct.setBounds(taskInfo.token, null)
+        wct.setBounds(taskInfo.token, Rect())
         if (isDesktopDensityOverrideSet()) {
             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
         }
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 b9cb5c7..9debb25 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
@@ -69,7 +69,7 @@
     ): Boolean {
         val change = findRelevantChange(info)
         val leash = change.leash
-        val taskId = change.taskInfo.taskId
+        val taskId = checkNotNull(change.taskInfo).taskId
         val startBounds = change.startAbsBounds
         val endBounds = change.endAbsBounds
         val windowDecor =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java
deleted file mode 100644
index 48a3fc2..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.pip;
-
-import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE;
-
-import android.app.AppOpsManager;
-import android.app.AppOpsManager.OnOpChangedListener;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.util.Pair;
-
-import com.android.wm.shell.common.ShellExecutor;
-
-public class PipAppOpsListener {
-    private static final String TAG = PipAppOpsListener.class.getSimpleName();
-
-    private Context mContext;
-    private ShellExecutor mMainExecutor;
-    private AppOpsManager mAppOpsManager;
-    private Callback mCallback;
-
-    private AppOpsManager.OnOpChangedListener mAppOpsChangedListener = new OnOpChangedListener() {
-        @Override
-        public void onOpChanged(String op, String packageName) {
-            try {
-                // Dismiss the PiP once the user disables the app ops setting for that package
-                final Pair<ComponentName, Integer> topPipActivityInfo =
-                        PipUtils.getTopPipActivity(mContext);
-                if (topPipActivityInfo.first != null) {
-                    final ApplicationInfo appInfo = mContext.getPackageManager()
-                            .getApplicationInfoAsUser(packageName, 0, topPipActivityInfo.second);
-                    if (appInfo.packageName.equals(topPipActivityInfo.first.getPackageName()) &&
-                            mAppOpsManager.checkOpNoThrow(OP_PICTURE_IN_PICTURE, appInfo.uid,
-                                    packageName) != MODE_ALLOWED) {
-                        mMainExecutor.execute(() -> mCallback.dismissPip());
-                    }
-                }
-            } catch (NameNotFoundException e) {
-                // Unregister the listener if the package can't be found
-                unregisterAppOpsListener();
-            }
-        }
-    };
-
-    public PipAppOpsListener(Context context, Callback callback, ShellExecutor mainExecutor) {
-        mContext = context;
-        mMainExecutor = mainExecutor;
-        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        mCallback = callback;
-    }
-
-    public void onActivityPinned(String packageName) {
-        // Register for changes to the app ops setting for this package while it is in PiP
-        registerAppOpsListener(packageName);
-    }
-
-    public void onActivityUnpinned() {
-        // Unregister for changes to the previously PiP'ed package
-        unregisterAppOpsListener();
-    }
-
-    private void registerAppOpsListener(String packageName) {
-        mAppOpsManager.startWatchingMode(OP_PICTURE_IN_PICTURE, packageName,
-                mAppOpsChangedListener);
-    }
-
-    private void unregisterAppOpsListener() {
-        mAppOpsManager.stopWatchingMode(mAppOpsChangedListener);
-    }
-
-    /** Callback for PipAppOpsListener to request changes to the PIP window. */
-    public interface Callback {
-        /** Dismisses the PIP window. */
-        void dismissPip();
-    }
-}
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 f396f3f..b872267 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
@@ -75,6 +75,7 @@
 import com.android.wm.shell.common.TabletopModeController;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
 import com.android.wm.shell.pip.IPip;
@@ -82,7 +83,6 @@
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index 43d3f36..b251f6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -41,7 +41,7 @@
 import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
-import com.android.wm.shell.pip.PipAppOpsListener;
+import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 02eeb2a..5e583c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -47,10 +47,10 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index 3af1b75..05e4af3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -55,6 +55,8 @@
             Consts.TAG_WM_SHELL),
     WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
+    WM_SHELL_BUBBLES(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+            "Bubbles"),
     TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
 
     private final boolean mEnabled;
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 dc91a11..29be343 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
@@ -385,8 +385,8 @@
     private static int estimateWindowBGColor(Drawable themeBGDrawable) {
         final DrawableColorTester themeBGTester = new DrawableColorTester(
                 themeBGDrawable, DrawableColorTester.TRANSLUCENT_FILTER /* filterType */);
-        if (themeBGTester.passFilterRatio() != 1) {
-            // the window background is translucent, unable to draw
+        if (themeBGTester.passFilterRatio() < 0.5f) {
+            // more than half pixels of the window background is translucent, unable to draw
             Slog.w(TAG, "Window background is translucent, fill background with black color");
             return getSystemBGColor();
         } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
index 672e57a..a9eb882 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
@@ -23,14 +23,14 @@
         appIcon: Drawable
 ) : DesktopModeWindowDecorationViewHolder(rootView) {
 
-    private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption)
-    private val captionHandle: View = rootView.findViewById(R.id.caption_handle)
-    private val openMenuButton: View = rootView.findViewById(R.id.open_menu_button)
-    private val closeWindowButton: ImageButton = rootView.findViewById(R.id.close_window)
-    private val expandMenuButton: ImageButton = rootView.findViewById(R.id.expand_menu_button)
-    private val maximizeWindowButton: ImageButton = rootView.findViewById(R.id.maximize_window)
-    private val appNameTextView: TextView = rootView.findViewById(R.id.application_name)
-    private val appIconImageView: ImageView = rootView.findViewById(R.id.application_icon)
+    private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
+    private val captionHandle: View = rootView.requireViewById(R.id.caption_handle)
+    private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button)
+    private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window)
+    private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button)
+    private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window)
+    private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name)
+    private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon)
 
     init {
         captionView.setOnTouchListener(onCaptionTouchListener)
@@ -47,7 +47,9 @@
     override fun bindData(taskInfo: RunningTaskInfo) {
 
         val captionDrawable = captionView.background as GradientDrawable
-        captionDrawable.setColor(taskInfo.taskDescription.statusBarColor)
+        taskInfo.taskDescription?.statusBarColor?.let {
+            captionDrawable.setColor(it)
+        }
 
         closeWindowButton.imageTintList = ColorStateList.valueOf(
                 getCaptionCloseButtonColor(taskInfo))
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
index 47a12a0..9374ac9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
@@ -17,8 +17,8 @@
         onCaptionButtonClickListener: View.OnClickListener
 ) : DesktopModeWindowDecorationViewHolder(rootView) {
 
-    private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption)
-    private val captionHandle: ImageButton = rootView.findViewById(R.id.caption_handle)
+    private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
+    private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
 
     init {
         captionView.setOnTouchListener(onCaptionTouchListener)
@@ -27,9 +27,10 @@
     }
 
     override fun bindData(taskInfo: RunningTaskInfo) {
-        val captionColor = taskInfo.taskDescription.statusBarColor
-        val captionDrawable = captionView.background as GradientDrawable
-        captionDrawable.setColor(captionColor)
+        taskInfo.taskDescription?.statusBarColor?.let { captionColor ->
+            val captionDrawable = captionView.background as GradientDrawable
+            captionDrawable.setColor(captionColor)
+        }
 
         captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
index d293cf7..49e8d15 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
@@ -25,11 +25,14 @@
    * with the caption background color.
    */
   protected fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean {
-    return if (Color.alpha(taskInfo.taskDescription.statusBarColor) != 0 &&
-        taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
-      Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5
-    } else {
-      taskInfo.taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
-    }
+    return taskInfo.taskDescription
+        ?.let { taskDescription ->
+          if (Color.alpha(taskDescription.statusBarColor) != 0 &&
+              taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
+            Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5
+          } else {
+            taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
+          }
+        } ?: false
   }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 443cea2..fe2da5d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -38,7 +38,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.policy.DividerSnapAlgorithm;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 2cc28ac..0ae2908 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -55,9 +55,9 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TabletopModeController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 2118d2c..f03ff00 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1518,10 +1518,15 @@
      *                      list.
      */
     public void switchMemberDeviceContent(CachedBluetoothDevice newMainDevice) {
-        // Backup from main device
+        // Remove the sub device from mMemberDevices first to prevent hash mismatch problem due
+        // to mDevice switch
+        removeMemberDevice(newMainDevice);
+
+        // Backup from current main device
         final BluetoothDevice tmpDevice = mDevice;
         final short tmpRssi = mRssi;
         final boolean tmpJustDiscovered = mJustDiscovered;
+
         // Set main device from sub device
         release();
         mDevice = newMainDevice.mDevice;
@@ -1535,6 +1540,9 @@
         newMainDevice.mRssi = tmpRssi;
         newMainDevice.mJustDiscovered = tmpJustDiscovered;
         newMainDevice.fillData();
+
+        // Add the sub device back into mMemberDevices with correct hash
+        addMemberDevice(newMainDevice);
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index efba953..111a2c1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -186,6 +186,14 @@
             if (cachedDevice.getHiSyncId() != hiSyncId) {
                 continue;
             }
+
+            // The remote device supports CSIP, the other ear should be processed as a member
+            // device. Ignore hiSyncId grouping from ASHA here.
+            if (cachedDevice.getProfiles().stream().anyMatch(
+                    profile -> profile instanceof CsipSetCoordinatorProfile)) {
+                continue;
+            }
+
             if (firstMatchedIndex == -1) {
                 // Found the first one
                 firstMatchedIndex = i;
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
index 1251b0d..9ab84d2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
@@ -26,6 +26,7 @@
 import static android.os.BatteryManager.EXTRA_PLUGGED;
 import static android.os.BatteryManager.EXTRA_PRESENT;
 import static android.os.BatteryManager.EXTRA_STATUS;
+import static android.os.OsProtoEnums.BATTERY_PLUGGED_NONE;
 
 import android.content.Context;
 import android.content.Intent;
@@ -40,6 +41,8 @@
  */
 public class BatteryStatus {
     private static final int LOW_BATTERY_THRESHOLD = 20;
+    private static final int SEVERE_LOW_BATTERY_THRESHOLD = 10;
+    private static final int EXTREME_LOW_BATTERY_THRESHOLD = 3;
     private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000;
 
     public static final int CHARGING_UNKNOWN = -1;
@@ -90,21 +93,7 @@
         present = batteryChangedIntent.getBooleanExtra(EXTRA_PRESENT, true);
         this.incompatibleCharger = incompatibleCharger;
 
-        final int maxChargingMicroAmp = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_CURRENT,
-                -1);
-        int maxChargingMicroVolt = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_VOLTAGE, -1);
-
-        if (maxChargingMicroVolt <= 0) {
-            maxChargingMicroVolt = DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT;
-        }
-        if (maxChargingMicroAmp > 0) {
-            // Calculating muW = muA * muV / (10^6 mu^2 / mu); splitting up the divisor
-            // to maintain precision equally on both factors.
-            maxChargingWattage = (maxChargingMicroAmp / 1000)
-                    * (maxChargingMicroVolt / 1000);
-        } else {
-            maxChargingWattage = -1;
-        }
+        maxChargingWattage = calculateMaxChargingMicroWatt(batteryChangedIntent);
     }
 
     /** Determine whether the device is plugged. */
@@ -126,7 +115,7 @@
 
     /** Determine whether the device is plugged in dock. */
     public boolean isPluggedInDock() {
-        return plugged == BatteryManager.BATTERY_PLUGGED_DOCK;
+        return isPluggedInDock(plugged);
     }
 
     /**
@@ -140,15 +129,15 @@
 
     /** Whether battery is low and needs to be charged. */
     public boolean isBatteryLow() {
-        return level < LOW_BATTERY_THRESHOLD;
+        return isLowBattery(level);
     }
 
     /** Whether battery defender is enabled. */
     public boolean isBatteryDefender() {
-        return chargingStatus == CHARGING_POLICY_ADAPTIVE_LONGLIFE;
+        return isBatteryDefender(chargingStatus);
     }
 
-    /** Return current chargin speed is fast, slow or normal. */
+    /** Return current charging speed is fast, slow or normal. */
     public final int getChargingSpeed(Context context) {
         final int slowThreshold = context.getResources().getInteger(
                 R.integer.config_chargingSlowlyThreshold);
@@ -218,4 +207,126 @@
                 || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS
                 || plugged == BatteryManager.BATTERY_PLUGGED_DOCK;
     }
+
+    /** Determine whether the device is plugged in dock. */
+    public static boolean isPluggedInDock(Intent batteryChangedIntent) {
+        return isPluggedInDock(
+                batteryChangedIntent.getIntExtra(EXTRA_PLUGGED, BATTERY_PLUGGED_NONE));
+    }
+
+    /** Determine whether the device is plugged in dock. */
+    public static boolean isPluggedInDock(int plugged) {
+        return plugged == BatteryManager.BATTERY_PLUGGED_DOCK;
+    }
+
+    /**
+     * Whether the battery is low or not.
+     *
+     * @param batteryChangedIntent the {@link ACTION_BATTERY_CHANGED} intent
+     * @return {@code true} if the battery level is less or equal to {@link LOW_BATTERY_THRESHOLD}
+     */
+    public static boolean isLowBattery(Intent batteryChangedIntent) {
+        int level = getBatteryLevel(batteryChangedIntent);
+        return isLowBattery(level);
+    }
+
+    /**
+     * Whether the battery is low or not.
+     *
+     * @param batteryLevel the battery level
+     * @return {@code true} if the battery level is less or equal to {@link LOW_BATTERY_THRESHOLD}
+     */
+    public static boolean isLowBattery(int batteryLevel) {
+        return batteryLevel <= LOW_BATTERY_THRESHOLD;
+    }
+
+    /**
+     * Whether the battery is severe low or not.
+     *
+     * @param batteryChangedIntent the ACTION_BATTERY_CHANGED intent
+     * @return {@code true} if the battery level is less or equal to {@link
+     *     SEVERE_LOW_BATTERY_THRESHOLD}
+     */
+    public static boolean isSevereLowBattery(Intent batteryChangedIntent) {
+        int level = getBatteryLevel(batteryChangedIntent);
+        return level <= SEVERE_LOW_BATTERY_THRESHOLD;
+    }
+
+    /**
+     * Whether the battery is extreme low or not.
+     *
+     * @param batteryChangedIntent the ACTION_BATTERY_CHANGED intent
+     * @return {@code true} if the battery level is less or equal to {@link
+     *     EXTREME_LOW_BATTERY_THRESHOLD}
+     */
+    public static boolean isExtremeLowBattery(Intent batteryChangedIntent) {
+        int level = getBatteryLevel(batteryChangedIntent);
+        return level <= EXTREME_LOW_BATTERY_THRESHOLD;
+    }
+
+    /**
+     * Whether the battery defender is enabled or not.
+     *
+     * @param batteryChangedIntent the ACTION_BATTERY_CHANGED intent
+     * @return {@code true} if the battery defender is enabled. It could be dock defend, dwell
+     *     defend, or temp defend
+     */
+    public static boolean isBatteryDefender(Intent batteryChangedIntent) {
+        int chargingStatus =
+                batteryChangedIntent.getIntExtra(EXTRA_CHARGING_STATUS, CHARGING_POLICY_DEFAULT);
+        return isBatteryDefender(chargingStatus);
+    }
+
+    /**
+     * Whether the battery defender is enabled or not.
+     *
+     * @param chargingStatus for {@link EXTRA_CHARGING_STATUS} field in the ACTION_BATTERY_CHANGED
+     *     intent
+     * @return {@code true} if the battery defender is enabled. It could be dock defend, dwell
+     *     defend, or temp defend
+     */
+    public static boolean isBatteryDefender(int chargingStatus) {
+        return chargingStatus == CHARGING_POLICY_ADAPTIVE_LONGLIFE;
+    }
+
+    /**
+     * Gets the max charging current and max charging voltage form {@link
+     * Intent.ACTION_BATTERY_CHANGED} and calculates the charging speed based on the {@link
+     * R.integer.config_chargingSlowlyThreshold} and {@link R.integer.config_chargingFastThreshold}.
+     *
+     * @param context the application context
+     * @param batteryChangedIntent the intent from {@link Intent.ACTION_BATTERY_CHANGED}
+     * @return the charging speed. {@link CHARGING_REGULAR}, {@link CHARGING_FAST}, {@link
+     *     CHARGING_SLOWLY} or {@link CHARGING_UNKNOWN}
+     */
+    public static int getChargingSpeed(Context context, Intent batteryChangedIntent) {
+        final int maxChargingMicroWatt = calculateMaxChargingMicroWatt(batteryChangedIntent);
+        if (maxChargingMicroWatt <= 0) {
+            return CHARGING_UNKNOWN;
+        } else if (maxChargingMicroWatt
+                < context.getResources().getInteger(R.integer.config_chargingSlowlyThreshold)) {
+            return CHARGING_SLOWLY;
+        } else if (maxChargingMicroWatt
+                > context.getResources().getInteger(R.integer.config_chargingFastThreshold)) {
+            return CHARGING_FAST;
+        } else {
+            return CHARGING_REGULAR;
+        }
+    }
+
+    private static int calculateMaxChargingMicroWatt(Intent batteryChangedIntent) {
+        final int maxChargingMicroAmp =
+                batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_CURRENT, -1);
+        int maxChargingMicroVolt = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_VOLTAGE, -1);
+        if (maxChargingMicroVolt <= 0) {
+            maxChargingMicroVolt = DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT;
+        }
+
+        if (maxChargingMicroAmp > 0) {
+            // Calculating µW = mA * mV
+            return (int) Math.round(maxChargingMicroAmp * 0.001 * maxChargingMicroVolt * 0.001);
+        } else {
+            return -1;
+        }
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
index a03acc3..73f6db6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
@@ -325,7 +325,7 @@
         return batteryLevel
     }
 
-    override fun onBoundsChange(bounds: Rect?) {
+    override fun onBoundsChange(bounds: Rect) {
         super.onBoundsChange(bounds)
         updateSize()
     }
diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/fuelgague/BatteryStatusTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/fuelgague/BatteryStatusTest.kt
new file mode 100644
index 0000000..6c0c1a7
--- /dev/null
+++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/fuelgague/BatteryStatusTest.kt
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fuelgague
+
+import android.content.Context
+import android.content.Intent
+import android.os.BatteryManager
+import android.os.BatteryManager.BATTERY_PLUGGED_AC
+import android.os.BatteryManager.BATTERY_PLUGGED_DOCK
+import android.os.BatteryManager.BATTERY_PLUGGED_USB
+import android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS
+import android.os.BatteryManager.BATTERY_STATUS_FULL
+import android.os.BatteryManager.BATTERY_STATUS_UNKNOWN
+import android.os.BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE
+import android.os.BatteryManager.CHARGING_POLICY_DEFAULT
+import android.os.BatteryManager.EXTRA_MAX_CHARGING_CURRENT
+import android.os.BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE
+import android.os.OsProtoEnums.BATTERY_PLUGGED_NONE
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.fuelgauge.BatteryStatus
+import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_FAST
+import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_REGULAR
+import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_SLOWLY
+import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_UNKNOWN
+import com.android.settingslib.fuelgauge.BatteryStatus.isBatteryDefender
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.Optional
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Suite
+import org.junit.runners.Suite.SuiteClasses
+
+@RunWith(Suite::class)
+@SuiteClasses(
+    BatteryStatusTest.NonParameterizedTest::class,
+    BatteryStatusTest.IsPluggedInTest::class,
+    BatteryStatusTest.IsChargedTest::class,
+    BatteryStatusTest.GetChargingSpeedTest::class,
+    BatteryStatusTest.IsPluggedInDockTest::class,
+)
+open class BatteryStatusTest {
+
+    @RunWith(AndroidJUnit4::class)
+    class NonParameterizedTest : BatteryStatusTest() {
+        @Test
+        fun isLowBattery_20Percent_returnsTrue() {
+            val level = 20
+            val intent = createIntent(batteryLevel = level)
+
+            assertWithMessage("failed by isLowBattery(Intent), level=$level")
+                .that(BatteryStatus.isLowBattery(intent))
+                .isTrue()
+            assertWithMessage("failed by isLowBattery($level)")
+                .that(BatteryStatus.isLowBattery(level))
+                .isTrue()
+        }
+
+        @Test
+        fun isLowBattery_21Percent_returnsFalse() {
+            val level = 21
+            val intent = createIntent(batteryLevel = level)
+
+            assertWithMessage("failed by isLowBattery(intent), level=$level")
+                .that(BatteryStatus.isLowBattery(intent))
+                .isFalse()
+            assertWithMessage("failed by isLowBattery($level)")
+                .that(BatteryStatus.isLowBattery(intent))
+                .isFalse()
+        }
+
+        @Test
+        fun isSevereLowBattery_10Percent_returnsTrue() {
+            val batteryChangedIntent = createIntent(batteryLevel = 10)
+
+            assertThat(BatteryStatus.isSevereLowBattery(batteryChangedIntent)).isTrue()
+        }
+
+        @Test
+        fun isSevereLowBattery_11Percent_returnFalse() {
+            val batteryChangedIntent = createIntent(batteryLevel = 11)
+
+            assertThat(BatteryStatus.isSevereLowBattery(batteryChangedIntent)).isFalse()
+        }
+
+        @Test
+        fun isExtremeLowBattery_3Percent_returnsTrue() {
+            val batteryChangedIntent = createIntent(batteryLevel = 3)
+
+            assertThat(BatteryStatus.isExtremeLowBattery(batteryChangedIntent)).isTrue()
+        }
+
+        @Test
+        fun isExtremeLowBattery_4Percent_returnsFalse() {
+            val batteryChangedIntent = createIntent(batteryLevel = 4)
+
+            assertThat(BatteryStatus.isExtremeLowBattery(batteryChangedIntent)).isFalse()
+        }
+
+        @Test
+        fun isBatteryDefender_chargingLongLife_returnsTrue() {
+            val chargingStatus = CHARGING_POLICY_ADAPTIVE_LONGLIFE
+            val batteryChangedIntent = createIntent(chargingStatus = chargingStatus)
+
+            assertIsBatteryDefender(chargingStatus, batteryChangedIntent).isTrue()
+        }
+
+        @Test
+        fun isBatteryDefender_nonChargingLongLife_returnsFalse() {
+            val chargingStatus = CHARGING_POLICY_DEFAULT
+            val batteryChangedIntent = createIntent(chargingStatus = chargingStatus)
+
+            assertIsBatteryDefender(chargingStatus, batteryChangedIntent).isFalse()
+        }
+
+        private fun assertIsBatteryDefender(chargingStatus: Int, batteryChangedIntent: Intent) =
+            object {
+                val assertions =
+                    listOf(
+                        "failed by isBatteryDefender(Intent), chargingStatus=$chargingStatus".let {
+                            assertWithMessage(it).that(isBatteryDefender(batteryChangedIntent))
+                        },
+                        "failed by isBatteryDefender($chargingStatus)".let {
+                            assertWithMessage(it).that(isBatteryDefender(chargingStatus))
+                        },
+                    )
+
+                fun isTrue() = assertions.forEach { it.isTrue() }
+
+                fun isFalse() = assertions.forEach { it.isFalse() }
+            }
+    }
+
+    @RunWith(Parameterized::class)
+    class IsPluggedInTest(
+        private val name: String,
+        private val plugged: Int,
+        val expected: Boolean
+    ) : BatteryStatusTest() {
+
+        @Test
+        fun isPluggedIn_() {
+            val batteryChangedIntent = createIntent(plugged = plugged)
+
+            assertWithMessage("failed by isPluggedIn(plugged=$plugged)")
+                .that(BatteryStatus.isPluggedIn(plugged))
+                .isEqualTo(expected)
+            assertWithMessage("failed by isPlugged(Intent), which plugged=$plugged")
+                .that(BatteryStatus.isPluggedIn(batteryChangedIntent))
+                .isEqualTo(expected)
+        }
+
+        companion object {
+            @Parameterized.Parameters(name = "{0}")
+            @JvmStatic
+            fun parameters() =
+                arrayListOf(
+                    arrayOf("withAC_returnsTrue", BATTERY_PLUGGED_AC, true),
+                    arrayOf("withDock_returnsTrue", BATTERY_PLUGGED_DOCK, true),
+                    arrayOf("withUSB_returnsTrue", BATTERY_PLUGGED_USB, true),
+                    arrayOf("withWireless_returnsTrue", BATTERY_PLUGGED_WIRELESS, true),
+                    arrayOf("pluggedNone_returnsTrue", BATTERY_PLUGGED_NONE, false),
+                )
+        }
+    }
+
+    @RunWith(Parameterized::class)
+    class IsPluggedInDockTest(
+        private val name: String,
+        private val plugged: Int,
+        val expected: Boolean
+    ) : BatteryStatusTest() {
+
+        @Test
+        fun isPluggedDockIn_() {
+            val batteryChangedIntent = createIntent(plugged = plugged)
+
+            assertWithMessage("failed by isPluggedInDock(plugged=$plugged)")
+                .that(BatteryStatus.isPluggedInDock(plugged))
+                .isEqualTo(expected)
+            assertWithMessage("failed by isPluggedInDock(Intent), which plugged=$plugged")
+                .that(BatteryStatus.isPluggedInDock(batteryChangedIntent))
+                .isEqualTo(expected)
+        }
+
+        companion object {
+            @Parameterized.Parameters(name = "{0}")
+            @JvmStatic
+            fun parameters() =
+                arrayListOf(
+                    arrayOf("withAC_returnsTrue", BATTERY_PLUGGED_AC, false),
+                    arrayOf("withDock_returnsTrue", BATTERY_PLUGGED_DOCK, true),
+                    arrayOf("withUSB_returnsTrue", BATTERY_PLUGGED_USB, false),
+                    arrayOf("withWireless_returnsTrue", BATTERY_PLUGGED_WIRELESS, false),
+                    arrayOf("pluggedNone_returnsTrue", BATTERY_PLUGGED_NONE, false),
+                )
+        }
+    }
+
+    @RunWith(Parameterized::class)
+    class IsChargedTest(
+        private val status: Int,
+        private val batteryLevel: Int,
+        private val expected: Boolean
+    ) : BatteryStatusTest() {
+
+        @Test
+        fun isCharged_() {
+            val batteryChangedIntent = createIntent(batteryLevel = batteryLevel, status = status)
+
+            assertWithMessage(
+                    "failed by isCharged(Intent), status=$status, batteryLevel=$batteryLevel"
+                )
+                .that(BatteryStatus.isCharged(batteryChangedIntent))
+                .isEqualTo(expected)
+            assertWithMessage("failed by isCharged($status, $batteryLevel)")
+                .that(BatteryStatus.isCharged(status, batteryLevel))
+                .isEqualTo(expected)
+        }
+
+        companion object {
+            @Parameterized.Parameters(name = "status{0}_level{1}_returns-{2}")
+            @JvmStatic
+            fun parameters() =
+                arrayListOf(
+                    arrayOf(BATTERY_STATUS_FULL, 99, true),
+                    arrayOf(BATTERY_STATUS_UNKNOWN, 100, true),
+                    arrayOf(BATTERY_STATUS_FULL, 100, true),
+                    arrayOf(BATTERY_STATUS_UNKNOWN, 99, false),
+                )
+        }
+    }
+
+    @RunWith(Parameterized::class)
+    class GetChargingSpeedTest(
+        private val name: String,
+        private val maxChargingCurrent: Optional<Int>,
+        private val maxChargingVoltage: Optional<Int>,
+        private val expectedChargingSpeed: Int,
+    ) {
+
+        val context: Context = ApplicationProvider.getApplicationContext()
+
+        @Test
+        fun getChargingSpeed_() {
+            val batteryChangedIntent =
+                Intent(Intent.ACTION_BATTERY_CHANGED).apply {
+                    maxChargingCurrent.ifPresent { putExtra(EXTRA_MAX_CHARGING_CURRENT, it) }
+                    maxChargingVoltage.ifPresent { putExtra(EXTRA_MAX_CHARGING_VOLTAGE, it) }
+                }
+
+            assertThat(BatteryStatus.getChargingSpeed(context, batteryChangedIntent))
+                .isEqualTo(expectedChargingSpeed)
+        }
+
+        companion object {
+            @Parameterized.Parameters(name = "{0}")
+            @JvmStatic
+            fun parameters() =
+                arrayListOf(
+                    arrayOf(
+                        "maxCurrent=n/a, maxVoltage=n/a -> UNKNOWN",
+                        Optional.empty<Int>(),
+                        Optional.empty<Int>(),
+                        CHARGING_UNKNOWN
+                    ),
+                    arrayOf(
+                        "maxCurrent=0, maxVoltage=9000000 -> UNKNOWN",
+                        Optional.of(0),
+                        Optional.of(0),
+                        CHARGING_UNKNOWN
+                    ),
+                    arrayOf(
+                        "maxCurrent=1500000, maxVoltage=5000000 -> CHARGING_REGULAR",
+                        Optional.of(1500000),
+                        Optional.of(5000000),
+                        CHARGING_REGULAR
+                    ),
+                    arrayOf(
+                        "maxCurrent=1000000, maxVoltage=5000000 -> CHARGING_REGULAR",
+                        Optional.of(1000000),
+                        Optional.of(5000000),
+                        CHARGING_REGULAR
+                    ),
+                    arrayOf(
+                        "maxCurrent=1500001, maxVoltage=5000000 -> CHARGING_FAST",
+                        Optional.of(1501000),
+                        Optional.of(5000000),
+                        CHARGING_FAST
+                    ),
+                    arrayOf(
+                        "maxCurrent=999999, maxVoltage=5000000 -> CHARGING_SLOWLY",
+                        Optional.of(999999),
+                        Optional.of(5000000),
+                        CHARGING_SLOWLY
+                    ),
+                )
+        }
+    }
+
+    protected fun createIntent(
+        batteryLevel: Int = 50,
+        chargingStatus: Int = CHARGING_POLICY_DEFAULT,
+        plugged: Int = BATTERY_PLUGGED_NONE,
+        status: Int = BatteryManager.BATTERY_STATUS_CHARGING,
+    ): Intent =
+        Intent(Intent.ACTION_BATTERY_CHANGED).apply {
+            putExtra(BatteryManager.EXTRA_STATUS, status)
+            putExtra(BatteryManager.EXTRA_LEVEL, batteryLevel)
+            putExtra(BatteryManager.EXTRA_SCALE, 100)
+            putExtra(BatteryManager.EXTRA_CHARGING_STATUS, chargingStatus)
+            putExtra(BatteryManager.EXTRA_PLUGGED, plugged)
+        }
+}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 851f976..07f7ecd 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -409,6 +409,7 @@
         "mockito-target-extended-minus-junit4",
         "androidx.test.ext.junit",
         "androidx.test.ext.truth",
+        "kotlin-test",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 37b1ee5..187d073 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -249,7 +249,7 @@
         // intent is to launch a dialog from another dialog.
         val animatedParent =
             openedDialogs.firstOrNull {
-                it.dialog.window.decorView.viewRootImpl == controller.viewRoot
+                it.dialog.window?.decorView?.viewRootImpl == controller.viewRoot
             }
         val controller =
             animatedParent?.dialogContentWithBackground?.let {
@@ -336,7 +336,7 @@
     ): ActivityLaunchAnimator.Controller? {
         val animatedDialog =
             openedDialogs.firstOrNull {
-                it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
+                it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl
             }
                 ?: return null
         return createActivityLaunchController(animatedDialog, cujType)
@@ -417,7 +417,7 @@
                 animatedDialog.prepareForStackDismiss()
 
                 // Remove the dim.
-                dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+                dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
             }
 
             override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
@@ -783,7 +783,7 @@
         }
 
         // Show the background dim.
-        dialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+        dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
 
         startAnimation(
             isLaunching = true,
@@ -863,7 +863,7 @@
             isLaunching = false,
             onLaunchAnimationStart = {
                 // Remove the dim background as soon as we start the animation.
-                dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+                dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
             },
             onLaunchAnimationEnd = {
                 val dialogContentWithBackground = this.dialogContentWithBackground!!
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 1a03ede..6c4b695 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -206,8 +206,9 @@
             return
         }
 
-        backgroundView = FrameLayout(launchContainer.context)
-        launchContainerOverlay.add(backgroundView)
+        backgroundView = FrameLayout(launchContainer.context).also {
+            launchContainerOverlay.add(it)
+        }
 
         // We wrap the ghosted view background and use it to draw the expandable background. Its
         // alpha will be set to 0 as soon as we start drawing the expanding background.
@@ -319,7 +320,7 @@
         backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
 
         GhostView.removeGhost(ghostedView)
-        launchContainerOverlay.remove(backgroundView)
+        backgroundView?.let { launchContainerOverlay.remove(it) }
 
         if (ghostedView is LaunchableView) {
             // Restore the ghosted view visibility.
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
index 142fd21..d6eba2e 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
@@ -283,7 +283,7 @@
 
         animator.addListener(
             object : AnimatorListenerAdapter() {
-                override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
+                override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
                     if (DEBUG) {
                         Log.d(TAG, "Animation started")
                     }
@@ -295,7 +295,7 @@
                     launchContainerOverlay.add(windowBackgroundLayer)
                 }
 
-                override fun onAnimationEnd(animation: Animator?) {
+                override fun onAnimationEnd(animation: Animator) {
                     if (DEBUG) {
                         Log.d(TAG, "Animation ended")
                     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index b555fa5..8dc7495 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -42,7 +42,9 @@
                 return baseTypeface
             }
 
-            val axes = FontVariationAxis.fromFontVariationSettings(fVar).toMutableList()
+            val axes = FontVariationAxis.fromFontVariationSettings(fVar)
+                ?.toMutableList()
+                ?: mutableListOf()
             axes.removeIf { !baseTypeface.isSupportedAxes(it.getOpenTypeTagValue()) }
             if (axes.isEmpty()) {
                 return baseTypeface
@@ -120,8 +122,8 @@
             }
             addListener(
                 object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) = textInterpolator.rebase()
-                    override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
+                    override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase()
+                    override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase()
                 }
             )
         }
@@ -302,11 +304,11 @@
             if (onAnimationEnd != null) {
                 val listener =
                     object : AnimatorListenerAdapter() {
-                        override fun onAnimationEnd(animation: Animator?) {
+                        override fun onAnimationEnd(animation: Animator) {
                             onAnimationEnd.run()
                             animator.removeListener(this)
                         }
-                        override fun onAnimationCancel(animation: Animator?) {
+                        override fun onAnimationCancel(animation: Animator) {
                             animator.removeListener(this)
                         }
                     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index 38b99cc..bd3706e 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -1046,7 +1046,7 @@
                         }
                     }
 
-                    override fun onAnimationCancel(animation: Animator?) {
+                    override fun onAnimationCancel(animation: Animator) {
                         cancelled = true
                     }
                 }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
index c3f44f8..f7ebe2f 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
@@ -37,7 +37,14 @@
 }
 
 /** Key for a scene. */
-class SceneKey(name: String, identity: Any = Object()) : Key(name, identity) {
+class SceneKey(
+    name: String,
+    identity: Any = Object(),
+) : Key(name, identity) {
+
+    /** The unique [ElementKey] identifying this scene's root element. */
+    val rootElementKey = ElementKey(name, identity)
+
     override fun toString(): String {
         return "SceneKey(name=$name)"
     }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
index 9752f53..f4e3902 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -35,7 +35,7 @@
 
 /** The transitions configuration of a [SceneTransitionLayout]. */
 class SceneTransitions(
-    val transitionSpecs: List<TransitionSpec>,
+    private val transitionSpecs: List<TransitionSpec>,
 ) {
     private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
 
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index afd49b4..48d5638e8b 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -75,7 +75,7 @@
     }
 }
 
-private class TransitionBuilderImpl : TransitionBuilder {
+internal class TransitionBuilderImpl : TransitionBuilder {
     val transformations = mutableListOf<Transformation>()
     override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index b9baa793..81b9eb0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -24,7 +24,7 @@
 import androidx.compose.animation.Crossfade
 import androidx.compose.animation.core.snap
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
+import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -46,6 +46,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.R
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
@@ -63,6 +64,13 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
+object Bouncer {
+    object Elements {
+        val Background = ElementKey("BouncerBackground")
+        val Content = ElementKey("BouncerContent")
+    }
+}
+
 /** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
 @SysUISingleton
 class BouncerScene
@@ -88,7 +96,7 @@
 }
 
 @Composable
-private fun BouncerScene(
+private fun SceneScope.BouncerScene(
     viewModel: BouncerViewModel,
     dialogFactory: BouncerSceneDialogFactory,
     modifier: Modifier = Modifier,
@@ -97,84 +105,90 @@
     val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
     val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
     var dialog: Dialog? by remember { mutableStateOf(null) }
+    val backgroundColor = MaterialTheme.colorScheme.surface
 
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        verticalArrangement = Arrangement.spacedBy(60.dp),
-        modifier =
-            modifier
-                .fillMaxSize()
-                .background(MaterialTheme.colorScheme.surface)
-                .padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp)
-    ) {
-        Crossfade(
-            targetState = message,
-            label = "Bouncer message",
-            animationSpec = if (message.isUpdateAnimated) tween() else snap(),
-        ) { message ->
-            Text(
-                text = message.text,
-                color = MaterialTheme.colorScheme.onSurface,
-                style = MaterialTheme.typography.bodyLarge,
-            )
+    Box(modifier) {
+        Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) {
+            drawRect(color = backgroundColor)
         }
 
-        Box(Modifier.weight(1f)) {
-            when (val nonNullViewModel = authMethodViewModel) {
-                is PinBouncerViewModel ->
-                    PinBouncer(
-                        viewModel = nonNullViewModel,
-                        modifier = Modifier.align(Alignment.Center),
-                    )
-                is PasswordBouncerViewModel ->
-                    PasswordBouncer(
-                        viewModel = nonNullViewModel,
-                        modifier = Modifier.align(Alignment.Center),
-                    )
-                is PatternBouncerViewModel ->
-                    PatternBouncer(
-                        viewModel = nonNullViewModel,
-                        modifier =
-                            Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
-                                .align(Alignment.BottomCenter),
-                    )
-                else -> Unit
-            }
-        }
-
-        Button(
-            onClick = viewModel::onEmergencyServicesButtonClicked,
-            colors =
-                ButtonDefaults.buttonColors(
-                    containerColor = MaterialTheme.colorScheme.tertiaryContainer,
-                    contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
-                ),
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.spacedBy(60.dp),
+            modifier =
+                Modifier.element(Bouncer.Elements.Content)
+                    .fillMaxSize()
+                    .padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp)
         ) {
-            Text(
-                text = stringResource(com.android.internal.R.string.lockscreen_emergency_call),
-                style = MaterialTheme.typography.bodyMedium,
-            )
-        }
-
-        if (dialogMessage != null) {
-            if (dialog == null) {
-                dialog =
-                    dialogFactory().apply {
-                        setMessage(dialogMessage)
-                        setButton(
-                            DialogInterface.BUTTON_NEUTRAL,
-                            context.getString(R.string.ok),
-                        ) { _, _ ->
-                            viewModel.onThrottlingDialogDismissed()
-                        }
-                        setCancelable(false)
-                        setCanceledOnTouchOutside(false)
-                        show()
-                    }
+            Crossfade(
+                targetState = message,
+                label = "Bouncer message",
+                animationSpec = if (message.isUpdateAnimated) tween() else snap(),
+            ) { message ->
+                Text(
+                    text = message.text,
+                    color = MaterialTheme.colorScheme.onSurface,
+                    style = MaterialTheme.typography.bodyLarge,
+                )
             }
-        } else {
-            dialog?.dismiss()
-            dialog = null
+
+            Box(Modifier.weight(1f)) {
+                when (val nonNullViewModel = authMethodViewModel) {
+                    is PinBouncerViewModel ->
+                        PinBouncer(
+                            viewModel = nonNullViewModel,
+                            modifier = Modifier.align(Alignment.Center),
+                        )
+                    is PasswordBouncerViewModel ->
+                        PasswordBouncer(
+                            viewModel = nonNullViewModel,
+                            modifier = Modifier.align(Alignment.Center),
+                        )
+                    is PatternBouncerViewModel ->
+                        PatternBouncer(
+                            viewModel = nonNullViewModel,
+                            modifier =
+                                Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
+                                    .align(Alignment.BottomCenter),
+                        )
+                    else -> Unit
+                }
+            }
+
+            Button(
+                onClick = viewModel::onEmergencyServicesButtonClicked,
+                colors =
+                    ButtonDefaults.buttonColors(
+                        containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+                        contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
+                    ),
+            ) {
+                Text(
+                    text = stringResource(com.android.internal.R.string.lockscreen_emergency_call),
+                    style = MaterialTheme.typography.bodyMedium,
+                )
+            }
+
+            if (dialogMessage != null) {
+                if (dialog == null) {
+                    dialog =
+                        dialogFactory().apply {
+                            setMessage(dialogMessage)
+                            setButton(
+                                DialogInterface.BUTTON_NEUTRAL,
+                                context.getString(R.string.ok),
+                            ) { _, _ ->
+                                viewModel.onThrottlingDialogDismissed()
+                            }
+                            setCancelable(false)
+                            setCanceledOnTouchOutside(false)
+                            show()
+                        }
+                }
+            } else {
+                dialog?.dismiss()
+                dialog = null
+            }
         }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 38b751c..889c026 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -31,9 +31,17 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+
+object Notifications {
+    object Elements {
+        val Notifications = ElementKey("Notifications")
+    }
+}
 
 @Composable
-fun Notifications(
+fun SceneScope.Notifications(
     modifier: Modifier = Modifier,
 ) {
     // TODO(b/272779828): implement.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
index 1bb341c..c84a5e9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
@@ -31,15 +31,27 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+
+object QuickSettings {
+    object Elements {
+        // TODO RENAME
+        val Content = ElementKey("QuickSettingsContent")
+        val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid")
+        val FooterActions = ElementKey("QuickSettingsFooterActions")
+    }
+}
 
 @Composable
-fun QuickSettings(
+fun SceneScope.QuickSettings(
     modifier: Modifier = Modifier,
 ) {
     // TODO(b/272780058): implement.
     Column(
         modifier =
             modifier
+                .element(QuickSettings.Elements.Content)
                 .fillMaxWidth()
                 .defaultMinSize(minHeight = 300.dp)
                 .clip(RoundedCornerShape(32.dp))
@@ -47,15 +59,19 @@
                 .padding(16.dp),
     ) {
         Text(
-            text = "Quick settings",
-            modifier = Modifier.align(Alignment.CenterHorizontally),
+            text = "Quick settings grid",
+            modifier =
+                Modifier.element(QuickSettings.Elements.CollapsedGrid)
+                    .align(Alignment.CenterHorizontally),
             style = MaterialTheme.typography.titleLarge,
             color = MaterialTheme.colorScheme.onPrimary,
         )
         Spacer(modifier = Modifier.weight(1f))
         Text(
             text = "QS footer actions",
-            modifier = Modifier.align(Alignment.CenterHorizontally),
+            modifier =
+                Modifier.element(QuickSettings.Elements.FooterActions)
+                    .align(Alignment.CenterHorizontally),
             style = MaterialTheme.typography.titleSmall,
             color = MaterialTheme.colorScheme.onPrimary,
         )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 29763c2..e5cd439 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -16,19 +16,17 @@
 
 package com.android.systemui.qs.ui.composable
 
-import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.footer.ui.compose.QuickSettings
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
@@ -69,23 +67,18 @@
 }
 
 @Composable
-private fun QuickSettingsScene(
+private fun SceneScope.QuickSettingsScene(
     viewModel: QuickSettingsSceneViewModel,
     modifier: Modifier = Modifier,
 ) {
     // TODO(b/280887232): implement the real UI.
 
-    Box(modifier = modifier) {
-        Column(
-            horizontalAlignment = Alignment.CenterHorizontally,
-            modifier = Modifier.align(Alignment.Center)
-        ) {
-            Text("Quick settings", style = MaterialTheme.typography.headlineMedium)
-            Row(
-                horizontalArrangement = Arrangement.spacedBy(8.dp),
-            ) {
-                Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") }
-            }
-        }
+    Box(
+        modifier
+            .fillMaxSize()
+            .clickable(onClick = { viewModel.onContentClicked() })
+            .padding(horizontal = 16.dp, vertical = 48.dp)
+    ) {
+        QuickSettings(modifier = Modifier.fillMaxHeight())
     }
 }
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 f91baf2..c865070 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
@@ -31,7 +31,6 @@
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction
 import com.android.compose.animation.scene.observableTransitionState
-import com.android.compose.animation.scene.transitions
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
@@ -78,7 +77,7 @@
     SceneTransitionLayout(
         currentScene = currentSceneKey.toTransitionSceneKey(),
         onChangeScene = viewModel::onSceneChanged,
-        transitions = transitions {},
+        transitions = SceneContainerTransitions,
         state = state,
         modifier = modifier.fillMaxSize(),
     ) {
@@ -98,7 +97,9 @@
             ) {
                 with(composableScene) {
                     this@scene.Content(
-                        modifier = Modifier.fillMaxSize(),
+                        modifier =
+                            Modifier.element(sceneKey.toTransitionSceneKey().rootElementKey)
+                                .fillMaxSize(),
                     )
                 }
             }
@@ -129,14 +130,6 @@
 }
 
 // TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout.
-private fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey {
-    return SceneTransitionSceneKey(
-        name = toString(),
-        identity = this,
-    )
-}
-
-// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout.
 private fun SceneTransitionSceneKey.toModel(): SceneModel {
     return SceneModel(key = identity as SceneKey)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
new file mode 100644
index 0000000..404bf81
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -0,0 +1,34 @@
+package com.android.systemui.scene.ui.composable
+
+import com.android.compose.animation.scene.transitions
+import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition
+import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
+import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettingsTransition
+
+/**
+ * Comprehensive definition of all transitions between scenes in [SceneContainer].
+ *
+ * Transitions are automatically reversible, so define only one transition per scene pair. By
+ * convention, use the more common transition direction when defining the pair order, e.g.
+ * Lockscreen to Bouncer rather than Bouncer to Lockscreen.
+ *
+ * The actual transition DSL must be placed in a separate file under the package
+ * [com.android.systemui.scene.ui.composable.transitions].
+ *
+ * Please keep the list sorted alphabetically.
+ */
+val SceneContainerTransitions = transitions {
+    from(Bouncer, to = Gone) { bouncerToGoneTransition() }
+    from(Gone, to = Shade) { goneToShadeTransition() }
+    from(Gone, to = QuickSettings) { goneToQuickSettingsTransition() }
+    from(Lockscreen, to = Bouncer) { lockscreenToBouncerTransition() }
+    from(Lockscreen, to = Shade) { lockscreenToShadeTransition() }
+    from(Lockscreen, to = QuickSettings) { lockscreenToQuickSettingsTransition() }
+    from(Lockscreen, to = Gone) { lockscreenToGoneTransition() }
+    from(Shade, to = QuickSettings) { shadeToQuickSettingsTransition() }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
new file mode 100644
index 0000000..8d0d705
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
@@ -0,0 +1,15 @@
+package com.android.systemui.scene.ui.composable
+
+import com.android.compose.animation.scene.SceneKey as SceneTransitionSceneKey
+import com.android.systemui.scene.shared.model.SceneKey
+
+val Lockscreen = SceneKey.Lockscreen.toTransitionSceneKey()
+val Bouncer = SceneKey.Bouncer.toTransitionSceneKey()
+val Shade = SceneKey.Shade.toTransitionSceneKey()
+val QuickSettings = SceneKey.QuickSettings.toTransitionSceneKey()
+val Gone = SceneKey.Gone.toTransitionSceneKey()
+
+// TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout.
+fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey {
+    return SceneTransitionSceneKey(name = toString(), identity = this)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromBouncerToGoneTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromBouncerToGoneTransition.kt
new file mode 100644
index 0000000..1a9face
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromBouncerToGoneTransition.kt
@@ -0,0 +1,11 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.ui.composable.Bouncer
+
+fun TransitionBuilder.bouncerToGoneTransition() {
+    spec = tween(durationMillis = 500)
+
+    fade(Bouncer.rootElementKey)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt
new file mode 100644
index 0000000..38712b0
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt
@@ -0,0 +1,11 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.ui.composable.QuickSettings
+
+fun TransitionBuilder.goneToQuickSettingsTransition() {
+    spec = tween(durationMillis = 500)
+
+    fade(QuickSettings.rootElementKey)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
new file mode 100644
index 0000000..1d57c1a
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
@@ -0,0 +1,11 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.ui.composable.Shade
+
+fun TransitionBuilder.goneToShadeTransition() {
+    spec = tween(durationMillis = 500)
+
+    fade(Shade.rootElementKey)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
new file mode 100644
index 0000000..1fee874
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
@@ -0,0 +1,14 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.bouncer.ui.composable.Bouncer
+
+fun TransitionBuilder.lockscreenToBouncerTransition() {
+    spec = tween(durationMillis = 500)
+
+    translate(Bouncer.Elements.Content, y = 300.dp)
+    fractionRange(end = 0.5f) { fade(Bouncer.Elements.Background) }
+    fractionRange(start = 0.5f) { fade(Bouncer.Elements.Content) }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToGoneTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToGoneTransition.kt
new file mode 100644
index 0000000..da6306d
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToGoneTransition.kt
@@ -0,0 +1,11 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.ui.composable.Lockscreen
+
+fun TransitionBuilder.lockscreenToGoneTransition() {
+    spec = tween(durationMillis = 500)
+
+    fade(Lockscreen.rootElementKey)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt
new file mode 100644
index 0000000..9a8a3e2
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt
@@ -0,0 +1,11 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.ui.composable.QuickSettings
+
+fun TransitionBuilder.lockscreenToQuickSettingsTransition() {
+    spec = tween(durationMillis = 500)
+
+    fade(QuickSettings.rootElementKey)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
new file mode 100644
index 0000000..7ecfb62
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
@@ -0,0 +1,24 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.footer.ui.compose.QuickSettings
+import com.android.systemui.shade.ui.composable.Shade
+
+fun TransitionBuilder.lockscreenToShadeTransition() {
+    spec = tween(durationMillis = 500)
+
+    punchHole(Shade.Elements.QuickSettings, bounds = Shade.Elements.Scrim, Shade.Shapes.Scrim)
+    translate(Shade.Elements.Scrim, Edge.Top, startsOutsideLayoutBounds = false)
+    fractionRange(end = 0.5f) {
+        fade(Shade.Elements.ScrimBackground)
+        translate(
+            QuickSettings.Elements.CollapsedGrid,
+            Edge.Top,
+            startsOutsideLayoutBounds = false,
+        )
+    }
+    fractionRange(start = 0.5f) { fade(Notifications.Elements.Notifications) }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
new file mode 100644
index 0000000..6c7964b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
@@ -0,0 +1,15 @@
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.footer.ui.compose.QuickSettings
+
+fun TransitionBuilder.shadeToQuickSettingsTransition() {
+    spec = tween(durationMillis = 500)
+
+    translate(Notifications.Elements.Notifications, Edge.Bottom)
+    fade(Notifications.Elements.Notifications)
+    timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index ff1cb5f..f985aa2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -16,16 +16,22 @@
 
 package com.android.systemui.shade.ui.composable
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -44,6 +50,26 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
+object Shade {
+    object Elements {
+        val QuickSettings = ElementKey("ShadeQuickSettings")
+        val Scrim = ElementKey("ShadeScrim")
+        val ScrimBackground = ElementKey("ShadeScrimBackground")
+    }
+
+    object Dimensions {
+        val ScrimCornerSize = 32.dp
+    }
+
+    object Shapes {
+        val Scrim =
+            RoundedCornerShape(
+                topStart = Dimensions.ScrimCornerSize,
+                topEnd = Dimensions.ScrimCornerSize,
+            )
+    }
+}
+
 /** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */
 @SysUISingleton
 class ShadeScene
@@ -79,20 +105,28 @@
 }
 
 @Composable
-private fun ShadeScene(
+private fun SceneScope.ShadeScene(
     viewModel: ShadeSceneViewModel,
     modifier: Modifier = Modifier,
 ) {
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        verticalArrangement = Arrangement.spacedBy(16.dp),
-        modifier =
-            modifier
-                .fillMaxSize()
-                .clickable(onClick = { viewModel.onContentClicked() })
-                .padding(horizontal = 16.dp, vertical = 48.dp)
-    ) {
-        QuickSettings(modifier = Modifier.height(160.dp))
-        Notifications(modifier = Modifier.weight(1f))
+    Box(modifier.element(Shade.Elements.Scrim)) {
+        Spacer(
+            modifier =
+                Modifier.element(Shade.Elements.ScrimBackground)
+                    .fillMaxSize()
+                    .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
+        )
+
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.spacedBy(16.dp),
+            modifier =
+                Modifier.fillMaxSize()
+                    .clickable(onClick = { viewModel.onContentClicked() })
+                    .padding(horizontal = 16.dp, vertical = 48.dp)
+        ) {
+            QuickSettings(modifier = Modifier.height(160.dp))
+            Notifications(modifier = Modifier.weight(1f))
+        }
     }
 }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
index f83fa33..affb76b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
@@ -57,7 +57,7 @@
 
         private fun readIntFromBundle(extras: Bundle, key: String): Int? =
             try {
-                extras.getString(key).toInt()
+                extras.getString(key)?.toInt()
             } catch (e: Exception) {
                 null
             }
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 7e892f7..d85e012 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -21,9 +21,11 @@
     <dimen name="status_bar_header_height_keyguard">@dimen/status_bar_height</dimen>
 
     <!-- padding for container with status icons and battery -->
-    <dimen name="status_bar_icons_padding_end">12dp</dimen>
+    <dimen name="status_bar_icons_padding_end">4dp</dimen>
     <!-- start padding is smaller to account for status icon margins coming from drawable itself -->
-    <dimen name="status_bar_icons_padding_start">11dp</dimen>
+    <dimen name="status_bar_icons_padding_start">3dp</dimen>
+    <dimen name="status_bar_icons_padding_bottom">2dp</dimen>
+    <dimen name="status_bar_icons_padding_top">2dp</dimen>
 
     <dimen name="status_bar_padding_end">0dp</dimen>
 
@@ -78,8 +80,8 @@
 
     <dimen name="large_screen_shade_header_height">42dp</dimen>
     <!-- start padding is smaller to account for status icon margins coming from drawable itself -->
-    <dimen name="shade_header_system_icons_padding_start">11dp</dimen>
-    <dimen name="shade_header_system_icons_padding_end">12dp</dimen>
+    <dimen name="shade_header_system_icons_padding_start">3dp</dimen>
+    <dimen name="shade_header_system_icons_padding_end">4dp</dimen>
 
     <!-- Lockscreen shade transition values -->
     <dimen name="lockscreen_shade_transition_by_tap_distance">200dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml
index d74eca6..dc1f0e4 100644
--- a/packages/SystemUI/res/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp/dimens.xml
@@ -16,9 +16,6 @@
 */
 -->
 <resources>
-    <!-- it's a bit smaller on 720dp to account for status_bar_icon_horizontal_margin -->
-    <dimen name="status_bar_icons_padding_start">10dp</dimen>
-
     <!-- gap on either side of status bar notification icons -->
     <dimen name="status_bar_icon_horizontal_margin">1sp</dimen>
 
@@ -30,9 +27,6 @@
 
     <dimen name="large_screen_shade_header_height">56dp</dimen>
 
-    <!-- it's a bit smaller on 720dp to account for status_bar_icon_horizontal_margin -->
-    <dimen name="shade_header_system_icons_padding_start">10dp</dimen>
-
     <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
     <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index b6ef594..6164f29f 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -774,6 +774,9 @@
     <!-- Flag to enable privacy dot views, it shall be true for normal case -->
     <bool name="config_enablePrivacyDot">true</bool>
 
+    <!-- Flag to enable privacy chip animation, it shall be true for normal case -->
+    <bool name="config_enablePrivacyChipAnimation">true</bool>
+
     <!-- Class for the communal source connector to be used -->
     <string name="config_communalSourceConnector" translatable="false"></string>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a056445..55978e6 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -351,9 +351,9 @@
 
     <!-- paddings for container with status icons and battery -->
     <!-- padding start is a bit smaller than end to account for status icon margin-->
-    <dimen name="status_bar_icons_padding_start">11dp</dimen>
+    <dimen name="status_bar_icons_padding_start">3dp</dimen>
 
-    <dimen name="status_bar_icons_padding_end">0dp</dimen>
+    <dimen name="status_bar_icons_padding_end">4dp</dimen>
     <dimen name="status_bar_icons_padding_bottom">0dp</dimen>
     <dimen name="status_bar_icons_padding_top">0dp</dimen>
 
@@ -364,7 +364,7 @@
     <dimen name="status_bar_padding_start">8dp</dimen>
 
     <!-- the padding on the end of the statusbar -->
-    <dimen name="status_bar_padding_end">8dp</dimen>
+    <dimen name="status_bar_padding_end">4dp</dimen>
 
     <!-- the padding on the top of the statusbar (usually 0) -->
     <dimen name="status_bar_padding_top">0dp</dimen>
@@ -1607,7 +1607,7 @@
     <!-- Status bar user chip -->
     <dimen name="status_bar_user_chip_avatar_size">16dp</dimen>
     <!-- below also works as break between user chip and hover state of status icons -->
-    <dimen name="status_bar_user_chip_end_margin">4dp</dimen>
+    <dimen name="status_bar_user_chip_end_margin">8dp</dimen>
     <dimen name="status_bar_user_chip_text_size">12sp</dimen>
 
     <!-- System UI Dialog -->
diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml
index cb2c3a1..2ec6180 100644
--- a/packages/SystemUI/res/xml/large_screen_shade_header.xml
+++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml
@@ -60,7 +60,7 @@
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toStartOf="@id/privacy_container"
             app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintEnd_toEndOf="@id/carrier_group"/>
+            app:layout_constraintStart_toEndOf="@id/carrier_group"/>
         <PropertySet android:alpha="1" />
     </Constraint>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt
index c142933..5edd283 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt
@@ -27,6 +27,6 @@
      */
     fun ActivityManager.isInForeground(packageName: String): Boolean {
         val tasks: List<ActivityManager.RunningTaskInfo> = getRunningTasks(1)
-        return tasks.isNotEmpty() && packageName == tasks[0].topActivity.packageName
+        return tasks.isNotEmpty() && packageName == tasks[0].topActivity?.packageName
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt
index d7e61d6..ebc57d2 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt
@@ -31,15 +31,15 @@
     var visibleOnScreen = false
 
     constructor(parcel: Parcel) : this() {
-        this.boundsOnScreen = parcel.readParcelable(Rect::javaClass.javaClass.classLoader)
+        this.boundsOnScreen = parcel.readParcelable(Rect::javaClass.javaClass.classLoader) ?: Rect()
         this.selectedPage = parcel.readInt()
         this.visibleOnScreen = parcel.readBoolean()
     }
 
-    override fun writeToParcel(dest: Parcel?, flags: Int) {
-        dest?.writeParcelable(boundsOnScreen, 0)
-        dest?.writeInt(selectedPage)
-        dest?.writeBoolean(visibleOnScreen)
+    override fun writeToParcel(dest: Parcel, flags: Int) {
+        dest.writeParcelable(boundsOnScreen, 0)
+        dest.writeInt(selectedPage)
+        dest.writeBoolean(visibleOnScreen)
     }
 
     override fun describeContents(): Int {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
index aca9907..dac130d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
@@ -39,7 +39,7 @@
 
     fun init() {
         rotationChangeProvider.addCallback(rotationListener)
-        rotationListener.onRotationChanged(context.display.rotation)
+        context.display?.rotation?.let { rotationListener.onRotationChanged(it) }
     }
 
     private val rotationListener = RotationListener { rotation ->
diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 78a5c98..495367b 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -105,7 +105,7 @@
 
         hideAnimator.addListener(
             object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
+                override fun onAnimationEnd(animation: Animator) {
                     super@BouncerKeyguardMessageArea.setMessage(msg, animate)
                 }
             }
@@ -118,7 +118,7 @@
 
         showAnimator.addListener(
             object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
+                override fun onAnimationEnd(animation: Animator) {
                     textAboutToShow = null
                 }
             }
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 4a6e53d..4f4eec6 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -144,7 +144,7 @@
                 smallClockOnAttachStateChangeListener =
                     object : OnAttachStateChangeListener {
                         var pastVisibility: Int? = null
-                        override fun onViewAttachedToWindow(view: View?) {
+                        override fun onViewAttachedToWindow(view: View) {
                             value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
                             if (view != null) {
                                 smallClockFrame = view.parent as FrameLayout
@@ -168,7 +168,7 @@
                             }
                         }
 
-                        override fun onViewDetachedFromWindow(p0: View?) {
+                        override fun onViewDetachedFromWindow(p0: View) {
                             smallClockFrame?.viewTreeObserver
                                     ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
                         }
@@ -178,10 +178,10 @@
 
                 largeClockOnAttachStateChangeListener =
                     object : OnAttachStateChangeListener {
-                        override fun onViewAttachedToWindow(p0: View?) {
+                        override fun onViewAttachedToWindow(p0: View) {
                             value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
                         }
-                        override fun onViewDetachedFromWindow(p0: View?) {
+                        override fun onViewDetachedFromWindow(p0: View) {
                         }
                 }
                 value.largeClock.view
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 99a7411..d9a1dc6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -78,6 +78,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
@@ -126,6 +127,7 @@
     private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
     private final BouncerMessageInteractor mBouncerMessageInteractor;
     private int mTranslationY;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     // Whether the volume keys should be handled by keyguard. If true, then
     // they will be handled here for specific media types such as music, otherwise
     // the audio service will bring up the volume dialog.
@@ -299,6 +301,10 @@
                     mViewMediatorCallback.keyguardDone(fromPrimaryAuth, targetUserId);
                 }
             }
+
+            if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                mKeyguardTransitionInteractor.startDismissKeyguardTransition();
+            }
         }
 
         @Override
@@ -424,6 +430,7 @@
             Provider<JavaAdapter> javaAdapter,
             UserInteractor userInteractor,
             FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
             Provider<AuthenticationInteractor> authenticationInteractor
     ) {
         super(view);
@@ -455,6 +462,7 @@
         mUserInteractor = userInteractor;
         mAuthenticationInteractor = authenticationInteractor;
         mJavaAdapter = javaAdapter;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
     }
 
     @Override
@@ -840,8 +848,7 @@
                     SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId);
                     boolean isLockscreenDisabled = mLockPatternUtils.isLockScreenDisabled(
                             KeyguardUpdateMonitor.getCurrentUser());
-
-                    if (securityMode == SecurityMode.None) {
+                    if (securityMode == SecurityMode.None || isLockscreenDisabled) {
                         finish = isLockscreenDisabled;
                         eventSubtype = BOUNCER_DISMISS_SIM;
                         uiEvent = BouncerUiEvent.BOUNCER_DISMISS_SIM;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
index 96ac8ad..e1c060f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
@@ -66,11 +66,11 @@
     }
 
     override fun createAnimator(
-        sceneRoot: ViewGroup?,
+        sceneRoot: ViewGroup,
         startValues: TransitionValues?,
         endValues: TransitionValues?
     ): Animator? {
-        if (sceneRoot == null || startValues == null || endValues == null) {
+        if (startValues == null || endValues == null) {
             return null
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 73b4c5f..757022d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -543,7 +543,8 @@
 
         @Nullable
         @Override
-        public Animator createAnimator(ViewGroup sceneRoot, @Nullable TransitionValues startValues,
+        public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+                @Nullable TransitionValues startValues,
                 @Nullable TransitionValues endValues) {
             if (startValues == null || endValues == null) {
                 return null;
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 04acd0b..27b9056 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -131,14 +131,14 @@
 import com.android.systemui.util.leak.LeakReporter;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 
-import dagger.Lazy;
-
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import dagger.Lazy;
+
 /**
  * Class to handle ugly dependencies throughout sysui until we determine the
  * long-term dependency injection solution.
@@ -280,7 +280,6 @@
     @Inject Lazy<AccessibilityManagerWrapper> mAccessibilityManagerWrapper;
     @Inject Lazy<SysuiColorExtractor> mSysuiColorExtractor;
     @Inject Lazy<TunablePaddingService> mTunablePaddingService;
-    @Inject Lazy<ForegroundServiceController> mForegroundServiceController;
     @Inject Lazy<UiOffloadThread> mUiOffloadThread;
     @Inject Lazy<PowerUI.WarningsUI> mWarningsUI;
     @Inject Lazy<LightBarController> mLightBarController;
@@ -458,8 +457,6 @@
 
         mProviders.put(TunablePaddingService.class, mTunablePaddingService::get);
 
-        mProviders.put(ForegroundServiceController.class, mForegroundServiceController::get);
-
         mProviders.put(UiOffloadThread.class, mUiOffloadThread::get);
 
         mProviders.put(PowerUI.WarningsUI.class, mWarningsUI::get);
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
deleted file mode 100644
index 15e8c4e..0000000
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui;
-
-import android.annotation.Nullable;
-import android.app.AppOpsManager;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.service.notification.StatusBarNotification;
-import android.util.ArraySet;
-import android.util.SparseArray;
-
-import com.android.internal.messages.nano.SystemMessageProto;
-import com.android.systemui.appops.AppOpsController;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.Assert;
-
-import javax.inject.Inject;
-
-/**
- * Tracks state of foreground services and notifications related to foreground services per user.
- */
-@SysUISingleton
-public class ForegroundServiceController {
-    public static final int[] APP_OPS = new int[] {AppOpsManager.OP_SYSTEM_ALERT_WINDOW};
-
-    private final SparseArray<ForegroundServicesUserState> mUserServices = new SparseArray<>();
-    private final Object mMutex = new Object();
-    private final Handler mMainHandler;
-
-    @Inject
-    public ForegroundServiceController(
-            AppOpsController appOpsController,
-            @Main Handler mainHandler) {
-        mMainHandler = mainHandler;
-        appOpsController.addCallback(APP_OPS, (code, uid, packageName, active) -> {
-            mMainHandler.post(() -> {
-                onAppOpChanged(code, uid, packageName, active);
-            });
-        });
-    }
-
-    /**
-     * @return true if this user has services missing notifications and therefore needs a
-     * disclosure notification for running a foreground service.
-     */
-    public boolean isDisclosureNeededForUser(int userId) {
-        synchronized (mMutex) {
-            final ForegroundServicesUserState services = mUserServices.get(userId);
-            if (services == null) return false;
-            return services.isDisclosureNeeded();
-        }
-    }
-
-    /**
-     * @return true if this user/pkg has a missing or custom layout notification and therefore needs
-     * a disclosure notification showing the user which appsOps the app is using.
-     */
-    public boolean isSystemAlertWarningNeeded(int userId, String pkg) {
-        synchronized (mMutex) {
-            final ForegroundServicesUserState services = mUserServices.get(userId);
-            if (services == null) return false;
-            return services.getStandardLayoutKeys(pkg) == null;
-        }
-    }
-
-    /**
-     * Gets active app ops for this user and package
-     */
-    @Nullable
-    public ArraySet<Integer> getAppOps(int userId, String pkg) {
-        synchronized (mMutex) {
-            final ForegroundServicesUserState services = mUserServices.get(userId);
-            if (services == null) {
-                return null;
-            }
-            return services.getFeatures(pkg);
-        }
-    }
-
-    /**
-     * Records active app ops and updates the app op for the pending or visible notifications
-     * with the given parameters.
-     * App Ops are stored in FSC in addition to NotificationEntry in case they change before we
-     * have a notification to tag.
-     * @param appOpCode code for appOp to add/remove
-     * @param uid of user the notification is sent to
-     * @param packageName package that created the notification
-     * @param active whether the appOpCode is active or not
-     */
-    void onAppOpChanged(int appOpCode, int uid, String packageName, boolean active) {
-        Assert.isMainThread();
-
-        int userId = UserHandle.getUserId(uid);
-        // Record active app ops
-        synchronized (mMutex) {
-            ForegroundServicesUserState userServices = mUserServices.get(userId);
-            if (userServices == null) {
-                userServices = new ForegroundServicesUserState();
-                mUserServices.put(userId, userServices);
-            }
-            if (active) {
-                userServices.addOp(packageName, appOpCode);
-            } else {
-                userServices.removeOp(packageName, appOpCode);
-            }
-        }
-    }
-
-    /**
-     * Looks up the {@link ForegroundServicesUserState} for the given {@code userId}, then performs
-     * the given {@link UserStateUpdateCallback} on it.  If no state exists for the user ID, creates
-     * a new one if {@code createIfNotFound} is true, then performs the update on the new state.
-     * If {@code createIfNotFound} is false, no update is performed.
-     *
-     * @return false if no user state was found and none was created; true otherwise.
-     */
-    boolean updateUserState(int userId,
-            UserStateUpdateCallback updateCallback,
-            boolean createIfNotFound) {
-        synchronized (mMutex) {
-            ForegroundServicesUserState userState = mUserServices.get(userId);
-            if (userState == null) {
-                if (createIfNotFound) {
-                    userState = new ForegroundServicesUserState();
-                    mUserServices.put(userId, userState);
-                } else {
-                    return false;
-                }
-            }
-            return updateCallback.updateUserState(userState);
-        }
-    }
-
-    /**
-     * @return true if {@code sbn} is the system-provided disclosure notification containing the
-     * list of running foreground services.
-     */
-    public boolean isDisclosureNotification(StatusBarNotification sbn) {
-        return sbn.getId() == SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES
-                && sbn.getTag() == null
-                && sbn.getPackageName().equals("android");
-    }
-
-    /**
-     * @return true if sbn is one of the window manager "drawing over other apps" notifications
-     */
-    public boolean isSystemAlertNotification(StatusBarNotification sbn) {
-        return sbn.getPackageName().equals("android")
-                && sbn.getTag() != null
-                && sbn.getTag().contains("AlertWindowNotification");
-    }
-
-    /**
-     * Callback provided to {@link #updateUserState(int, UserStateUpdateCallback, boolean)}
-     * to perform the update.
-     */
-    interface UserStateUpdateCallback {
-        /**
-         * Perform update operations on the provided {@code userState}.
-         *
-         * @return true if the update succeeded.
-         */
-        boolean updateUserState(ForegroundServicesUserState userState);
-
-        /** Called if the state was not found and was not created. */
-        default void userStateNotFound(int userId) {
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
deleted file mode 100644
index a1a3b72..0000000
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.os.Bundle;
-import android.service.notification.StatusBarNotification;
-import android.util.Log;
-
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
-
-import javax.inject.Inject;
-
-/** Updates foreground service notification state in response to notification data events. */
-@SysUISingleton
-public class ForegroundServiceNotificationListener {
-
-    private static final String TAG = "FgServiceController";
-    private static final boolean DBG = false;
-
-    private final Context mContext;
-    private final ForegroundServiceController mForegroundServiceController;
-    private final NotifPipeline mNotifPipeline;
-
-    @Inject
-    public ForegroundServiceNotificationListener(Context context,
-            ForegroundServiceController foregroundServiceController,
-            NotifPipeline notifPipeline) {
-        mContext = context;
-        mForegroundServiceController = foregroundServiceController;
-        mNotifPipeline = notifPipeline;
-    }
-
-    /** Initializes this listener by connecting it to the notification pipeline. */
-    public void init() {
-        mNotifPipeline.addCollectionListener(new NotifCollectionListener() {
-            @Override
-            public void onEntryAdded(NotificationEntry entry) {
-                addNotification(entry, entry.getImportance());
-            }
-
-            @Override
-            public void onEntryUpdated(NotificationEntry entry) {
-                updateNotification(entry, entry.getImportance());
-            }
-
-            @Override
-            public void onEntryRemoved(NotificationEntry entry, int reason) {
-                removeNotification(entry.getSbn());
-            }
-        });
-    }
-
-    /**
-     * @param entry notification that was just posted
-     */
-    private void addNotification(NotificationEntry entry, int importance) {
-        updateNotification(entry, importance);
-    }
-
-    /**
-     * @param sbn notification that was just removed
-     */
-    private void removeNotification(StatusBarNotification sbn) {
-        mForegroundServiceController.updateUserState(
-                sbn.getUserId(),
-                new ForegroundServiceController.UserStateUpdateCallback() {
-                    @Override
-                    public boolean updateUserState(ForegroundServicesUserState userState) {
-                        if (mForegroundServiceController.isDisclosureNotification(sbn)) {
-                            // if you remove the dungeon entirely, we take that to mean there are
-                            // no running services
-                            userState.setRunningServices(null, 0);
-                            return true;
-                        } else {
-                            // this is safe to call on any notification, not just
-                            // FLAG_FOREGROUND_SERVICE
-                            return userState.removeNotification(sbn.getPackageName(), sbn.getKey());
-                        }
-                    }
-
-                    @Override
-                    public void userStateNotFound(int userId) {
-                        if (DBG) {
-                            Log.w(TAG, String.format(
-                                    "user %d with no known notifications got removeNotification "
-                                            + "for %s",
-                                    sbn.getUserId(), sbn));
-                        }
-                    }
-                },
-                false /* don't create */);
-    }
-
-    /**
-     * @param entry notification that was just changed in some way
-     */
-    private void updateNotification(NotificationEntry entry, int newImportance) {
-        final StatusBarNotification sbn = entry.getSbn();
-        mForegroundServiceController.updateUserState(
-                sbn.getUserId(),
-                userState -> {
-                    if (mForegroundServiceController.isDisclosureNotification(sbn)) {
-                        final Bundle extras = sbn.getNotification().extras;
-                        if (extras != null) {
-                            final String[] svcs = extras.getStringArray(
-                                    Notification.EXTRA_FOREGROUND_APPS);
-                            userState.setRunningServices(svcs, sbn.getNotification().when);
-                        }
-                    } else {
-                        userState.removeNotification(sbn.getPackageName(), sbn.getKey());
-                        if (0 != (sbn.getNotification().flags
-                                & Notification.FLAG_FOREGROUND_SERVICE)) {
-                            if (newImportance > NotificationManager.IMPORTANCE_MIN) {
-                                userState.addImportantNotification(sbn.getPackageName(),
-                                        sbn.getKey());
-                            }
-                        }
-                        final Notification.Builder builder =
-                                Notification.Builder.recoverBuilder(
-                                        mContext, sbn.getNotification());
-                        if (builder.usesStandardHeader()) {
-                            userState.addStandardLayoutNotification(
-                                    sbn.getPackageName(), sbn.getKey());
-                        }
-                    }
-                    return true;
-                },
-                true /* create if not found */);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 58adfa1..58c8000 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -82,6 +82,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.io.PrintWriter;
@@ -288,12 +289,13 @@
             @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
             @NonNull PromptViewModel promptViewModel,
             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
-            @NonNull @Background DelayableExecutor bgExecutor) {
+            @NonNull @Background DelayableExecutor bgExecutor,
+            @NonNull VibratorHelper vibratorHelper) {
         this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps,
                 wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
                 jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor,
                 promptCredentialInteractor, promptViewModel, credentialViewModelProvider,
-                new Handler(Looper.getMainLooper()), bgExecutor);
+                new Handler(Looper.getMainLooper()), bgExecutor, vibratorHelper);
     }
 
     @VisibleForTesting
@@ -314,7 +316,8 @@
             @NonNull PromptViewModel promptViewModel,
             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
             @NonNull Handler mainHandler,
-            @NonNull @Background DelayableExecutor bgExecutor) {
+            @NonNull @Background DelayableExecutor bgExecutor,
+            @NonNull VibratorHelper vibratorHelper) {
         super(config.mContext);
 
         mConfig = config;
@@ -364,7 +367,8 @@
         if (featureFlags.isEnabled(Flags.BIOMETRIC_BP_STRONG)) {
             showPrompt(config, layoutInflater, promptViewModel,
                     Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
-                    Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
+                    Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds),
+                    vibratorHelper, featureFlags);
         } else {
             showLegacyPrompt(config, layoutInflater, fpProps, faceProps);
         }
@@ -388,7 +392,10 @@
     private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
             @NonNull PromptViewModel viewModel,
             @Nullable FingerprintSensorPropertiesInternal fpProps,
-            @Nullable FaceSensorPropertiesInternal faceProps) {
+            @Nullable FaceSensorPropertiesInternal faceProps,
+            @NonNull VibratorHelper vibratorHelper,
+            @NonNull FeatureFlags featureFlags
+    ) {
         if (Utils.isBiometricAllowed(config.mPromptInfo)) {
             mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication(
                     config.mPromptInfo,
@@ -401,7 +408,8 @@
             mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
                     // TODO(b/201510778): This uses the wrong timeout in some cases
                     getJankListener(view, TRANSIT, AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
-                    mBackgroundView, mBiometricCallback, mApplicationCoroutineScope);
+                    mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
+                    vibratorHelper, featureFlags);
 
             // TODO(b/251476085): migrate these dependencies
             if (fpProps != null && fpProps.isAnyUdfpsType()) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 3df7ca5..7b288a8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -85,9 +85,12 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.data.repository.BiometricType;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.Execution;
 
+import kotlin.Unit;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -101,7 +104,6 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-import kotlin.Unit;
 import kotlinx.coroutines.CoroutineScope;
 
 /**
@@ -183,6 +185,7 @@
     @NonNull private final UdfpsUtils mUdfpsUtils;
     private final @Background DelayableExecutor mBackgroundExecutor;
     private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
+    @NonNull private final VibratorHelper mVibratorHelper;
 
     @VisibleForTesting
     final TaskStackListener mTaskStackListener = new TaskStackListener() {
@@ -771,7 +774,8 @@
             @NonNull InteractionJankMonitor jankMonitor,
             @Main Handler handler,
             @Background DelayableExecutor bgExecutor,
-            @NonNull UdfpsUtils udfpsUtils) {
+            @NonNull UdfpsUtils udfpsUtils,
+            @NonNull VibratorHelper vibratorHelper) {
         mContext = context;
         mFeatureFlags = featureFlags;
         mExecution = execution;
@@ -794,6 +798,7 @@
         mFaceEnrolledForUser = new SparseBooleanArray();
         mUdfpsUtils = udfpsUtils;
         mApplicationCoroutineScope = applicationCoroutineScope;
+        mVibratorHelper = vibratorHelper;
 
         mLogContextInteractor = logContextInteractor;
         mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
@@ -1044,7 +1049,7 @@
         final int userId = mCurrentDialogArgs.argi1;
         if (isFaceAuthEnrolled(userId) && isFingerprintEnrolled(userId)) {
             messageRes = modality == TYPE_FACE
-                    ? R.string.biometric_face_not_recognized
+                    ? R.string.fingerprint_dialog_use_fingerprint_instead
                     : R.string.fingerprint_error_not_match;
         } else {
             messageRes = R.string.biometric_not_recognized;
@@ -1341,7 +1346,7 @@
                 wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
                 mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider,
                 mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel,
-                mCredentialViewModelProvider, bgExecutor);
+                mCredentialViewModelProvider, bgExecutor, mVibratorHelper);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 946ddba..ea9fe5f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -230,7 +230,7 @@
                         lightRevealScrim.revealAmount = animator.animatedValue as Float
                     }
                     addListener(object : AnimatorListenerAdapter() {
-                        override fun onAnimationEnd(animation: Animator?) {
+                        override fun onAnimationEnd(animation: Animator) {
                             // Reset light reveal scrim to the default, so the CentralSurfaces
                             // can handle any subsequent light reveal changes
                             // (ie: from dozing changes)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
index 5ede16d..4c2dc41 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
@@ -147,12 +147,12 @@
             retractDwellAnimator = AnimatorSet().apply {
                 playTogether(retractDwellRippleAnimator, retractAlphaAnimator)
                 addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationStart(animation: Animator?) {
+                    override fun onAnimationStart(animation: Animator) {
                         dwellPulseOutAnimator?.cancel()
                         drawDwell = true
                     }
 
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         drawDwell = false
                         resetDwellAlpha()
                     }
@@ -182,13 +182,13 @@
                     invalidate()
                 }
                 addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationStart(animation: Animator?) {
+                    override fun onAnimationStart(animation: Animator) {
                         retractDwellAnimator?.cancel()
                         dwellPulseOutAnimator?.cancel()
                         drawDwell = true
                     }
 
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         drawDwell = false
                         resetDwellAlpha()
                     }
@@ -239,14 +239,14 @@
                     expandDwellRippleAnimator
             )
             addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationStart(animation: Animator?) {
+                override fun onAnimationStart(animation: Animator) {
                     retractDwellAnimator?.cancel()
                     fadeDwellAnimator?.cancel()
                     visibility = VISIBLE
                     drawDwell = true
                 }
 
-                override fun onAnimationEnd(animation: Animator?) {
+                override fun onAnimationEnd(animation: Animator) {
                     drawDwell = false
                 }
             })
@@ -273,12 +273,12 @@
 
         unlockedRippleAnimator = rippleAnimator.apply {
             addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationStart(animation: Animator?) {
+                override fun onAnimationStart(animation: Animator) {
                     drawRipple = true
                     visibility = VISIBLE
                 }
 
-                override fun onAnimationEnd(animation: Animator?) {
+                override fun onAnimationEnd(animation: Animator) {
                     onAnimationEnd?.run()
                     drawRipple = false
                     visibility = GONE
@@ -327,7 +327,7 @@
         }
     }
 
-    override fun onDraw(canvas: Canvas?) {
+    override fun onDraw(canvas: Canvas) {
         // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
         // the active effect area. Values here should be kept in sync with the
         // animation implementation in the ripple shader. (Twice bigger)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
index b9fa240..a24a47b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
@@ -40,7 +40,7 @@
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val faceAuthInteractor: KeyguardFaceAuthInteractor,
 ) : View.AccessibilityDelegate() {
-    override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) {
+    override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
         super.onInitializeAccessibilityNodeInfo(host, info)
         if (keyguardUpdateMonitor.shouldListenForFace()) {
             val clickActionToRetryFace =
@@ -52,7 +52,7 @@
         }
     }
 
-    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+    override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
         return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) {
             keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION)
             faceAuthInteractor.onAccessibilityAction()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 7f696fd..eb6864d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -119,7 +119,7 @@
     private var overlayView: View? = null
         set(value) {
             field?.let { oldView ->
-                val lottie = oldView.findViewById(R.id.sidefps_animation) as LottieAnimationView
+                val lottie = oldView.requireViewById(R.id.sidefps_animation) as LottieAnimationView
                 lottie.pauseAnimation()
                 windowManager.removeView(oldView)
                 orientationListener.disable()
@@ -274,7 +274,7 @@
             }
         overlayOffsets = offsets
 
-        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
+        val lottie = view.requireViewById(R.id.sidefps_animation) as LottieAnimationView
         view.rotation =
             display.asSideFpsAnimationRotation(
                 offsets.isYAligned(),
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 39a45f7..b23e085 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -646,8 +646,9 @@
             shouldPilfer = true;
         }
 
-        // Pilfer only once per gesture
-        if (shouldPilfer && !mPointerPilfered) {
+        // Pilfer only once per gesture, don't pilfer for BP
+        if (shouldPilfer && !mPointerPilfered
+                && getBiometricSessionType() != SESSION_BIOMETRIC_PROMPT) {
             mInputManager.pilferPointers(
                     mOverlay.getOverlayView().getViewRootImpl().getInputToken());
             mPointerPilfered = true;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
index 8352d0a..5dafa61 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
@@ -38,7 +38,8 @@
     override fun getDrawable(): UdfpsDrawable = fingerprintDrawable
 
     fun updateAccessibilityViewLocation(sensorBounds: Rect) {
-        val fingerprintAccessibilityView: View = findViewById(R.id.udfps_enroll_accessibility_view)
+        val fingerprintAccessibilityView: View =
+            requireViewById(R.id.udfps_enroll_accessibility_view)
         val params: ViewGroup.LayoutParams = fingerprintAccessibilityView.layoutParams
         params.width = sensorBounds.width()
         params.height = sensorBounds.height()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt
index fb7b56e..8497879 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt
@@ -33,7 +33,7 @@
     @Main private val resources: Resources,
     private val keyguardViewManager: StatusBarKeyguardViewManager,
 ) : View.AccessibilityDelegate() {
-    override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) {
+    override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
         super.onInitializeAccessibilityNodeInfo(host, info)
         val clickAction =
             AccessibilityNodeInfo.AccessibilityAction(
@@ -43,7 +43,7 @@
         info.addAction(clickAction)
     }
 
-    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+    override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
         // when an a11y service is enabled, double tapping on the fingerprint sensor should
         // show the primary bouncer
         return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index db30a55..e3fd3ce1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -306,8 +306,9 @@
         activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
         view.mUseExpandedOverlay = useExpandedOverlay
         view.startIconAsyncInflate {
-            (view.findViewById(R.id.udfps_animation_view_internal) as View).accessibilityDelegate =
-                udfpsKeyguardAccessibilityDelegate
+            val animationViewInternal: View =
+                view.requireViewById(R.id.udfps_animation_view_internal)
+            animationViewInternal.accessibilityDelegate = udfpsKeyguardAccessibilityDelegate
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
index 1f1a1b5..2a02667 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
@@ -89,7 +89,7 @@
                 )
             val hat = gkResponse.gatekeeperHAT
             lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle)
-            emit(CredentialStatus.Success.Verified(hat))
+            emit(CredentialStatus.Success.Verified(checkNotNull(hat)))
         } else if (response.timeout > 0) {
             // if requests are being throttled, update the error message every
             // second until the temporary lock has expired
@@ -226,8 +226,7 @@
             is BiometricPromptRequest.Credential.Password ->
                 DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT
         }
-    return devicePolicyManager.resources.getString(id) {
-        // use fallback a string if not found
+    val getFallbackString = {
         val defaultId =
             when (request) {
                 is BiometricPromptRequest.Credential.Pin ->
@@ -239,6 +238,8 @@
             }
         getString(defaultId)
     }
+
+    return devicePolicyManager.resources?.getString(id, getFallbackString) ?: getFallbackString()
 }
 
 private fun Context.getLastAttemptBeforeWipeUserMessage(
@@ -266,8 +267,8 @@
                 DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS
             else -> DevicePolicyResources.UNDEFINED
         }
-    return devicePolicyManager.resources.getString(id) {
-        // use fallback a string if not found
+
+    val getFallbackString = {
         val defaultId =
             when (userType) {
                 UserType.PRIMARY ->
@@ -279,4 +280,6 @@
             }
         getString(defaultId)
     }
+
+    return devicePolicyManager.resources?.getString(id, getFallbackString) ?: getFallbackString()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
index a3f34ce..b940ec8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
@@ -122,7 +122,7 @@
                 titleView.ellipsize = TextUtils.TruncateAt.MARQUEE
                 titleView.marqueeRepeatLimit = -1
                 // select to enable marquee unless a screen reader is enabled
-                titleView.isSelected = accessibilityManager.shouldMarquee()
+                titleView.isSelected = accessibilityManager?.shouldMarquee() ?: false
             } else {
                 titleView.isSingleLine = false
                 titleView.ellipsize = null
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index a4cd3fa..d054751 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.biometrics.ui.binder
 
 import android.animation.Animator
+import android.annotation.SuppressLint
 import android.content.Context
 import android.hardware.biometrics.BiometricAuthenticator
 import android.hardware.biometrics.BiometricConstants
@@ -25,6 +26,8 @@
 import android.os.Bundle
 import android.text.method.ScrollingMovementMethod
 import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.view.MotionEvent
 import android.view.View
 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
 import android.view.accessibility.AccessibilityManager
@@ -54,9 +57,13 @@
 import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
 import com.android.systemui.biometrics.ui.viewmodel.PromptSize
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.VibratorHelper
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
@@ -68,6 +75,7 @@
 object BiometricViewBinder {
 
     /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */
+    @SuppressLint("ClickableViewAccessibility")
     @JvmStatic
     fun bind(
         view: BiometricPromptLayout,
@@ -77,6 +85,8 @@
         backgroundView: View,
         legacyCallback: Callback,
         applicationScope: CoroutineScope,
+        vibratorHelper: VibratorHelper,
+        featureFlags: FeatureFlags,
     ): AuthBiometricViewAdapter {
         val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
 
@@ -85,9 +95,9 @@
         val textColorHint =
             view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
 
-        val titleView = view.findViewById<TextView>(R.id.title)
-        val subtitleView = view.findViewById<TextView>(R.id.subtitle)
-        val descriptionView = view.findViewById<TextView>(R.id.description)
+        val titleView = view.requireViewById<TextView>(R.id.title)
+        val subtitleView = view.requireViewById<TextView>(R.id.subtitle)
+        val descriptionView = view.requireViewById<TextView>(R.id.description)
 
         // set selected to enable marquee unless a screen reader is enabled
         titleView.isSelected =
@@ -96,18 +106,18 @@
             !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled
         descriptionView.movementMethod = ScrollingMovementMethod()
 
-        val iconViewOverlay = view.findViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
-        val iconView = view.findViewById<LottieAnimationView>(R.id.biometric_icon)
-        val indicatorMessageView = view.findViewById<TextView>(R.id.indicator)
+        val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
+        val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon)
+        val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator)
 
         // Negative-side (left) buttons
-        val negativeButton = view.findViewById<Button>(R.id.button_negative)
-        val cancelButton = view.findViewById<Button>(R.id.button_cancel)
-        val credentialFallbackButton = view.findViewById<Button>(R.id.button_use_credential)
+        val negativeButton = view.requireViewById<Button>(R.id.button_negative)
+        val cancelButton = view.requireViewById<Button>(R.id.button_cancel)
+        val credentialFallbackButton = view.requireViewById<Button>(R.id.button_use_credential)
 
         // Positive-side (right) buttons
-        val confirmationButton = view.findViewById<Button>(R.id.button_confirm)
-        val retryButton = view.findViewById<Button>(R.id.button_try_again)
+        val confirmationButton = view.requireViewById<Button>(R.id.button_confirm)
+        val retryButton = view.requireViewById<Button>(R.id.button_try_again)
 
         // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers
         val adapter =
@@ -293,21 +303,19 @@
 
                 // reuse the icon as a confirm button
                 launch {
-                    viewModel.isConfirmButtonVisible
+                    viewModel.isIconConfirmButton
                         .map { isPending ->
                             when {
                                 isPending && iconController.actsAsConfirmButton ->
-                                    View.OnClickListener { viewModel.confirmAuthenticated() }
+                                    View.OnTouchListener { _: View, event: MotionEvent ->
+                                        viewModel.onOverlayTouch(event)
+                                    }
                                 else -> null
                             }
                         }
-                        .collect { onClick ->
-                            iconViewOverlay.setOnClickListener(onClick)
-                            iconView.setOnClickListener(onClick)
-                            if (onClick == null) {
-                                iconViewOverlay.isClickable = false
-                                iconView.isClickable = false
-                            }
+                        .collect { onTouch ->
+                            iconViewOverlay.setOnTouchListener(onTouch)
+                            iconView.setOnTouchListener(onTouch)
                         }
                 }
 
@@ -333,6 +341,14 @@
                             backgroundView.setOnClickListener(null)
                             backgroundView.importantForAccessibility =
                                 IMPORTANT_FOR_ACCESSIBILITY_NO
+
+                            // Allow icon to be used as confirmation button with a11y enabled
+                            if (accessibilityManager.isTouchExplorationEnabled) {
+                                iconViewOverlay.setOnClickListener {
+                                    viewModel.confirmAuthenticated()
+                                }
+                                iconView.setOnClickListener { viewModel.confirmAuthenticated() }
+                            }
                         }
                         if (authState.isAuthenticatedAndConfirmed) {
                             view.announceForAccessibility(
@@ -383,6 +399,18 @@
                         }
                     }
                 }
+
+                // Play haptics
+                if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+                    launch {
+                        viewModel.hapticsToPlay.collect { hapticFeedbackConstant ->
+                            if (hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) {
+                                vibratorHelper.performHapticFeedback(view, hapticFeedbackConstant)
+                                viewModel.clearHaptics()
+                            }
+                        }
+                    }
+                }
             }
         }
 
@@ -476,7 +504,7 @@
             modalities.hasFaceAndFingerprint &&
                 (viewModel.fingerprintStartMode.first() != FingerprintStartMode.Pending) &&
                 (authenticatedModality == BiometricModality.Face) ->
-                R.string.biometric_dialog_tap_confirm_with_face
+                R.string.biometric_dialog_tap_confirm_with_face_alt_1
             else -> null
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index 1a286cf..370b36b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -72,7 +72,7 @@
             }
         }
 
-        val iconHolderView = view.findViewById<View>(R.id.biometric_icon_frame)
+        val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
         val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
         val fullSizeYOffset =
             view.resources.getDimension(R.dimen.biometric_dialog_medium_to_large_translation_offset)
@@ -205,7 +205,7 @@
 }
 
 private fun View.isLandscape(): Boolean {
-    val r = context.display.rotation
+    val r = context.display?.rotation
     return r == Surface.ROTATION_90 || r == Surface.ROTATION_270
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index dca19c5..89561a5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -17,11 +17,15 @@
 
 import android.hardware.biometrics.BiometricPrompt
 import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.view.MotionEvent
 import com.android.systemui.biometrics.AuthBiometricView
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
 import com.android.systemui.biometrics.domain.model.BiometricModalities
 import com.android.systemui.biometrics.shared.model.BiometricModality
 import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.statusbar.VibratorHelper
 import javax.inject.Inject
 import kotlinx.coroutines.Job
@@ -43,6 +47,7 @@
 constructor(
     private val interactor: PromptSelectorInteractor,
     private val vibrator: VibratorHelper,
+    private val featureFlags: FeatureFlags,
 ) {
     /** The set of modalities available for this prompt */
     val modalities: Flow<BiometricModalities> =
@@ -63,11 +68,18 @@
     /** If the user has successfully authenticated and confirmed (when explicitly required). */
     val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow()
 
+    private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
     /**
      * If the API caller or the user's personal preferences require explicit confirmation after
      * successful authentication.
      */
-    val isConfirmationRequired: Flow<Boolean> = interactor.isConfirmationRequired
+    val isConfirmationRequired: Flow<Boolean> =
+        combine(_isOverlayTouched, interactor.isConfirmationRequired) {
+            isOverlayTouched,
+            isConfirmationRequired ->
+            !isOverlayTouched && isConfirmationRequired
+        }
 
     /** The kind of credential the user has. */
     val credentialKind: Flow<PromptKind> = interactor.credentialKind
@@ -90,6 +102,11 @@
     private val _forceLargeSize = MutableStateFlow(false)
     private val _forceMediumSize = MutableStateFlow(false)
 
+    private val _hapticsToPlay = MutableStateFlow(HapticFeedbackConstants.NO_HAPTICS)
+
+    /** Event fired to the view indicating a [HapticFeedbackConstants] to be played */
+    val hapticsToPlay = _hapticsToPlay.asStateFlow()
+
     /** The size of the prompt. */
     val size: Flow<PromptSize> =
         combine(
@@ -141,6 +158,12 @@
             }
             .distinctUntilChanged()
 
+    /** If the icon can be used as a confirmation button. */
+    val isIconConfirmButton: Flow<Boolean> =
+        combine(size, interactor.isConfirmationRequired) { size, isConfirmationRequired ->
+            size.isNotSmall && isConfirmationRequired
+        }
+
     /** If the negative button should be shown. */
     val isNegativeButtonVisible: Flow<Boolean> =
         combine(
@@ -289,8 +312,10 @@
             if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
         _forceMediumSize.value = true
         _legacyState.value =
-            if (alreadyAuthenticated) {
+            if (alreadyAuthenticated && isConfirmationRequired.first()) {
                 AuthBiometricView.STATE_PENDING_CONFIRMATION
+            } else if (alreadyAuthenticated && !isConfirmationRequired.first()) {
+                AuthBiometricView.STATE_AUTHENTICATED
             } else {
                 AuthBiometricView.STATE_HELP
             }
@@ -388,18 +413,10 @@
     }
 
     private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
-        val availableModalities = modalities.first()
         val confirmationRequired = isConfirmationRequired.first()
 
-        if (availableModalities.hasFaceAndFingerprint) {
-            // coex only needs confirmation when face is successful, unless it happens on the
-            // first attempt (i.e. without failure) before fingerprint scanning starts
-            val fingerprintStarted = fingerprintStartMode.first() != FingerprintStartMode.Pending
-            if (modality == BiometricModality.Face) {
-                return fingerprintStarted || confirmationRequired
-            }
-        }
-        if (availableModalities.hasFaceOnly) {
+        // Only worry about confirmationRequired if face was used to unlock
+        if (modality == BiometricModality.Face) {
             return confirmationRequired
         }
         // fingerprint only never requires confirmation
@@ -430,6 +447,26 @@
     }
 
     /**
+     * Touch event occurred on the overlay
+     *
+     * Tracks whether a finger is currently down to set [_isOverlayTouched] to be used as user
+     * confirmation
+     */
+    fun onOverlayTouch(event: MotionEvent): Boolean {
+        if (event.actionMasked == MotionEvent.ACTION_DOWN) {
+            _isOverlayTouched.value = true
+
+            if (_isAuthenticated.value.needsUserConfirmation) {
+                confirmAuthenticated()
+            }
+            return true
+        } else if (event.actionMasked == MotionEvent.ACTION_UP) {
+            _isOverlayTouched.value = false
+        }
+        return false
+    }
+
+    /**
      * Switch to the credential view.
      *
      * TODO(b/251476085): this should be decoupled from the shared panel controller
@@ -438,11 +475,26 @@
         _forceLargeSize.value = true
     }
 
-    private fun VibratorHelper.success(modality: BiometricModality) =
-        vibrateAuthSuccess("$TAG, modality = $modality BP::success")
+    private fun VibratorHelper.success(modality: BiometricModality) {
+        if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+            _hapticsToPlay.value = HapticFeedbackConstants.CONFIRM
+        } else {
+            vibrateAuthSuccess("$TAG, modality = $modality BP::success")
+        }
+    }
 
-    private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) =
-        vibrateAuthError("$TAG, modality = $modality BP::error")
+    private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) {
+        if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+            _hapticsToPlay.value = HapticFeedbackConstants.REJECT
+        } else {
+            vibrateAuthError("$TAG, modality = $modality BP::error")
+        }
+    }
+
+    /** Clears the [hapticsToPlay] variable by setting it to the NO_HAPTICS default. */
+    fun clearHaptics() {
+        _hapticsToPlay.value = HapticFeedbackConstants.NO_HAPTICS
+    }
 
     companion object {
         private const val TAG = "PromptViewModel"
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index 5ca36ab..ddb09749 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -156,9 +156,9 @@
         }
         windowLayoutParams.packageName = context.opPackageName
         rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
-            override fun onViewDetachedFromWindow(view: View?) {}
+            override fun onViewDetachedFromWindow(view: View) {}
 
-            override fun onViewAttachedToWindow(view: View?) {
+            override fun onViewAttachedToWindow(view: View) {
                 layoutRipple()
                 rippleView.startRipple(Runnable {
                     windowManager.removeView(rippleView)
@@ -176,7 +176,7 @@
         val height = bounds.height()
         val maxDiameter = Integer.max(width, height) * 2f
         rippleView.setMaxSize(maxDiameter, maxDiameter)
-        when (context.display.rotation) {
+        when (context.display?.rotation) {
             Surface.ROTATION_0 -> {
                 rippleView.setCenter(
                         width * normalizedPortPosX, height * normalizedPortPosY)
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt
index 63d57cc..a9f3b77 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt
@@ -29,7 +29,7 @@
  */
 class FalsingA11yDelegate @Inject constructor(private val falsingCollector: FalsingCollector) :
     View.AccessibilityDelegate() {
-    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+    override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
         if (action == ACTION_CLICK) {
             falsingCollector.onA11yAction()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt
index 0b8e83e..1b45ecd 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt
@@ -59,7 +59,7 @@
         context.startActivity(intent, transition.first.toBundle())
         val runner = RemoteAnimationAdapter(NULL_ACTIVITY_TRANSITION, 0, 0)
         try {
-            WindowManagerGlobal.getWindowManagerService()
+            checkNotNull(WindowManagerGlobal.getWindowManagerService())
                 .overridePendingAppTransitionRemote(runner, displayTracker.defaultDisplayId)
         } catch (e: Exception) {
             Log.e(TAG, "Error overriding clipboard app transition", e)
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
index 3e6ac86..c0d1951 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
@@ -100,7 +100,7 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale())
 
     override fun getResolutionScale(): Float {
-        context.display.getDisplayInfo(displayInfo.value)
+        context.display?.getDisplayInfo(displayInfo.value)
         val maxDisplayMode =
             displayUtils.getMaximumResolutionDisplayMode(displayInfo.value.supportedModes)
         maxDisplayMode?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt
index f17d0f3..0b327a1 100644
--- a/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt
@@ -66,9 +66,9 @@
 
         contrastButtons =
             mapOf(
-                CONTRAST_LEVEL_STANDARD to findViewById(R.id.contrast_button_standard),
-                CONTRAST_LEVEL_MEDIUM to findViewById(R.id.contrast_button_medium),
-                CONTRAST_LEVEL_HIGH to findViewById(R.id.contrast_button_high)
+                CONTRAST_LEVEL_STANDARD to requireViewById(R.id.contrast_button_standard),
+                CONTRAST_LEVEL_MEDIUM to requireViewById(R.id.contrast_button_medium),
+                CONTRAST_LEVEL_HIGH to requireViewById(R.id.contrast_button_high)
             )
 
         contrastButtons.forEach { (contrastLevel, contrastButton) ->
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index e8c97bf..4a534e9c 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -190,7 +190,7 @@
                     PREFS_CONTROLS_SEEDING_COMPLETED, mutableSetOf<String>())
                 val servicePackageSet = serviceInfoSet.map { it.packageName }
                 prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED,
-                    completedSeedingPackageSet.intersect(servicePackageSet)).apply()
+                    completedSeedingPackageSet?.intersect(servicePackageSet) ?: emptySet()).apply()
 
                 var changed = false
                 favoriteComponentSet.subtract(serviceInfoSet).forEach {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
index 23721c9..8bae667 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -193,7 +193,7 @@
 
                         ControlsAnimations.enterAnimation(pageIndicator).apply {
                             addListener(object : AnimatorListenerAdapter() {
-                                override fun onAnimationEnd(animation: Animator?) {
+                                override fun onAnimationEnd(animation: Animator) {
                                     // Position the tooltip if necessary after animations are complete
                                     // so we can get the position on screen. The tooltip is not
                                     // rooted in the layout root.
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt
index ff55b76d..a13f717 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt
@@ -106,10 +106,8 @@
                 }
             )
 
-            getWindow().apply {
-                setType(WINDOW_TYPE)
-                setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
-            }
+            window?.setType(WINDOW_TYPE)
+            window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
             setOnShowListener(DialogInterface.OnShowListener { _ ->
                 val editText = requireViewById<EditText>(R.id.controls_pin_input)
                 editText.setHint(instructions)
@@ -153,9 +151,7 @@
             )
         }
         return builder.create().apply {
-            getWindow().apply {
-                setType(WINDOW_TYPE)
-            }
+            window?.setType(WINDOW_TYPE)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
index c04bc87..abe3423 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -384,7 +384,7 @@
                 )
             }
             addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
+                override fun onAnimationEnd(animation: Animator) {
                     stateAnimator = null
                 }
             })
@@ -438,7 +438,7 @@
                 duration = 200L
                 interpolator = Interpolators.LINEAR
                 addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         statusRowUpdater.invoke()
                     }
                 })
@@ -450,7 +450,7 @@
             statusAnimator = AnimatorSet().apply {
                 playSequentially(fadeOut, fadeIn)
                 addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         status.alpha = STATUS_ALPHA_ENABLED
                         statusAnimator = null
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
index be50a14..98f17f4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
@@ -132,8 +132,8 @@
 
     init {
         // To pass touches to the task inside TaskView.
-        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
-        window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+        window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
+        window?.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
 
         setContentView(R.layout.controls_detail_dialog)
 
@@ -182,7 +182,7 @@
         }
 
         // consume all insets to achieve slide under effect
-        window.getDecorView().setOnApplyWindowInsetsListener {
+        checkNotNull(window).decorView.setOnApplyWindowInsetsListener {
             v: View, insets: WindowInsets ->
                 val l = v.getPaddingLeft()
                 val r = v.getPaddingRight()
@@ -202,7 +202,7 @@
     }
 
     fun getTaskViewBounds(): Rect {
-        val wm = context.getSystemService(WindowManager::class.java)
+        val wm = checkNotNull(context.getSystemService(WindowManager::class.java))
         val windowMetrics = wm.getCurrentWindowMetrics()
         val rect = windowMetrics.bounds
         val metricInsets = windowMetrics.windowInsets
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
index ad2b785..dbbda9a 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
@@ -67,7 +67,8 @@
                     iconMap.put(resourceId, icon)
                 }
             }
-            return RenderInfo(icon!!.constantState.newDrawable(context.resources), fg, bg)
+            return RenderInfo(
+                checkNotNull(icon?.constantState).newDrawable(context.resources), fg, bg)
         }
 
         fun registerComponentIcon(componentName: ComponentName, icon: Drawable) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
index 84cda5a..3c2bfa0 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
@@ -94,10 +94,8 @@
             )
         }
         cvh.visibleDialog = builder.create().apply {
-            getWindow().apply {
-                setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
-                show()
-            }
+            window?.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+            show()
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
index 1461135..b2c95a6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
@@ -244,7 +244,7 @@
                     cvh.clipLayer.level = it.animatedValue as Int
                 }
                 addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         rangeAnimator = null
                     }
                 })
@@ -335,7 +335,7 @@
         }
 
         override fun onScroll(
-            e1: MotionEvent,
+            e1: MotionEvent?,
             e2: MotionEvent,
             xDiff: Float,
             yDiff: Float
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
index bcfeeb9e..cef45dc 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
@@ -51,7 +51,7 @@
                 val callback =
                     object : ConfigurationController.ConfigurationListener {
                         override fun onConfigChanged(newConfig: Configuration?) {
-                            context.display.getMetrics(displayMetricsHolder)
+                            context.display?.getMetrics(displayMetricsHolder)
                             trySend(displayMetricsHolder)
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 35a5d61..5b56223 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -302,6 +302,19 @@
             R.bool.flag_stop_pulsing_face_scanning_animation,
             "stop_pulsing_face_scanning_animation")
 
+    /**
+     * TODO(b/278086361): Tracking bug
+     * Complete rewrite of the interactions between System UI and Window Manager involving keyguard
+     * state. When enabled, calls to ActivityTaskManagerService from System UI will exclusively
+     * occur from [WmLockscreenVisibilityManager] rather than the legacy KeyguardViewMediator.
+     *
+     * This flag is under development; some types of unlock may not animate properly if you enable
+     * it.
+     */
+    @JvmField
+    val KEYGUARD_WM_STATE_REFACTOR: UnreleasedFlag =
+            unreleasedFlag("keyguard_wm_state_refactor")
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag("power_menu_lite")
@@ -378,13 +391,13 @@
 
     // TODO(b/293863612): Tracking Bug
     @JvmField val INCOMPATIBLE_CHARGING_BATTERY_ICON =
-        unreleasedFlag("incompatible_charging_battery_icon")
+        releasedFlag("incompatible_charging_battery_icon")
 
     // TODO(b/293585143): Tracking Bug
     val INSTANT_TETHER = unreleasedFlag("instant_tether")
 
     // TODO(b/294588085): Tracking Bug
-    val WIFI_SECONDARY_NETWORKS = unreleasedFlag("wifi_secondary_networks")
+    val WIFI_SECONDARY_NETWORKS = releasedFlag("wifi_secondary_networks")
 
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index 7078341..b5b56b2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -161,7 +161,7 @@
     }
 
     private fun updateIconTile() {
-        val iconTile = rootView.findViewById(BACKLIGHT_ICON_ID) as ImageView
+        val iconTile = rootView.requireViewById(BACKLIGHT_ICON_ID) as ImageView
         val backgroundDrawable = iconTile.background as ShapeDrawable
         if (currentLevel == 0) {
             iconTile.setColorFilter(dimmedIconColor)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index e6053fb..9d2771e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -73,6 +73,14 @@
 import com.android.internal.policy.IKeyguardStateCallback;
 import com.android.keyguard.mediator.ScreenOnCoordinator;
 import com.android.systemui.SystemUIApplication;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier;
+import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder;
+import com.android.systemui.keyguard.ui.binder.WindowManagerLockscreenVisibilityViewBinder;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSurfaceBehindViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.WindowManagerLockscreenVisibilityViewModel;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
@@ -85,10 +93,13 @@
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.CoroutineScope;
+
 public class KeyguardService extends Service {
     static final String TAG = "KeyguardService";
     static final String PERMISSION = android.Manifest.permission.CONTROL_KEYGUARD;
 
+    private final FeatureFlags mFlags;
     private final KeyguardViewMediator mKeyguardViewMediator;
     private final KeyguardLifecyclesDispatcher mKeyguardLifecyclesDispatcher;
     private final ScreenOnCoordinator mScreenOnCoordinator;
@@ -291,13 +302,33 @@
                            KeyguardLifecyclesDispatcher keyguardLifecyclesDispatcher,
                            ScreenOnCoordinator screenOnCoordinator,
                            ShellTransitions shellTransitions,
-                           DisplayTracker displayTracker) {
+                           DisplayTracker displayTracker,
+                           WindowManagerLockscreenVisibilityViewModel
+                                   wmLockscreenVisibilityViewModel,
+                           WindowManagerLockscreenVisibilityManager wmLockscreenVisibilityManager,
+                           KeyguardSurfaceBehindViewModel keyguardSurfaceBehindViewModel,
+                           KeyguardSurfaceBehindParamsApplier keyguardSurfaceBehindAnimator,
+                           @Application CoroutineScope scope,
+                           FeatureFlags featureFlags) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
         mScreenOnCoordinator = screenOnCoordinator;
         mShellTransitions = shellTransitions;
         mDisplayTracker = displayTracker;
+        mFlags = featureFlags;
+
+        if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+            WindowManagerLockscreenVisibilityViewBinder.bind(
+                    wmLockscreenVisibilityViewModel,
+                    wmLockscreenVisibilityManager,
+                    scope);
+
+            KeyguardSurfaceBehindViewBinder.bind(
+                    keyguardSurfaceBehindViewModel,
+                    keyguardSurfaceBehindAnimator,
+                    scope);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 58780f4..32a9559 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -405,7 +405,9 @@
      * the device.
      */
     fun canPerformInWindowLauncherAnimations(): Boolean {
-        return isNexusLauncherUnderneath() &&
+        // TODO(b/278086361): Refactor in-window animations.
+        return !featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR) &&
+                isNexusLauncherUnderneath() &&
                 // If the launcher is underneath, but we're about to launch an activity, don't do
                 // the animations since they won't be visible.
                 !notificationShadeWindowController.isLaunchingActivity &&
@@ -849,54 +851,57 @@
         }
 
         surfaceBehindRemoteAnimationTargets?.forEach { surfaceBehindRemoteAnimationTarget ->
-            val surfaceHeight: Int = surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height()
+            if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                val surfaceHeight: Int =
+                        surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height()
 
-            var scaleFactor = (SURFACE_BEHIND_START_SCALE_FACTOR +
-                    (1f - SURFACE_BEHIND_START_SCALE_FACTOR) *
-                    MathUtils.clamp(amount, 0f, 1f))
+                var scaleFactor = (SURFACE_BEHIND_START_SCALE_FACTOR +
+                        (1f - SURFACE_BEHIND_START_SCALE_FACTOR) *
+                        MathUtils.clamp(amount, 0f, 1f))
 
-            // If we're dismissing via swipe to the Launcher, we'll play in-window scale animations,
-            // so don't also scale the window.
-            if (keyguardStateController.isDismissingFromSwipe &&
-                    willUnlockWithInWindowLauncherAnimations) {
-                scaleFactor = 1f
-            }
-
-            // Translate up from the bottom.
-            surfaceBehindMatrix.setTranslate(
-                    surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(),
-                    surfaceBehindRemoteAnimationTarget.screenSpaceBounds.top.toFloat() +
-                            surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount)
-            )
-
-            // Scale up from a point at the center-bottom of the surface.
-            surfaceBehindMatrix.postScale(
-                    scaleFactor,
-                    scaleFactor,
-                    keyguardViewController.viewRootImpl.width / 2f,
-                    surfaceHeight * SURFACE_BEHIND_SCALE_PIVOT_Y
-            )
-
-            // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is
-            // unable to draw
-            val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget.leash
-            if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE &&
-                    sc?.isValid == true) {
-                with(SurfaceControl.Transaction()) {
-                    setMatrix(sc, surfaceBehindMatrix, tmpFloat)
-                    setCornerRadius(sc, roundedCornerRadius)
-                    setAlpha(sc, animationAlpha)
-                    apply()
+                // If we're dismissing via swipe to the Launcher, we'll play in-window scale
+                // animations, so don't also scale the window.
+                if (keyguardStateController.isDismissingFromSwipe &&
+                        willUnlockWithInWindowLauncherAnimations) {
+                    scaleFactor = 1f
                 }
-            } else {
-                applyParamsToSurface(
-                        SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
-                            surfaceBehindRemoteAnimationTarget.leash)
-                            .withMatrix(surfaceBehindMatrix)
-                            .withCornerRadius(roundedCornerRadius)
-                            .withAlpha(animationAlpha)
-                            .build()
+
+                // Translate up from the bottom.
+                surfaceBehindMatrix.setTranslate(
+                        surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(),
+                        surfaceBehindRemoteAnimationTarget.screenSpaceBounds.top.toFloat() +
+                                surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount)
                 )
+
+                // Scale up from a point at the center-bottom of the surface.
+                surfaceBehindMatrix.postScale(
+                        scaleFactor,
+                        scaleFactor,
+                        keyguardViewController.viewRootImpl.width / 2f,
+                        surfaceHeight * SURFACE_BEHIND_SCALE_PIVOT_Y
+                )
+
+                // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is
+                // unable to draw
+                val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget.leash
+                if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE &&
+                        sc?.isValid == true) {
+                    with(SurfaceControl.Transaction()) {
+                        setMatrix(sc, surfaceBehindMatrix, tmpFloat)
+                        setCornerRadius(sc, roundedCornerRadius)
+                        setAlpha(sc, animationAlpha)
+                        apply()
+                    }
+                } else {
+                    applyParamsToSurface(
+                            SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
+                                    surfaceBehindRemoteAnimationTarget.leash)
+                                    .withMatrix(surfaceBehindMatrix)
+                                    .withCornerRadius(roundedCornerRadius)
+                                    .withAlpha(animationAlpha)
+                                    .build()
+                    )
+                }
             }
         }
 
@@ -985,10 +990,12 @@
         if (keyguardStateController.isShowing) {
             // Hide the keyguard, with no fade out since we animated it away during the unlock.
 
-            keyguardViewController.hide(
-                surfaceBehindRemoteAnimationStartTime,
-                0 /* fadeOutDuration */
-            )
+            if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                keyguardViewController.hide(
+                        surfaceBehindRemoteAnimationStartTime,
+                        0 /* fadeOutDuration */
+                )
+            }
         } else {
             Log.i(TAG, "#hideKeyguardViewAfterRemoteAnimation called when keyguard view is not " +
                     "showing. Ignoring...")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d2047d9..a55cfa7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -171,8 +171,6 @@
 import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
-import dagger.Lazy;
-
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -182,6 +180,7 @@
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
+import dagger.Lazy;
 import kotlinx.coroutines.CoroutineDispatcher;
 
 /**
@@ -1035,12 +1034,19 @@
                 IRemoteAnimationFinishedCallback finishedCallback) {
             Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation");
             startKeyguardExitAnimation(transit, apps, wallpapers, nonApps, finishedCallback);
+            if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationStart(
+                        transit, apps, wallpapers, nonApps, finishedCallback);
+            }
             Trace.endSection();
         }
 
         @Override // Binder interface
         public void onAnimationCancelled() {
             cancelKeyguardExitAnimation();
+            if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationCancelled();
+            }
         }
     };
 
@@ -1106,7 +1112,7 @@
 
                         mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f);
                         mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration);
-                        mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR);
+                        //mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR);
                         mOccludeByDreamAnimator.addUpdateListener(
                                 animation -> {
                                     SyncRtSurfaceTransactionApplier.SurfaceParams.Builder
@@ -1335,6 +1341,8 @@
             mDreamingToLockscreenTransitionViewModel;
     private RemoteAnimationTarget mRemoteAnimationTarget;
 
+    private Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager;
+
     /**
      * Injected constructor. See {@link KeyguardModule}.
      */
@@ -1377,7 +1385,8 @@
             SystemClock systemClock,
             @Main CoroutineDispatcher mainDispatcher,
             Lazy<DreamingToLockscreenTransitionViewModel> dreamingToLockscreenTransitionViewModel,
-            SystemPropertiesHelper systemPropertiesHelper) {
+            SystemPropertiesHelper systemPropertiesHelper,
+            Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager) {
         mContext = context;
         mUserTracker = userTracker;
         mFalsingCollector = falsingCollector;
@@ -1443,8 +1452,9 @@
         mUiEventLogger = uiEventLogger;
         mSessionTracker = sessionTracker;
 
-        mMainDispatcher = mainDispatcher;
         mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
+        mWmLockscreenVisibilityManager = wmLockscreenVisibilityManager;
+        mMainDispatcher = mainDispatcher;
     }
 
     public void userActivity() {
@@ -2677,6 +2687,12 @@
             if (DEBUG) {
                 Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ")");
             }
+
+            if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                // Handled in WmLockscreenVisibilityManager if flag is enabled.
+                return;
+            }
+
             try {
                 ActivityTaskManager.getService().setLockScreenShown(showing, aodShowing);
             } catch (RemoteException e) {
@@ -2716,7 +2732,11 @@
             }
             mHiding = false;
 
-            mKeyguardViewControllerLazy.get().show(options);
+            if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                // Handled directly in StatusBarKeyguardViewManager if enabled.
+                mKeyguardViewControllerLazy.get().show(options);
+            }
+
             resetKeyguardDonePendingLocked();
             mHideAnimationRun = false;
             adjustStatusBarLocked();
@@ -2787,19 +2807,22 @@
             mUpdateMonitor.setKeyguardGoingAway(true);
             mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(true);
 
-            // Don't actually hide the Keyguard at the moment, wait for window
-            // manager until it tells us it's safe to do so with
-            // startKeyguardExitAnimation.
-            // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager will be in
-            // order.
-            final int keyguardFlag = flags;
-            mUiBgExecutor.execute(() -> {
-                try {
-                    ActivityTaskManager.getService().keyguardGoingAway(keyguardFlag);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Error while calling WindowManager", e);
-                }
-            });
+            // Handled in WmLockscreenVisibilityManager if flag is enabled.
+            if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                // Don't actually hide the Keyguard at the moment, wait for window manager until it
+                // tells us it's safe to do so with startKeyguardExitAnimation.
+                // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager will be
+                // in order.
+                final int keyguardFlag = flags;
+                mUiBgExecutor.execute(() -> {
+                    try {
+                        ActivityTaskManager.getService().keyguardGoingAway(keyguardFlag);
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Error while calling WindowManager", e);
+                    }
+                });
+            }
+
             Trace.endSection();
         }
     };
@@ -2913,7 +2936,10 @@
             if (!mHiding
                     && !mSurfaceBehindRemoteAnimationRequested
                     && !mKeyguardStateController.isFlingingToDismissKeyguardDuringSwipeGesture()) {
-                if (finishedCallback != null) {
+                // If the flag is enabled, remote animation state is handled in
+                // WmLockscreenVisibilityManager.
+                if (finishedCallback != null
+                        && !mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
                     // There will not execute animation, send a finish callback to ensure the remote
                     // animation won't hang there.
                     try {
@@ -2939,10 +2965,12 @@
                         new IRemoteAnimationFinishedCallback() {
                             @Override
                             public void onAnimationFinished() throws RemoteException {
-                                try {
-                                    finishedCallback.onAnimationFinished();
-                                } catch (RemoteException e) {
-                                    Slog.w(TAG, "Failed to call onAnimationFinished", e);
+                                if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                                    try {
+                                        finishedCallback.onAnimationFinished();
+                                    } catch (RemoteException e) {
+                                        Slog.w(TAG, "Failed to call onAnimationFinished", e);
+                                    }
                                 }
                                 onKeyguardExitFinished();
                                 mKeyguardViewControllerLazy.get().hide(0 /* startTime */,
@@ -2969,7 +2997,11 @@
             // it will dismiss the panel in that case.
             } else if (!mStatusBarStateController.leaveOpenOnKeyguardHide()
                     && apps != null && apps.length > 0) {
-                mSurfaceBehindRemoteAnimationFinishedCallback = finishedCallback;
+                if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                    // Handled in WmLockscreenVisibilityManager. Other logic in this class will
+                    // short circuit when this is null.
+                    mSurfaceBehindRemoteAnimationFinishedCallback = finishedCallback;
+                }
                 mSurfaceBehindRemoteAnimationRunning = true;
 
                 mInteractionJankMonitor.begin(
@@ -2989,7 +3021,10 @@
                         createInteractionJankMonitorConf(
                                 CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RemoteAnimationDisabled"));
 
-                mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration);
+                if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                    // Handled directly in StatusBarKeyguardViewManager if enabled.
+                    mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration);
+                }
 
                 // TODO(bc-animation): When remote animation is enabled for keyguard exit animation,
                 // apps, wallpapers and finishedCallback are set to non-null. nonApps is not yet
@@ -3003,13 +3038,17 @@
                     }
                     if (apps == null || apps.length == 0) {
                         Slog.e(TAG, "Keyguard exit without a corresponding app to show.");
+
                         try {
-                            finishedCallback.onAnimationFinished();
+                            if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                                finishedCallback.onAnimationFinished();
+                            }
                         } catch (RemoteException e) {
                             Slog.e(TAG, "RemoteException");
                         } finally {
                             mInteractionJankMonitor.end(CUJ_LOCKSCREEN_UNLOCK_ANIMATION);
                         }
+
                         return;
                     }
 
@@ -3033,7 +3072,9 @@
                         @Override
                         public void onAnimationEnd(Animator animation) {
                             try {
-                                finishedCallback.onAnimationFinished();
+                                if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                                    finishedCallback.onAnimationFinished();
+                                }
                             } catch (RemoteException e) {
                                 Slog.e(TAG, "RemoteException");
                             } finally {
@@ -3044,7 +3085,9 @@
                         @Override
                         public void onAnimationCancel(Animator animation) {
                             try {
-                                finishedCallback.onAnimationFinished();
+                                if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                                    finishedCallback.onAnimationFinished();
+                                }
                             } catch (RemoteException e) {
                                 Slog.e(TAG, "RemoteException");
                             } finally {
@@ -3193,7 +3236,10 @@
                 flags |= KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT;
             }
 
-            ActivityTaskManager.getService().keyguardGoingAway(flags);
+            if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                // Handled in WmLockscreenVisibilityManager.
+                ActivityTaskManager.getService().keyguardGoingAway(flags);
+            }
             mKeyguardStateController.notifyKeyguardGoingAway(true);
         } catch (RemoteException e) {
             mSurfaceBehindRemoteAnimationRequested = false;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
new file mode 100644
index 0000000..75677f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard
+
+import android.app.IActivityTaskManager
+import android.util.Log
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.RemoteAnimationTarget
+import android.view.WindowManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Manages lockscreen and AOD visibility state via the [IActivityTaskManager], and keeps track of
+ * remote animations related to changes in lockscreen visibility.
+ */
+@SysUISingleton
+class WindowManagerLockscreenVisibilityManager
+@Inject
+constructor(
+    @Main private val executor: Executor,
+    private val activityTaskManagerService: IActivityTaskManager,
+    private val keyguardStateController: KeyguardStateController,
+    private val keyguardSurfaceBehindAnimator: KeyguardSurfaceBehindParamsApplier,
+) {
+
+    /**
+     * Whether the lockscreen is showing, which we pass to [IActivityTaskManager.setLockScreenShown]
+     * in order to show the lockscreen and hide the surface behind the keyguard (or the inverse).
+     */
+    private var isLockscreenShowing = true
+
+    /**
+     * Whether AOD is showing, which we pass to [IActivityTaskManager.setLockScreenShown] in order
+     * to show AOD when the lockscreen is visible.
+     */
+    private var isAodVisible = false
+
+    /**
+     * Whether the keyguard is currently "going away", which we triggered via a call to
+     * [IActivityTaskManager.keyguardGoingAway]. When we tell WM that the keyguard is going away,
+     * the app/launcher surface behind the keyguard is made visible, and WM calls
+     * [onKeyguardGoingAwayRemoteAnimationStart] with a RemoteAnimationTarget so that we can animate
+     * it.
+     *
+     * Going away does not inherently result in [isLockscreenShowing] being set to false; we need to
+     * do that ourselves once we are done animating the surface.
+     *
+     * THIS IS THE ONLY PLACE 'GOING AWAY' TERMINOLOGY SHOULD BE USED. 'Going away' is a WM concept
+     * and we have gotten into trouble using it to mean various different things in the past. Unlock
+     * animations may still be visible when the keyguard is NOT 'going away', for example, when we
+     * play in-window animations, we set the surface to alpha=1f and end the animation immediately.
+     * The remainder of the animation occurs in-window, so while you might expect that the keyguard
+     * is still 'going away' because unlock animations are playing, it's actually not.
+     *
+     * If you want to know if the keyguard is 'going away', you probably want to check if we have
+     * STARTED but not FINISHED a transition to GONE.
+     *
+     * The going away animation will run until:
+     * - We manually call [endKeyguardGoingAwayAnimation] after we're done animating.
+     * - We call [setLockscreenShown] = true, which cancels the going away animation.
+     * - WM calls [onKeyguardGoingAwayRemoteAnimationCancelled] for another reason (such as the 10
+     *   second timeout).
+     */
+    private var isKeyguardGoingAway = false
+        private set(value) {
+            // TODO(b/278086361): Extricate the keyguard state controller.
+            keyguardStateController.notifyKeyguardGoingAway(value)
+            field = value
+        }
+
+    /** Callback provided by WM to call once we're done with the going away animation. */
+    private var goingAwayRemoteAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null
+
+    /**
+     * Set the visibility of the surface behind the keyguard, making the appropriate calls to Window
+     * Manager to effect the change.
+     */
+    fun setSurfaceBehindVisibility(visible: Boolean) {
+        if (isKeyguardGoingAway == visible) {
+            Log.d(TAG, "WmLockscreenVisibilityManager#setVisibility -> already visible=$visible")
+            return
+        }
+
+        // The surface behind is always visible if the lockscreen is not showing, so we're already
+        // visible.
+        if (visible && !isLockscreenShowing) {
+            Log.d(TAG, "#setVisibility -> already visible since the lockscreen isn't showing")
+            return
+        }
+
+        if (visible) {
+            // Make the surface visible behind the keyguard by calling keyguardGoingAway. The
+            // lockscreen is still showing as well, allowing us to animate unlocked.
+            Log.d(TAG, "ActivityTaskManagerService#keyguardGoingAway()")
+            activityTaskManagerService.keyguardGoingAway(0)
+            isKeyguardGoingAway = true
+        } else {
+            // Hide the surface by setting the lockscreen showing.
+            setLockscreenShown(true)
+        }
+    }
+
+    fun setAodVisible(aodVisible: Boolean) {
+        setWmLockscreenState(aodVisible = aodVisible)
+    }
+
+    /** Sets the visibility of the lockscreen. */
+    fun setLockscreenShown(lockscreenShown: Boolean) {
+        setWmLockscreenState(lockscreenShowing = lockscreenShown)
+    }
+
+    fun onKeyguardGoingAwayRemoteAnimationStart(
+        @WindowManager.TransitionOldType transit: Int,
+        apps: Array<RemoteAnimationTarget>,
+        wallpapers: Array<RemoteAnimationTarget>,
+        nonApps: Array<RemoteAnimationTarget>,
+        finishedCallback: IRemoteAnimationFinishedCallback
+    ) {
+        goingAwayRemoteAnimationFinishedCallback = finishedCallback
+        keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0])
+    }
+
+    fun onKeyguardGoingAwayRemoteAnimationCancelled() {
+        // If WM cancelled the animation, we need to end immediately even if we're still using the
+        // animation.
+        endKeyguardGoingAwayAnimation()
+    }
+
+    /**
+     * Whether the going away remote animation target is in-use, which means we're animating it or
+     * intend to animate it.
+     *
+     * Some unlock animations (such as the translation spring animation) are non-deterministic and
+     * might end after the transition to GONE ends. In that case, we want to keep the remote
+     * animation running until the spring ends.
+     */
+    fun setUsingGoingAwayRemoteAnimation(usingTarget: Boolean) {
+        if (!usingTarget) {
+            endKeyguardGoingAwayAnimation()
+        }
+    }
+
+    private fun setWmLockscreenState(
+        lockscreenShowing: Boolean = this.isLockscreenShowing,
+        aodVisible: Boolean = this.isAodVisible
+    ) {
+        Log.d(
+            TAG,
+            "#setWmLockscreenState(" +
+                "isLockscreenShowing=$lockscreenShowing, " +
+                "aodVisible=$aodVisible)."
+        )
+
+        if (this.isLockscreenShowing == lockscreenShowing && this.isAodVisible == aodVisible) {
+            return
+        }
+
+        activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible)
+        this.isLockscreenShowing = lockscreenShowing
+        this.isAodVisible = aodVisible
+    }
+
+    private fun endKeyguardGoingAwayAnimation() {
+        if (!isKeyguardGoingAway) {
+            Log.d(
+                TAG,
+                "#endKeyguardGoingAwayAnimation() called when isKeyguardGoingAway=false. " +
+                    "Short-circuiting."
+            )
+            return
+        }
+
+        executor.execute {
+            Log.d(TAG, "Finishing remote animation.")
+            goingAwayRemoteAnimationFinishedCallback?.onAnimationFinished()
+            goingAwayRemoteAnimationFinishedCallback = null
+
+            isKeyguardGoingAway = false
+
+            keyguardSurfaceBehindAnimator.notifySurfaceReleased()
+        }
+    }
+
+    companion object {
+        private val TAG = this::class.java.simpleName
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 4205ed2..dc19ea2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -46,6 +46,7 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager;
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
 import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
@@ -73,12 +74,11 @@
 import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
+import java.util.concurrent.Executor;
+
 import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
-
-import java.util.concurrent.Executor;
-
 import kotlinx.coroutines.CoroutineDispatcher;
 
 /**
@@ -144,7 +144,8 @@
             SystemClock systemClock,
             @Main CoroutineDispatcher mainDispatcher,
             Lazy<DreamingToLockscreenTransitionViewModel> dreamingToLockscreenTransitionViewModel,
-            SystemPropertiesHelper systemPropertiesHelper) {
+            SystemPropertiesHelper systemPropertiesHelper,
+            Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager) {
         return new KeyguardViewMediator(
                 context,
                 uiEventLogger,
@@ -186,7 +187,8 @@
                 systemClock,
                 mainDispatcher,
                 dreamingToLockscreenTransitionViewModel,
-                systemPropertiesHelper);
+                systemPropertiesHelper,
+                wmLockscreenVisibilityManager);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index c019d21..5d7a3d4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -59,7 +59,7 @@
         conflatedCallbackFlow {
             val callback =
                 object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
-                    override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
+                    override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
                         val hasCards = response?.walletCards?.isNotEmpty() == true
                         trySendWithFailureLogging(
                             state(
@@ -71,7 +71,7 @@
                         )
                     }
 
-                    override fun onWalletCardRetrievalError(error: GetWalletCardsError?) {
+                    override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
                         Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"")
                         trySendWithFailureLogging(
                             KeyguardQuickAffordanceConfig.LockScreenState.Hidden,
@@ -133,13 +133,13 @@
         return suspendCancellableCoroutine { continuation ->
             val callback =
                 object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
-                    override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
+                    override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
                         continuation.resumeWith(
                             Result.success(response?.walletCards ?: emptyList())
                         )
                     }
 
-                    override fun onWalletCardRetrievalError(error: GetWalletCardsError?) {
+                    override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
                         continuation.resumeWith(Result.success(emptyList()))
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 30f8f3e..6894147 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -46,6 +46,7 @@
 import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
 import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.log.FaceAuthenticationLogger
@@ -160,7 +161,7 @@
     @FaceDetectTableLog private val faceDetectLog: TableLogBuffer,
     @FaceAuthTableLog private val faceAuthLog: TableLogBuffer,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    featureFlags: FeatureFlags,
+    private val featureFlags: FeatureFlags,
     facePropertyRepository: FacePropertyRepository,
     dumpManager: DumpManager,
 ) : DeviceEntryFaceAuthRepository, Dumpable {
@@ -286,8 +287,12 @@
         // starts going to sleep.
         merge(
                 keyguardRepository.wakefulness.map { it.isStartingToSleepOrAsleep() },
-                keyguardRepository.isKeyguardGoingAway,
-                userRepository.userSwitchingInProgress
+                if (featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+                    keyguardTransitionInteractor.isInTransitionToState(KeyguardState.GONE)
+                } else {
+                    keyguardRepository.isKeyguardGoingAway
+                },
+                userRepository.userSwitchingInProgress,
             )
             .onEach { anyOfThemIsTrue ->
                 if (anyOfThemIsTrue) {
@@ -581,7 +586,7 @@
             // We always want to invoke face detect in the main thread.
             faceAuthLogger.faceDetectionStarted()
             faceManager?.detectFace(
-                detectCancellationSignal,
+                checkNotNull(detectCancellationSignal),
                 detectionCallback,
                 FaceAuthenticateOptions.Builder().setUserId(currentUserId).build()
             )
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 e35c369..42cd3a5 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
@@ -99,7 +99,16 @@
     /** Is an activity showing over the keyguard? */
     val isKeyguardOccluded: Flow<Boolean>
 
-    /** Observable for the signal that keyguard is about to go away. */
+    /**
+     * Observable for the signal that keyguard is about to go away.
+     *
+     * TODO(b/278086361): Remove once KEYGUARD_WM_STATE_REFACTOR flag is removed.
+     */
+    @Deprecated(
+        "Use KeyguardTransitionInteractor flows instead. The closest match for 'going " +
+            "away' is isInTransitionToState(GONE), but consider using more specific flows " +
+            "whenever possible."
+    )
     val isKeyguardGoingAway: Flow<Boolean>
 
     /** Is the always-on display available to be used? */
@@ -365,10 +374,11 @@
 
                 awaitClose { keyguardStateController.removeCallback(callback) }
             }
+            .distinctUntilChanged()
             .stateIn(
-                scope = scope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = keyguardStateController.isUnlocked,
+                scope,
+                SharingStarted.Eagerly,
+                initialValue = false,
             )
 
     override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
index 246ee33..2f80106 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -32,6 +32,11 @@
     @Binds fun keyguardRepository(impl: KeyguardRepositoryImpl): KeyguardRepository
 
     @Binds
+    fun keyguardSurfaceBehindRepository(
+        impl: KeyguardSurfaceBehindRepositoryImpl
+    ): KeyguardSurfaceBehindRepository
+
+    @Binds
     fun keyguardTransitionRepository(
         impl: KeyguardTransitionRepositoryImpl
     ): KeyguardTransitionRepository
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt
new file mode 100644
index 0000000..014b7fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * State related to SysUI's handling of the surface behind the keyguard (typically an app or the
+ * launcher). We manipulate this surface during unlock animations.
+ */
+interface KeyguardSurfaceBehindRepository {
+
+    /** Whether we're running animations on the surface. */
+    val isAnimatingSurface: Flow<Boolean>
+
+    /** Set whether we're running animations on the surface. */
+    fun setAnimatingSurface(animating: Boolean)
+}
+
+@SysUISingleton
+class KeyguardSurfaceBehindRepositoryImpl @Inject constructor() : KeyguardSurfaceBehindRepository {
+    private val _isAnimatingSurface = MutableStateFlow(false)
+    override val isAnimatingSurface = _isAnimatingSurface.asStateFlow()
+
+    override fun setAnimatingSurface(animating: Boolean) {
+        _isAnimatingSurface.value = animating
+    }
+}
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 6a2511f..1c0b73f 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
@@ -163,12 +163,13 @@
 
     private fun constructCircleRevealFromPoint(point: Point): LightRevealEffect {
         return with(point) {
+            val display = checkNotNull(context.display)
             CircleReveal(
                 x,
                 y,
                 startRadius = 0,
                 endRadius =
-                    max(max(x, context.display.width - x), max(y, context.display.height - y)),
+                    max(max(x, display.width - x), max(y, display.height - y)),
             )
         }
     }
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 8f0b91b..271bc38 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
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.domain.interactor
 
 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.keyguard.data.repository.KeyguardTransitionRepository
@@ -26,6 +25,7 @@
 import com.android.systemui.util.kotlin.Utils.Companion.toQuad
 import com.android.systemui.util.kotlin.Utils.Companion.toQuint
 import com.android.systemui.util.kotlin.sample
+import com.android.wm.shell.animation.Interpolators
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
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 6b28b27..aa771fd 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
@@ -20,8 +20,11 @@
 import com.android.app.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
 import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -34,7 +37,11 @@
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 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
 
 @SysUISingleton
@@ -45,6 +52,7 @@
     override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
+    private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
 ) :
     TransitionInteractor(
@@ -53,6 +61,7 @@
 
     override fun start() {
         listenForLockscreenToGone()
+        listenForLockscreenToGoneDragging()
         listenForLockscreenToOccluded()
         listenForLockscreenToCamera()
         listenForLockscreenToAodOrDozing()
@@ -62,6 +71,63 @@
         listenForLockscreenToAlternateBouncer()
     }
 
+    /**
+     * Whether we want the surface behind the keyguard visible for the transition from LOCKSCREEN,
+     * or null if we don't care and should just use a reasonable default.
+     *
+     * [KeyguardSurfaceBehindInteractor] will switch to this flow whenever a transition from
+     * LOCKSCREEN is running.
+     */
+    val surfaceBehindVisibility: Flow<Boolean?> =
+        transitionInteractor.startedKeyguardTransitionStep
+            .map { startedStep ->
+                if (startedStep.to != KeyguardState.GONE) {
+                    // LOCKSCREEN to anything but GONE does not require any special surface
+                    // visibility handling.
+                    return@map null
+                }
+
+                true // TODO(b/278086361): Implement continuous swipe to unlock.
+            }
+            .onStart {
+                // Default to null ("don't care, use a reasonable default").
+                emit(null)
+            }
+            .distinctUntilChanged()
+
+    /**
+     * The surface behind view params to use for the transition from LOCKSCREEN, or null if we don't
+     * care and should use a reasonable default.
+     */
+    val surfaceBehindModel: Flow<KeyguardSurfaceBehindModel?> =
+        combine(
+                transitionInteractor.startedKeyguardTransitionStep,
+                transitionInteractor.transitionStepsFromState(KeyguardState.LOCKSCREEN)
+            ) { startedStep, fromLockscreenStep ->
+                if (startedStep.to != KeyguardState.GONE) {
+                    // Only LOCKSCREEN -> GONE has specific surface params (for the unlock
+                    // animation).
+                    return@combine null
+                } else if (fromLockscreenStep.value > 0.5f) {
+                    // Start the animation once we're 50% transitioned to GONE.
+                    KeyguardSurfaceBehindModel(
+                        animateFromAlpha = 0f,
+                        alpha = 1f,
+                        animateFromTranslationY = 500f,
+                        translationY = 0f
+                    )
+                } else {
+                    KeyguardSurfaceBehindModel(
+                        alpha = 0f,
+                    )
+                }
+            }
+            .onStart {
+                // Default to null ("don't care, use a reasonable default").
+                emit(null)
+            }
+            .distinctUntilChanged()
+
     private fun listenForLockscreenToDreaming() {
         val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING)
         scope.launch {
@@ -169,7 +235,8 @@
                             }
 
                             // If canceled, just put the state back
-                            // TODO: This logic should happen in FromPrimaryBouncerInteractor.
+                            // TODO(b/278086361): This logic should happen in
+                            //  FromPrimaryBouncerInteractor.
                             if (nextState == TransitionState.CANCELED) {
                                 transitionRepository.startTransition(
                                     TransitionInfo(
@@ -201,7 +268,32 @@
         }
     }
 
+    fun dismissKeyguard() {
+        startTransitionTo(KeyguardState.GONE)
+    }
+
     private fun listenForLockscreenToGone() {
+        if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+            return
+        }
+
+        scope.launch {
+            keyguardInteractor.isKeyguardGoingAway
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { pair ->
+                    val (isKeyguardGoingAway, lastStartedStep) = pair
+                    if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+                        startTransitionTo(KeyguardState.GONE)
+                    }
+                }
+        }
+    }
+
+    private fun listenForLockscreenToGoneDragging() {
+        if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
                 .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
@@ -291,7 +383,7 @@
     }
 
     companion object {
-        private val DEFAULT_DURATION = 500.milliseconds
+        private val DEFAULT_DURATION = 400.milliseconds
         val TO_DREAMING_DURATION = 933.milliseconds
         val TO_OCCLUDED_DURATION = 450.milliseconds
     }
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 9142d1f..c9f32da 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
@@ -17,23 +17,28 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.animation.ValueAnimator
-import com.android.app.animation.Interpolators
 import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.util.kotlin.Utils.Companion.toQuad
 import com.android.systemui.util.kotlin.Utils.Companion.toQuint
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
+import com.android.wm.shell.animation.Interpolators
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -44,6 +49,7 @@
     override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
+    private val flags: FeatureFlags,
     private val keyguardSecurityModel: KeyguardSecurityModel,
 ) :
     TransitionInteractor(
@@ -57,6 +63,57 @@
         listenForPrimaryBouncerToDreamingLockscreenHosted()
     }
 
+    val surfaceBehindVisibility: Flow<Boolean?> =
+        combine(
+                transitionInteractor.startedKeyguardTransitionStep,
+                transitionInteractor.transitionStepsFromState(KeyguardState.PRIMARY_BOUNCER)
+            ) { startedStep, fromBouncerStep ->
+                if (startedStep.to != KeyguardState.GONE) {
+                    return@combine null
+                }
+
+                fromBouncerStep.value > 0.5f
+            }
+            .onStart {
+                // Default to null ("don't care, use a reasonable default").
+                emit(null)
+            }
+            .distinctUntilChanged()
+
+    val surfaceBehindModel: Flow<KeyguardSurfaceBehindModel?> =
+        combine(
+                transitionInteractor.startedKeyguardTransitionStep,
+                transitionInteractor.transitionStepsFromState(KeyguardState.PRIMARY_BOUNCER)
+            ) { startedStep, fromBouncerStep ->
+                if (startedStep.to != KeyguardState.GONE) {
+                    // BOUNCER to anything but GONE does not require any special surface
+                    // visibility handling.
+                    return@combine null
+                }
+
+                if (fromBouncerStep.value > 0.5f) {
+                    KeyguardSurfaceBehindModel(
+                        animateFromAlpha = 0f,
+                        alpha = 1f,
+                        animateFromTranslationY = 500f,
+                        translationY = 0f,
+                    )
+                } else {
+                    KeyguardSurfaceBehindModel(
+                        alpha = 0f,
+                    )
+                }
+            }
+            .onStart {
+                // Default to null ("don't care, use a reasonable default").
+                emit(null)
+            }
+            .distinctUntilChanged()
+
+    fun dismissPrimaryBouncer() {
+        startTransitionTo(KeyguardState.GONE)
+    }
+
     private fun listenForPrimaryBouncerToLockscreenOrOccluded() {
         scope.launch {
             keyguardInteractor.primaryBouncerShowing
@@ -124,28 +181,34 @@
     private fun listenForPrimaryBouncerToDreamingLockscreenHosted() {
         scope.launch {
             keyguardInteractor.primaryBouncerShowing
-                .sample(
-                    combine(
-                        keyguardInteractor.isActiveDreamLockscreenHosted,
-                        transitionInteractor.startedKeyguardTransitionStep,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect {
-                    (isBouncerShowing, isActiveDreamLockscreenHosted, lastStartedTransitionStep) ->
-                    if (
-                        !isBouncerShowing &&
-                            isActiveDreamLockscreenHosted &&
-                            lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER
-                    ) {
-                        startTransitionTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
+                    .sample(
+                            combine(
+                                    keyguardInteractor.isActiveDreamLockscreenHosted,
+                                    transitionInteractor.startedKeyguardTransitionStep,
+                                    ::Pair
+                            ),
+                            ::toTriple
+                    )
+                    .collect { (isBouncerShowing, isActiveDreamLockscreenHosted, lastStartedTransitionStep) ->
+                        if (
+                                !isBouncerShowing &&
+                                isActiveDreamLockscreenHosted &&
+                                lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER
+                        ) {
+                            startTransitionTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
+                        }
                     }
-                }
         }
     }
 
     private fun listenForPrimaryBouncerToGone() {
+        if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+            // This is handled in KeyguardSecurityContainerController and
+            // StatusBarKeyguardViewManager, which calls the transition interactor to kick off a
+            // transition vs. listening to legacy state flags.
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
                 .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
@@ -160,7 +223,7 @@
                             )
                         // IME for password requires a slightly faster animation
                         val duration =
-                            if (securityMode == Password) {
+                            if (securityMode == KeyguardSecurityModel.SecurityMode.Password) {
                                 TO_GONE_SHORT_DURATION
                             } else {
                                 TO_GONE_DURATION
@@ -188,7 +251,7 @@
 
     companion object {
         private val DEFAULT_DURATION = 300.milliseconds
-        val TO_GONE_DURATION = 250.milliseconds
+        val TO_GONE_DURATION = 500.milliseconds
         val TO_GONE_SHORT_DURATION = 200.milliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 53d3c07..562c4db 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -34,12 +34,12 @@
 import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
 import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.ScreenModel
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.util.kotlin.sample
-import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
@@ -53,6 +53,7 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
+import javax.inject.Inject
 
 /**
  * Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -264,7 +265,26 @@
         repository.setAnimateDozingTransitions(animate)
     }
 
+    fun isKeyguardDismissable(): Boolean {
+        return repository.isKeyguardUnlocked.value
+    }
+
     companion object {
         private const val TAG = "KeyguardInteractor"
+
+        fun isKeyguardVisibleInState(state: KeyguardState): Boolean {
+            return when (state) {
+                KeyguardState.OFF -> true
+                KeyguardState.DOZING -> true
+                KeyguardState.DREAMING -> true
+                KeyguardState.AOD -> true
+                KeyguardState.ALTERNATE_BOUNCER -> true
+                KeyguardState.PRIMARY_BOUNCER -> true
+                KeyguardState.LOCKSCREEN -> true
+                KeyguardState.GONE -> false
+                KeyguardState.OCCLUDED -> true
+                KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> false
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
new file mode 100644
index 0000000..bf04f8f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardSurfaceBehindRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+@SysUISingleton
+class KeyguardSurfaceBehindInteractor
+@Inject
+constructor(
+    private val repository: KeyguardSurfaceBehindRepository,
+    private val fromLockscreenInteractor: FromLockscreenTransitionInteractor,
+    private val fromPrimaryBouncerInteractor: FromPrimaryBouncerTransitionInteractor,
+    transitionInteractor: KeyguardTransitionInteractor,
+) {
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val viewParams: Flow<KeyguardSurfaceBehindModel> =
+        transitionInteractor.isInTransitionToAnyState
+            .flatMapLatest { isInTransition ->
+                if (!isInTransition) {
+                    defaultParams
+                } else {
+                    combine(
+                        transitionSpecificViewParams,
+                        defaultParams,
+                    ) { transitionParams, defaultParams ->
+                        transitionParams ?: defaultParams
+                    }
+                }
+            }
+
+    val isAnimatingSurface = repository.isAnimatingSurface
+
+    private val defaultParams =
+        transitionInteractor.finishedKeyguardState.map { state ->
+            KeyguardSurfaceBehindModel(
+                alpha =
+                    if (WindowManagerLockscreenVisibilityInteractor.isSurfaceVisible(state)) 1f
+                    else 0f
+            )
+        }
+
+    /**
+     * View params provided by the transition interactor for the most recently STARTED transition.
+     * This is used to run transition-specific animations on the surface.
+     *
+     * If null, there are no transition-specific view params needed for this transition and we will
+     * use a reasonable default.
+     */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val transitionSpecificViewParams: Flow<KeyguardSurfaceBehindModel?> =
+        transitionInteractor.startedKeyguardTransitionStep.flatMapLatest { startedStep ->
+            when (startedStep.from) {
+                KeyguardState.LOCKSCREEN -> fromLockscreenInteractor.surfaceBehindModel
+                KeyguardState.PRIMARY_BOUNCER -> fromPrimaryBouncerInteractor.surfaceBehindModel
+                // Return null for other states, where no transition specific params are needed.
+                else -> flowOf(null)
+            }
+        }
+
+    fun setAnimatingSurface(animating: Boolean) {
+        repository.setAnimatingSurface(animating)
+    }
+}
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 8c4c7ae..9382618 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
@@ -17,17 +17,20 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 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.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -36,6 +39,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
@@ -46,8 +50,12 @@
 class KeyguardTransitionInteractor
 @Inject
 constructor(
-    private val repository: KeyguardTransitionRepository,
     @Application val scope: CoroutineScope,
+    private val repository: KeyguardTransitionRepository,
+    private val keyguardInteractor: dagger.Lazy<KeyguardInteractor>,
+    private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>,
+    private val fromPrimaryBouncerTransitionInteractor:
+        dagger.Lazy<FromPrimaryBouncerTransitionInteractor>,
 ) {
     private val TAG = this::class.simpleName
 
@@ -128,12 +136,11 @@
         repository.transition(PRIMARY_BOUNCER, GONE)
 
     /** OFF->LOCKSCREEN transition information. */
-    val offToLockscreenTransition: Flow<TransitionStep> =
-        repository.transition(KeyguardState.OFF, LOCKSCREEN)
+    val offToLockscreenTransition: Flow<TransitionStep> = repository.transition(OFF, LOCKSCREEN)
 
     /** DOZING->LOCKSCREEN transition information. */
     val dozingToLockscreenTransition: Flow<TransitionStep> =
-        repository.transition(KeyguardState.DOZING, LOCKSCREEN)
+        repository.transition(DOZING, LOCKSCREEN)
 
     /**
      * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
@@ -157,17 +164,30 @@
     val finishedKeyguardTransitionStep: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
 
-    /** The destination state of the last started transition */
+    /** The destination state of the last started transition. */
     val startedKeyguardState: StateFlow<KeyguardState> =
         startedKeyguardTransitionStep
             .map { step -> step.to }
-            .stateIn(scope, SharingStarted.Eagerly, KeyguardState.OFF)
+            .stateIn(scope, SharingStarted.Eagerly, OFF)
 
     /** The last completed [KeyguardState] transition */
     val finishedKeyguardState: StateFlow<KeyguardState> =
         finishedKeyguardTransitionStep
             .map { step -> step.to }
             .stateIn(scope, SharingStarted.Eagerly, LOCKSCREEN)
+
+    /**
+     * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed
+     * it.
+     */
+    val isInTransitionToAnyState =
+            combine(
+                    startedKeyguardTransitionStep,
+                    finishedKeyguardState,
+            ) { startedStep, finishedState ->
+                startedStep.to != finishedState
+            }
+
     /**
      * The amount of transition into or out of the given [KeyguardState].
      *
@@ -187,4 +207,41 @@
                 }
             }
     }
+
+    fun transitionStepsFromState(fromState: KeyguardState): Flow<TransitionStep> {
+        return repository.transitions.filter { step -> step.from == fromState }
+    }
+
+    fun transitionStepsToState(toState: KeyguardState): Flow<TransitionStep> {
+        return repository.transitions.filter { step -> step.to == toState }
+    }
+
+    /**
+     * Called to start a transition that will ultimately dismiss the keyguard from the current
+     * state.
+     */
+    fun startDismissKeyguardTransition() {
+        when (startedKeyguardState.value) {
+            LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
+            PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer()
+            else ->
+                Log.e(
+                    "KeyguardTransitionInteractor",
+                    "We don't know how to dismiss keyguard from state " +
+                        "${startedKeyguardState.value}"
+                )
+        }
+    }
+
+    /** Whether we're in a transition to the given [KeyguardState], but haven't yet completed it. */
+    fun isInTransitionToState(
+            state: KeyguardState,
+    ): Flow<Boolean> {
+        return combine(
+                startedKeyguardTransitionStep,
+                finishedKeyguardState,
+        ) { startedStep, finishedState ->
+            startedStep.to == state && finishedState != state
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
new file mode 100644
index 0000000..96bfdc6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -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.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+@SysUISingleton
+class WindowManagerLockscreenVisibilityInteractor
+@Inject
+constructor(
+    keyguardInteractor: KeyguardInteractor,
+    transitionInteractor: KeyguardTransitionInteractor,
+    surfaceBehindInteractor: KeyguardSurfaceBehindInteractor,
+    fromLockscreenInteractor: FromLockscreenTransitionInteractor,
+    fromBouncerInteractor: FromPrimaryBouncerTransitionInteractor,
+) {
+    private val defaultSurfaceBehindVisibility =
+        transitionInteractor.finishedKeyguardState.map(::isSurfaceVisible)
+
+    /**
+     * Surface visibility provided by the From*TransitionInteractor responsible for the currently
+     * RUNNING transition, or null if the current transition does not require special surface
+     * visibility handling.
+     *
+     * An example of transition-specific visibility is swipe to unlock, where the surface should
+     * only be visible after swiping 20% of the way up the screen, and should become invisible again
+     * if the user swipes back down.
+     */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val transitionSpecificSurfaceBehindVisibility: Flow<Boolean?> =
+        transitionInteractor.startedKeyguardTransitionStep
+            .flatMapLatest { startedStep ->
+                when (startedStep.from) {
+                    KeyguardState.LOCKSCREEN -> {
+                        fromLockscreenInteractor.surfaceBehindVisibility
+                    }
+                    KeyguardState.PRIMARY_BOUNCER -> {
+                        fromBouncerInteractor.surfaceBehindVisibility
+                    }
+                    else -> flowOf(null)
+                }
+            }
+            .distinctUntilChanged()
+
+    /**
+     * Surface visibility, which is either determined by the default visibility in the FINISHED
+     * KeyguardState, or the transition-specific visibility used during certain RUNNING transitions.
+     */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val surfaceBehindVisibility: Flow<Boolean> =
+        transitionInteractor
+                .isInTransitionToAnyState
+            .flatMapLatest { isInTransition ->
+                if (!isInTransition) {
+                    defaultSurfaceBehindVisibility
+                } else {
+                    combine(
+                        transitionSpecificSurfaceBehindVisibility,
+                        defaultSurfaceBehindVisibility,
+                    ) { transitionVisibility, defaultVisibility ->
+                        // Defer to the transition-specific visibility since we're RUNNING a
+                        // transition, but fall back to the default visibility if the current
+                        // transition's interactor did not specify a visibility.
+                        transitionVisibility ?: defaultVisibility
+                    }
+                }
+            }
+            .distinctUntilChanged()
+
+    /**
+     * Whether we're animating, or intend to animate, the surface behind the keyguard via remote
+     * animation. This is used to keep the RemoteAnimationTarget alive until we're done using it.
+     */
+    val usingKeyguardGoingAwayAnimation: Flow<Boolean> =
+        combine(
+                transitionInteractor.isInTransitionToState(KeyguardState.GONE),
+                transitionInteractor.finishedKeyguardState,
+                surfaceBehindInteractor.isAnimatingSurface
+            ) { isInTransitionToGone, finishedState, isAnimatingSurface ->
+                // We may still be animating the surface after the keyguard is fully GONE, since
+                // some animations (like the translation spring) are not tied directly to the
+                // transition step amount.
+                isInTransitionToGone || (finishedState == KeyguardState.GONE && isAnimatingSurface)
+            }
+            .distinctUntilChanged()
+
+    /**
+     * Whether the lockscreen is visible, from the Window Manager (WM) perspective.
+     *
+     * Note: This may briefly be true even if the lockscreen UI has animated out (alpha = 0f), as we
+     * only inform WM once we're done with the keyguard and we're fully GONE. Don't use this if you
+     * want to know if the AOD/clock/notifs/etc. are visible.
+     */
+    val lockscreenVisibility: Flow<Boolean> =
+        combine(
+                transitionInteractor.startedKeyguardTransitionStep,
+                transitionInteractor.finishedKeyguardState,
+            ) { startedStep, finishedState ->
+                // If we finished the transition, use the finished state. If we're running a
+                // transition, use the state we're transitioning FROM. This can be different from
+                // the last finished state if a transition is interrupted. For example, if we were
+                // transitioning from GONE to AOD and then started AOD -> LOCKSCREEN mid-transition,
+                // we want to immediately use the visibility for AOD (lockscreenVisibility=true)
+                // even though the lastFinishedState is still GONE (lockscreenVisibility=false).
+                if (finishedState == startedStep.to) finishedState else startedStep.from
+            }
+            .map(::isLockscreenVisible)
+            .distinctUntilChanged()
+
+    /**
+     * Whether always-on-display (AOD) is visible when the lockscreen is visible, from window
+     * manager's perspective.
+     *
+     * Note: This may be true even if AOD is not user-visible, such as when the light sensor
+     * indicates the device is in the user's pocket. Don't use this if you want to know if the AOD
+     * clock/smartspace/notif icons are visible.
+     */
+    val aodVisibility: Flow<Boolean> =
+        combine(
+                keyguardInteractor.isDozing,
+                keyguardInteractor.biometricUnlockState,
+            ) { isDozing, biometricUnlockState ->
+                // AOD is visible if we're dozing, unless we are wake and unlocking (where we go
+                // directly from AOD to unlocked while dozing).
+                isDozing && !BiometricUnlockModel.isWakeAndUnlock(biometricUnlockState)
+            }
+            .distinctUntilChanged()
+
+    companion object {
+        fun isSurfaceVisible(state: KeyguardState): Boolean {
+            return !isLockscreenVisible(state)
+        }
+
+        fun isLockscreenVisible(state: KeyguardState): Boolean {
+            return state != KeyguardState.GONE
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt
new file mode 100644
index 0000000..7fb5cfd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.shared.model
+
+/**
+ * Models the appearance of the surface behind the keyguard, and (optionally) how it should be
+ * animating.
+ *
+ * This is intended to be an atomic, high-level description of the surface's appearance and related
+ * animations, which we can derive from the STARTED/FINISHED transition states rather than the
+ * individual TransitionSteps.
+ *
+ * For example, if we're transitioning from LOCKSCREEN to GONE, that means we should be
+ * animatingFromAlpha 0f -> 1f and animatingFromTranslationY 500f -> 0f.
+ * KeyguardSurfaceBehindAnimator can decide how best to implement this, depending on previously
+ * running animations, spring momentum, and other state.
+ */
+data class KeyguardSurfaceBehindModel(
+    val alpha: Float = 1f,
+
+    /**
+     * If provided, animate from this value to [alpha] unless an animation is already running, in
+     * which case we'll animate from the current value to [alpha].
+     */
+    val animateFromAlpha: Float = alpha,
+    val translationY: Float = 0f,
+
+    /**
+     * If provided, animate from this value to [translationY] unless an animation is already
+     * running, in which case we'll animate from the current value to [translationY].
+     */
+    val animateFromTranslationY: Float = translationY,
+) {
+    fun willAnimateAlpha(): Boolean {
+        return animateFromAlpha != alpha
+    }
+
+    fun willAnimateTranslationY(): Boolean {
+        return animateFromTranslationY != translationY
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 0ead8c4..44acf4f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -472,7 +472,7 @@
             return true
         }
 
-        override fun onLongClickUseDefaultHapticFeedback(view: View?) = false
+        override fun onLongClickUseDefaultHapticFeedback(view: View) = false
     }
 
     @Deprecated("Deprecated as part of b/278057014")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index 63a6791..83b5463 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -304,7 +304,7 @@
             return true
         }
 
-        override fun onLongClickUseDefaultHapticFeedback(view: View?) = false
+        override fun onLongClickUseDefaultHapticFeedback(view: View) = false
 
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
index 162c109..82610e6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
@@ -43,7 +43,7 @@
         vibratorHelper: VibratorHelper,
         activityStarter: ActivityStarter
     ): DisposableHandle {
-        val view = parentView.findViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button)
+        val view = parentView.requireViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button)
 
         val disposableHandle =
             view.repeatWhenAttached {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
new file mode 100644
index 0000000..fe2df21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.graphics.Matrix
+import android.util.Log
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.SyncRtSurfaceTransactionApplier
+import android.view.View
+import androidx.dynamicanimation.animation.FloatValueHolder
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.TAG
+import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
+import com.android.wm.shell.animation.Interpolators
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Applies [KeyguardSurfaceBehindViewParams] to a RemoteAnimationTarget, starting and managing
+ * animations as needed.
+ */
+@SysUISingleton
+class KeyguardSurfaceBehindParamsApplier
+@Inject
+constructor(
+    @Main private val executor: Executor,
+    private val keyguardViewController: KeyguardViewController,
+    private val interactor: KeyguardSurfaceBehindInteractor,
+) {
+    private var surfaceBehind: RemoteAnimationTarget? = null
+    private val surfaceTransactionApplier: SyncRtSurfaceTransactionApplier
+        get() = SyncRtSurfaceTransactionApplier(keyguardViewController.viewRootImpl.view)
+
+    private val matrix = Matrix()
+    private val tmpFloat = FloatArray(9)
+
+    private var animatedTranslationY = FloatValueHolder()
+    private val translateYSpring =
+        SpringAnimation(animatedTranslationY).apply {
+            spring =
+                SpringForce().apply {
+                    stiffness = 200f
+                    dampingRatio = 1f
+                }
+            addUpdateListener { _, _, _ -> applyToSurfaceBehind() }
+            addEndListener { _, _, _, _ ->
+                try {
+                    updateIsAnimatingSurface()
+                } catch (e: NullPointerException) {
+                    // TODO(b/291645410): Remove when we can isolate DynamicAnimations.
+                    e.printStackTrace()
+                }
+            }
+        }
+
+    private var animatedAlpha = 0f
+    private var alphaAnimator =
+        ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = 500
+            interpolator = Interpolators.ALPHA_IN
+            addUpdateListener {
+                animatedAlpha = it.animatedValue as Float
+                applyToSurfaceBehind()
+            }
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator?) {
+                        updateIsAnimatingSurface()
+                    }
+                }
+            )
+        }
+
+    /**
+     * ViewParams to apply to the surface provided to [applyParamsToSurface]. If the surface is null
+     * these will be applied once someone gives us a surface via [applyParamsToSurface].
+     */
+    var viewParams: KeyguardSurfaceBehindModel = KeyguardSurfaceBehindModel()
+        set(newParams) {
+            field = newParams
+            startOrUpdateAnimators()
+            applyToSurfaceBehind()
+        }
+
+    /**
+     * Provides us with a surface to animate. We'll apply the [viewParams] to this surface and start
+     * any necessary animations.
+     */
+    fun applyParamsToSurface(surface: RemoteAnimationTarget) {
+        this.surfaceBehind = surface
+        startOrUpdateAnimators()
+    }
+
+    /**
+     * Notifies us that the [RemoteAnimationTarget] has been released, one way or another.
+     * Attempting to animate a released target will cause a crash.
+     *
+     * This can be called either because we finished animating the surface naturally, or by WM
+     * because external factors cancelled the remote animation (timeout, re-lock, etc). If it's the
+     * latter, cancel any outstanding animations we have.
+     */
+    fun notifySurfaceReleased() {
+        surfaceBehind = null
+
+        if (alphaAnimator.isRunning) {
+            alphaAnimator.cancel()
+        }
+
+        if (translateYSpring.isRunning) {
+            translateYSpring.cancel()
+        }
+    }
+
+    private fun startOrUpdateAnimators() {
+        if (surfaceBehind == null) {
+            return
+        }
+
+        if (viewParams.willAnimateAlpha()) {
+            var fromAlpha = viewParams.animateFromAlpha
+
+            if (alphaAnimator.isRunning) {
+                alphaAnimator.cancel()
+                fromAlpha = animatedAlpha
+            }
+
+            alphaAnimator.setFloatValues(fromAlpha, viewParams.alpha)
+            alphaAnimator.start()
+        }
+
+        if (viewParams.willAnimateTranslationY()) {
+            if (!translateYSpring.isRunning) {
+                // If the spring isn't running yet, set the start value. Otherwise, respect the
+                // current position.
+                animatedTranslationY.value = viewParams.animateFromTranslationY
+            }
+
+            translateYSpring.animateToFinalPosition(viewParams.translationY)
+        }
+
+        updateIsAnimatingSurface()
+    }
+
+    private fun updateIsAnimatingSurface() {
+        interactor.setAnimatingSurface(translateYSpring.isRunning || alphaAnimator.isRunning)
+    }
+
+    private fun applyToSurfaceBehind() {
+        surfaceBehind?.leash?.let { sc ->
+            executor.execute {
+                if (surfaceBehind == null) {
+                    Log.d(
+                        TAG,
+                        "Attempting to modify params of surface that isn't " +
+                            "animating. Ignoring."
+                    )
+                    matrix.set(Matrix.IDENTITY_MATRIX)
+                    return@execute
+                }
+
+                val translationY =
+                    if (translateYSpring.isRunning) animatedTranslationY.value
+                    else viewParams.translationY
+
+                val alpha =
+                    if (alphaAnimator.isRunning) {
+                        animatedAlpha
+                    } else {
+                        viewParams.alpha
+                    }
+
+                if (
+                    keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE &&
+                        sc.isValid
+                ) {
+                    with(SurfaceControl.Transaction()) {
+                        setMatrix(
+                            sc,
+                            matrix.apply { setTranslate(/* dx= */ 0f, translationY) },
+                            tmpFloat
+                        )
+                        setAlpha(sc, alpha)
+                        apply()
+                    }
+                } else {
+                    surfaceTransactionApplier.scheduleApply(
+                        SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(sc)
+                            .withMatrix(matrix.apply { setTranslate(/* dx= */ 0f, translationY) })
+                            .withAlpha(alpha)
+                            .build()
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindViewBinder.kt
new file mode 100644
index 0000000..599f69f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindViewBinder.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.keyguard.ui.binder
+
+import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSurfaceBehindViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Binds the [WindowManagerLockscreenVisibilityManager] "view", which manages the visibility of the
+ * surface behind the keyguard.
+ */
+object KeyguardSurfaceBehindViewBinder {
+    @JvmStatic
+    fun bind(
+        viewModel: KeyguardSurfaceBehindViewModel,
+        applier: KeyguardSurfaceBehindParamsApplier,
+        scope: CoroutineScope
+    ) {
+        scope.launch { viewModel.surfaceBehindViewParams.collect { applier.viewParams = it } }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
index b568a9a..3bb01f2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
@@ -42,13 +42,13 @@
         view.accessibilityDelegate = viewModel.accessibilityDelegate
 
         // bind child views
-        UdfpsAodFingerprintViewBinder.bind(view.findViewById(R.id.udfps_aod_fp), aodViewModel)
+        UdfpsAodFingerprintViewBinder.bind(view.requireViewById(R.id.udfps_aod_fp), aodViewModel)
         UdfpsFingerprintViewBinder.bind(
-            view.findViewById(R.id.udfps_lockscreen_fp),
+            view.requireViewById(R.id.udfps_lockscreen_fp),
             fingerprintViewModel
         )
         UdfpsBackgroundViewBinder.bind(
-            view.findViewById(R.id.udfps_keyguard_fp_bg),
+            view.requireViewById(R.id.udfps_keyguard_fp_bg),
             backgroundViewModel
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityViewBinder.kt
new file mode 100644
index 0000000..fc0c78a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityViewBinder.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager
+import com.android.systemui.keyguard.ui.viewmodel.WindowManagerLockscreenVisibilityViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Binds the [WindowManagerLockscreenVisibilityManager] "view", which manages the visibility of the
+ * surface behind the keyguard.
+ */
+object WindowManagerLockscreenVisibilityViewBinder {
+    @JvmStatic
+    fun bind(
+        viewModel: WindowManagerLockscreenVisibilityViewModel,
+        lockscreenVisibilityManager: WindowManagerLockscreenVisibilityManager,
+        scope: CoroutineScope
+    ) {
+        scope.launch {
+            viewModel.surfaceBehindVisibility.collect {
+                lockscreenVisibilityManager.setSurfaceBehindVisibility(it)
+            }
+        }
+
+        scope.launch {
+            viewModel.lockscreenVisibility.collect {
+                lockscreenVisibilityManager.setLockscreenShown(it)
+            }
+        }
+
+        scope.launch {
+            viewModel.aodVisibility.collect { lockscreenVisibilityManager.setAodVisible(it) }
+        }
+
+        scope.launch {
+            viewModel.surfaceBehindAnimating.collect {
+                lockscreenVisibilityManager.setUsingGoingAwayRemoteAnimation(it)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 58bc552..580db35 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -117,7 +117,7 @@
     private var host: SurfaceControlViewHost
 
     val surfacePackage: SurfaceControlViewHost.SurfacePackage
-        get() = host.surfacePackage
+        get() = checkNotNull(host.surfacePackage)
 
     private lateinit var largeClockHostView: FrameLayout
     private lateinit var smallClockHostView: FrameLayout
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSurfaceBehindViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSurfaceBehindViewModel.kt
new file mode 100644
index 0000000..4f52962
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSurfaceBehindViewModel.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.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor
+import javax.inject.Inject
+
+@SysUISingleton
+class KeyguardSurfaceBehindViewModel
+@Inject
+constructor(interactor: KeyguardSurfaceBehindInteractor) {
+    val surfaceBehindViewParams = interactor.viewParams
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/WindowManagerLockscreenVisibilityViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/WindowManagerLockscreenVisibilityViewModel.kt
new file mode 100644
index 0000000..f797640
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/WindowManagerLockscreenVisibilityViewModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor
+import javax.inject.Inject
+
+@SysUISingleton
+class WindowManagerLockscreenVisibilityViewModel
+@Inject
+constructor(interactor: WindowManagerLockscreenVisibilityInteractor) {
+    val surfaceBehindVisibility = interactor.surfaceBehindVisibility
+    val surfaceBehindAnimating = interactor.usingKeyguardGoingAwayAnimation
+    val lockscreenVisibility = interactor.lockscreenVisibility
+    val aodVisibility = interactor.aodVisibility
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index e064839..5f7991e 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -70,7 +70,7 @@
     var lifecycleOwner: ViewLifecycleOwner? = null
     val onAttachListener =
         object : View.OnAttachStateChangeListener {
-            override fun onViewAttachedToWindow(v: View?) {
+            override fun onViewAttachedToWindow(v: View) {
                 Assert.isMainThread()
                 lifecycleOwner?.onDestroy()
                 lifecycleOwner =
@@ -81,7 +81,7 @@
                     )
             }
 
-            override fun onViewDetachedFromWindow(v: View?) {
+            override fun onViewDetachedFromWindow(v: View) {
                 lifecycleOwner?.onDestroy()
                 lifecycleOwner = null
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index 35f5a8c..a91917a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -514,7 +514,7 @@
          * Returns true when the down event of the scroll hits within the target box of the thumb.
          */
         override fun onScroll(
-            eventStart: MotionEvent,
+            eventStart: MotionEvent?,
             event: MotionEvent,
             distanceX: Float,
             distanceY: Float
@@ -528,7 +528,7 @@
          * Gestures that include a fling are considered a false gesture on the seek bar.
          */
         override fun onFling(
-            eventStart: MotionEvent,
+            eventStart: MotionEvent?,
             event: MotionEvent,
             velocityX: Float,
             velocityY: Float
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
index 207df6b..a1291a4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
@@ -149,11 +149,7 @@
         // Check if smartspace has explicitly specified whether to re-activate resumable media.
         // The default behavior is to trigger if the smartspace data is active.
         val shouldTriggerResume =
-            if (data.cardAction?.extras?.containsKey(EXTRA_KEY_TRIGGER_RESUME) == true) {
-                data.cardAction.extras.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true)
-            } else {
-                true
-            }
+            data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true
         val shouldReactivate =
             shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive
 
@@ -269,9 +265,7 @@
                     "Cannot create dismiss action click action: extras missing dismiss_intent."
                 )
             } else if (
-                dismissIntent.getComponent() != null &&
-                    dismissIntent.getComponent().getClassName() ==
-                        EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
+                dismissIntent.component?.className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
             ) {
                 // Dismiss the card Smartspace data through Smartspace trampoline activity.
                 context.startActivity(dismissIntent)
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 576eb9e..282a65a 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
@@ -22,6 +22,7 @@
 import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
 import android.app.PendingIntent
 import android.app.StatusBarManager
+import android.app.smartspace.SmartspaceAction
 import android.app.smartspace.SmartspaceConfig
 import android.app.smartspace.SmartspaceManager
 import android.app.smartspace.SmartspaceSession
@@ -1623,20 +1624,18 @@
      *   SmartspaceTarget's data is invalid.
      */
     private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
-        var dismissIntent: Intent? = null
-        if (target.baseAction != null && target.baseAction.extras != null) {
-            dismissIntent =
-                target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY)
-                    as Intent?
-        }
+        val baseAction: SmartspaceAction? = target.baseAction
+        val dismissIntent =
+            baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
 
         val isActive =
             when {
                 !mediaFlags.isPersistentSsCardEnabled() -> true
-                target.baseAction == null -> true
-                else ->
-                    target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) !=
-                        EXTRA_VALUE_TRIGGER_PERIODIC
+                baseAction == null -> true
+                else -> {
+                    val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
+                    triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
+                }
             }
 
         packageName(target)?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
index d6f941d..6a8ffb7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
@@ -65,7 +65,7 @@
 
     private val sessionListener =
         object : MediaSessionManager.OnActiveSessionsChangedListener {
-            override fun onActiveSessionsChanged(controllers: List<MediaController>) {
+            override fun onActiveSessionsChanged(controllers: List<MediaController>?) {
                 handleControllersChanged(controllers)
             }
         }
@@ -190,16 +190,18 @@
         }
     }
 
-    private fun handleControllersChanged(controllers: List<MediaController>) {
+    private fun handleControllersChanged(controllers: List<MediaController>?) {
         packageControllers.clear()
-        controllers.forEach { controller ->
+        controllers?.forEach { controller ->
             packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) }
                 ?: run {
                     val tokens = mutableListOf(controller)
                     packageControllers.put(controller.packageName, tokens)
                 }
         }
-        tokensWithNotifications.retainAll(controllers.map { TokenId(it.sessionToken) })
+        controllers?.map { TokenId(it.sessionToken) }?.let {
+            tokensWithNotifications.retainAll(it)
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
index b46ebb2..b9cc772 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
@@ -195,7 +195,7 @@
                 }
                 addListener(
                     object : AnimatorListenerAdapter() {
-                        override fun onAnimationEnd(animation: Animator?) {
+                        override fun onAnimationEnd(animation: Animator) {
                             backgroundAnimation = null
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
index 937a618..646d1d0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
@@ -98,11 +98,11 @@
                         addListener(
                             object : AnimatorListenerAdapter() {
                                 var cancelled = false
-                                override fun onAnimationCancel(animation: Animator?) {
+                                override fun onAnimationCancel(animation: Animator) {
                                     cancelled = true
                                 }
 
-                                override fun onAnimationEnd(animation: Animator?) {
+                                override fun onAnimationEnd(animation: Animator) {
                                     if (cancelled) {
                                         return
                                     }
@@ -226,7 +226,7 @@
                 )
                 addListener(
                     object : AnimatorListenerAdapter() {
-                        override fun onAnimationEnd(animation: Animator?) {
+                        override fun onAnimationEnd(animation: Animator) {
                             rippleData.progress = 0f
                             rippleAnimation = null
                             invalidateSelf()
@@ -270,11 +270,8 @@
         return bounds
     }
 
-    override fun onStateChange(stateSet: IntArray?): Boolean {
+    override fun onStateChange(stateSet: IntArray): Boolean {
         val changed = super.onStateChange(stateSet)
-        if (stateSet == null) {
-            return changed
-        }
 
         val wasPressed = pressed
         var enabled = false
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
index 1ace316..ce50a11 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
@@ -127,19 +127,19 @@
         object : GestureDetector.SimpleOnGestureListener() {
             override fun onFling(
                 eStart: MotionEvent?,
-                eCurrent: MotionEvent?,
+                eCurrent: MotionEvent,
                 vX: Float,
                 vY: Float
             ) = onFling(vX, vY)
 
             override fun onScroll(
                 down: MotionEvent?,
-                lastMotion: MotionEvent?,
+                lastMotion: MotionEvent,
                 distanceX: Float,
                 distanceY: Float
-            ) = onScroll(down!!, lastMotion!!, distanceX)
+            ) = onScroll(down!!, lastMotion, distanceX)
 
-            override fun onDown(e: MotionEvent?): Boolean {
+            override fun onDown(e: MotionEvent): Boolean {
                 if (falsingProtectionNeeded) {
                     falsingCollector.onNotificationStartDismissing()
                 }
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 fe8ebaf..c1c757e 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
@@ -180,20 +180,20 @@
                 object : AnimatorListenerAdapter() {
                     private var cancelled: Boolean = false
 
-                    override fun onAnimationCancel(animation: Animator?) {
+                    override fun onAnimationCancel(animation: Animator) {
                         cancelled = true
                         animationPending = false
                         rootView?.removeCallbacks(startAnimation)
                     }
 
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         isCrossFadeAnimatorRunning = false
                         if (!cancelled) {
                             applyTargetStateIfNotAnimating()
                         }
                     }
 
-                    override fun onAnimationStart(animation: Animator?) {
+                    override fun onAnimationStart(animation: Animator) {
                         cancelled = false
                         animationPending = false
                     }
@@ -606,7 +606,7 @@
         val viewHost = UniqueObjectHostView(context)
         viewHost.addOnAttachStateChangeListener(
             object : View.OnAttachStateChangeListener {
-                override fun onViewAttachedToWindow(p0: View?) {
+                override fun onViewAttachedToWindow(p0: View) {
                     if (rootOverlay == null) {
                         rootView = viewHost.viewRootImpl.view
                         rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
@@ -614,7 +614,7 @@
                     viewHost.removeOnAttachStateChangeListener(this)
                 }
 
-                override fun onViewDetachedFromWindow(p0: View?) {}
+                override fun onViewDetachedFromWindow(p0: View) {}
             }
         )
         return viewHost
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
index be570b4..631a0b8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
@@ -144,12 +144,12 @@
         setListeningToMediaData(true)
         hostView.addOnAttachStateChangeListener(
             object : OnAttachStateChangeListener {
-                override fun onViewAttachedToWindow(v: View?) {
+                override fun onViewAttachedToWindow(v: View) {
                     setListeningToMediaData(true)
                     updateViewVisibility()
                 }
 
-                override fun onViewDetachedFromWindow(v: View?) {
+                override fun onViewDetachedFromWindow(v: View) {
                     setListeningToMediaData(false)
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
index 583c626..16dfc21 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
@@ -117,7 +117,7 @@
                     }
                     addListener(
                         object : AnimatorListenerAdapter() {
-                            override fun onAnimationEnd(animation: Animator?) {
+                            override fun onAnimationEnd(animation: Animator) {
                                 heightAnimator = null
                             }
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index bbd3d33..da8e106 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -201,13 +201,13 @@
     }
 
     override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) {
-        val packageName = newInfo.routeInfo.clientPackageName
+        val packageName: String? = newInfo.routeInfo.clientPackageName
         var iconInfo = MediaTttUtils.getIconInfoFromPackageName(
             context,
             packageName,
             isReceiver = true,
         ) {
-            logger.logPackageNotFound(packageName)
+            packageName?.let { logger.logPackageNotFound(it) }
         }
 
         if (newInfo.appNameOverride != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
index 5013802..fbf7e25 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
@@ -68,9 +68,9 @@
         )
         rippleView.addOnAttachStateChangeListener(
             object : View.OnAttachStateChangeListener {
-                override fun onViewDetachedFromWindow(view: View?) {}
+                override fun onViewDetachedFromWindow(view: View) {}
 
-                override fun onViewAttachedToWindow(view: View?) {
+                override fun onViewAttachedToWindow(view: View) {
                     if (view == null) {
                         return
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 0b0535d..35018f1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -54,7 +54,7 @@
         // Reset all listeners to animator.
         animator.removeAllListeners()
         animator.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator?) {
+            override fun onAnimationEnd(animation: Animator) {
                 onAnimationEnd?.run()
                 isStarted = false
             }
@@ -86,7 +86,7 @@
             invalidate()
         }
         animator.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator?) {
+            override fun onAnimationEnd(animation: Animator) {
                 animation?.let { visibility = GONE }
                 onAnimationEnd?.run()
                 isStarted = false
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index f75f8b9..87d0098 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -162,7 +162,7 @@
         logger: MediaTttSenderLogger,
         instanceId: InstanceId,
     ): ChipbarInfo {
-        val packageName = routeInfo.clientPackageName
+        val packageName = checkNotNull(routeInfo.clientPackageName)
         val otherDeviceName =
             if (routeInfo.name.isBlank()) {
                 context.getString(R.string.media_ttt_default_device_type)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
index c816446..64de9bd 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
@@ -88,7 +88,7 @@
                 .inflate(R.layout.media_projection_recent_tasks, parent, /* attachToRoot= */ false)
                 as ViewGroup
 
-        val container = recentsRoot.findViewById<View>(R.id.media_projection_recent_tasks_container)
+        val container = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_container)
         container.setTaskHeightSize()
 
         val progress = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_loader)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt
index 38d4e69..6480a47 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt
@@ -81,8 +81,8 @@
             return MediaProjectionState.EntireScreen
         }
         val matchingTask =
-            tasksRepository.findRunningTaskFromWindowContainerToken(session.tokenToRecord)
-                ?: return MediaProjectionState.EntireScreen
+            tasksRepository.findRunningTaskFromWindowContainerToken(
+                checkNotNull(session.tokenToRecord)) ?: return MediaProjectionState.EntireScreen
         return MediaProjectionState.SingleTask(matchingTask)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
index 4d30634..63d4634 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
@@ -77,8 +77,13 @@
             .build()
     }
 
-    private fun PackageManager.getApplicationLabel(packageName: String?): String? =
-        runCatching { getApplicationInfo(packageName, /* flags= */ 0)!! }
+    private fun PackageManager.getApplicationLabel(packageName: String?): String? {
+        if (packageName == null) {
+            return null
+        }
+
+        return runCatching { getApplicationInfo(packageName, /* flags= */ 0)!! }
             .getOrNull()
             ?.let { info -> getApplicationLabel(info).toString() }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
index 5f338c3..46c8d35 100644
--- a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
@@ -132,13 +132,13 @@
             LayoutInflater.from(context)
                 .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view)
 
-        noConversationsView.findViewById<View>(R.id.got_it_button).setOnClickListener {
+        noConversationsView.requireViewById<View>(R.id.got_it_button).setOnClickListener {
             onGotItClicked()
         }
 
         // The Tile preview has colorBackground as its background. Change it so it's different than
         // the activity's background.
-        val item = noConversationsView.findViewById<LinearLayout>(android.R.id.background)
+        val item = noConversationsView.requireViewById<LinearLayout>(android.R.id.background)
         val shape = item.background as GradientDrawable
         val ta =
             context.theme.obtainStyledAttributes(
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
index f4aa27d..c202f14 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
@@ -192,7 +192,7 @@
             return null
         }
         val closeAppButton =
-            window.layoutInflater.inflate(
+            checkNotNull(window).layoutInflater.inflate(
                 R.layout.privacy_dialog_card_button,
                 expandedLayout,
                 false
@@ -248,7 +248,7 @@
 
     private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View {
         val manageButton =
-            window.layoutInflater.inflate(
+            checkNotNull(window).layoutInflater.inflate(
                 R.layout.privacy_dialog_card_button,
                 expandedLayout,
                 false
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
index b340043..23894a3 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
@@ -50,22 +50,20 @@
 
     public override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        window.apply {
-            addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
-            setGravity(Gravity.CENTER)
-        }
+        window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
+        window?.setGravity(Gravity.CENTER)
         setContentView(R.layout.screen_share_dialog)
-        dialogTitle = findViewById(R.id.screen_share_dialog_title)
-        warning = findViewById(R.id.text_warning)
-        startButton = findViewById(android.R.id.button1)
-        cancelButton = findViewById(android.R.id.button2)
+        dialogTitle = requireViewById(R.id.screen_share_dialog_title)
+        warning = requireViewById(R.id.text_warning)
+        startButton = requireViewById(android.R.id.button1)
+        cancelButton = requireViewById(android.R.id.button2)
         updateIcon()
         initScreenShareOptions()
         createOptionsView(getOptionsViewLayoutId())
     }
 
     private fun updateIcon() {
-        val icon = findViewById<ImageView>(R.id.screen_share_dialog_icon)
+        val icon = requireViewById<ImageView>(R.id.screen_share_dialog_icon)
         if (dialogIconTint != null) {
             icon.setColorFilter(context.getColor(dialogIconTint))
         }
@@ -92,7 +90,7 @@
                 options
             )
         adapter.setDropDownViewResource(R.layout.screen_share_dialog_spinner_item_text)
-        screenShareModeSpinner = findViewById(R.id.screen_share_mode_spinner)
+        screenShareModeSpinner = requireViewById(R.id.screen_share_mode_spinner)
         screenShareModeSpinner.adapter = adapter
         screenShareModeSpinner.onItemSelectedListener = this
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index 604d449..e8683fb 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -100,11 +100,11 @@
     @LayoutRes override fun getOptionsViewLayoutId(): Int = R.layout.screen_record_options
 
     private fun initRecordOptionsView() {
-        audioSwitch = findViewById(R.id.screenrecord_audio_switch)
-        tapsSwitch = findViewById(R.id.screenrecord_taps_switch)
-        tapsView = findViewById(R.id.show_taps)
+        audioSwitch = requireViewById(R.id.screenrecord_audio_switch)
+        tapsSwitch = requireViewById(R.id.screenrecord_taps_switch)
+        tapsView = requireViewById(R.id.show_taps)
         updateTapsViewVisibility()
-        options = findViewById(R.id.screen_recording_options)
+        options = requireViewById(R.id.screen_recording_options)
         val a: ArrayAdapter<*> =
             ScreenRecordingAdapter(context, android.R.layout.simple_spinner_dropdown_item, MODES)
         a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index ecd4568..aa6bfc3 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -83,7 +83,7 @@
         if (overrideTransition) {
             val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
             try {
-                WindowManagerGlobal.getWindowManagerService()
+                checkNotNull(WindowManagerGlobal.getWindowManagerService())
                     .overridePendingAppTransitionRemote(runner, displayTracker.defaultDisplayId)
             } catch (e: Exception) {
                 Log.e(TAG, "Error overriding screenshot app transition", e)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index d0cbb1b..132cd61 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2131,6 +2131,7 @@
         }
         updateExpansionAndVisibility();
         mNotificationStackScrollLayoutController.setPanelFlinging(false);
+        mShadeLog.d("onFlingEnd called"); // TODO(b/277909752): remove log when bug is fixed
         // expandImmediate should be always reset at the end of animation
         mQsController.setExpandImmediate(false);
     }
@@ -2666,6 +2667,11 @@
             setListening(true);
         }
         if (mBarState != SHADE) {
+            // TODO(b/277909752): remove below logs when bug is fixed
+            mShadeLog.d("onExpandingFinished called");
+            if (mSplitShadeEnabled && !mQsController.getExpanded()) {
+                mShadeLog.d("onExpandingFinished called before QS got expanded");
+            }
             // updating qsExpandImmediate is done in onPanelStateChanged for unlocked shade but
             // on keyguard panel state is always OPEN so we need to have that extra update
             mQsController.setExpandImmediate(false);
@@ -4718,6 +4724,7 @@
     }
 
     private void onPanelStateChanged(@PanelState int state) {
+        mShadeLog.logPanelStateChanged(state);
         mQsController.updateExpansionEnabledAmbient();
 
         if (state == STATE_OPEN && mCurrentPanelState != state) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index baac57c..ac7aeae 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -787,6 +787,12 @@
 
     /** update Qs height state */
     public void setExpansionHeight(float height) {
+        // TODO(b/277909752): remove below log when bug is fixed
+        if (mSplitShadeEnabled && mShadeExpandedFraction == 1.0f && height == 0) {
+            Log.wtf(TAG,
+                    "setting QS height to 0 in split shade while shade is open(ing). "
+                            + "Value of mExpandImmediate = " + mExpandImmediate);
+        }
         int maxHeight = getMaxExpansionHeight();
         height = Math.min(Math.max(
                 height, getMinExpansionHeight()), maxHeight);
@@ -933,7 +939,6 @@
         return mShadeExpandedHeight;
     }
 
-    @VisibleForTesting
     void setExpandImmediate(boolean expandImmediate) {
         if (expandImmediate != mExpandImmediate) {
             mShadeLog.logQsExpandImmediateChanged(expandImmediate);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index c6cb9c4..bea12de 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -130,12 +130,12 @@
     private lateinit var carrierIconSlots: List<String>
     private lateinit var mShadeCarrierGroupController: ShadeCarrierGroupController
 
-    private val batteryIcon: BatteryMeterView = header.findViewById(R.id.batteryRemainingIcon)
-    private val clock: Clock = header.findViewById(R.id.clock)
-    private val date: TextView = header.findViewById(R.id.date)
-    private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons)
-    private val mShadeCarrierGroup: ShadeCarrierGroup = header.findViewById(R.id.carrier_group)
-    private val systemIcons: View = header.findViewById(R.id.shade_header_system_icons)
+    private val batteryIcon: BatteryMeterView = header.requireViewById(R.id.batteryRemainingIcon)
+    private val clock: Clock = header.requireViewById(R.id.clock)
+    private val date: TextView = header.requireViewById(R.id.date)
+    private val iconContainer: StatusIconContainer = header.requireViewById(R.id.statusIcons)
+    private val mShadeCarrierGroup: ShadeCarrierGroup = header.requireViewById(R.id.carrier_group)
+    private val systemIcons: View = header.requireViewById(R.id.shade_header_system_icons)
 
     private var roundedCorners = 0
     private var cutout: DisplayCutout? = null
@@ -582,7 +582,7 @@
     inner class CustomizerAnimationListener(
         private val enteringCustomizing: Boolean,
     ) : AnimatorListenerAdapter() {
-        override fun onAnimationEnd(animation: Animator?) {
+        override fun onAnimationEnd(animation: Animator) {
             super.onAnimationEnd(animation)
             header.animate().setListener(null)
             if (enteringCustomizing) {
@@ -590,7 +590,7 @@
             }
         }
 
-        override fun onAnimationStart(animation: Animator?) {
+        override fun onAnimationStart(animation: Animator) {
             super.onAnimationStart(animation)
             if (!enteringCustomizing) {
                 customizing = false
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index c3ef925..8d23f5d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -336,6 +336,17 @@
         )
     }
 
+    fun logPanelStateChanged(@PanelState panelState: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = panelState.panelStateToString()
+            },
+            { "New panel State: $str1" }
+        )
+    }
+
     fun flingQs(flingType: Int, isClick: Boolean) {
         buffer.log(
             TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index 6e76784..05b1ac6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -106,7 +106,7 @@
             featureFlags: FeatureFlags,
         ): NotificationShadeWindowView {
             if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
-                return root.findViewById(R.id.legacy_window_root)
+                return root.requireViewById(R.id.legacy_window_root)
             }
             return root as NotificationShadeWindowView?
                 ?: throw IllegalStateException("root view not a NotificationShadeWindowView")
@@ -118,7 +118,7 @@
         fun providesNotificationStackScrollLayout(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): NotificationStackScrollLayout {
-            return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller)
+            return notificationShadeWindowView.requireViewById(R.id.notification_stack_scroller)
         }
 
         @Provides
@@ -153,7 +153,7 @@
         fun providesNotificationPanelView(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): NotificationPanelView {
-            return notificationShadeWindowView.findViewById(R.id.notification_panel)
+            return notificationShadeWindowView.requireViewById(R.id.notification_panel)
         }
 
         /**
@@ -175,7 +175,7 @@
         fun providesLightRevealScrim(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): LightRevealScrim {
-            return notificationShadeWindowView.findViewById(R.id.light_reveal_scrim)
+            return notificationShadeWindowView.requireViewById(R.id.light_reveal_scrim)
         }
 
         @Provides
@@ -183,7 +183,7 @@
         fun providesKeyguardRootView(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): KeyguardRootView {
-            return notificationShadeWindowView.findViewById(R.id.keyguard_root_view)
+            return notificationShadeWindowView.requireViewById(R.id.keyguard_root_view)
         }
 
         @Provides
@@ -191,7 +191,7 @@
         fun providesSharedNotificationContainer(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): SharedNotificationContainer {
-            return notificationShadeWindowView.findViewById(R.id.shared_notification_container)
+            return notificationShadeWindowView.requireViewById(R.id.shared_notification_container)
         }
 
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@@ -200,7 +200,7 @@
         fun providesAuthRippleView(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): AuthRippleView? {
-            return notificationShadeWindowView.findViewById(R.id.auth_ripple)
+            return notificationShadeWindowView.requireViewById(R.id.auth_ripple)
         }
 
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@@ -212,9 +212,9 @@
             featureFlags: FeatureFlags
         ): LockIconView {
             if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
-                return keyguardRootView.findViewById(R.id.lock_icon_view)
+                return keyguardRootView.requireViewById(R.id.lock_icon_view)
             } else {
-                return notificationPanelView.findViewById(R.id.lock_icon_view)
+                return notificationPanelView.requireViewById(R.id.lock_icon_view)
             }
         }
 
@@ -224,7 +224,7 @@
         fun providesTapAgainView(
             notificationPanelView: NotificationPanelView,
         ): TapAgainView {
-            return notificationPanelView.findViewById(R.id.shade_falsing_tap_again)
+            return notificationPanelView.requireViewById(R.id.shade_falsing_tap_again)
         }
 
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@@ -233,7 +233,7 @@
         fun providesNotificationsQuickSettingsContainer(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): NotificationsQuickSettingsContainer {
-            return notificationShadeWindowView.findViewById(R.id.notification_container_parent)
+            return notificationShadeWindowView.requireViewById(R.id.notification_container_parent)
         }
 
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@@ -243,7 +243,7 @@
         fun providesShadeHeaderView(
             notificationShadeWindowView: NotificationShadeWindowView,
         ): MotionLayout {
-            val stub = notificationShadeWindowView.findViewById<ViewStub>(R.id.qs_header_stub)
+            val stub = notificationShadeWindowView.requireViewById<ViewStub>(R.id.qs_header_stub)
             val layoutId = R.layout.combined_qs_header
             stub.layoutResource = layoutId
             return stub.inflate() as MotionLayout
@@ -260,7 +260,7 @@
         @SysUISingleton
         @Named(SHADE_HEADER)
         fun providesBatteryMeterView(@Named(SHADE_HEADER) view: MotionLayout): BatteryMeterView {
-            return view.findViewById(R.id.batteryRemainingIcon)
+            return view.requireViewById(R.id.batteryRemainingIcon)
         }
 
         @Provides
@@ -295,7 +295,7 @@
         fun providesOngoingPrivacyChip(
             @Named(SHADE_HEADER) header: MotionLayout,
         ): OngoingPrivacyChip {
-            return header.findViewById(R.id.privacy_chip)
+            return header.requireViewById(R.id.privacy_chip)
         }
 
         @Provides
@@ -304,7 +304,7 @@
         fun providesStatusIconContainer(
             @Named(SHADE_HEADER) header: MotionLayout,
         ): StatusIconContainer {
-            return header.findViewById(R.id.statusIcons)
+            return header.requireViewById(R.id.statusIcons)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/TouchLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/TouchLogger.kt
index d96dc12..58704bf 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/TouchLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/TouchLogger.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.view.MotionEvent
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 87abc92..8edc26d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -25,7 +25,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
 
 /** Models UI state and handles user input for the shade scene. */
@@ -39,14 +39,22 @@
 ) {
     /** The key of the scene we should switch to when swiping up. */
     val upDestinationSceneKey: StateFlow<SceneKey> =
-        authenticationInteractor.isUnlocked
-            .map { isUnlocked -> upDestinationSceneKey(isUnlocked = isUnlocked) }
+        combine(
+                authenticationInteractor.isUnlocked,
+                authenticationInteractor.canSwipeToDismiss,
+            ) { isUnlocked, canSwipeToDismiss ->
+                upDestinationSceneKey(
+                    isUnlocked = isUnlocked,
+                    canSwipeToDismiss = canSwipeToDismiss,
+                )
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue =
                     upDestinationSceneKey(
                         isUnlocked = authenticationInteractor.isUnlocked.value,
+                        canSwipeToDismiss = authenticationInteractor.canSwipeToDismiss.value,
                     ),
             )
 
@@ -57,7 +65,12 @@
 
     private fun upDestinationSceneKey(
         isUnlocked: Boolean,
+        canSwipeToDismiss: Boolean,
     ): SceneKey {
-        return if (isUnlocked) SceneKey.Gone else SceneKey.Lockscreen
+        return when {
+            canSwipeToDismiss -> SceneKey.Lockscreen
+            isUnlocked -> SceneKey.Gone
+            else -> SceneKey.Lockscreen
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt
index 37140ec..5209767 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt
@@ -37,8 +37,8 @@
 
     init {
         inflate(context, R.layout.battery_status_chip, this)
-        roundedContainer = findViewById(R.id.rounded_container)
-        batteryMeterView = findViewById(R.id.battery_meter_view)
+        roundedContainer = requireViewById(R.id.rounded_container)
+        batteryMeterView = requireViewById(R.id.battery_meter_view)
         updateResources()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 5e449900..3120128 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -423,15 +423,14 @@
         revealGradientCenter.y = top + (revealGradientHeight / 2f)
     }
 
-    override fun onDraw(canvas: Canvas?) {
+    override fun onDraw(canvas: Canvas) {
         if (
-            canvas == null ||
-                revealGradientWidth <= 0 ||
-                revealGradientHeight <= 0 ||
-                revealAmount == 0f
+            revealGradientWidth <= 0 ||
+            revealGradientHeight <= 0 ||
+            revealAmount == 0f
         ) {
             if (revealAmount < 1f) {
-                canvas?.drawColor(revealGradientEndColor)
+                canvas.drawColor(revealGradientEndColor)
             }
             return
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 4710574..672796a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -474,7 +474,7 @@
         }
         if (endlistener != null) {
             dragDownAnimator.addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
+                override fun onAnimationEnd(animation: Animator) {
                     endlistener.invoke()
                 }
             })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt
index 750272d..17b4e3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt
@@ -66,7 +66,7 @@
                 inBitmap = oldIn.copy(Bitmap.Config.ARGB_8888, false /* isMutable */)
                 oldIn.recycle()
             }
-            val outBitmap = Bitmap.createBitmap(inBitmap.width, inBitmap.height,
+            val outBitmap = Bitmap.createBitmap(inBitmap?.width ?: 0, inBitmap?.height ?: 0,
                     Bitmap.Config.ARGB_8888)
 
             input = Allocation.createFromBitmap(renderScript, inBitmap,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 0e20df6..6ad1395 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -272,7 +272,7 @@
                             blurUtils.blurRadiusOfRatio(animation.animatedValue as Float)
                 }
                 addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         keyguardAnimator = null
                         wakeAndUnlockBlurRadius = 0f
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index eddb683..d1e0a71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -234,7 +234,7 @@
             }
 
             // Set the dot's view gravity to hug the status bar
-            (corner.findViewById<View>(R.id.privacy_dot)
+            (corner.requireViewById<View>(R.id.privacy_dot)
                     .layoutParams as FrameLayout.LayoutParams)
                         .gravity = rotatedCorner.innerGravity()
         }
@@ -255,7 +255,7 @@
         // in every rotation. The only thing we need to check is rtl
         val rtl = state.layoutRtl
         val size = Point()
-        tl.context.display.getRealSize(size)
+        tl.context.display?.getRealSize(size)
         val currentRotation = RotationUtils.getExactRotation(tl.context)
 
         val displayWidth: Int
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 6e8b8bd..1ad4620 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -168,10 +168,8 @@
         }
 
         val keyFrame1Height = dotSize * 2
-        val v = currentAnimatedView!!.view
-        val chipVerticalCenter = v.top + v.measuredHeight / 2
-        val height1 = ValueAnimator.ofInt(
-                currentAnimatedView!!.view.measuredHeight, keyFrame1Height).apply {
+        val chipVerticalCenter = chipBounds.top + chipBounds.height() / 2
+        val height1 = ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply {
             startDelay = 8.frames
             duration = 6.frames
             interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
index 23edf17..2403920 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
@@ -164,7 +164,9 @@
         }
 
         private fun isChipAnimationEnabled(): Boolean {
-            return DeviceConfig.getBoolean(NAMESPACE_PRIVACY, CHIP_ANIMATION_ENABLED, true)
+            val defaultValue =
+                context.resources.getBoolean(R.bool.config_enablePrivacyChipAnimation)
+            return DeviceConfig.getBoolean(NAMESPACE_PRIVACY, CHIP_ANIMATION_ENABLED, defaultValue)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
index e018397..f40f570 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
@@ -243,7 +243,7 @@
         if (!event.showAnimation && event.forceVisible) {
             // If animations are turned off, we'll transition directly to the dot
             animationState.value = SHOWING_PERSISTENT_DOT
-            notifyTransitionToPersistentDot()
+            notifyTransitionToPersistentDot(event)
             return
         }
 
@@ -335,7 +335,7 @@
         }
         animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
         if (hasPersistentDot) {
-            val dotAnim = notifyTransitionToPersistentDot()
+            val dotAnim = notifyTransitionToPersistentDot(currentlyDisplayedEvent)
             if (dotAnim != null) {
                 animators.add(dotAnim)
             }
@@ -344,12 +344,12 @@
         return AnimatorSet().also { it.playTogether(animators) }
     }
 
-    private fun notifyTransitionToPersistentDot(): Animator? {
+    private fun notifyTransitionToPersistentDot(event: StatusEvent?): Animator? {
         logger?.logTransitionToPersistentDotCallbackInvoked()
         val anims: List<Animator> =
             listeners.mapNotNull {
                 it.onSystemStatusAnimationTransitionToPersistentDot(
-                    currentlyDisplayedEvent?.contentDescription
+                    event?.contentDescription
                 )
             }
         if (anims.isNotEmpty()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 903d485..c5de165 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -179,15 +179,20 @@
         }
         if (weatherTarget != null) {
             val clickIntent = weatherTarget.headerAction?.intent
-            val weatherData = WeatherData.fromBundle(weatherTarget.baseAction.extras, { v ->
-                if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                    activityStarter.startActivity(
-                        clickIntent,
-                        true, /* dismissShade */
-                        null,
-                        false)
+            val weatherData = weatherTarget.baseAction?.extras?.let { extras ->
+                WeatherData.fromBundle(
+                    extras,
+                ) { _ ->
+                    if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                        activityStarter.startActivity(
+                            clickIntent,
+                            true, /* dismissShade */
+                            null,
+                            false)
+                    }
                 }
-            })
+            }
+
             if (weatherData != null) {
                 keyguardUpdateMonitor.sendWeatherData(weatherData)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt
index 16f1a45..1b43922 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt
@@ -74,7 +74,7 @@
                     root.setTag(R.id.view_group_fade_helper_previous_value_tag, newAlpha)
                 }
                 addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         endRunnable?.run()
                     }
                 })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java
deleted file mode 100644
index f04b24e..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.coordinator;
-
-import static android.app.NotificationManager.IMPORTANCE_MIN;
-
-import android.app.Notification;
-import android.service.notification.StatusBarNotification;
-
-import com.android.systemui.ForegroundServiceController;
-import com.android.systemui.appops.AppOpsController;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.statusbar.notification.collection.ListEntry;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
-import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-
-import javax.inject.Inject;
-
-/**
- * Handles ForegroundService and AppOp interactions with notifications.
- *  Tags notifications with appOps
- *  Lifetime extends notifications associated with an ongoing ForegroundService.
- *  Filters out notifications that represent foreground services that are no longer running
- *  Puts foreground service notifications into the FGS section. See {@link NotifCoordinators} for
- *      section ordering priority.
- *
- * Previously this logic lived in
- *  frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceController
- *  frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener
- *  frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender
- */
-@CoordinatorScope
-public class AppOpsCoordinator implements Coordinator {
-    private static final String TAG = "AppOpsCoordinator";
-
-    private final ForegroundServiceController mForegroundServiceController;
-    private final AppOpsController mAppOpsController;
-    private final DelayableExecutor mMainExecutor;
-
-    private NotifPipeline mNotifPipeline;
-
-    @Inject
-    public AppOpsCoordinator(
-            ForegroundServiceController foregroundServiceController,
-            AppOpsController appOpsController,
-            @Main DelayableExecutor mainExecutor) {
-        mForegroundServiceController = foregroundServiceController;
-        mAppOpsController = appOpsController;
-        mMainExecutor = mainExecutor;
-    }
-
-    @Override
-    public void attach(NotifPipeline pipeline) {
-        mNotifPipeline = pipeline;
-
-        // filter out foreground service notifications that aren't necessary anymore
-        mNotifPipeline.addPreGroupFilter(mNotifFilter);
-
-    }
-
-    public NotifSectioner getSectioner() {
-        return mNotifSectioner;
-    }
-
-    /**
-     * Filters out notifications that represent foreground services that are no longer running or
-     * that already have an app notification with the appOps tagged to
-     */
-    private final NotifFilter mNotifFilter = new NotifFilter(TAG) {
-        @Override
-        public boolean shouldFilterOut(NotificationEntry entry, long now) {
-            StatusBarNotification sbn = entry.getSbn();
-
-            // Filters out system-posted disclosure notifications when unneeded
-            if (mForegroundServiceController.isDisclosureNotification(sbn)
-                    && !mForegroundServiceController.isDisclosureNeededForUser(
-                            sbn.getUser().getIdentifier())) {
-                return true;
-            }
-            return false;
-        }
-    };
-
-    /**
-     * Puts colorized foreground service and call notifications into its own section.
-     */
-    private final NotifSectioner mNotifSectioner = new NotifSectioner("ForegroundService",
-            NotificationPriorityBucketKt.BUCKET_FOREGROUND_SERVICE) {
-        @Override
-        public boolean isInSection(ListEntry entry) {
-            NotificationEntry notificationEntry = entry.getRepresentativeEntry();
-            if (notificationEntry != null) {
-                return isColorizedForegroundService(notificationEntry) || isCall(notificationEntry);
-            }
-            return false;
-        }
-
-        private boolean isColorizedForegroundService(NotificationEntry entry) {
-            Notification notification = entry.getSbn().getNotification();
-            return notification.isForegroundService()
-                    && notification.isColorized()
-                    && entry.getImportance() > IMPORTANCE_MIN;
-        }
-
-        private boolean isCall(NotificationEntry entry) {
-            Notification notification = entry.getSbn().getNotification();
-            return entry.getImportance() > IMPORTANCE_MIN
-                    && notification.isStyle(Notification.CallStyle.class);
-        }
-    };
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
new file mode 100644
index 0000000..63997f8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator;
+
+import static android.app.NotificationManager.IMPORTANCE_MIN;
+
+import android.app.Notification;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.statusbar.notification.collection.NotifPipeline;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
+import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt;
+
+import javax.inject.Inject;
+
+/**
+ * Handles sectioning for foreground service notifications.
+ *  Puts non-min colorized foreground service notifications into the FGS section. See
+ *  {@link NotifCoordinators} for section ordering priority.
+ */
+@CoordinatorScope
+public class ColorizedFgsCoordinator implements Coordinator {
+    private static final String TAG = "ColorizedCoordinator";
+
+    @Inject
+    public ColorizedFgsCoordinator() {
+    }
+
+    @Override
+    public void attach(NotifPipeline pipeline) {
+    }
+
+    public NotifSectioner getSectioner() {
+        return mNotifSectioner;
+    }
+
+
+    /**
+     * Puts colorized foreground service and call notifications into its own section.
+     */
+    private final NotifSectioner mNotifSectioner = new NotifSectioner("ColorizedSectioner",
+            NotificationPriorityBucketKt.BUCKET_FOREGROUND_SERVICE) {
+        @Override
+        public boolean isInSection(ListEntry entry) {
+            NotificationEntry notificationEntry = entry.getRepresentativeEntry();
+            if (notificationEntry != null) {
+                return isColorizedForegroundService(notificationEntry) || isCall(notificationEntry);
+            }
+            return false;
+        }
+
+        private boolean isColorizedForegroundService(NotificationEntry entry) {
+            Notification notification = entry.getSbn().getNotification();
+            return notification.isForegroundService()
+                    && notification.isColorized()
+                    && entry.getImportance() > IMPORTANCE_MIN;
+        }
+
+        private boolean isCall(NotificationEntry entry) {
+            Notification notification = entry.getSbn().getNotification();
+            return entry.getImportance() > IMPORTANCE_MIN
+                    && notification.isStyle(Notification.CallStyle.class);
+        }
+    };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 0ccab9e..226a957 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -33,34 +33,34 @@
 
 @CoordinatorScope
 class NotifCoordinatorsImpl @Inject constructor(
-    sectionStyleProvider: SectionStyleProvider,
-    featureFlags: FeatureFlags,
-    dataStoreCoordinator: DataStoreCoordinator,
-    hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
-    hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
-    keyguardCoordinator: KeyguardCoordinator,
-    rankingCoordinator: RankingCoordinator,
-    appOpsCoordinator: AppOpsCoordinator,
-    deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
-    bubbleCoordinator: BubbleCoordinator,
-    headsUpCoordinator: HeadsUpCoordinator,
-    gutsCoordinator: GutsCoordinator,
-    conversationCoordinator: ConversationCoordinator,
-    debugModeCoordinator: DebugModeCoordinator,
-    groupCountCoordinator: GroupCountCoordinator,
-    groupWhenCoordinator: GroupWhenCoordinator,
-    mediaCoordinator: MediaCoordinator,
-    preparationCoordinator: PreparationCoordinator,
-    remoteInputCoordinator: RemoteInputCoordinator,
-    rowAppearanceCoordinator: RowAppearanceCoordinator,
-    stackCoordinator: StackCoordinator,
-    shadeEventCoordinator: ShadeEventCoordinator,
-    smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
-    viewConfigCoordinator: ViewConfigCoordinator,
-    visualStabilityCoordinator: VisualStabilityCoordinator,
-    sensitiveContentCoordinator: SensitiveContentCoordinator,
-    dismissibilityCoordinator: DismissibilityCoordinator,
-    dreamCoordinator: DreamCoordinator,
+        sectionStyleProvider: SectionStyleProvider,
+        featureFlags: FeatureFlags,
+        dataStoreCoordinator: DataStoreCoordinator,
+        hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
+        hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
+        keyguardCoordinator: KeyguardCoordinator,
+        rankingCoordinator: RankingCoordinator,
+        colorizedFgsCoordinator: ColorizedFgsCoordinator,
+        deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
+        bubbleCoordinator: BubbleCoordinator,
+        headsUpCoordinator: HeadsUpCoordinator,
+        gutsCoordinator: GutsCoordinator,
+        conversationCoordinator: ConversationCoordinator,
+        debugModeCoordinator: DebugModeCoordinator,
+        groupCountCoordinator: GroupCountCoordinator,
+        groupWhenCoordinator: GroupWhenCoordinator,
+        mediaCoordinator: MediaCoordinator,
+        preparationCoordinator: PreparationCoordinator,
+        remoteInputCoordinator: RemoteInputCoordinator,
+        rowAppearanceCoordinator: RowAppearanceCoordinator,
+        stackCoordinator: StackCoordinator,
+        shadeEventCoordinator: ShadeEventCoordinator,
+        smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
+        viewConfigCoordinator: ViewConfigCoordinator,
+        visualStabilityCoordinator: VisualStabilityCoordinator,
+        sensitiveContentCoordinator: SensitiveContentCoordinator,
+        dismissibilityCoordinator: DismissibilityCoordinator,
+        dreamCoordinator: DreamCoordinator,
 ) : NotifCoordinators {
 
     private val mCoreCoordinators: MutableList<CoreCoordinator> = ArrayList()
@@ -79,7 +79,7 @@
         mCoordinators.add(hideNotifsForOtherUsersCoordinator)
         mCoordinators.add(keyguardCoordinator)
         mCoordinators.add(rankingCoordinator)
-        mCoordinators.add(appOpsCoordinator)
+        mCoordinators.add(colorizedFgsCoordinator)
         mCoordinators.add(deviceProvisionedCoordinator)
         mCoordinators.add(bubbleCoordinator)
         mCoordinators.add(debugModeCoordinator)
@@ -106,7 +106,7 @@
 
         // Manually add Ordered Sections
         mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
-        mOrderedSections.add(appOpsCoordinator.sectioner) // ForegroundService
+        mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
         mOrderedSections.add(conversationCoordinator.peopleAlertingSectioner) // People Alerting
         mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent
         mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 106d11f..7d1cca8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.init
 
 import android.service.notification.StatusBarNotification
-import com.android.systemui.ForegroundServiceNotificationListener
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager
@@ -70,7 +69,6 @@
     private val animatedImageNotificationManager: AnimatedImageNotificationManager,
     private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
     private val bubblesOptional: Optional<Bubbles>,
-    private val fgsNotifListener: ForegroundServiceNotificationListener,
     private val featureFlags: FeatureFlags
 ) : NotificationsController {
 
@@ -105,7 +103,6 @@
         notificationsMediaManager.setUpWithPresenter(presenter)
         notificationLogger.setUpWithContainer(listContainer)
         peopleSpaceWidgetManager.attach(notificationListener)
-        fgsNotifListener.init()
     }
 
     // TODO: Convert all functions below this line into listeners instead of public methods
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 38a1579..9c4aa07 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -60,7 +60,7 @@
     override fun onFinishInflate() {
         super.onFinishInflate()
 
-        appControlRow = findViewById(R.id.app_control)
+        appControlRow = requireViewById(R.id.app_control)
     }
 
     /**
@@ -143,9 +143,9 @@
     lateinit var switch: Switch
 
     override fun onFinishInflate() {
-        iconView = findViewById(R.id.icon)
-        channelName = findViewById(R.id.app_name)
-        switch = findViewById(R.id.toggle)
+        iconView = requireViewById(R.id.icon)
+        channelName = requireViewById(R.id.app_name)
+        switch = requireViewById(R.id.toggle)
 
         setOnClickListener { switch.toggle() }
     }
@@ -174,9 +174,9 @@
 
     override fun onFinishInflate() {
         super.onFinishInflate()
-        channelName = findViewById(R.id.channel_name)
-        channelDescription = findViewById(R.id.channel_description)
-        switch = findViewById(R.id.toggle)
+        channelName = requireViewById(R.id.channel_name)
+        channelDescription = requireViewById(R.id.channel_description)
+        switch = requireViewById(R.id.toggle)
         switch.setOnCheckedChangeListener { _, b ->
             channel?.let {
                 controller.proposeEditForChannel(it, if (b) it.importance else IMPORTANCE_NONE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 2ccbc9f..baeae79 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -914,8 +914,8 @@
             val packages: Array<String> =
                 context.resources.getStringArray(R.array.system_ui_packages)
             for (pkg in packages) {
-                if (intent.component == null) break
-                if (pkg == intent.component.packageName) {
+                val componentName = intent.component ?: break
+                if (pkg == componentName.packageName) {
                     return UserHandle(UserHandle.myUserId())
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 632f241..127569d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -634,7 +634,7 @@
 
     private final ActivityIntentHelper mActivityIntentHelper;
 
-    private final NotificationStackScrollLayoutController mStackScrollerController;
+    public final NotificationStackScrollLayoutController mStackScrollerController;
 
     private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener =
             (extractor, which) -> updateTheme();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
index 34bbd13..cdd410e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.animation.requiresRemeasuring
 
 /**
  * Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI
@@ -98,7 +99,7 @@
         ambientIndicationArea?.let { nonNullAmbientIndicationArea ->
             // remove old ambient indication from its parent
             val originalAmbientIndicationView =
-                oldBottomArea.findViewById<View>(R.id.ambient_indication_container)
+                oldBottomArea.requireViewById<View>(R.id.ambient_indication_container)
             (originalAmbientIndicationView.parent as ViewGroup).removeView(
                 originalAmbientIndicationView
             )
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 5c1f824b..38c3815 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -87,6 +87,7 @@
     private int mStatusBarPaddingEnd;
     private int mMinDotWidth;
     private View mSystemIconsContainer;
+    private View mSystemIcons;
     private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow(
             DarkChange.EMPTY);
 
@@ -119,6 +120,7 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mSystemIconsContainer = findViewById(R.id.system_icons_container);
+        mSystemIcons = findViewById(R.id.system_icons);
         mMultiUserAvatar = findViewById(R.id.multi_user_avatar);
         mCarrierLabel = findViewById(R.id.keyguard_carrier_text);
         mBatteryView = mSystemIconsContainer.findViewById(R.id.battery);
@@ -167,6 +169,13 @@
                 mStatusIconContainer.getPaddingBottom()
         );
 
+        mSystemIcons.setPaddingRelative(
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_start),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_top),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_end),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_bottom)
+        );
+
         // Respect font size setting.
         mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                 getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index d546a84..83a040c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -208,25 +208,29 @@
         ViewGroup.LayoutParams layoutParams = getLayoutParams();
         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
         layoutParams.height = mStatusBarHeight - waterfallTopInset;
+        updatePaddings();
+        setLayoutParams(layoutParams);
+    }
 
-        int statusBarPaddingTop = getResources().getDimensionPixelSize(
-                R.dimen.status_bar_padding_top);
+    private void updatePaddings() {
         int statusBarPaddingStart = getResources().getDimensionPixelSize(
                 R.dimen.status_bar_padding_start);
-        int statusBarPaddingEnd = getResources().getDimensionPixelSize(
-                R.dimen.status_bar_padding_end);
 
-        View sbContents = findViewById(R.id.status_bar_contents);
-        sbContents.setPaddingRelative(
+        findViewById(R.id.status_bar_contents).setPaddingRelative(
                 statusBarPaddingStart,
-                statusBarPaddingTop,
-                statusBarPaddingEnd,
+                getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end),
                 0);
 
         findViewById(R.id.notification_lights_out)
                 .setPaddingRelative(0, statusBarPaddingStart, 0, 0);
 
-        setLayoutParams(layoutParams);
+        findViewById(R.id.system_icons).setPaddingRelative(
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_start),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_top),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_end),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_bottom)
+        );
     }
 
     private void updateLayoutForCutout() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 2affb817..931aedd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -75,13 +75,13 @@
     }
 
     override fun onViewAttached() {
-        statusContainer = mView.findViewById(R.id.system_icons)
+        statusContainer = mView.requireViewById(R.id.system_icons)
         statusContainer.setOnHoverListener(
             statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer))
         if (moveFromCenterAnimationController == null) return
 
-        val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up)
-        val systemIconArea: ViewGroup = mView.findViewById(R.id.status_bar_end_side_content)
+        val statusBarLeftSide: View = mView.requireViewById(R.id.status_bar_start_side_except_heads_up)
+        val systemIconArea: ViewGroup = mView.requireViewById(R.id.status_bar_end_side_content)
 
         val viewsToAnimate = arrayOf(
             statusBarLeftSide,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
index c850d4f..ad18170 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
@@ -117,11 +117,11 @@
      * status bar area is contiguous.
      */
     fun currentRotationHasCornerCutout(): Boolean {
-        val cutout = context.display.cutout ?: return false
+        val cutout = checkNotNull(context.display).cutout ?: return false
         val topBounds = cutout.boundingRectTop
 
         val point = Point()
-        context.display.getRealSize(point)
+        checkNotNull(context.display).getRealSize(point)
 
         return topBounds.left <= 0 || topBounds.right >= point.x
     }
@@ -161,7 +161,7 @@
      */
     fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Pair<Int, Int> =
         traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") {
-            val displayCutout = context.display.cutout
+            val displayCutout = checkNotNull(context.display).cutout
             val key = getCacheKey(rotation, displayCutout)
 
             val screenBounds = context.resources.configuration.windowConfiguration.maxBounds
@@ -198,7 +198,7 @@
     fun getStatusBarContentAreaForRotation(
         @Rotation rotation: Int
     ): Rect {
-        val displayCutout = context.display.cutout
+        val displayCutout = checkNotNull(context.display).cutout
         val key = getCacheKey(rotation, displayCutout)
         return insetsCache[key] ?: getAndSetCalculatedAreaForRotation(
                 rotation, displayCutout, getResourcesForRotation(rotation, context), key)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 5c1dfbe..ea57eb4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -22,6 +22,8 @@
 import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;
 
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -60,10 +62,14 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.bouncer.ui.BouncerView;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.navigationbar.TaskbarDelegate;
@@ -86,8 +92,6 @@
 import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 
-import dagger.Lazy;
-
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -97,6 +101,9 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+import kotlinx.coroutines.CoroutineDispatcher;
+
 /**
  * Manages creating, showing, hiding and resetting the keyguard within the status bar. Calls back
  * via {@link ViewMediatorCallback} to poke the wake lock and report that the keyguard is done,
@@ -281,6 +288,9 @@
     private int mLastBiometricMode;
     private boolean mLastScreenOffAnimationPlaying;
     private float mQsExpansion;
+
+    private FeatureFlags mFlags;
+
     final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
     private boolean mIsBackAnimationEnabled;
     private final boolean mUdfpsNewTouchDetectionEnabled;
@@ -326,6 +336,7 @@
             }
         }
     };
+    private Lazy<WindowManagerLockscreenVisibilityInteractor> mWmLockscreenVisibilityInteractor;
 
     @Inject
     public StatusBarKeyguardViewManager(
@@ -352,7 +363,10 @@
             BouncerView primaryBouncerView,
             AlternateBouncerInteractor alternateBouncerInteractor,
             UdfpsOverlayInteractor udfpsOverlayInteractor,
-            ActivityStarter activityStarter
+            ActivityStarter activityStarter,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
+            @Main CoroutineDispatcher mainDispatcher,
+            Lazy<WindowManagerLockscreenVisibilityInteractor> wmLockscreenVisibilityInteractor
     ) {
         mContext = context;
         mViewMediatorCallback = callback;
@@ -370,6 +384,7 @@
         mShadeController = shadeController;
         mLatencyTracker = latencyTracker;
         mKeyguardSecurityModel = keyguardSecurityModel;
+        mFlags = featureFlags;
         mPrimaryBouncerCallbackInteractor = primaryBouncerCallbackInteractor;
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
         mPrimaryBouncerView = primaryBouncerView;
@@ -381,8 +396,14 @@
         mUdfpsNewTouchDetectionEnabled = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION);
         mUdfpsOverlayInteractor = udfpsOverlayInteractor;
         mActivityStarter = activityStarter;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+        mMainDispatcher = mainDispatcher;
+        mWmLockscreenVisibilityInteractor = wmLockscreenVisibilityInteractor;
     }
 
+    KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    CoroutineDispatcher mMainDispatcher;
+
     @Override
     public void registerCentralSurfaces(CentralSurfaces centralSurfaces,
             ShadeViewController shadeViewController,
@@ -429,6 +450,14 @@
         }
     }
 
+    private KeyguardStateController.Callback mKeyguardStateControllerCallback =
+            new KeyguardStateController.Callback() {
+                @Override
+                public void onUnlockedChanged() {
+                    updateAlternateBouncerShowing(mAlternateBouncerInteractor.maybeHide());
+                }
+            };
+
     private void registerListeners() {
         mKeyguardUpdateManager.registerCallback(mUpdateMonitorCallback);
         mStatusBarStateController.addCallback(this);
@@ -442,6 +471,32 @@
             mDockManager.addListener(mDockEventListener);
             mIsDocked = mDockManager.isDocked();
         }
+        mKeyguardStateController.addCallback(mKeyguardStateControllerCallback);
+
+        if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+            mShadeViewController.postToView(() ->
+                    collectFlow(
+                        getViewRootImpl().getView(),
+                        combineFlows(
+                                mKeyguardTransitionInteractor.getFinishedKeyguardState(),
+                                mWmLockscreenVisibilityInteractor.get()
+                                        .getUsingKeyguardGoingAwayAnimation(),
+                                (finishedState, animating) ->
+                                        KeyguardInteractor.Companion.isKeyguardVisibleInState(
+                                                finishedState)
+                                                || animating),
+                        this::consumeShowStatusBarKeyguardView));
+        }
+    }
+
+    private void consumeShowStatusBarKeyguardView(boolean show) {
+        if (show != mLastShowing) {
+            if (show) {
+                show(null);
+            } else {
+                hide(0, 0);
+            }
+        }
     }
 
     /** Register a callback, to be invoked by the Predictive Back system. */
@@ -1313,6 +1368,10 @@
             hideAlternateBouncer(false);
             executeAfterKeyguardGoneAction();
         }
+
+        if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+            mKeyguardTransitionInteractor.startDismissKeyguardTransition();
+        }
     }
 
     /** Display security message to relevant KeyguardMessageArea. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index e8da951..1bceb29 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -105,18 +105,18 @@
             }
         }
         addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationCancel(animation: Animator?) {
+            override fun onAnimationCancel(animation: Animator) {
                 if (lightRevealScrim.revealEffect !is CircleReveal) {
                     lightRevealScrim.revealAmount = 1f
                 }
             }
 
-            override fun onAnimationEnd(animation: Animator?) {
+            override fun onAnimationEnd(animation: Animator) {
                 lightRevealAnimationPlaying = false
                 interactionJankMonitor.end(CUJ_SCREEN_OFF)
             }
 
-            override fun onAnimationStart(animation: Animator?) {
+            override fun onAnimationStart(animation: Animator) {
                 interactionJankMonitor.begin(
                         notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF)
             }
@@ -345,7 +345,7 @@
         // portrait. If we're in another orientation, disable the screen off animation so we don't
         // animate in the keyguard AOD UI sideways or upside down.
         if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
-            context.display.rotation != Surface.ROTATION_0) {
+            context.display?.rotation != Surface.ROTATION_0) {
             return false
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
index 270c592..1259477 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
@@ -34,7 +34,7 @@
 
     override fun onFinishInflate() {
         super.onFinishInflate()
-        text = findViewById(R.id.current_user_name)
-        avatar = findViewById(R.id.current_user_avatar)
+        text = requireViewById(R.id.current_user_name)
+        avatar = requireViewById(R.id.current_user_avatar)
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
index 4950482..ffb743f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
@@ -128,7 +128,8 @@
 
         val prefs = userContextProvider.userContext.getSharedPreferences(
             PREFS_CONTROLS_FILE, Context.MODE_PRIVATE)
-        val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet())
+        val seededPackages =
+            prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet()
 
         val controlsController = controlsComponent.getControlsController().get()
         val componentsToSeed = mutableListOf<ComponentName>()
@@ -174,7 +175,8 @@
     }
 
     private fun addPackageToSeededSet(prefs: SharedPreferences, pkg: String) {
-        val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet())
+        val seededPackages =
+            prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet()
         val updatedPkgs = seededPackages.toMutableSet()
         updatedPkgs.add(pkg)
         prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, updatedPkgs).apply()
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
index 27aaa68..eb7d339 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
@@ -353,8 +353,8 @@
                     // before CoreStartables run, and will not be removed.
                     // In many cases, it reports the battery level of the stylus.
                     registerBatteryListener(deviceId)
-                } else if (device.bluetoothAddress != null) {
-                    onStylusBluetoothConnected(deviceId, device.bluetoothAddress)
+                } else {
+                    device.bluetoothAddress?.let { onStylusBluetoothConnected(deviceId, it) }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt
index 72786ef..5ad9630 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt
@@ -60,7 +60,7 @@
 
     override fun getWidth(): Int {
         val displayMetrics = context.resources.displayMetrics.apply {
-            context.display.getRealMetrics(this)
+            checkNotNull(context.display).getRealMetrics(this)
         }
         return displayMetrics.widthPixels
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
index 088cd93..ee84580 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
@@ -52,22 +52,22 @@
     override fun show() {
         // need to call show() first in order to construct the listView
         super.show()
-        val listView = getListView()
+        listView?.apply {
+            isVerticalScrollBarEnabled = false
+            isHorizontalScrollBarEnabled = false
 
-        listView.setVerticalScrollBarEnabled(false)
-        listView.setHorizontalScrollBarEnabled(false)
+            // Creates a transparent spacer between items
+            val shape = ShapeDrawable()
+            shape.alpha = 0
+            divider = shape
+            dividerHeight = res.getDimensionPixelSize(
+                R.dimen.bouncer_user_switcher_popup_divider_height)
 
-        // Creates a transparent spacer between items
-        val shape = ShapeDrawable()
-        shape.setAlpha(0)
-        listView.setDivider(shape)
-        listView.setDividerHeight(res.getDimensionPixelSize(
-            R.dimen.bouncer_user_switcher_popup_divider_height))
-
-        val height = res.getDimensionPixelSize(R.dimen.bouncer_user_switcher_popup_header_height)
-        listView.addHeaderView(createSpacer(height), null, false)
-        listView.addFooterView(createSpacer(height), null, false)
-        setWidth(findMaxWidth(listView))
+            val height = res.getDimensionPixelSize(R.dimen.bouncer_user_switcher_popup_header_height)
+            addHeaderView(createSpacer(height), null, false)
+            addFooterView(createSpacer(height), null, false)
+            setWidth(findMaxWidth(this))
+        }
 
         super.show()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
index f026f0f..bfed0c4 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -227,7 +227,8 @@
                     )
                 }
                 try {
-                    WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
+                    checkNotNull(WindowManagerGlobal.getWindowManagerService())
+                        .lockNow(/* options= */ null)
                 } catch (e: RemoteException) {
                     Log.e(
                         TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
index e74232d..7f16e47 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
@@ -67,7 +67,7 @@
         val resourceId: Int? = getGuestUserRecordNameResourceId(record)
         return when {
             resourceId != null -> context.getString(resourceId)
-            record.info != null -> record.info.name
+            record.info != null -> checkNotNull(record.info.name)
             else ->
                 context.getString(
                     getUserSwitcherActionTextResourceId(
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
index 56c5d3b..7866d76 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
@@ -223,11 +223,11 @@
         }
     }
 
-    override fun dispatchDraw(canvas: Canvas?) {
-        canvas?.save()
-        canvas?.clipRect(boundsRect)
+    override fun dispatchDraw(canvas: Canvas) {
+        canvas.save()
+        canvas.clipRect(boundsRect)
         super.dispatchDraw(canvas)
-        canvas?.restore()
+        canvas.restore()
     }
 
     private fun updateBounds() {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 12d7b4d..0d0a646 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -29,7 +29,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 /** A class allowing Java classes to collect on Kotlin flows. */
@@ -75,3 +75,7 @@
         repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } }
     }
 }
+
+fun <A, B, R> combineFlows(flow1: Flow<A>, flow2: Flow<B>, bifunction: (A, B) -> R): Flow<R> {
+    return combine(flow1, flow2, bifunction)
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index cb18229..9f7ab7b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -150,7 +150,7 @@
     private fun getPatternTopGuideline(): Float {
         val cs = ConstraintSet()
         val container =
-            mKeyguardPatternView.findViewById(R.id.pattern_container) as ConstraintLayout
+            mKeyguardPatternView.requireViewById(R.id.pattern_container) as ConstraintLayout
         cs.clone(container)
         return cs.getConstraint(R.id.pattern_top_guideline).layout.guidePercent
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 4dc7652..309d9e0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -114,7 +114,7 @@
 
         objectKeyguardPINView =
             View.inflate(mContext, R.layout.keyguard_pin_view, null)
-                .findViewById(R.id.keyguard_pin_view) as KeyguardPINView
+                .requireViewById(R.id.keyguard_pin_view) as KeyguardPINView
     }
 
     private fun constructPinViewController(
@@ -175,7 +175,7 @@
 
     private fun getPinTopGuideline(): Float {
         val cs = ConstraintSet()
-        val container = objectKeyguardPINView.findViewById(R.id.pin_container) as ConstraintLayout
+        val container = objectKeyguardPINView.requireViewById(R.id.pin_container) as ConstraintLayout
         cs.clone(container)
         return cs.getConstraint(R.id.pin_pad_top_guideline).layout.guidePercent
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 8f27b59..17bb3d5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -47,6 +47,8 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.log.SessionTracker
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.plugins.FalsingManager
@@ -145,6 +147,7 @@
     private lateinit var testableResources: TestableResources
     private lateinit var sceneTestUtils: SceneTestUtils
     private lateinit var sceneInteractor: SceneInteractor
+    private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     private lateinit var authenticationInteractor: AuthenticationInteractor
     private lateinit var sceneTransitionStateFlow: MutableStateFlow<ObservableTransitionState>
 
@@ -184,6 +187,7 @@
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
         featureFlags.set(Flags.SCENE_CONTAINER, false)
         featureFlags.set(Flags.BOUNCER_USER_SWITCHER, false)
+        featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false)
 
         keyguardPasswordViewController =
             KeyguardPasswordViewController(
@@ -206,6 +210,9 @@
         whenever(userInteractor.getSelectedUserId()).thenReturn(TARGET_USER_ID)
         sceneTestUtils = SceneTestUtils(this)
         sceneInteractor = sceneTestUtils.sceneInteractor()
+        keyguardTransitionInteractor =
+            KeyguardTransitionInteractorFactory.create(sceneTestUtils.testScope.backgroundScope)
+                .keyguardTransitionInteractor
         sceneTransitionStateFlow =
             MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Lockscreen))
         sceneInteractor.setTransitionState(sceneTransitionStateFlow)
@@ -243,9 +250,9 @@
                 { JavaAdapter(sceneTestUtils.testScope.backgroundScope) },
                 userInteractor,
                 faceAuthAccessibilityDelegate,
-            ) {
-                authenticationInteractor
-            }
+                keyguardTransitionInteractor,
+                { authenticationInteractor },
+            )
     }
 
     @Test
@@ -500,30 +507,6 @@
     }
 
     @Test
-    fun showNextSecurityScreenOrFinish_SimPinToAnotherSimPin_None() {
-        // GIVEN the current security method is SimPin
-        whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false)
-        whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID))
-            .thenReturn(false)
-        underTest.showSecurityScreen(SecurityMode.SimPin)
-
-        // WHEN a request is made from the SimPin screens to show the next security method
-        whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID))
-            .thenReturn(SecurityMode.SimPin)
-        whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true)
-
-        underTest.showNextSecurityScreenOrFinish(
-            /* authenticated= */ true,
-            TARGET_USER_ID,
-            /* bypassSecondaryLockScreen= */ true,
-            SecurityMode.SimPin
-        )
-
-        // THEN the next security method of None will dismiss keyguard.
-        verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
-    }
-
-    @Test
     fun onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() {
         val registeredSwipeListener = registeredSwipeListener
         whenever(keyguardUpdateMonitor.isFaceDetectionRunning).thenReturn(false)
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
index f8262d4..210f3cb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
@@ -21,9 +21,9 @@
 
     private lateinit var keyguardStatusView: KeyguardStatusView
     private val mediaView: View
-        get() = keyguardStatusView.findViewById(R.id.status_view_media_container)
+        get() = keyguardStatusView.requireViewById(R.id.status_view_media_container)
     private val statusViewContainer: ViewGroup
-        get() = keyguardStatusView.findViewById(R.id.status_view_container)
+        get() = keyguardStatusView.requireViewById(R.id.status_view_container)
     private val childrenExcludingMedia
         get() = statusViewContainer.children.filter { it != mediaView }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index 956e0b81..b18137c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -49,7 +49,7 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
@@ -164,8 +164,9 @@
                 mVibrator,
                 mAuthRippleController,
                 mResources,
-                new KeyguardTransitionInteractor(mTransitionRepository,
-                        TestScopeProvider.getTestScope().getBackgroundScope()),
+                KeyguardTransitionInteractorFactory.create(
+                        TestScopeProvider.getTestScope().getBackgroundScope(),
+                                mTransitionRepository).getKeyguardTransitionInteractor(),
                 KeyguardInteractorFactory.create(mFeatureFlags).getKeyguardInteractor(),
                 mFeatureFlags,
                 mPrimaryBouncerInteractor
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt
index 6fe8892..9f9b9a4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt
@@ -19,9 +19,11 @@
 import android.testing.AndroidTestingRunner
 import android.transition.TransitionValues
 import android.view.View
+import android.view.ViewGroup
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardStatusViewController.SplitShadeTransitionAdapter
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -84,5 +86,5 @@
     startValues: TransitionValues?,
     endValues: TransitionValues?
 ): Animator? {
-    return createAnimator(/* sceneRoot= */ null, startValues, endValues)
+    return createAnimator(/* sceneRoot= */ mock(), startValues, endValues)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
deleted file mode 100644
index b47b08c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
+++ /dev/null
@@ -1,487 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui;
-
-import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.TestCase.fail;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.service.notification.StatusBarNotification;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.widget.RemoteViews;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.messages.nano.SystemMessageProto;
-import com.android.systemui.appops.AppOpsController;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class ForegroundServiceControllerTest extends SysuiTestCase {
-    private ForegroundServiceController mFsc;
-    private ForegroundServiceNotificationListener mListener;
-    private NotifCollectionListener mCollectionListener;
-    @Mock private AppOpsController mAppOpsController;
-    @Mock private Handler mMainHandler;
-    @Mock private NotifPipeline mNotifPipeline;
-
-    @Before
-    public void setUp() throws Exception {
-        // allow the TestLooper to be asserted as the main thread these tests
-        allowTestableLooperAsMainThread();
-
-        MockitoAnnotations.initMocks(this);
-        mFsc = new ForegroundServiceController(mAppOpsController, mMainHandler);
-        mListener = new ForegroundServiceNotificationListener(
-                mContext, mFsc, mNotifPipeline);
-        mListener.init();
-        ArgumentCaptor<NotifCollectionListener> entryListenerCaptor =
-                ArgumentCaptor.forClass(NotifCollectionListener.class);
-        verify(mNotifPipeline).addCollectionListener(
-                entryListenerCaptor.capture());
-        mCollectionListener = entryListenerCaptor.getValue();
-    }
-
-    @Test
-    public void testAppOpsChangedCalledFromBgThread() {
-        try {
-            // WHEN onAppOpChanged is called from a different thread than the MainLooper
-            disallowTestableLooperAsMainThread();
-            NotificationEntry entry = createFgEntry();
-            mFsc.onAppOpChanged(
-                    AppOpsManager.OP_CAMERA,
-                    entry.getSbn().getUid(),
-                    entry.getSbn().getPackageName(),
-                    true);
-
-            // This test is run on the TestableLooper, which is not the MainLooper, so
-            // we expect an exception to be thrown
-            fail("onAppOpChanged shouldn't be allowed to be called from a bg thread.");
-        } catch (IllegalStateException e) {
-            // THEN expect an exception
-        }
-    }
-
-    @Test
-    public void testAppOpsCRUD() {
-        // no crash on remove that doesn't exist
-        mFsc.onAppOpChanged(9, 1000, "pkg1", false);
-        assertNull(mFsc.getAppOps(0, "pkg1"));
-
-        // multiuser & multipackage
-        mFsc.onAppOpChanged(8, 50, "pkg1", true);
-        mFsc.onAppOpChanged(1, 60, "pkg3", true);
-        mFsc.onAppOpChanged(7, 500000, "pkg2", true);
-
-        assertEquals(1, mFsc.getAppOps(0, "pkg1").size());
-        assertTrue(mFsc.getAppOps(0, "pkg1").contains(8));
-
-        assertEquals(1, mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size());
-        assertTrue(mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7));
-
-        assertEquals(1, mFsc.getAppOps(0, "pkg3").size());
-        assertTrue(mFsc.getAppOps(0, "pkg3").contains(1));
-
-        // multiple ops for the same package
-        mFsc.onAppOpChanged(9, 50, "pkg1", true);
-        mFsc.onAppOpChanged(5, 50, "pkg1", true);
-
-        assertEquals(3, mFsc.getAppOps(0, "pkg1").size());
-        assertTrue(mFsc.getAppOps(0, "pkg1").contains(8));
-        assertTrue(mFsc.getAppOps(0, "pkg1").contains(9));
-        assertTrue(mFsc.getAppOps(0, "pkg1").contains(5));
-
-        assertEquals(1, mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size());
-        assertTrue(mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7));
-
-        // remove one of the multiples
-        mFsc.onAppOpChanged(9, 50, "pkg1", false);
-        assertEquals(2, mFsc.getAppOps(0, "pkg1").size());
-        assertTrue(mFsc.getAppOps(0, "pkg1").contains(8));
-        assertTrue(mFsc.getAppOps(0, "pkg1").contains(5));
-
-        // remove last op
-        mFsc.onAppOpChanged(1, 60, "pkg3", false);
-        assertNull(mFsc.getAppOps(0, "pkg3"));
-    }
-
-    @Test
-    public void testDisclosurePredicate() {
-        StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1",
-                5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
-        StatusBarNotification sbn_user1_disclosure = makeMockSBN(USERID_ONE, "android",
-                SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
-                null, Notification.FLAG_NO_CLEAR);
-
-        assertTrue(mFsc.isDisclosureNotification(sbn_user1_disclosure));
-        assertFalse(mFsc.isDisclosureNotification(sbn_user1_app1));
-    }
-
-    @Test
-    public void testNeedsDisclosureAfterRemovingUnrelatedNotification() {
-        final String PKG1 = "com.example.app100";
-
-        StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
-                5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
-        StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1);
-
-        // first add a normal notification
-        entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
-        // nothing required yet
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        // now the app starts a fg service
-        entryAdded(makeMockDisclosure(USERID_ONE, new String[]{PKG1}),
-                NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
-        // add the fg notification
-        entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has got it covered
-        // remove the boring notification
-        entryRemoved(sbn_user1_app1);
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has STILL got it covered
-        entryRemoved(sbn_user1_app1_fg);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
-    }
-
-    @Test
-    public void testSimpleAddRemove() {
-        final String PKG1 = "com.example.app1";
-        final String PKG2 = "com.example.app2";
-
-        StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
-                5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
-        entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
-
-        // no services are "running"
-        entryAdded(makeMockDisclosure(USERID_ONE, null),
-                NotificationManager.IMPORTANCE_DEFAULT);
-
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG1}),
-                NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        // switch to different package
-        entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG2}),
-                NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        entryUpdated(makeMockDisclosure(USERID_TWO, new String[]{PKG1}),
-                NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO)); // finally user2 needs one too
-
-        entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG2, PKG1}),
-                NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        entryRemoved(makeMockDisclosure(USERID_ONE, null /*unused*/));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        entryRemoved(makeMockDisclosure(USERID_TWO, null /*unused*/));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-    }
-
-    @Test
-    public void testDisclosureBasic() {
-        final String PKG1 = "com.example.app0";
-
-        StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
-                5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
-        StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1);
-
-        entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT); // not fg
-        entryAdded(makeMockDisclosure(USERID_ONE, new String[]{PKG1}),
-                NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
-        entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has got it covered
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        // let's take out the other notification and see what happens.
-
-        entryRemoved(sbn_user1_app1);
-        assertFalse(
-                mFsc.isDisclosureNeededForUser(USERID_ONE)); // still covered by sbn_user1_app1_fg
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        // let's attempt to downgrade the notification from FLAG_FOREGROUND and see what we get
-        StatusBarNotification sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1);
-        sbn_user1_app1_fg_sneaky.getNotification().flags = 0;
-        entryUpdated(sbn_user1_app1_fg_sneaky,
-                NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        // ok, ok, we'll put it back
-        sbn_user1_app1_fg_sneaky.getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE;
-        entryUpdated(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        entryRemoved(sbn_user1_app1_fg_sneaky);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required!
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-
-        // now let's test an upgrade
-        entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-        sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
-        entryUpdated(sbn_user1_app1,
-                NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification
-
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
-
-        // remove it, make sure we're out of compliance again
-        entryRemoved(sbn_user1_app1); // was fg, should return true
-        entryRemoved(sbn_user1_app1);
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
-
-        // importance upgrade
-        entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN);
-        assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-        sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
-        entryUpdated(sbn_user1_app1_fg,
-                NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification
-
-        // finally, let's turn off the service
-        entryAdded(makeMockDisclosure(USERID_ONE, null),
-                NotificationManager.IMPORTANCE_DEFAULT);
-
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE));
-        assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO));
-    }
-
-    @Test
-    public void testNoNotifsNorAppOps_noSystemAlertWarningRequired() {
-        // no notifications nor app op signals that this package/userId requires system alert
-        // warning
-        assertFalse(mFsc.isSystemAlertWarningNeeded(USERID_ONE, "any"));
-    }
-
-    @Test
-    public void testCustomLayouts_systemAlertWarningRequired() {
-        // GIVEN a notification with a custom layout
-        final String pkg = "com.example.app0";
-        StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0,
-                false);
-
-        // WHEN the custom layout entry is added
-        entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN);
-
-        // THEN a system alert warning is required since there aren't any notifications that can
-        // display the app ops
-        assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
-    }
-
-    @Test
-    public void testStandardLayoutExists_noSystemAlertWarningRequired() {
-        // GIVEN two notifications (one with a custom layout, the other with a standard layout)
-        final String pkg = "com.example.app0";
-        StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0,
-                false);
-        StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true);
-
-        // WHEN the entries are added
-        entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN);
-        entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN);
-
-        // THEN no system alert warning is required, since there is at least one notification
-        // with a standard layout that can display the app ops on the notification
-        assertFalse(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
-    }
-
-    @Test
-    public void testStandardLayoutRemoved_systemAlertWarningRequired() {
-        // GIVEN two notifications (one with a custom layout, the other with a standard layout)
-        final String pkg = "com.example.app0";
-        StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0,
-                false);
-        StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true);
-
-        // WHEN the entries are added and then the standard layout notification is removed
-        entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN);
-        entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN);
-        entryRemoved(standardLayoutNotif);
-
-        // THEN a system alert warning is required since there aren't any notifications that can
-        // display the app ops
-        assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
-    }
-
-    @Test
-    public void testStandardLayoutUpdatedToCustomLayout_systemAlertWarningRequired() {
-        // GIVEN a standard layout notification and then an updated version with a customLayout
-        final String pkg = "com.example.app0";
-        StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true);
-        StatusBarNotification updatedToCustomLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, false);
-
-        // WHEN the entries is added and then updated to a custom layout
-        entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN);
-        entryUpdated(updatedToCustomLayoutNotif, NotificationManager.IMPORTANCE_MIN);
-
-        // THEN a system alert warning is required since there aren't any notifications that can
-        // display the app ops
-        assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg));
-    }
-
-    private StatusBarNotification makeMockSBN(int userId, String pkg, int id, String tag,
-            int flags) {
-        final Notification n = mock(Notification.class);
-        n.extras = new Bundle();
-        n.flags = flags;
-        return makeMockSBN(userId, pkg, id, tag, n);
-    }
-
-    private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag,
-            Notification n) {
-        final StatusBarNotification sbn = mock(StatusBarNotification.class);
-        when(sbn.getNotification()).thenReturn(n);
-        when(sbn.getId()).thenReturn(id);
-        when(sbn.getPackageName()).thenReturn(pkg);
-        when(sbn.getTag()).thenReturn(tag);
-        when(sbn.getUserId()).thenReturn(userid);
-        when(sbn.getUser()).thenReturn(new UserHandle(userid));
-        when(sbn.getKey()).thenReturn("MOCK:"+userid+"|"+pkg+"|"+id+"|"+tag);
-        return sbn;
-    }
-
-    private StatusBarNotification makeMockSBN(int uid, String pkg, int id,
-            boolean usesStdLayout) {
-        StatusBarNotification sbn = makeMockSBN(uid, pkg, id, "foo", 0);
-        if (usesStdLayout) {
-            sbn.getNotification().contentView = null;
-            sbn.getNotification().headsUpContentView = null;
-            sbn.getNotification().bigContentView = null;
-        } else {
-            sbn.getNotification().contentView = mock(RemoteViews.class);
-        }
-        return sbn;
-    }
-
-    private StatusBarNotification makeMockFgSBN(int uid, String pkg, int id,
-            boolean usesStdLayout) {
-        StatusBarNotification sbn =
-                makeMockSBN(uid, pkg, id, "foo", Notification.FLAG_FOREGROUND_SERVICE);
-        if (usesStdLayout) {
-            sbn.getNotification().contentView = null;
-            sbn.getNotification().headsUpContentView = null;
-            sbn.getNotification().bigContentView = null;
-        } else {
-            sbn.getNotification().contentView = mock(RemoteViews.class);
-        }
-        return sbn;
-    }
-
-    private StatusBarNotification makeMockFgSBN(int uid, String pkg) {
-        return makeMockSBN(uid, pkg, 1000, "foo", Notification.FLAG_FOREGROUND_SERVICE);
-    }
-
-    private StatusBarNotification makeMockDisclosure(int userid, String[] pkgs) {
-        final Notification n = mock(Notification.class);
-        n.flags = Notification.FLAG_ONGOING_EVENT;
-        final Bundle extras = new Bundle();
-        if (pkgs != null) extras.putStringArray(Notification.EXTRA_FOREGROUND_APPS, pkgs);
-        n.extras = extras;
-        n.when = System.currentTimeMillis() - 10000; // ten seconds ago
-        final StatusBarNotification sbn = makeMockSBN(userid, "android",
-                SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
-                null, n);
-        sbn.getNotification().extras = extras;
-        return sbn;
-    }
-
-    private NotificationEntry addFgEntry() {
-        NotificationEntry entry = createFgEntry();
-        mCollectionListener.onEntryAdded(entry);
-        return entry;
-    }
-
-    private NotificationEntry createFgEntry() {
-        return new NotificationEntryBuilder()
-                .setSbn(makeMockFgSBN(0, TEST_PACKAGE_NAME, 1000, true))
-                .setImportance(NotificationManager.IMPORTANCE_DEFAULT)
-                .build();
-    }
-
-    private void entryRemoved(StatusBarNotification notification) {
-        mCollectionListener.onEntryRemoved(
-                new NotificationEntryBuilder()
-                        .setSbn(notification)
-                        .build(),
-                REASON_APP_CANCEL);
-    }
-
-    private void entryAdded(StatusBarNotification notification, int importance) {
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setSbn(notification)
-                .setImportance(importance)
-                .build();
-        mCollectionListener.onEntryAdded(entry);
-    }
-
-    private void entryUpdated(StatusBarNotification notification, int importance) {
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setSbn(notification)
-                .setImportance(importance)
-                .build();
-        mCollectionListener.onEntryUpdated(entry);
-    }
-
-    @UserIdInt private static final int USERID_ONE = 10; // UserManagerService.MIN_USER_ID;
-    @UserIdInt private static final int USERID_TWO = USERID_ONE + 1;
-    private static final String TEST_PACKAGE_NAME = "test";
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index 316de59..2233e322 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -70,7 +70,7 @@
         assertTrue(dialog.isShowing)
 
         // The dialog is now fullscreen.
-        val window = dialog.window
+        val window = checkNotNull(dialog.window)
         val decorView = window.decorView as DecorView
         assertEquals(MATCH_PARENT, window.attributes.width)
         assertEquals(MATCH_PARENT, window.attributes.height)
@@ -172,14 +172,15 @@
         // Important: the power menu animation relies on this behavior to know when to animate (see
         // http://ag/16774605).
         val dialog = runOnMainThreadAndWaitForIdleSync { TestDialog(context) }
-        dialog.window.setWindowAnimations(0)
-        assertEquals(0, dialog.window.attributes.windowAnimations)
+        val window = checkNotNull(dialog.window)
+        window.setWindowAnimations(0)
+        assertEquals(0, window.attributes.windowAnimations)
 
         val touchSurface = createTouchSurface()
         runOnMainThreadAndWaitForIdleSync {
             dialogLaunchAnimator.showFromView(dialog, touchSurface)
         }
-        assertNotEquals(0, dialog.window.attributes.windowAnimations)
+        assertNotEquals(0, window.attributes.windowAnimations)
     }
 
     @Test
@@ -351,13 +352,14 @@
 
         init {
             // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw.
-            window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
+            checkNotNull(window).setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
         }
 
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setContentView(contentView)
 
+            val window = checkNotNull(window)
             window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT)
             window.setBackgroundDrawable(windowBackground)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index e3e6130..4e52e64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -139,6 +139,7 @@
     @Before
     fun setup() {
         featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt)
+        featureFlags.set(Flags.ONE_WAY_HAPTICS_API_MIGRATION, false)
     }
 
     @After
@@ -151,7 +152,10 @@
     @Test
     fun testNotifiesAnimatedIn() {
         initializeFingerprintContainer()
-        verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
+        verify(callback).onDialogAnimatedIn(
+            authContainer?.requestId ?: 0L,
+            true /* startFingerprintNow */
+        )
     }
 
     @Test
@@ -196,7 +200,10 @@
         waitForIdleSync()
 
         // attaching the view resets the state and allows this to happen again
-        verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
+        verify(callback).onDialogAnimatedIn(
+            authContainer?.requestId ?: 0L,
+            true /* startFingerprintNow */
+        )
     }
 
     @Test
@@ -211,7 +218,10 @@
 
         // the first time is triggered by initializeFingerprintContainer()
         // the second time was triggered by dismissWithoutCallback()
-        verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
+        verify(callback, times(2)).onDialogAnimatedIn(
+            authContainer?.requestId ?: 0L,
+            true /* startFingerprintNow */
+        )
     }
 
     @Test
@@ -517,10 +527,11 @@
         { authBiometricFingerprintViewModel },
         { promptSelectorInteractor },
         { bpCredentialInteractor },
-        PromptViewModel(promptSelectorInteractor, vibrator),
+        PromptViewModel(promptSelectorInteractor, vibrator, featureFlags),
         { credentialViewModel },
         Handler(TestableLooper.get(this).looper),
-        fakeExecutor
+        fakeExecutor,
+        vibrator
     ) {
         override fun postOnAnimation(runnable: Runnable) {
             runnable.run()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 3d4171f..48e5131 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -197,6 +197,8 @@
     private ArgumentCaptor<String> mMessageCaptor;
     @Mock
     private Resources mResources;
+    @Mock
+    private VibratorHelper mVibratorHelper;
 
     private TestableContext mContextSpy;
     private Execution mExecution;
@@ -515,7 +517,7 @@
 
         assertThat(mModalityCaptor.getValue().intValue()).isEqualTo(modality);
         assertThat(mMessageCaptor.getValue()).isEqualTo(
-                mContext.getString(R.string.biometric_face_not_recognized));
+                mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead));
     }
 
     @Test
@@ -1097,7 +1099,7 @@
                     () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,
                     () -> mCredentialViewModel, () -> mPromptViewModel,
                     mInteractionJankMonitor, mHandler,
-                    mBackgroundExecutor, mUdfpsUtils);
+                    mBackgroundExecutor, mUdfpsUtils, mVibratorHelper);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index 2b08c66..994db46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -164,7 +164,7 @@
         context.addMockSystemService(WindowManager::class.java, windowManager)
 
         whenEver(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
-        whenEver(sideFpsView.findViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
+        whenEver(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
             .thenReturn(mock(LottieAnimationView::class.java))
         with(mock(ViewPropertyAnimator::class.java)) {
             whenEver(sideFpsView.animate()).thenReturn(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 40b1f20..4d19543 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -19,6 +19,7 @@
 import android.hardware.biometrics.PromptInfo
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.view.HapticFeedbackConstants
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
@@ -33,6 +34,8 @@
 import com.android.systemui.biometrics.shared.model.BiometricModality
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
@@ -71,13 +74,15 @@
 
     private lateinit var selector: PromptSelectorInteractor
     private lateinit var viewModel: PromptViewModel
+    private val featureFlags = FakeFeatureFlags()
 
     @Before
     fun setup() {
         selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
         selector.resetPrompt()
 
-        viewModel = PromptViewModel(selector, vibrator)
+        viewModel = PromptViewModel(selector, vibrator, featureFlags)
+        featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
     }
 
     @Test
@@ -149,6 +154,29 @@
             verify(vibrator, never()).vibrateAuthError(any())
         }
 
+    @Test
+    fun playSuccessHaptic_onwayHapticsEnabled_SetsConfirmConstant() = runGenericTest {
+        featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+        viewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
+
+        if (expectConfirmation) {
+            viewModel.confirmAuthenticated()
+        }
+
+        val currentConstant by collectLastValue(viewModel.hapticsToPlay)
+        assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.CONFIRM)
+    }
+
+    @Test
+    fun playErrorHaptic_onwayHapticsEnabled_SetsRejectConstant() = runGenericTest {
+        featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+        viewModel.showTemporaryError("test", "messageAfterError", false)
+
+        val currentConstant by collectLastValue(viewModel.hapticsToPlay)
+        assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.REJECT)
+    }
+
     private suspend fun TestScope.showAuthenticated(
         authenticatedModality: BiometricModality,
         expectConfirmation: Boolean,
@@ -499,6 +527,7 @@
         val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
         val size by collectLastValue(viewModel.size)
         val legacyState by collectLastValue(viewModel.legacyState)
+        val confirmationRequired by collectLastValue(viewModel.isConfirmationRequired)
 
         if (testCase.isCoex && testCase.authenticatedByFingerprint) {
             viewModel.ensureFingerprintHasStarted(isDelayed = true)
@@ -507,7 +536,11 @@
         viewModel.showHelp(helpMessage)
 
         assertThat(size).isEqualTo(PromptSize.MEDIUM)
-        assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION)
+        if (confirmationRequired == true) {
+            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION)
+        } else {
+            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED)
+        }
         assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
         assertThat(messageVisible).isTrue()
         assertThat(authenticating).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
index d6cafcb..5a5c058 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
@@ -211,7 +211,7 @@
                 context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_y)
         val expectedCenterX: Float
         val expectedCenterY: Float
-        when (context.display.rotation) {
+        when (checkNotNull(context.display).rotation) {
             Surface.ROTATION_90 -> {
                 expectedCenterX = width * normalizedPortPosY
                 expectedCenterY = height * (1 - normalizedPortPosX)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt
index 42f28c8..2ae342a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt
@@ -125,7 +125,7 @@
             control
         )
         cvh.bindData(cws, false)
-        val chevronIcon = baseLayout.findViewById<View>(R.id.chevron_icon)
+        val chevronIcon = baseLayout.requireViewById<View>(R.id.chevron_icon)
 
         assertThat(chevronIcon.visibility).isEqualTo(View.VISIBLE)
     }
@@ -138,4 +138,4 @@
 private val DRAWABLE = GradientDrawable()
 private val COLOR = ColorStateList.valueOf(0xffff00)
 private val DEFAULT_CONTROL = Control.StatelessBuilder(
-        CONTROL_ID, mock(PendingIntent::class.java)).build()
\ No newline at end of file
+        CONTROL_ID, mock(PendingIntent::class.java)).build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index fcd6568..a400ff9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -365,7 +365,8 @@
         val selectedItems =
             listOf(
                 SelectedItem.StructureItem(
-                    StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList())
+                    StructureInfo(checkNotNull(ComponentName.unflattenFromString("pkg/.cls1")),
+                        "a", ArrayList())
                 ),
             )
         preferredPanelRepository.setSelectedComponent(
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 8ae9989..b05de48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -224,7 +224,7 @@
                 mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager,
                 mShadeWindowLogger);
         mFeatureFlags = new FakeFeatureFlags();
-
+        mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false);
 
         DejankUtils.setImmediate(true);
 
@@ -957,7 +957,8 @@
                 mSystemClock,
                 mDispatcher,
                 () -> mDreamingToLockscreenTransitionViewModel,
-                mSystemPropertiesHelper);
+                mSystemPropertiesHelper,
+                () -> mock(WindowManagerLockscreenVisibilityManager.class));
         mViewMediator.start();
 
         mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 85ee0e4..fe5b812 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
+import com.android.systemui.flags.Flags.KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
@@ -168,7 +169,11 @@
         biometricSettingsRepository = FakeBiometricSettingsRepository()
         deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
         trustRepository = FakeTrustRepository()
-        featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) }
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(FACE_AUTH_REFACTOR, true)
+                set(KEYGUARD_WM_STATE_REFACTOR, false)
+            }
         val withDeps =
             KeyguardInteractorFactory.create(
                 featureFlags = featureFlags,
@@ -332,9 +337,6 @@
                 )
                 .isFalse()
 
-            whenever(faceManager.sensorPropertiesInternal).thenReturn(null)
-            assertThat(createDeviceEntryFaceAuthRepositoryImpl().isDetectionSupported).isFalse()
-
             whenever(faceManager.sensorPropertiesInternal).thenReturn(listOf())
             assertThat(createDeviceEntryFaceAuthRepositoryImpl().isDetectionSupported).isFalse()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 5e3376a..5ead16b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -63,6 +63,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -193,7 +194,7 @@
             assertThat(underTest.isKeyguardShowing()).isFalse()
 
             val captor = argumentCaptor<KeyguardStateController.Callback>()
-            verify(keyguardStateController).addCallback(captor.capture())
+            verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture())
 
             whenever(keyguardStateController.isShowing).thenReturn(true)
             captor.value.onKeyguardShowingChanged()
@@ -255,7 +256,7 @@
             assertThat(latest).isFalse()
 
             val captor = argumentCaptor<KeyguardStateController.Callback>()
-            verify(keyguardStateController).addCallback(captor.capture())
+            verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture())
 
             whenever(keyguardStateController.isOccluded).thenReturn(true)
             captor.value.onKeyguardShowingChanged()
@@ -280,7 +281,7 @@
             assertThat(isKeyguardUnlocked).isFalse()
 
             val captor = argumentCaptor<KeyguardStateController.Callback>()
-            verify(keyguardStateController).addCallback(captor.capture())
+            verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture())
 
             whenever(keyguardStateController.isUnlocked).thenReturn(true)
             captor.value.onUnlockedChanged()
@@ -454,7 +455,7 @@
             assertThat(latest).isFalse()
 
             val captor = argumentCaptor<KeyguardStateController.Callback>()
-            verify(keyguardStateController).addCallback(captor.capture())
+            verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture())
 
             whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(true)
             captor.value.onKeyguardGoingAwayChanged()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepositoryImplTest.kt
new file mode 100644
index 0000000..bed959f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepositoryImplTest.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.keyguard.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardSurfaceBehindRepositoryImplTest : SysuiTestCase() {
+    private val testScope = TestScope()
+
+    private lateinit var underTest: KeyguardSurfaceBehindRepositoryImpl
+
+    @Before
+    fun setUp() {
+        underTest = KeyguardSurfaceBehindRepositoryImpl()
+    }
+
+    @Test
+    fun testSetAnimatingSurface() {
+        testScope.runTest {
+            val values by collectValues(underTest.isAnimatingSurface)
+
+            runCurrent()
+            underTest.setAnimatingSurface(true)
+            runCurrent()
+            underTest.setAnimatingSurface(false)
+            runCurrent()
+
+            // Default (first) value should be false.
+            assertThat(values).isEqualTo(listOf(false, true, false))
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
new file mode 100644
index 0000000..e2bf2f8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.FakeFeatureFlags
+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.shade.data.repository.FakeShadeRepository
+import dagger.Lazy
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import junit.framework.Assert.fail
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromLockscreenTransitionInteractorTest : KeyguardTransitionInteractorTestCase() {
+    private lateinit var underTest: FromLockscreenTransitionInteractor
+
+    // Override the fromLockscreenTransitionInteractor provider from the superclass so our underTest
+    // interactor is provided to any classes that need it.
+    override var fromLockscreenTransitionInteractorLazy: Lazy<FromLockscreenTransitionInteractor>? =
+        Lazy {
+            underTest
+        }
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+
+        underTest =
+            FromLockscreenTransitionInteractor(
+                transitionRepository = super.transitionRepository,
+                transitionInteractor = super.transitionInteractor,
+                scope = super.testScope.backgroundScope,
+                keyguardInteractor = super.keyguardInteractor,
+                flags = FakeFeatureFlags(),
+                shadeRepository = FakeShadeRepository(),
+            )
+    }
+
+    @Test
+    fun testSurfaceBehindVisibility_nonNullOnlyForRelevantTransitions() =
+        testScope.runTest {
+            val values by collectValues(underTest.surfaceBehindVisibility)
+            runCurrent()
+
+            // Transition-specific surface visibility should be null ("don't care") initially.
+            assertEquals(
+                listOf(
+                    null,
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                )
+            )
+
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    null, // LOCKSCREEN -> AOD does not have any specific surface visibility.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    null,
+                    true, // Surface is made visible immediately during LOCKSCREEN -> GONE
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testSurfaceBehindModel() =
+        testScope.runTest {
+            val values by collectValues(underTest.surfaceBehindModel)
+            runCurrent()
+
+            assertEquals(
+                values,
+                listOf(
+                    null, // We should start null ("don't care").
+                )
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    null, // LOCKSCREEN -> AOD does not have specific view params.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    value = 0.01f,
+                )
+            )
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    value = 0.99f,
+                )
+            )
+            runCurrent()
+
+            assertEquals(3, values.size)
+            val model1percent = values[1]
+            val model99percent = values[2]
+
+            try {
+                // We should initially have an alpha of 0f when unlocking, so the surface is not
+                // visible
+                // while lockscreen UI animates out.
+                assertEquals(0f, model1percent!!.alpha)
+
+                // By the end it should probably be visible.
+                assertTrue(model99percent!!.alpha > 0f)
+            } catch (e: NullPointerException) {
+                fail("surfaceBehindModel was unexpectedly null.")
+            }
+        }
+}
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
new file mode 100644
index 0000000..85bc374
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.FakeFeatureFlags
+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 dagger.Lazy
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import junit.framework.Assert.fail
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromPrimaryBouncerTransitionInteractorTest : KeyguardTransitionInteractorTestCase() {
+    private lateinit var underTest: FromPrimaryBouncerTransitionInteractor
+
+    // Override the fromPrimaryBouncerTransitionInteractor provider from the superclass so our
+    // underTest interactor is provided to any classes that need it.
+    override var fromPrimaryBouncerTransitionInteractorLazy:
+        Lazy<FromPrimaryBouncerTransitionInteractor>? =
+        Lazy {
+            underTest
+        }
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+
+        underTest =
+            FromPrimaryBouncerTransitionInteractor(
+                transitionRepository = super.transitionRepository,
+                transitionInteractor = super.transitionInteractor,
+                scope = super.testScope.backgroundScope,
+                keyguardInteractor = super.keyguardInteractor,
+                flags = FakeFeatureFlags(),
+                keyguardSecurityModel = mock(),
+            )
+    }
+
+    @Test
+    fun testSurfaceBehindVisibility() =
+        testScope.runTest {
+            val values by collectValues(underTest.surfaceBehindVisibility)
+            runCurrent()
+
+            // Transition-specific surface visibility should be null ("don't care") initially.
+            assertEquals(
+                listOf(
+                    null,
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    null, // PRIMARY_BOUNCER -> LOCKSCREEN does not have any specific visibility.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                )
+            )
+
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                    value = 0.01f,
+                )
+            )
+
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    null,
+                    false, // Surface is only made visible once the bouncer UI animates out.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                    value = 0.99f,
+                )
+            )
+
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    null,
+                    false,
+                    true, // Surface should eventually be visible.
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testSurfaceBehindModel() =
+        testScope.runTest {
+            val values by collectValues(underTest.surfaceBehindModel)
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    null, // PRIMARY_BOUNCER -> LOCKSCREEN does not have specific view params.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                    value = 0.01f,
+                )
+            )
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                    value = 0.99f,
+                )
+            )
+            runCurrent()
+
+            assertEquals(3, values.size)
+            val model1percent = values[1]
+            val model99percent = values[2]
+
+            try {
+                // We should initially have an alpha of 0f when unlocking, so the surface is not
+                // visible
+                // while lockscreen UI animates out.
+                assertEquals(0f, model1percent!!.alpha)
+
+                // By the end it should probably be visible.
+                assertTrue(model99percent!!.alpha > 0f)
+            } catch (e: NullPointerException) {
+                fail("surfaceBehindModel was unexpectedly null.")
+            }
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
new file mode 100644
index 0000000..fdcc66b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class KeyguardSurfaceBehindInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardSurfaceBehindInteractor
+    private lateinit var repository: FakeKeyguardSurfaceBehindRepository
+
+    @Mock
+    private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
+    @Mock
+    private lateinit var fromPrimaryBouncerTransitionInteractor:
+        FromPrimaryBouncerTransitionInteractor
+
+    private val lockscreenSurfaceBehindModel = KeyguardSurfaceBehindModel(alpha = 0.33f)
+    private val primaryBouncerSurfaceBehindModel = KeyguardSurfaceBehindModel(alpha = 0.66f)
+
+    private val testScope = TestScope()
+
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+    private lateinit var transitionInteractor: KeyguardTransitionInteractor
+
+    @Before
+    fun setUp() {
+        initMocks(this)
+
+        whenever(fromLockscreenTransitionInteractor.surfaceBehindModel)
+            .thenReturn(flowOf(lockscreenSurfaceBehindModel))
+        whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindModel)
+            .thenReturn(flowOf(primaryBouncerSurfaceBehindModel))
+
+        transitionRepository = FakeKeyguardTransitionRepository()
+
+        transitionInteractor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = testScope.backgroundScope,
+                    repository = transitionRepository,
+                )
+                .keyguardTransitionInteractor
+
+        repository = FakeKeyguardSurfaceBehindRepository()
+        underTest =
+            KeyguardSurfaceBehindInteractor(
+                repository = repository,
+                fromLockscreenInteractor = fromLockscreenTransitionInteractor,
+                fromPrimaryBouncerInteractor = fromPrimaryBouncerTransitionInteractor,
+                transitionInteractor = transitionInteractor,
+            )
+    }
+
+    @Test
+    fun viewParamsSwitchToCorrectFlow() =
+        testScope.runTest {
+            val values by collectValues(underTest.viewParams)
+
+            // Start on the LOCKSCREEN.
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+
+            runCurrent()
+
+            // We're on LOCKSCREEN; we should be using the default params.
+            assertEquals(1, values.size)
+            assertTrue(values[0].alpha == 0f)
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+
+            runCurrent()
+
+            // We're going from LOCKSCREEN -> GONE, we should be using the lockscreen interactor's
+            // surface behind model.
+            assertEquals(2, values.size)
+            assertEquals(values[1], lockscreenSurfaceBehindModel)
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                )
+            )
+
+            runCurrent()
+
+            // We're going from PRIMARY_BOUNCER -> GONE, we should be using the bouncer interactor's
+            // surface behind model.
+            assertEquals(3, values.size)
+            assertEquals(values[2], primaryBouncerSurfaceBehindModel)
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                )
+            )
+
+            runCurrent()
+
+            // Once PRIMARY_BOUNCER -> GONE finishes, we should be using default params, which is
+            // alpha=1f when we're GONE.
+            assertEquals(4, values.size)
+            assertEquals(1f, values[3].alpha)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
new file mode 100644
index 0000000..8db19ae
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.util.mockito.mock
+import dagger.Lazy
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+
+open class KeyguardTransitionInteractorTestCase : SysuiTestCase() {
+    val testDispatcher = StandardTestDispatcher()
+    val testScope = TestScope(testDispatcher)
+
+    lateinit var keyguardRepository: FakeKeyguardRepository
+    lateinit var transitionRepository: FakeKeyguardTransitionRepository
+
+    lateinit var keyguardInteractor: KeyguardInteractor
+    lateinit var transitionInteractor: KeyguardTransitionInteractor
+
+    /**
+     * Replace these lazy providers with non-null ones if you want test dependencies to use a real
+     * instance of the interactor for the test.
+     */
+    open var fromLockscreenTransitionInteractorLazy: Lazy<FromLockscreenTransitionInteractor>? =
+        null
+    open var fromPrimaryBouncerTransitionInteractorLazy:
+        Lazy<FromPrimaryBouncerTransitionInteractor>? =
+        null
+
+    open fun setUp() {
+        keyguardRepository = FakeKeyguardRepository()
+        transitionRepository = FakeKeyguardTransitionRepository()
+
+        keyguardInteractor =
+            KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor
+
+        transitionInteractor =
+            KeyguardTransitionInteractorFactory.create(
+                    repository = transitionRepository,
+                    keyguardInteractor = keyguardInteractor,
+                    scope = testScope.backgroundScope,
+                    fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractorLazy
+                            ?: Lazy { mock() },
+                    fromPrimaryBouncerTransitionInteractor =
+                        fromPrimaryBouncerTransitionInteractorLazy ?: Lazy { mock() },
+                )
+                .also {
+                    fromLockscreenTransitionInteractorLazy = it.fromLockscreenTransitionInteractor
+                    fromPrimaryBouncerTransitionInteractorLazy =
+                        it.fromPrimaryBouncerTransitionInteractor
+                }
+                .keyguardTransitionInteractor
+    }
+}
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 aa6bd4e..4b221a0 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
@@ -104,12 +104,21 @@
 
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
-        featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, true) }
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.FACE_AUTH_REFACTOR, true)
+                set(Flags.KEYGUARD_WM_STATE_REFACTOR, false)
+            }
 
         transitionInteractor =
             KeyguardTransitionInteractorFactory.create(
                     scope = testScope,
                     repository = transitionRepository,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
+                    fromPrimaryBouncerTransitionInteractor = {
+                        fromPrimaryBouncerTransitionInteractor
+                    },
                 )
                 .keyguardTransitionInteractor
 
@@ -119,6 +128,7 @@
                     keyguardInteractor = createKeyguardInteractor(),
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
+                    flags = featureFlags,
                     shadeRepository = shadeRepository,
                 )
                 .apply { start() }
@@ -129,6 +139,7 @@
                     keyguardInteractor = createKeyguardInteractor(),
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
+                    flags = featureFlags,
                     keyguardSecurityModel = keyguardSecurityModel,
                 )
                 .apply { start() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
new file mode 100644
index 0000000..73ecae5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.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.whenever
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: WindowManagerLockscreenVisibilityInteractor
+
+    @Mock private lateinit var surfaceBehindInteractor: KeyguardSurfaceBehindInteractor
+    @Mock
+    private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
+    @Mock
+    private lateinit var fromPrimaryBouncerTransitionInteractor:
+        FromPrimaryBouncerTransitionInteractor
+
+    private val lockscreenSurfaceVisibilityFlow = MutableStateFlow<Boolean?>(false)
+    private val primaryBouncerSurfaceVisibilityFlow = MutableStateFlow<Boolean?>(false)
+    private val surfaceBehindIsAnimatingFlow = MutableStateFlow(false)
+
+    private val testScope = TestScope()
+
+    private lateinit var keyguardInteractor: KeyguardInteractor
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+    private lateinit var transitionInteractor: KeyguardTransitionInteractor
+
+    @Before
+    fun setUp() {
+        initMocks(this)
+
+        whenever(fromLockscreenTransitionInteractor.surfaceBehindVisibility)
+            .thenReturn(lockscreenSurfaceVisibilityFlow)
+        whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindVisibility)
+            .thenReturn(primaryBouncerSurfaceVisibilityFlow)
+        whenever(surfaceBehindInteractor.isAnimatingSurface)
+            .thenReturn(surfaceBehindIsAnimatingFlow)
+
+        transitionRepository = FakeKeyguardTransitionRepository()
+
+        transitionInteractor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = testScope.backgroundScope,
+                    repository = transitionRepository,
+                )
+                .also { keyguardInteractor = it.keyguardInteractor }
+                .keyguardTransitionInteractor
+
+        underTest =
+            WindowManagerLockscreenVisibilityInteractor(
+                keyguardInteractor = keyguardInteractor,
+                transitionInteractor = transitionInteractor,
+                surfaceBehindInteractor = surfaceBehindInteractor,
+                fromLockscreenTransitionInteractor,
+                fromPrimaryBouncerTransitionInteractor,
+            )
+    }
+
+    @Test
+    fun surfaceBehindVisibility_switchesToCorrectFlow() =
+        testScope.runTest {
+            val values by collectValues(underTest.surfaceBehindVisibility)
+
+            // Start on LOCKSCREEN.
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // We should start with the surface invisible on LOCKSCREEN.
+                ),
+                values
+            )
+
+            val lockscreenSpecificSurfaceVisibility = true
+            lockscreenSurfaceVisibilityFlow.emit(lockscreenSpecificSurfaceVisibility)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+
+            runCurrent()
+
+            // We started a transition from LOCKSCREEN, we should be using the value emitted by the
+            // lockscreenSurfaceVisibilityFlow.
+            assertEquals(
+                listOf(
+                    false,
+                    lockscreenSpecificSurfaceVisibility,
+                ),
+                values
+            )
+
+            // Go back to LOCKSCREEN, since we won't emit 'true' twice in a row.
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    lockscreenSpecificSurfaceVisibility,
+                    false, // FINISHED (LOCKSCREEN)
+                ),
+                values
+            )
+
+            val bouncerSpecificVisibility = true
+            primaryBouncerSurfaceVisibilityFlow.emit(bouncerSpecificVisibility)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                )
+            )
+
+            runCurrent()
+
+            // We started a transition from PRIMARY_BOUNCER, we should be using the value emitted by
+            // the
+            // primaryBouncerSurfaceVisibilityFlow.
+            assertEquals(
+                listOf(
+                    false,
+                    lockscreenSpecificSurfaceVisibility,
+                    false,
+                    bouncerSpecificVisibility,
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testUsingGoingAwayAnimation_duringTransitionToGone() =
+        testScope.runTest {
+            val values by collectValues(underTest.usingKeyguardGoingAwayAnimation)
+
+            // Start on LOCKSCREEN.
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // Not using the animation when we're just sitting on LOCKSCREEN.
+                ),
+                values
+            )
+
+            surfaceBehindIsAnimatingFlow.emit(true)
+            runCurrent()
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true, // Still true when we're FINISHED -> GONE, since we're still animating.
+                ),
+                values
+            )
+
+            surfaceBehindIsAnimatingFlow.emit(false)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true,
+                    false, // False once the animation ends.
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testNotUsingGoingAwayAnimation_evenWhenAnimating_ifStateIsNotGone() =
+        testScope.runTest {
+            val values by collectValues(underTest.usingKeyguardGoingAwayAnimation)
+
+            // Start on LOCKSCREEN.
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // Not using the animation when we're just sitting on LOCKSCREEN.
+                ),
+                values
+            )
+
+            surfaceBehindIsAnimatingFlow.emit(true)
+            runCurrent()
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true, // We're happily animating while transitioning to gone.
+                ),
+                values
+            )
+
+            // Oh no, we're still surfaceBehindAnimating=true, but no longer transitioning to GONE.
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true,
+                    false, // Despite the animator still running, this should be false.
+                ),
+                values
+            )
+
+            surfaceBehindIsAnimatingFlow.emit(false)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true,
+                    false, // The animator ending should have no effect.
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun lockscreenVisibility_visibleWhenGone() =
+        testScope.runTest {
+            val values by collectValues(underTest.lockscreenVisibility)
+
+            // Start on LOCKSCREEN.
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    true, // Unsurprisingly, we should start with the lockscreen visible on
+                    // LOCKSCREEN.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    true, // Lockscreen remains visible while we're transitioning to GONE.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    true,
+                    false, // Once we're fully GONE, the lockscreen should not be visible.
+                ),
+                values
+            )
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
new file mode 100644
index 0000000..a22f603
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import android.testing.TestableLooper.RunWithLooper
+import android.view.RemoteAnimationTarget
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.AnimatorTestRule
+import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertNull
+import junit.framework.Assert.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.doAnswer
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@RunWithLooper(setAsMainLooper = true)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class KeyguardSurfaceBehindParamsApplierTest : SysuiTestCase() {
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
+    private lateinit var underTest: KeyguardSurfaceBehindParamsApplier
+    private lateinit var executor: FakeExecutor
+
+    @Mock private lateinit var keyguardViewController: KeyguardViewController
+
+    @Mock private lateinit var interactor: KeyguardSurfaceBehindInteractor
+
+    @Mock private lateinit var remoteAnimationTarget: RemoteAnimationTarget
+
+    private var isAnimatingSurface: Boolean? = null
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        executor = FakeExecutor(FakeSystemClock())
+        underTest =
+            KeyguardSurfaceBehindParamsApplier(
+                executor = executor,
+                keyguardViewController = keyguardViewController,
+                interactor = interactor,
+            )
+
+        doAnswer {
+                (it.arguments[0] as Boolean).let { animating -> isAnimatingSurface = animating }
+            }
+            .whenever(interactor)
+            .setAnimatingSurface(anyBoolean())
+    }
+
+    @After
+    fun tearDown() {
+        animatorTestRule.advanceTimeBy(1000.toLong())
+    }
+
+    @Test
+    fun testNotAnimating_setParamsWithNoAnimation() {
+        underTest.viewParams =
+            KeyguardSurfaceBehindModel(
+                alpha = 0.3f,
+                translationY = 300f,
+            )
+
+        // A surface has not yet been provided, so we shouldn't have set animating to false OR true
+        // just yet.
+        assertNull(isAnimatingSurface)
+
+        underTest.applyParamsToSurface(remoteAnimationTarget)
+
+        // We should now explicitly not be animating the surface.
+        assertFalse(checkNotNull(isAnimatingSurface))
+    }
+
+    @Test
+    fun testAnimating_paramsThenSurfaceProvided() {
+        underTest.viewParams =
+            KeyguardSurfaceBehindModel(
+                animateFromAlpha = 0f,
+                alpha = 0.3f,
+                animateFromTranslationY = 0f,
+                translationY = 300f,
+            )
+
+        // A surface has not yet been provided, so we shouldn't have set animating to false OR true
+        // just yet.
+        assertNull(isAnimatingSurface)
+
+        underTest.applyParamsToSurface(remoteAnimationTarget)
+
+        // We should now be animating the surface.
+        assertTrue(checkNotNull(isAnimatingSurface))
+    }
+
+    @Test
+    fun testAnimating_surfaceThenParamsProvided() {
+        underTest.applyParamsToSurface(remoteAnimationTarget)
+
+        // The default params (which do not animate) should have been applied, so we're explicitly
+        // NOT animating yet.
+        assertFalse(checkNotNull(isAnimatingSurface))
+
+        underTest.viewParams =
+            KeyguardSurfaceBehindModel(
+                animateFromAlpha = 0f,
+                alpha = 0.3f,
+                animateFromTranslationY = 0f,
+                translationY = 300f,
+            )
+
+        // We should now be animating the surface.
+        assertTrue(checkNotNull(isAnimatingSurface))
+    }
+
+    @Test
+    fun testAnimating_thenReleased_animatingIsFalse() {
+        underTest.viewParams =
+            KeyguardSurfaceBehindModel(
+                animateFromAlpha = 0f,
+                alpha = 0.3f,
+                animateFromTranslationY = 0f,
+                translationY = 300f,
+            )
+        underTest.applyParamsToSurface(remoteAnimationTarget)
+
+        assertTrue(checkNotNull(isAnimatingSurface))
+
+        underTest.notifySurfaceReleased()
+
+        // Releasing the surface should immediately cancel animators.
+        assertFalse(checkNotNull(isAnimatingSurface))
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
new file mode 100644
index 0000000..623c877
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import android.app.IActivityTaskManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
+    private lateinit var underTest: WindowManagerLockscreenVisibilityManager
+    private lateinit var executor: FakeExecutor
+
+    @Mock private lateinit var activityTaskManagerService: IActivityTaskManager
+
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+
+    @Mock private lateinit var keyguardSurfaceBehindAnimator: KeyguardSurfaceBehindParamsApplier
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        executor = FakeExecutor(FakeSystemClock())
+
+        underTest =
+            WindowManagerLockscreenVisibilityManager(
+                executor = executor,
+                activityTaskManagerService = activityTaskManagerService,
+                keyguardStateController = keyguardStateController,
+                keyguardSurfaceBehindAnimator = keyguardSurfaceBehindAnimator,
+            )
+    }
+
+    @Test
+    fun testLockscreenVisible_andAodVisible() {
+        underTest.setLockscreenShown(true)
+        underTest.setAodVisible(true)
+
+        verify(activityTaskManagerService).setLockScreenShown(true, true)
+        verifyNoMoreInteractions(activityTaskManagerService)
+    }
+
+    @Test
+    fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible() {
+        underTest.setLockscreenShown(true)
+        underTest.setAodVisible(true)
+
+        verify(activityTaskManagerService).setLockScreenShown(true, true)
+        verifyNoMoreInteractions(activityTaskManagerService)
+
+        underTest.setSurfaceBehindVisibility(true)
+
+        verify(activityTaskManagerService).keyguardGoingAway(anyInt())
+        verifyNoMoreInteractions(activityTaskManagerService)
+    }
+
+    @Test
+    fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway() {
+        underTest.setLockscreenShown(false)
+        underTest.setAodVisible(false)
+
+        verify(activityTaskManagerService).setLockScreenShown(false, false)
+        verifyNoMoreInteractions(activityTaskManagerService)
+
+        underTest.setSurfaceBehindVisibility(true)
+
+        verifyNoMoreInteractions(activityTaskManagerService)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index c67f535..bfc6f31 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -21,9 +21,7 @@
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
index 80ab418..0ad14d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
@@ -83,16 +83,21 @@
         bouncerRepository = FakeKeyguardBouncerRepository()
         transitionRepository = FakeKeyguardTransitionRepository()
         shadeRepository = FakeShadeRepository()
-        val transitionInteractor =
-            KeyguardTransitionInteractor(
-                transitionRepository,
-                testScope.backgroundScope,
-            )
         val keyguardInteractor =
             KeyguardInteractorFactory.create(
+                    repository = keyguardRepository,
                     featureFlags = featureFlags,
                 )
                 .keyguardInteractor
+
+        val transitionInteractor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = testScope.backgroundScope,
+                    repository = transitionRepository,
+                    keyguardInteractor = keyguardInteractor,
+                )
+                .keyguardTransitionInteractor
+
         underTest =
             FingerprintViewModel(
                 context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
index 0456824..edcaa1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -98,15 +98,20 @@
                 bouncerRepository = it.bouncerRepository
             }
 
+        val transitionInteractor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = testScope.backgroundScope,
+                    repository = transitionRepository,
+                    keyguardInteractor = keyguardInteractor,
+                )
+                .keyguardTransitionInteractor
+
         underTest =
             UdfpsLockscreenViewModel(
                 context,
                 lockscreenColorResId,
                 alternateBouncerResId,
-                KeyguardTransitionInteractor(
-                    transitionRepository,
-                    testScope.backgroundScope,
-                ),
+                transitionInteractor,
                 UdfpsKeyguardInteractor(
                     configRepository,
                     BurnInInteractor(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index 5b8272b0..ef51e47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -30,6 +30,7 @@
 import com.android.internal.logging.InstanceId
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.keyguard.TestScopeProvider
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
@@ -37,6 +38,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -66,7 +68,6 @@
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -132,7 +133,7 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        context.resources.configuration.locales = LocaleList(Locale.US, Locale.UK)
+        context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK))
         transitionRepository = FakeKeyguardTransitionRepository()
         mediaCarouselController =
             MediaCarouselController(
@@ -152,7 +153,11 @@
                 debugLogger,
                 mediaFlags,
                 keyguardUpdateMonitor,
-                KeyguardTransitionInteractor(transitionRepository, TestScope().backgroundScope),
+                KeyguardTransitionInteractorFactory.create(
+                        scope = TestScopeProvider.getTestScope().backgroundScope,
+                        repository = transitionRepository,
+                    )
+                    .keyguardTransitionInteractor,
                 globalSettings
             )
         verify(configurationController).addCallback(capture(configListener))
@@ -730,13 +735,13 @@
 
     @Test
     fun testOnLocaleListChanged_playersAreAddedBack() {
-        context.resources.configuration.locales = LocaleList(Locale.US, Locale.UK, Locale.CANADA)
+        context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK, Locale.CANADA))
         testConfigurationChange(configListener.value::onLocaleListChanged)
 
         verify(pageIndicator, never()).tintList =
             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
 
-        context.resources.configuration.locales = LocaleList(Locale.UK, Locale.US, Locale.CANADA)
+        context.resources.configuration.setLocales(LocaleList(Locale.UK, Locale.US, Locale.CANADA))
         testConfigurationChange(configListener.value::onLocaleListChanged)
 
         verify(pageIndicator).tintList =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
index ee3b80a..906420d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
@@ -123,7 +123,7 @@
 
     private fun givenDisplay(width: Int, height: Int, isTablet: Boolean = false) {
         val bounds = Rect(0, 0, width, height)
-        val windowMetrics = WindowMetrics(bounds, null)
+        val windowMetrics = WindowMetrics(bounds, { null }, 1.0f)
         whenever(windowManager.maximumWindowMetrics).thenReturn(windowMetrics)
         whenever(windowManager.currentWindowMetrics).thenReturn(windowMetrics)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
index 3a74c72..7bd97ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -108,20 +108,6 @@
         }
 
     @Test
-    fun mediaProjectionState_onSessionSet_tokenNull_emitsEntireScreen() =
-        testScope.runTest {
-            val state by collectLastValue(repo.mediaProjectionState)
-            runCurrent()
-
-            fakeMediaProjectionManager.dispatchOnSessionSet(
-                session =
-                    ContentRecordingSession.createTaskSession(/* taskWindowContainerToken= */ null)
-            )
-
-            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
-        }
-
-    @Test
     fun mediaProjectionState_sessionSet_taskWithToken_noMatchingRunningTask_emitsEntireScreen() =
         testScope.runTest {
             val state by collectLastValue(repo.mediaProjectionState)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index fab1de0..2d3dc58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -73,7 +73,7 @@
                 context,
                 windowManager,
                 ViewConfiguration.get(context),
-                Handler.createAsync(Looper.myLooper()),
+                Handler.createAsync(checkNotNull(Looper.myLooper())),
                 vibratorHelper,
                 configurationController,
                 latencyTracker,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index d933b57..1536c17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -69,6 +69,7 @@
 import com.android.wm.shell.bubbles.Bubbles
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
+import kotlin.test.assertNotNull
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -672,7 +673,7 @@
             extras().bool(EXTRA_USE_STYLUS_MODE).isTrue()
         }
         iconCaptor.value?.let { icon ->
-            assertThat(icon).isNotNull()
+            assertNotNull(icon)
             assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget)
         }
     }
@@ -755,7 +756,7 @@
             assertThat(shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL)
             assertThat(longLabel).isEqualTo(NOTE_TASK_LONG_LABEL)
             assertThat(isLongLived).isEqualTo(true)
-            assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget)
+            assertThat(icon?.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget)
             assertThat(extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE))
                 .isEqualTo(NOTE_TASK_PACKAGE_NAME)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 8caf6dc..53c04cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.settings.FakeDisplayTracker
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -120,6 +121,13 @@
             bouncerInteractor = bouncerInteractor,
         )
 
+    private val shadeSceneViewModel =
+        ShadeSceneViewModel(
+            applicationScope = testScope.backgroundScope,
+            authenticationInteractor = authenticationInteractor,
+            bouncerInteractor = bouncerInteractor,
+        )
+
     private val keyguardRepository = utils.keyguardRepository()
     private val keyguardInteractor =
         utils.keyguardInteractor(
@@ -196,7 +204,44 @@
             assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Gone)
             emulateUserDrivenTransition(
                 to = upDestinationSceneKey,
-                expectedVisible = false,
+            )
+        }
+
+    @Test
+    fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
+        testScope.runTest {
+            val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey)
+            setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe)
+            assertCurrentScene(SceneKey.Lockscreen)
+
+            // Emulate a user swipe to the shade scene.
+            emulateUserDrivenTransition(to = SceneKey.Shade)
+            assertCurrentScene(SceneKey.Shade)
+
+            assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Lockscreen)
+            emulateUserDrivenTransition(
+                to = upDestinationSceneKey,
+            )
+        }
+
+    @Test
+    fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() =
+        testScope.runTest {
+            val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey)
+            setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe)
+            assertCurrentScene(SceneKey.Lockscreen)
+
+            // Emulate a user swipe to dismiss the lockscreen.
+            emulateUserDrivenTransition(to = SceneKey.Gone)
+            assertCurrentScene(SceneKey.Gone)
+
+            // Emulate a user swipe to the shade scene.
+            emulateUserDrivenTransition(to = SceneKey.Shade)
+            assertCurrentScene(SceneKey.Shade)
+
+            assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Gone)
+            emulateUserDrivenTransition(
+                to = upDestinationSceneKey,
             )
         }
 
@@ -379,12 +424,9 @@
      * catching up with the requested scene change (see [emulateUiSceneTransition]).
      *
      * @param to The scene to transition to.
-     * @param expectedVisible Whether [SceneContainerViewModel.isVisible] should be set at the end
-     *   of the UI transition.
      */
     private fun TestScope.emulateUserDrivenTransition(
         to: SceneKey?,
-        expectedVisible: Boolean = true,
     ) {
         checkNotNull(to)
 
@@ -392,7 +434,7 @@
         assertThat(sceneContainerViewModel.currentScene.value.key).isEqualTo(to)
 
         emulateUiSceneTransition(
-            expectedVisible = expectedVisible,
+            expectedVisible = to != SceneKey.Gone,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
index 07feedf..ad6909d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -126,6 +126,7 @@
 
     private fun onSpinnerItemSelected(position: Int) {
         val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner)
-        spinner.onItemSelectedListener.onItemSelected(spinner, mock(), position, /* id= */ 0)
+        checkNotNull(spinner.onItemSelectedListener)
+            .onItemSelected(spinner, mock(), position, /* id= */ 0)
     }
 }
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 112a09b..577b6e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
@@ -584,7 +584,7 @@
     private fun emptyInsets() = mock(WindowInsets::class.java)
 
     private fun WindowInsets.withCutout(): WindowInsets {
-        whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
+        whenever(checkNotNull(displayCutout).safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
         return this
     }
 
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 8d3c4b2..405199e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
@@ -567,7 +567,7 @@
     private fun emptyInsets() = mock(WindowInsets::class.java)
 
     private fun WindowInsets.withCutout(): WindowInsets {
-        whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
+        whenever(checkNotNull(displayCutout).safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
         return this
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
index 2501f85..8f8b840 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -138,19 +138,19 @@
 
     @Before
     fun setup() {
-        whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
+        whenever<Clock>(view.requireViewById(R.id.clock)).thenReturn(clock)
         whenever(clock.context).thenReturn(mockedContext)
 
-        whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
+        whenever<TextView>(view.requireViewById(R.id.date)).thenReturn(date)
         whenever(date.context).thenReturn(mockedContext)
 
-        whenever<ShadeCarrierGroup>(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup)
+        whenever<ShadeCarrierGroup>(view.requireViewById(R.id.carrier_group)).thenReturn(carrierGroup)
 
-        whenever<BatteryMeterView>(view.findViewById(R.id.batteryRemainingIcon))
+        whenever<BatteryMeterView>(view.requireViewById(R.id.batteryRemainingIcon))
             .thenReturn(batteryMeterView)
 
-        whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons)
-        whenever<View>(view.findViewById(R.id.shade_header_system_icons)).thenReturn(systemIcons)
+        whenever<StatusIconContainer>(view.requireViewById(R.id.statusIcons)).thenReturn(statusIcons)
+        whenever<View>(view.requireViewById(R.id.shade_header_system_icons)).thenReturn(systemIcons)
 
         viewContext = Mockito.spy(context)
         whenever(view.context).thenReturn(viewContext)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 7443097..69b9525 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -42,6 +42,7 @@
     private val authenticationInteractor =
         utils.authenticationInteractor(
             repository = utils.authenticationRepository(),
+            sceneInteractor = sceneInteractor,
         )
 
     private val underTest =
@@ -76,6 +77,30 @@
         }
 
     @Test
+    fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            utils.authenticationRepository.setLockscreenEnabled(true)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+            sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason")
+            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason")
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen)
+        }
+
+    @Test
+    fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            utils.authenticationRepository.setLockscreenEnabled(true)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+            sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason")
+            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason")
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
     fun onContentClicked_deviceUnlocked_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
index 58b44ae..19dc72d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
@@ -236,7 +236,8 @@
         `when`(precondition.conditionsMet()).thenReturn(true)
 
         // Given a session is created
-        val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
+        val weatherView =
+            checkNotNull(controller.buildAndConnectWeatherView(fakeParent, customView))
         controller.stateChangeListener.onViewAttachedToWindow(weatherView)
         verify(smartspaceManager).createSmartspaceSession(any())
 
@@ -258,7 +259,8 @@
 
         // Given a session is created
         val customView = Mockito.mock(TestView::class.java)
-        val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
+        val weatherView =
+            checkNotNull(controller.buildAndConnectWeatherView(fakeParent, customView))
         controller.stateChangeListener.onViewAttachedToWindow(weatherView)
         verify(smartspaceManager).createSmartspaceSession(any())
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt
index 724ea02..e4da53a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt
@@ -47,7 +47,7 @@
         processor = MediaArtworkProcessor()
 
         val point = Point()
-        context.display.getSize(point)
+        checkNotNull(context.display).getSize(point)
         screenWidth = point.x
         screenHeight = point.y
     }
@@ -106,4 +106,4 @@
         // THEN the processed bitmap is null
         assertThat(background).isNull()
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 2de5705..9036f22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -278,7 +278,7 @@
         `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
 
         // WHEN a connection attempt is made and view is attached
-        val view = controller.buildAndConnectView(fakeParent)
+        val view = controller.buildAndConnectView(fakeParent)!!
         controller.stateChangeListener.onViewAttachedToWindow(view)
 
         // THEN no session is created
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java
similarity index 69%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java
index bc32759..f2207af 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java
@@ -24,97 +24,53 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Person;
 import android.content.Intent;
 import android.graphics.Color;
 import android.os.UserHandle;
-import android.service.notification.StatusBarNotification;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.appops.AppOpsController;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class AppOpsCoordinatorTest extends SysuiTestCase {
-    private static final String TEST_PKG = "test_pkg";
-    private static final int NOTIF_USER_ID = 0;
+public class ColorizedFgsCoordinatorTest extends SysuiTestCase {
 
-    @Mock private ForegroundServiceController mForegroundServiceController;
-    @Mock private AppOpsController mAppOpsController;
+    private static final int NOTIF_USER_ID = 0;
     @Mock private NotifPipeline mNotifPipeline;
 
     private NotificationEntryBuilder mEntryBuilder;
-    private AppOpsCoordinator mAppOpsCoordinator;
-    private NotifFilter mForegroundFilter;
+    private ColorizedFgsCoordinator mColorizedFgsCoordinator;
     private NotifSectioner mFgsSection;
 
-    private FakeSystemClock mClock = new FakeSystemClock();
-    private FakeExecutor mExecutor = new FakeExecutor(mClock);
-
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
         allowTestableLooperAsMainThread();
 
-        mAppOpsCoordinator =
-                new AppOpsCoordinator(
-                        mForegroundServiceController,
-                        mAppOpsController,
-                        mExecutor);
+        mColorizedFgsCoordinator = new ColorizedFgsCoordinator();
 
         mEntryBuilder = new NotificationEntryBuilder()
                 .setUser(new UserHandle(NOTIF_USER_ID));
 
-        mAppOpsCoordinator.attach(mNotifPipeline);
+        mColorizedFgsCoordinator.attach(mNotifPipeline);
 
-        // capture filter
-        ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class);
-        verify(mNotifPipeline, times(1)).addPreGroupFilter(filterCaptor.capture());
-        mForegroundFilter = filterCaptor.getValue();
-
-        mFgsSection = mAppOpsCoordinator.getSectioner();
-    }
-
-    @Test
-    public void filterTest_disclosureUnnecessary() {
-        NotificationEntry entry = mEntryBuilder.build();
-        StatusBarNotification sbn = entry.getSbn();
-
-        // GIVEN the notification is a disclosure notification
-        when(mForegroundServiceController.isDisclosureNotification(sbn)).thenReturn(true);
-
-        // GIVEN the disclosure isn't needed for this user
-        when(mForegroundServiceController.isDisclosureNeededForUser(sbn.getUserId()))
-                .thenReturn(false);
-
-        // THEN filter out the notification
-        assertTrue(mForegroundFilter.shouldFilterOut(entry, 0));
+        mFgsSection = mColorizedFgsCoordinator.getSectioner();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
index 03d3854..56d2397 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
@@ -212,13 +212,13 @@
     @Test
     fun localeListChanged_listenerNotified() {
         val config = mContext.resources.configuration
-        config.locales = LocaleList(Locale.CANADA, Locale.GERMANY)
+        config.setLocales(LocaleList(Locale.CANADA, Locale.GERMANY))
         mConfigurationController.onConfigurationChanged(config)
 
         val listener = createAndAddListener()
 
         // WHEN the locales are updated
-        config.locales = LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE)
+        config.setLocales(LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE))
         mConfigurationController.onConfigurationChanged(config)
 
         // THEN the listener is notified
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
index 1759fb7..210c5ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
@@ -463,10 +463,10 @@
         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
             mock(DumpManager::class.java))
 
-        configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160)
+        configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
         val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
 
-        configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600)
+        configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600))
 
         // WHEN: get insets on the second display
         val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
@@ -482,14 +482,14 @@
         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
             mock(DumpManager::class.java))
 
-        configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160)
+        configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
         val firstDisplayInsetsFirstCall = provider
             .getStatusBarContentAreaForRotation(ROTATION_NONE)
 
-        configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600)
+        configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600))
         provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
 
-        configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160)
+        configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
 
         // WHEN: get insets on the first display again
         val firstDisplayInsetsSecondCall = provider
@@ -577,4 +577,4 @@
                 " expected=$expected actual=$actual",
                 expected.equals(actual))
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index ed9cf3f..0da7360 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -35,6 +35,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
+
 import android.service.trust.TrustAgentService;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -73,6 +75,8 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.navigationbar.TaskbarDelegate;
 import com.android.systemui.plugins.ActivityStarter;
@@ -201,7 +205,10 @@
                         mBouncerView,
                         mAlternateBouncerInteractor,
                         mUdfpsOverlayInteractor,
-                        mActivityStarter) {
+                        mActivityStarter,
+                        mock(KeyguardTransitionInteractor.class),
+                        StandardTestDispatcher(null, null),
+                        () -> mock(WindowManagerLockscreenVisibilityInteractor.class)) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
@@ -701,7 +708,10 @@
                         mBouncerView,
                         mAlternateBouncerInteractor,
                         mUdfpsOverlayInteractor,
-                        mActivityStarter) {
+                        mActivityStarter,
+                        mock(KeyguardTransitionInteractor.class),
+                        StandardTestDispatcher(null, null),
+                        () -> mock(WindowManagerLockscreenVisibilityInteractor.class)) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index 9c52788..34c4ac1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -32,7 +32,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.testing.FakeMetricsLogger;
-import com.android.systemui.ForegroundServiceNotificationListener;
 import com.android.systemui.InitController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter;
@@ -94,7 +93,6 @@
         mDependency.injectTestDependency(ShadeController.class, mShadeController);
         mDependency.injectMockDependency(NotificationRemoteInputManager.Callback.class);
         mDependency.injectMockDependency(NotificationShadeWindowController.class);
-        mDependency.injectMockDependency(ForegroundServiceNotificationListener.class);
 
         NotificationShadeWindowView notificationShadeWindowView =
                 mock(NotificationShadeWindowView.class);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt
new file mode 100644
index 0000000..823f29a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.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.keyguard.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeKeyguardSurfaceBehindRepository : KeyguardSurfaceBehindRepository {
+    private val _isAnimatingSurface = MutableStateFlow(false)
+    override val isAnimatingSurface = _isAnimatingSurface.asStateFlow()
+
+    override fun setAnimatingSurface(animating: Boolean) {
+        _isAnimatingSurface.value = animating
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt
index 312ade5..23faaf3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt
@@ -18,6 +18,8 @@
 
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.util.mockito.mock
+import dagger.Lazy
 import kotlinx.coroutines.CoroutineScope
 
 /**
@@ -30,18 +32,36 @@
     fun create(
         scope: CoroutineScope,
         repository: KeyguardTransitionRepository = FakeKeyguardTransitionRepository(),
+        keyguardInteractor: KeyguardInteractor =
+            KeyguardInteractorFactory.create().keyguardInteractor,
+        fromLockscreenTransitionInteractor: Lazy<FromLockscreenTransitionInteractor> = Lazy {
+            mock<FromLockscreenTransitionInteractor>()
+        },
+        fromPrimaryBouncerTransitionInteractor: Lazy<FromPrimaryBouncerTransitionInteractor> =
+            Lazy {
+                mock<FromPrimaryBouncerTransitionInteractor>()
+            },
     ): WithDependencies {
         return WithDependencies(
             repository = repository,
+            keyguardInteractor = keyguardInteractor,
+            fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractor,
+            fromPrimaryBouncerTransitionInteractor = fromPrimaryBouncerTransitionInteractor,
             KeyguardTransitionInteractor(
                 scope = scope,
                 repository = repository,
+                keyguardInteractor = { keyguardInteractor },
+                fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractor,
+                fromPrimaryBouncerTransitionInteractor = fromPrimaryBouncerTransitionInteractor,
             )
         )
     }
 
     data class WithDependencies(
         val repository: KeyguardTransitionRepository,
+        val keyguardInteractor: KeyguardInteractor,
+        val fromLockscreenTransitionInteractor: Lazy<FromLockscreenTransitionInteractor>,
+        val fromPrimaryBouncerTransitionInteractor: Lazy<FromPrimaryBouncerTransitionInteractor>,
         val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     )
 }
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index e19ef10..d6702b7 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -134,6 +134,7 @@
 import android.service.autofill.CompositeUserData;
 import android.service.autofill.Dataset;
 import android.service.autofill.Dataset.DatasetEligibleReason;
+import android.service.autofill.Field;
 import android.service.autofill.FieldClassification;
 import android.service.autofill.FieldClassification.Match;
 import android.service.autofill.FieldClassificationUserData;
@@ -1839,6 +1840,13 @@
         }
     }
 
+    /**
+     * Computes datasets that are eligible to be shown based on provider detections.
+     * Datasets are populated in the provided container for them to be later merged with the
+     * PCC eligible datasets based on preference strategy.
+     * @param response
+     * @param container
+     */
     private void computeDatasetsForProviderAndUpdateContainer(
             FillResponse response, DatasetComputationContainer container) {
         @DatasetEligibleReason int globalPickReason = PICK_REASON_UNKNOWN;
@@ -1939,6 +1947,13 @@
         container.mAutofillIds = eligibleAutofillIds;
     }
 
+    /**
+     * Computes datasets that are eligible to be shown based on PCC detections.
+     * Datasets are populated in the provided container for them to be later merged with the
+     * provider eligible datasets based on preference strategy.
+     * @param response
+     * @param container
+     */
     private void computeDatasetsForPccAndUpdateContainer(
             FillResponse response, DatasetComputationContainer container) {
         List<Dataset> datasets = response.getDatasets();
@@ -1973,11 +1988,33 @@
                 ArrayList<Dataset.DatasetFieldFilter> fieldFilters = new ArrayList<>();
                 Set<AutofillId> datasetAutofillIds = new LinkedHashSet<>();
 
+                boolean isDatasetAvailable = false;
+                Set<AutofillId> additionalDatasetAutofillIds = new LinkedHashSet<>();
+                Set<AutofillId> additionalEligibleAutofillIds = new LinkedHashSet<>();
+
                 for (int j = 0; j < dataset.getAutofillDatatypes().size(); j++) {
                     if (dataset.getAutofillDatatypes().get(j) == null) {
+                        // TODO : revisit pickReason logic
                         if (dataset.getFieldIds() != null && dataset.getFieldIds().get(j) != null) {
                             pickReason = PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER;
                         }
+                        // Check if the autofill id at this index is detected by PCC.
+                        // If not, add that id here, otherwise, we can have duplicates when later
+                        // merging with provider datasets.
+                        // Howover, this doesn't make datasetAvailable for PCC on its own.
+                        // For that, there has to be a datatype detected by PCC, and the dataset
+                        // for that datatype provided by the provider.
+                        AutofillId autofillId = dataset.getFieldIds().get(j);
+                        if (!mClassificationState.mClassificationCombinedHintsMap
+                                .containsKey(autofillId)) {
+                            additionalEligibleAutofillIds.add(autofillId);
+                            additionalDatasetAutofillIds.add(autofillId);
+                            // For each of the field, copy over values.
+                            copyFieldsFromDataset(dataset, j, autofillId, fieldIds, fieldValues,
+                                    fieldPresentations, fieldDialogPresentations,
+                                    fieldInlinePresentations, fieldInlineTooltipPresentations,
+                                    fieldFilters);
+                        }
                         continue;
                     }
                     String hint = dataset.getAutofillDatatypes().get(j);
@@ -1985,22 +2022,18 @@
                     if (hintsToAutofillIdMap.containsKey(hint)) {
                         ArrayList<AutofillId> tempIds =
                                 new ArrayList<>(hintsToAutofillIdMap.get(hint));
-
+                        if (tempIds.isEmpty()) {
+                            continue;
+                        }
+                        isDatasetAvailable = true;
                         for (AutofillId autofillId : tempIds) {
                             eligibleAutofillIds.add(autofillId);
                             datasetAutofillIds.add(autofillId);
                             // For each of the field, copy over values.
-                            fieldIds.add(autofillId);
-                            fieldValues.add(dataset.getFieldValues().get(j));
-                            //  TODO(b/266379948): might need to make it more efficient by not
-                            //  copying over value if it didn't exist. This would require creating
-                            //  a getter for the presentations arraylist.
-                            fieldPresentations.add(dataset.getFieldPresentation(j));
-                            fieldDialogPresentations.add(dataset.getFieldDialogPresentation(j));
-                            fieldInlinePresentations.add(dataset.getFieldInlinePresentation(j));
-                            fieldInlineTooltipPresentations.add(
-                                    dataset.getFieldInlineTooltipPresentation(j));
-                            fieldFilters.add(dataset.getFilter(j));
+                            copyFieldsFromDataset(dataset, j, autofillId, fieldIds, fieldValues,
+                                    fieldPresentations, fieldDialogPresentations,
+                                    fieldInlinePresentations, fieldInlineTooltipPresentations,
+                                    fieldFilters);
                         }
                     }
                     // TODO(b/266379948):  handle the case:
@@ -2009,34 +2042,38 @@
                     // TODO(b/266379948):  also handle the case where there could be more types in
                     // the dataset, provided by the provider, however, they aren't applicable.
                 }
-                Dataset newDataset =
-                        new Dataset(
-                                fieldIds,
-                                fieldValues,
-                                fieldPresentations,
-                                fieldDialogPresentations,
-                                fieldInlinePresentations,
-                                fieldInlineTooltipPresentations,
-                                fieldFilters,
-                                new ArrayList<>(),
-                                dataset.getFieldContent(),
-                                null,
-                                null,
-                                null,
-                                null,
-                                dataset.getId(),
-                                dataset.getAuthentication());
-                newDataset.setEligibleReasonReason(pickReason);
-                eligibleDatasets.add(newDataset);
-                Set<Dataset> newDatasets;
-                for (AutofillId autofillId : datasetAutofillIds) {
-                    if (map.containsKey(autofillId)) {
-                        newDatasets = map.get(autofillId);
-                    } else {
-                        newDatasets = new LinkedHashSet<>();
+                if (isDatasetAvailable) {
+                    datasetAutofillIds.addAll(additionalDatasetAutofillIds);
+                    eligibleAutofillIds.addAll(additionalEligibleAutofillIds);
+                    Dataset newDataset =
+                            new Dataset(
+                                    fieldIds,
+                                    fieldValues,
+                                    fieldPresentations,
+                                    fieldDialogPresentations,
+                                    fieldInlinePresentations,
+                                    fieldInlineTooltipPresentations,
+                                    fieldFilters,
+                                    new ArrayList<>(),
+                                    dataset.getFieldContent(),
+                                    null,
+                                    null,
+                                    null,
+                                    null,
+                                    dataset.getId(),
+                                    dataset.getAuthentication());
+                    newDataset.setEligibleReasonReason(pickReason);
+                    eligibleDatasets.add(newDataset);
+                    Set<Dataset> newDatasets;
+                    for (AutofillId autofillId : datasetAutofillIds) {
+                        if (map.containsKey(autofillId)) {
+                            newDatasets = map.get(autofillId);
+                        } else {
+                            newDatasets = new LinkedHashSet<>();
+                        }
+                        newDatasets.add(newDataset);
+                        map.put(autofillId, newDatasets);
                     }
-                    newDatasets.add(newDataset);
-                    map.put(autofillId, newDatasets);
                 }
             }
             container.mAutofillIds = eligibleAutofillIds;
@@ -2045,6 +2082,31 @@
         }
     }
 
+    private void copyFieldsFromDataset(
+            Dataset dataset,
+            int index,
+            AutofillId autofillId,
+            ArrayList<AutofillId> fieldIds,
+            ArrayList<AutofillValue> fieldValues,
+            ArrayList<RemoteViews> fieldPresentations,
+            ArrayList<RemoteViews> fieldDialogPresentations,
+            ArrayList<InlinePresentation> fieldInlinePresentations,
+            ArrayList<InlinePresentation> fieldInlineTooltipPresentations,
+            ArrayList<Dataset.DatasetFieldFilter> fieldFilters) {
+        // copy over values
+        fieldIds.add(autofillId);
+        fieldValues.add(dataset.getFieldValues().get(index));
+        //  TODO(b/266379948): might need to make it more efficient by not
+        //  copying over value if it didn't exist. This would require creating
+        //  a getter for the presentations arraylist.
+        fieldPresentations.add(dataset.getFieldPresentation(index));
+        fieldDialogPresentations.add(dataset.getFieldDialogPresentation(index));
+        fieldInlinePresentations.add(dataset.getFieldInlinePresentation(index));
+        fieldInlineTooltipPresentations.add(
+                dataset.getFieldInlineTooltipPresentation(index));
+        fieldFilters.add(dataset.getFilter(index));
+    }
+
     // FillServiceCallbacks
     @Override
     @SuppressWarnings("GuardedBy")
@@ -2578,10 +2640,7 @@
                     if (sDebug) Slog.d(TAG,  "Updating client state from auth dataset");
                     mClientState = newClientState;
                 }
-                Dataset dataset = (Dataset) result;
-                FillResponse temp = new FillResponse.Builder().addDataset(dataset).build();
-                temp = getEffectiveFillResponse(temp);
-                dataset = temp.getDatasets().get(0);
+                Dataset dataset = getEffectiveDatasetForAuthentication((Dataset) result);
                 final Dataset oldDataset = authenticatedResponse.getDatasets().get(datasetIdx);
                 if (!isAuthResultDatasetEphemeral(oldDataset, data)) {
                     authenticatedResponse.getDatasets().set(datasetIdx, dataset);
@@ -2607,6 +2666,39 @@
         }
     }
 
+    Dataset getEffectiveDatasetForAuthentication(Dataset authenticatedDataset) {
+        FillResponse response = new FillResponse.Builder().addDataset(authenticatedDataset).build();
+        response = getEffectiveFillResponse(response);
+        if (DBG) {
+            Slog.d(TAG, "DBG: authenticated effective response: " + response);
+        }
+        if (response == null || response.getDatasets().size() == 0) {
+            Log.wtf(TAG, "No datasets in fill response on authentication. response = "
+                    + (response == null ? "null" : response.toString()));
+            return authenticatedDataset;
+        }
+        List<Dataset> datasets = response.getDatasets();
+        Dataset result = response.getDatasets().get(0);
+        if (datasets.size() > 1) {
+            Dataset.Builder builder = new Dataset.Builder();
+            for (Dataset dataset : datasets) {
+                if (!dataset.getFieldIds().isEmpty()) {
+                    for (int i = 0; i < dataset.getFieldIds().size(); i++) {
+                        builder.setField(dataset.getFieldIds().get(i),
+                                new Field.Builder().setValue(dataset.getFieldValues().get(i))
+                                        .build());
+                    }
+                }
+            }
+            result = builder.setId(authenticatedDataset.getId()).build();
+        }
+
+        if (DBG) {
+            Slog.d(TAG, "DBG: authenticated effective dataset after auth: " + result);
+        }
+        return result;
+    }
+
     /**
      * Returns whether the dataset returned from the authentication result is ephemeral or not.
      * See {@link AutofillManager#EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET} for more
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 992c18a..66ea4d0 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -2501,12 +2501,12 @@
                         FGS_STOP_REASON_STOP_FOREGROUND,
                         FGS_TYPE_POLICY_CHECK_UNKNOWN);
 
-                // foregroundServiceType is used in logFGSStateChangeLocked(), so we can't clear it
-                // earlier.
-                r.foregroundServiceType = 0;
                 synchronized (mFGSLogger) {
                     mFGSLogger.logForegroundServiceStop(r.appInfo.uid, r);
                 }
+                // foregroundServiceType is used in logFGSStateChangeLocked(), so we can't clear it
+                // earlier.
+                r.foregroundServiceType = 0;
                 r.mFgsNotificationWasDeferred = false;
                 signalForegroundServiceObserversLocked(r);
                 resetFgsRestrictionLocked(r);
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index 786e1cc..f6859d1 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -141,6 +141,10 @@
         // grab the appropriate types
         final IntArray apiTypes =
                 convertFgsTypeToApiTypes(record.foregroundServiceType);
+        if (apiTypes.size() == 0) {
+            Slog.w(TAG, "Foreground service start for UID: "
+                    + uid + " does not have any types");
+        }
         // now we need to iterate through the types
         // and insert the new record as needed
         final IntArray apiTypesFound = new IntArray();
@@ -201,6 +205,9 @@
         // and also clean up the start calls stack by UID
         final IntArray apiTypes = convertFgsTypeToApiTypes(record.foregroundServiceType);
         final UidState uidState = mUids.get(uid);
+        if (apiTypes.size() == 0) {
+            Slog.w(TAG, "FGS stop call for: " + uid + " has no types!");
+        }
         if (uidState == null) {
             Slog.w(TAG, "FGS stop call being logged with no start call for UID for UID "
                     + uid
@@ -460,16 +467,17 @@
     public void logFgsApiEvent(ServiceRecord r, int fgsState,
             @FgsApiState int apiState,
             @ForegroundServiceApiType int apiType, long timestamp) {
-        long apiDurationBeforeFgsStart = r.createRealTime - timestamp;
-        long apiDurationAfterFgsEnd = timestamp - r.mFgsExitTime;
+        long apiDurationBeforeFgsStart = 0;
+        long apiDurationAfterFgsEnd = 0;
         UidState uidState = mUids.get(r.appInfo.uid);
-        if (uidState != null) {
-            if (uidState.mFirstFgsTimeStamp.contains(apiType)) {
-                apiDurationBeforeFgsStart = uidState.mFirstFgsTimeStamp.get(apiType) - timestamp;
-            }
-            if (uidState.mLastFgsTimeStamp.contains(apiType)) {
-                apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType);
-            }
+        if (uidState == null) {
+            return;
+        }
+        if (uidState.mFirstFgsTimeStamp.contains(apiType)) {
+            apiDurationBeforeFgsStart = uidState.mFirstFgsTimeStamp.get(apiType) - timestamp;
+        }
+        if (uidState.mLastFgsTimeStamp.contains(apiType)) {
+            apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType);
         }
         final int[] apiTypes = new int[1];
         apiTypes[0] = apiType;
@@ -525,10 +533,11 @@
             @ForegroundServiceApiType int apiType, long timestamp) {
         long apiDurationAfterFgsEnd = 0;
         UidState uidState = mUids.get(uid);
-        if (uidState != null) {
-            if (uidState.mLastFgsTimeStamp.contains(apiType)) {
-                apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType);
-            }
+        if (uidState == null) {
+            return;
+        }
+        if (uidState.mLastFgsTimeStamp.contains(apiType)) {
+            apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType);
         }
         final int[] apiTypes = new int[1];
         apiTypes[0] = apiType;
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index f7bbc8b..8a8e2af 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -286,6 +286,12 @@
                 + ")");
     }
 
+    private String getFgsInfoForWtf() {
+        return " cmp: " + this.getComponentName().toShortString()
+                + " sdk: " + this.appInfo.targetSdkVersion
+                ;
+    }
+
     void maybeLogFgsLogicChange() {
         final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
                 mAllowWIUInBindService);
@@ -311,7 +317,8 @@
                 + " OS:" // Orig-start
                 + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService)
                 + " NS:" // New-start
-                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings);
+                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings)
+                + getFgsInfoForWtf();
         Slog.wtf(TAG_SERVICE, message);
     }
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index d8266ec..9f9e2eb 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -435,7 +435,7 @@
         // LE Audio it stays the same and we must trigger the proper stream volume alignment, if
         // LE Audio communication device is activated after the audio system has already switched to
         // MODE_IN_CALL mode.
-        if (isBluetoothLeAudioRequested()) {
+        if (isBluetoothLeAudioRequested() && device != null) {
             final int streamType = mAudioService.getBluetoothContextualVolumeStream();
             final int leAudioVolIndex = getVssVolumeForDevice(streamType, device.getInternalType());
             final int leAudioMaxVolIndex = getMaxVssVolumeForStream(streamType);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 5a92cb4..13e3fc7 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -2070,12 +2070,18 @@
     @GuardedBy("mDevicesLock")
     private void makeLeAudioDeviceAvailable(
             AudioDeviceBroker.BtDeviceInfo btInfo, int streamType, String eventSource) {
-        final String address = btInfo.mDevice.getAddress();
-        final String name = BtHelper.getName(btInfo.mDevice);
         final int volumeIndex = btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10;
         final int device = btInfo.mAudioSystemDevice;
 
         if (device != AudioSystem.DEVICE_NONE) {
+            final String address = btInfo.mDevice.getAddress();
+            String name = BtHelper.getName(btInfo.mDevice);
+
+            // The BT Stack does not provide a name for LE Broadcast devices
+            if (device == AudioSystem.DEVICE_OUT_BLE_BROADCAST && name.equals("")) {
+                name = "Broadcast";
+            }
+
             /* Audio Policy sees Le Audio similar to A2DP. Let's make sure
              * AUDIO_POLICY_FORCE_NO_BT_A2DP is not set
              */
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index cb5e7f1..2ae3118 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -150,6 +150,10 @@
     // Timestamp when hardware authentication occurred
     private long mAuthenticatedTimeMs;
 
+    @NonNull
+    private final OperationContextExt mOperationContext;
+
+
     AuthSession(@NonNull Context context,
             @NonNull BiometricContext biometricContext,
             @NonNull IStatusBarService statusBarService,
@@ -215,6 +219,7 @@
         mFingerprintSensorProperties = fingerprintSensorProperties;
         mCancelled = false;
         mBiometricFrameworkStatsLogger = logger;
+        mOperationContext = new OperationContextExt(true /* isBP */);
 
         try {
             mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);
@@ -581,6 +586,8 @@
         } else {
             Slog.d(TAG, "delaying fingerprint sensor start");
         }
+
+        mBiometricContext.updateContext(mOperationContext, isCrypto());
     }
 
     // call once anytime after onDialogAnimatedIn() to indicate it's appropriate to start the
@@ -743,12 +750,12 @@
                         + ", Client: " + BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT
                         + ", RequireConfirmation: " + mPreAuthInfo.confirmationRequested
                         + ", State: " + FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__CONFIRMED
-                        + ", Latency: " + latency);
+                        + ", Latency: " + latency
+                        + ", SessionId: " + mOperationContext.getId());
             }
 
             mBiometricFrameworkStatsLogger.authenticate(
-                    mBiometricContext.updateContext(new OperationContextExt(true /* isBP */),
-                            isCrypto()),
+                    mOperationContext,
                     statsModality(),
                     BiometricsProtoEnums.ACTION_UNKNOWN,
                     BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT,
@@ -780,13 +787,13 @@
                         + ", Client: " + BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT
                         + ", Reason: " + reason
                         + ", Error: " + error
-                        + ", Latency: " + latency);
+                        + ", Latency: " + latency
+                        + ", SessionId: " + mOperationContext.getId());
             }
             // Auth canceled
             if (error != 0) {
                 mBiometricFrameworkStatsLogger.error(
-                        mBiometricContext.updateContext(new OperationContextExt(true /* isBP */),
-                                isCrypto()),
+                        mOperationContext,
                         statsModality(),
                         BiometricsProtoEnums.ACTION_AUTHENTICATE,
                         BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT,
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStats.java b/services/core/java/com/android/server/biometrics/AuthenticationStats.java
new file mode 100644
index 0000000..137a418
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStats.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+/**
+ * Utility class for on-device biometric authentication data, including total authentication,
+ * rejections, and the number of sent enrollment notifications.
+ */
+public class AuthenticationStats {
+
+    private final int mUserId;
+    private int mTotalAttempts;
+    private int mRejectedAttempts;
+    private int mEnrollmentNotifications;
+    private final int mModality;
+
+    public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts,
+            int enrollmentNotifications, final int modality) {
+        mUserId = userId;
+        mTotalAttempts = totalAttempts;
+        mRejectedAttempts = rejectedAttempts;
+        mEnrollmentNotifications = enrollmentNotifications;
+        mModality = modality;
+    }
+
+    public AuthenticationStats(final int userId, final int modality) {
+        mUserId = userId;
+        mTotalAttempts = 0;
+        mRejectedAttempts = 0;
+        mEnrollmentNotifications = 0;
+        mModality = modality;
+    }
+
+    public int getUserId() {
+        return mUserId;
+    }
+
+    public int getTotalAttempts() {
+        return mTotalAttempts;
+    }
+
+    public int getRejectedAttempts() {
+        return mRejectedAttempts;
+    }
+
+    public int getEnrollmentNotifications() {
+        return mEnrollmentNotifications;
+    }
+
+    public int getModality() {
+        return mModality;
+    }
+
+    /** Calculate FRR. */
+    public float getFrr() {
+        if (mTotalAttempts > 0) {
+            return mRejectedAttempts / (float) mTotalAttempts;
+        } else {
+            return -1.0f;
+        }
+    }
+
+    /** Update total authentication attempts and rejections. */
+    public void authenticate(boolean authenticated) {
+        if (!authenticated) {
+            mRejectedAttempts++;
+        }
+        mTotalAttempts++;
+    }
+
+    /** Reset total authentication attempts and rejections. */
+    public void resetData() {
+        mTotalAttempts = 0;
+        mRejectedAttempts = 0;
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
new file mode 100644
index 0000000..c9cd814
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Calculate and collect on-device False Rejection Rates (FRR).
+ * FRR = All [given biometric modality] unlock failures / all [given biometric modality] unlock
+ * attempts.
+ */
+public class AuthenticationStatsCollector {
+
+    private static final String TAG = "AuthenticationStatsCollector";
+
+    // The minimum number of attempts that will calculate the FRR and trigger the notification.
+    private static final int MINIMUM_ATTEMPTS = 500;
+    // The maximum number of eligible biometric enrollment notification can be sent.
+    private static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 2;
+
+    private final float mThreshold;
+    private final int mModality;
+
+    @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap;
+
+    public AuthenticationStatsCollector(@NonNull Context context, int modality) {
+        mThreshold = context.getResources()
+                .getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1);
+        mUserAuthenticationStatsMap = new HashMap<>();
+        mModality = modality;
+    }
+
+    /** Update total authentication and rejected attempts. */
+    public void authenticate(int userId, boolean authenticated) {
+        // Check if this is a new user.
+        if (!mUserAuthenticationStatsMap.containsKey(userId)) {
+            mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality));
+        }
+
+        AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
+
+        authenticationStats.authenticate(authenticated);
+
+        persistDataIfNeeded(userId);
+        sendNotificationIfNeeded(userId);
+    }
+
+    private void sendNotificationIfNeeded(int userId) {
+        AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
+        if (authenticationStats.getTotalAttempts() >= MINIMUM_ATTEMPTS) {
+            // Send notification if FRR exceeds the threshold
+            if (authenticationStats.getEnrollmentNotifications() < MAXIMUM_ENROLLMENT_NOTIFICATIONS
+                    && authenticationStats.getFrr() >= mThreshold) {
+                // TODO(wenhuiy): Send notifications.
+            }
+
+            authenticationStats.resetData();
+        }
+    }
+
+    private void persistDataIfNeeded(int userId) {
+        AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
+        if (authenticationStats.getTotalAttempts() % 50 == 0) {
+            // TODO(wenhuiy): Persist data.
+        }
+    }
+
+    /**
+     * Only being used in tests. Callers should not make any changes to the returned
+     * authentication stats.
+     *
+     * @return AuthenticationStats of the user, or null if the stats doesn't exist.
+     */
+    @Nullable
+    @VisibleForTesting
+    AuthenticationStats getAuthenticationStatsForUser(int userId) {
+        return mUserAuthenticationStatsMap.getOrDefault(userId, null);
+    }
+
+    @VisibleForTesting
+    void setAuthenticationStatsForUser(int userId, AuthenticationStats authenticationStats) {
+        mUserAuthenticationStatsMap.put(userId, authenticationStats);
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
index c76a2e3..87037af 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
@@ -27,6 +27,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.biometrics.AuthenticationStatsCollector;
 import com.android.server.biometrics.Utils;
 
 /**
@@ -41,6 +42,7 @@
     private final int mStatsAction;
     private final int mStatsClient;
     private final BiometricFrameworkStatsLogger mSink;
+    @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector;
     @NonNull private final ALSProbe mALSProbe;
 
     private long mFirstAcquireTimeMs;
@@ -49,7 +51,8 @@
     /** Get a new logger with all unknown fields (for operations that do not require logs). */
     public static BiometricLogger ofUnknown(@NonNull Context context) {
         return new BiometricLogger(context, BiometricsProtoEnums.MODALITY_UNKNOWN,
-                BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN);
+                BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN,
+                null /* AuthenticationStatsCollector */);
     }
 
     /**
@@ -64,26 +67,32 @@
      * @param statsClient One of {@link BiometricsProtoEnums} CLIENT_* constants.
      */
     public BiometricLogger(
-            @NonNull Context context, int statsModality, int statsAction, int statsClient) {
+            @NonNull Context context, int statsModality, int statsAction, int statsClient,
+            AuthenticationStatsCollector authenticationStatsCollector) {
         this(statsModality, statsAction, statsClient,
                 BiometricFrameworkStatsLogger.getInstance(),
+                authenticationStatsCollector,
                 context.getSystemService(SensorManager.class));
     }
 
     @VisibleForTesting
     BiometricLogger(
             int statsModality, int statsAction, int statsClient,
-            BiometricFrameworkStatsLogger logSink, SensorManager sensorManager) {
+            BiometricFrameworkStatsLogger logSink,
+            @NonNull AuthenticationStatsCollector statsCollector,
+            SensorManager sensorManager) {
         mStatsModality = statsModality;
         mStatsAction = statsAction;
         mStatsClient = statsClient;
         mSink = logSink;
+        mAuthenticationStatsCollector = statsCollector;
         mALSProbe = new ALSProbe(sensorManager);
     }
 
     /** Creates a new logger with the action replaced with the new action. */
     public BiometricLogger swapAction(@NonNull Context context, int statsAction) {
-        return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient);
+        return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient,
+                null /* AuthenticationStatsCollector */);
     }
 
     /** Disable logging metrics and only log critical events, such as system health issues. */
@@ -192,10 +201,13 @@
     public void logOnAuthenticated(Context context, OperationContextExt operationContext,
             boolean authenticated, boolean requireConfirmation, int targetUserId,
             boolean isBiometricPrompt) {
+        // Do not log metrics when fingerprint enrollment reason is ENROLL_FIND_SENSOR
         if (!mShouldLogMetrics) {
             return;
         }
 
+        mAuthenticationStatsCollector.authenticate(targetUserId, authenticated);
+
         int authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__UNKNOWN;
         if (!authenticated) {
             authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__REJECTED;
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 33ed63c..a7d160c 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
@@ -49,6 +49,7 @@
 import android.view.Surface;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.AuthenticationStatsCollector;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -112,6 +113,8 @@
     private final BiometricContext mBiometricContext;
     @NonNull
     private final AuthSessionCoordinator mAuthSessionCoordinator;
+    @NonNull
+    private final AuthenticationStatsCollector mAuthenticationStatsCollector;
     @Nullable
     private IFace mDaemon;
 
@@ -173,6 +176,9 @@
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
         mDaemon = daemon;
 
+        mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+                BiometricsProtoEnums.MODALITY_FACE);
+
         for (SensorProps prop : props) {
             final int sensorId = prop.commonProps.sensorId;
 
@@ -283,7 +289,8 @@
                     mContext, mFaceSensors.get(sensorId).getLazySession(), userId,
                     mContext.getOpPackageName(), sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext,
                     mFaceSensors.get(sensorId).getAuthenticatorIds());
 
@@ -341,7 +348,8 @@
             final FaceInvalidationClient client = new FaceInvalidationClient(mContext,
                     mFaceSensors.get(sensorId).getLazySession(), userId, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext,
                     mFaceSensors.get(sensorId).getAuthenticatorIds(), callback);
             scheduleForSensor(sensorId, client);
@@ -372,7 +380,8 @@
                     mFaceSensors.get(sensorId).getLazySession(), token,
                     new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext);
             scheduleForSensor(sensorId, client);
         });
@@ -386,7 +395,8 @@
                     mFaceSensors.get(sensorId).getLazySession(), token, userId,
                     opPackageName, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, challenge);
             scheduleForSensor(sensorId, client);
         });
@@ -407,7 +417,8 @@
                     opPackageName, id, FaceUtils.getInstance(sensorId), disabledFeatures,
                     ENROLL_TIMEOUT_SEC, previewSurface, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_ENROLL,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, maxTemplatesPerUser, debugConsent);
             scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
                             mBiometricStateCallback, new ClientMonitorCallback() {
@@ -443,7 +454,8 @@
             final FaceDetectClient client = new FaceDetectClient(mContext,
                     mFaceSensors.get(sensorId).getLazySession(),
                     token, id, callback, options,
-                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
+                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, isStrongBiometric);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
@@ -471,7 +483,8 @@
                     mContext, mFaceSensors.get(sensorId).getLazySession(), token, requestId,
                     callback, operationId, restricted, options, cookie,
                     false /* requireConfirmation */,
-                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
+                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, isStrongBiometric,
                     mUsageStats, mFaceSensors.get(sensorId).getLockoutCache(),
                     allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId));
@@ -540,7 +553,8 @@
                     new ClientMonitorCallbackConverter(receiver), faceIds, userId,
                     opPackageName, FaceUtils.getInstance(sensorId), sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext,
                     mFaceSensors.get(sensorId).getAuthenticatorIds());
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
@@ -554,7 +568,8 @@
                     mContext, mFaceSensors.get(sensorId).getLazySession(), userId,
                     mContext.getOpPackageName(), sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, hardwareAuthToken,
                     mFaceSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher,
                     Utils.getCurrentStrength(sensorId));
@@ -624,7 +639,8 @@
                             mFaceSensors.get(sensorId).getLazySession(), userId,
                             mContext.getOpPackageName(), sensorId,
                             createLogger(BiometricsProtoEnums.ACTION_ENUMERATE,
-                                    BiometricsProtoEnums.CLIENT_UNKNOWN),
+                                    BiometricsProtoEnums.CLIENT_UNKNOWN,
+                                    mAuthenticationStatsCollector),
                             mBiometricContext,
                             FaceUtils.getInstance(sensorId),
                             mFaceSensors.get(sensorId).getAuthenticatorIds());
@@ -636,9 +652,10 @@
         });
     }
 
-    private BiometricLogger createLogger(int statsAction, int statsClient) {
+    private BiometricLogger createLogger(int statsAction, int statsClient,
+            AuthenticationStatsCollector authenticationStatsCollector) {
         return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FACE,
-                statsAction, statsClient);
+                statsAction, statsClient, authenticationStatsCollector);
     }
 
     @Override
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 1e33c96..10991d5 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
@@ -52,6 +52,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.biometrics.AuthenticationStatsCollector;
 import com.android.server.biometrics.SensorServiceStateProto;
 import com.android.server.biometrics.SensorStateProto;
 import com.android.server.biometrics.UserStateProto;
@@ -124,6 +125,7 @@
     @Nullable private IBiometricsFace mDaemon;
     @NonNull private final HalResultController mHalResultController;
     @NonNull private final BiometricContext mBiometricContext;
+    @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector;
     // for requests that do not use biometric prompt
     @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
     private int mCurrentUserId = UserHandle.USER_NULL;
@@ -364,6 +366,9 @@
             mCurrentUserId = UserHandle.USER_NULL;
         });
 
+        mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+                BiometricsProtoEnums.MODALITY_FACE);
+
         try {
             ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
         } catch (RemoteException e) {
@@ -554,7 +559,7 @@
                     mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId,
                     opPackageName, mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext, sSystemClock.millis());
             mGeneratedChallengeCache = client;
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
@@ -586,7 +591,7 @@
             final FaceRevokeChallengeClient client = new FaceRevokeChallengeClient(mContext,
                     mLazyDaemon, token, userId, opPackageName, mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext);
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
                 @Override
@@ -617,7 +622,7 @@
                     opPackageName, id, FaceUtils.getLegacyInstance(mSensorId), disabledFeatures,
                     ENROLL_TIMEOUT_SEC, previewSurface, mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_ENROLL,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext);
 
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
@@ -677,7 +682,8 @@
             final FaceAuthenticationClient client = new FaceAuthenticationClient(mContext,
                     mLazyDaemon, token, requestId, receiver, operationId, restricted,
                     options, cookie, false /* requireConfirmation */,
-                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
+                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, isStrongBiometric, mLockoutTracker,
                     mUsageStats, allowBackgroundAuthentication,
                     Utils.getCurrentStrength(mSensorId));
@@ -713,7 +719,7 @@
                     new ClientMonitorCallbackConverter(receiver), faceId, userId, opPackageName,
                     FaceUtils.getLegacyInstance(mSensorId), mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext, mAuthenticatorIds);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
@@ -731,7 +737,7 @@
                     opPackageName,
                     FaceUtils.getLegacyInstance(mSensorId), mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext, mAuthenticatorIds);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
@@ -750,7 +756,7 @@
             final FaceResetLockoutClient client = new FaceResetLockoutClient(mContext,
                     mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext, hardwareAuthToken);
             mScheduler.scheduleClientMonitor(client);
         });
@@ -821,7 +827,7 @@
             final FaceInternalCleanupClient client = new FaceInternalCleanupClient(mContext,
                     mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_ENUMERATE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext,
                     FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds);
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback,
@@ -953,7 +959,7 @@
         final FaceUpdateActiveUserClient client = new FaceUpdateActiveUserClient(mContext,
                 mLazyDaemon, targetUserId, mContext.getOpPackageName(), mSensorId,
                 createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                        BiometricsProtoEnums.CLIENT_UNKNOWN),
+                        BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                 mBiometricContext, hasEnrolled, mAuthenticatorIds);
         mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
             @Override
@@ -968,9 +974,10 @@
         });
     }
 
-    private BiometricLogger createLogger(int statsAction, int statsClient) {
+    private BiometricLogger createLogger(int statsAction, int statsClient,
+            AuthenticationStatsCollector authenticationStatsCollector) {
         return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FACE,
-                statsAction, statsClient);
+                statsAction, statsClient, authenticationStatsCollector);
     }
 
     /**
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 0421d78..2d062db 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
@@ -57,6 +57,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.AuthenticationStatsCollector;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -122,6 +123,7 @@
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     @Nullable private ISidefpsController mSidefpsController;
     private AuthSessionCoordinator mAuthSessionCoordinator;
+    @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector;
 
     private final class BiometricTaskStackListener extends TaskStackListener {
         @Override
@@ -181,6 +183,9 @@
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
         mDaemon = daemon;
 
+        mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+                BiometricsProtoEnums.MODALITY_FINGERPRINT);
+
         final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context);
 
         for (SensorProps prop : props) {
@@ -338,7 +343,8 @@
                             mFingerprintSensors.get(sensorId).getLazySession(), userId,
                             mContext.getOpPackageName(), sensorId,
                             createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                                    BiometricsProtoEnums.CLIENT_UNKNOWN),
+                                    BiometricsProtoEnums.CLIENT_UNKNOWN,
+                                    mAuthenticationStatsCollector),
                             mBiometricContext,
                             mFingerprintSensors.get(sensorId).getAuthenticatorIds());
             scheduleForSensor(sensorId, client);
@@ -363,7 +369,8 @@
                     mContext, mFingerprintSensors.get(sensorId).getLazySession(), userId,
                     mContext.getOpPackageName(), sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, hardwareAuthToken,
                     mFingerprintSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher,
                     Utils.getCurrentStrength(sensorId));
@@ -380,7 +387,7 @@
                             mFingerprintSensors.get(sensorId).getLazySession(), token,
                             new ClientMonitorCallbackConverter(receiver), userId, opPackageName,
                             sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                             mBiometricContext);
             scheduleForSensor(sensorId, client);
         });
@@ -395,7 +402,8 @@
                             mFingerprintSensors.get(sensorId).getLazySession(), token,
                             userId, opPackageName, sensorId,
                             createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                                    BiometricsProtoEnums.CLIENT_UNKNOWN),
+                                    BiometricsProtoEnums.CLIENT_UNKNOWN,
+                                    mAuthenticationStatsCollector),
                             mBiometricContext, challenge);
             scheduleForSensor(sensorId, client);
         });
@@ -415,7 +423,7 @@
                     new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken,
                     opPackageName, FingerprintUtils.getInstance(sensorId), sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_ENROLL,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext,
                     mFingerprintSensors.get(sensorId).getSensorProperties(),
                     mUdfpsOverlayController, mSidefpsController,
@@ -455,7 +463,8 @@
             final FingerprintDetectClient client = new FingerprintDetectClient(mContext,
                     mFingerprintSensors.get(sensorId).getLazySession(), token, id, callback,
                     options,
-                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
+                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, mUdfpsOverlayController, isStrongBiometric);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
@@ -477,7 +486,8 @@
                     mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, requestId,
                     callback, operationId, restricted, options, cookie,
                     false /* requireConfirmation */,
-                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
+                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mFingerprintSensors.get(sensorId).getLockoutCache(),
                     mUdfpsOverlayController, mSidefpsController,
@@ -566,7 +576,8 @@
                     new ClientMonitorCallbackConverter(receiver), fingerprintIds, userId,
                     opPackageName, FingerprintUtils.getInstance(sensorId), sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext,
                     mFingerprintSensors.get(sensorId).getAuthenticatorIds());
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
@@ -588,7 +599,8 @@
                             mFingerprintSensors.get(sensorId).getLazySession(), userId,
                             mContext.getOpPackageName(), sensorId,
                             createLogger(BiometricsProtoEnums.ACTION_ENUMERATE,
-                                    BiometricsProtoEnums.CLIENT_UNKNOWN),
+                                    BiometricsProtoEnums.CLIENT_UNKNOWN,
+                                    mAuthenticationStatsCollector),
                             mBiometricContext,
                             FingerprintUtils.getInstance(sensorId),
                             mFingerprintSensors.get(sensorId).getAuthenticatorIds());
@@ -600,9 +612,10 @@
         });
     }
 
-    private BiometricLogger createLogger(int statsAction, int statsClient) {
+    private BiometricLogger createLogger(int statsAction, int statsClient,
+            AuthenticationStatsCollector authenticationStatsCollector) {
         return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT,
-                statsAction, statsClient);
+                statsAction, statsClient, authenticationStatsCollector);
     }
 
     @Override
@@ -635,7 +648,8 @@
                     new FingerprintInvalidationClient(mContext,
                             mFingerprintSensors.get(sensorId).getLazySession(), userId, sensorId,
                             createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                                    BiometricsProtoEnums.CLIENT_UNKNOWN),
+                                    BiometricsProtoEnums.CLIENT_UNKNOWN,
+                                    mAuthenticationStatsCollector),
                             mBiometricContext,
                             mFingerprintSensors.get(sensorId).getAuthenticatorIds(), callback);
             scheduleForSensor(sensorId, client);
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 92b216d..4b07dca 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
@@ -52,6 +52,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.biometrics.AuthenticationStatsCollector;
 import com.android.server.biometrics.SensorServiceStateProto;
 import com.android.server.biometrics.SensorStateProto;
 import com.android.server.biometrics.UserStateProto;
@@ -123,6 +124,7 @@
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     @Nullable private ISidefpsController mSidefpsController;
     @NonNull private final BiometricContext mBiometricContext;
+    @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector;
     // for requests that do not use biometric prompt
     @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
     private int mCurrentUserId = UserHandle.USER_NULL;
@@ -351,6 +353,9 @@
             mCurrentUserId = UserHandle.USER_NULL;
         });
 
+        mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+                BiometricsProtoEnums.MODALITY_FINGERPRINT);
+
         try {
             ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
         } catch (RemoteException e) {
@@ -497,7 +502,8 @@
                 new FingerprintUpdateActiveUserClient(mContext, mLazyDaemon, targetUserId,
                         mContext.getOpPackageName(), mSensorProperties.sensorId,
                         createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                                BiometricsProtoEnums.CLIENT_UNKNOWN),
+                                BiometricsProtoEnums.CLIENT_UNKNOWN,
+                                mAuthenticationStatsCollector),
                         mBiometricContext,
                         this::getCurrentUser, hasEnrolled, mAuthenticatorIds, force);
         mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
@@ -544,7 +550,8 @@
             final FingerprintResetLockoutClient client = new FingerprintResetLockoutClient(mContext,
                     userId, mContext.getOpPackageName(), sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, mLockoutTracker);
             mScheduler.scheduleClientMonitor(client);
         });
@@ -559,7 +566,8 @@
                             new ClientMonitorCallbackConverter(receiver), userId, opPackageName,
                             mSensorProperties.sensorId,
                             createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                                    BiometricsProtoEnums.CLIENT_UNKNOWN),
+                                    BiometricsProtoEnums.CLIENT_UNKNOWN,
+                                    mAuthenticationStatsCollector),
                             mBiometricContext);
             mScheduler.scheduleClientMonitor(client);
         });
@@ -573,7 +581,8 @@
                     mContext, mLazyDaemon, token, userId, opPackageName,
                     mSensorProperties.sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN,
+                            mAuthenticationStatsCollector),
                     mBiometricContext);
             mScheduler.scheduleClientMonitor(client);
         });
@@ -594,7 +603,7 @@
                     FingerprintUtils.getLegacyInstance(mSensorId), ENROLL_TIMEOUT_SEC,
                     mSensorProperties.sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_ENROLL,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext, mUdfpsOverlayController, mSidefpsController, enrollReason);
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
                 @Override
@@ -639,7 +648,8 @@
             final boolean isStrongBiometric = Utils.isStrongBiometric(mSensorProperties.sensorId);
             final FingerprintDetectClient client = new FingerprintDetectClient(mContext,
                     mLazyDaemon, token, id, listener, options,
-                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
+                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, mUdfpsOverlayController, isStrongBiometric);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
@@ -660,7 +670,8 @@
             final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient(
                     mContext, mLazyDaemon, token, requestId, listener, operationId,
                     restricted, options, cookie, false /* requireConfirmation */,
-                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
+                    createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
+                            mAuthenticationStatsCollector),
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mLockoutTracker,
                     mUdfpsOverlayController, mSidefpsController,
@@ -706,7 +717,7 @@
                     userId, opPackageName, FingerprintUtils.getLegacyInstance(mSensorId),
                     mSensorProperties.sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext, mAuthenticatorIds);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
@@ -726,7 +737,7 @@
                     FingerprintUtils.getLegacyInstance(mSensorId),
                     mSensorProperties.sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext, mAuthenticatorIds);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
@@ -741,7 +752,7 @@
                     mContext, mLazyDaemon, userId, mContext.getOpPackageName(),
                     mSensorProperties.sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_ENUMERATE,
-                            BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
                     mBiometricContext,
                     FingerprintUtils.getLegacyInstance(mSensorId), mAuthenticatorIds);
             mScheduler.scheduleClientMonitor(client, callback);
@@ -762,9 +773,10 @@
                 mBiometricStateCallback));
     }
 
-    private BiometricLogger createLogger(int statsAction, int statsClient) {
+    private BiometricLogger createLogger(int statsAction, int statsClient,
+            AuthenticationStatsCollector authenticationStatsCollector) {
         return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT,
-                statsAction, statsClient);
+                statsAction, statsClient, authenticationStatsCollector);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index a622d07..bb6e11d 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -90,7 +90,7 @@
       ]
     }
   ],
-  "presubmit-large": [
+  "postsubmit": [
     {
       "name": "CtsContentTestCases",
       "options": [
@@ -104,9 +104,7 @@
           "include-filter": "android.content.pm.cts"
         }
       ]
-    }
-  ],
-  "postsubmit": [
+    },
     {
       "name": "CtsPermissionTestCases",
       "options": [
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b9ac5c3..5a6851e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2784,6 +2784,9 @@
             } else if (isEmbedded()) {
                 associateStartingWindowWithTaskIfNeeded();
             }
+            if (mTransitionController.isCollecting()) {
+                mStartingData.mTransitionId = mTransitionController.getCollectingTransitionId();
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index 105b2bb..148bf9b 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -16,16 +16,14 @@
 
 package com.android.server.wm;
 
-import static com.android.server.wm.SnapshotController.ACTIVITY_CLOSE;
-import static com.android.server.wm.SnapshotController.ACTIVITY_OPEN;
-import static com.android.server.wm.SnapshotController.TASK_CLOSE;
-import static com.android.server.wm.SnapshotController.TASK_OPEN;
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.os.Environment;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.util.ArraySet;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -35,7 +33,6 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
-import com.android.server.wm.SnapshotController.TransitionState;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -62,12 +59,6 @@
     static final String SNAPSHOTS_DIRNAME = "activity_snapshots";
 
     /**
-     * The pending activities which should capture snapshot when process transition finish.
-     */
-    @VisibleForTesting
-    final ArraySet<ActivityRecord> mPendingCaptureActivity = new ArraySet<>();
-
-    /**
      * The pending activities which should remove snapshot from memory when process transition
      * finish.
      */
@@ -86,6 +77,10 @@
     @VisibleForTesting
     final ArraySet<ActivityRecord> mPendingLoadActivity = new ArraySet<>();
 
+    private final ArraySet<ActivityRecord> mOnBackPressedActivities = new ArraySet<>();
+
+    private final ArrayList<ActivityRecord> mTmpBelowActivities = new ArrayList<>();
+    private final ArrayList<WindowContainer> mTmpTransitionParticipants = new ArrayList<>();
     private final SnapshotPersistQueue mSnapshotPersistQueue;
     private final PersistInfoProvider mPersistInfoProvider;
     private final AppSnapshotLoader mSnapshotLoader;
@@ -117,20 +112,6 @@
         setSnapshotEnabled(snapshotEnabled);
     }
 
-    void systemReady() {
-        if (shouldDisableSnapshots()) {
-            return;
-        }
-        mService.mSnapshotController.registerTransitionStateConsumer(
-                ACTIVITY_OPEN, this::handleOpenActivityTransition);
-        mService.mSnapshotController.registerTransitionStateConsumer(
-                ACTIVITY_CLOSE, this::handleCloseActivityTransition);
-        mService.mSnapshotController.registerTransitionStateConsumer(
-                TASK_OPEN, this::handleOpenTaskTransition);
-        mService.mSnapshotController.registerTransitionStateConsumer(
-                TASK_CLOSE, this::handleCloseTaskTransition);
-    }
-
     @Override
     protected float initSnapshotScale() {
         final float config = mService.mContext.getResources().getFloat(
@@ -173,6 +154,7 @@
 
                         @Override
                         void write() {
+                            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "cleanUpUserFiles");
                             final File file = mPersistInfoProvider.getDirectory(userId);
                             if (file.exists()) {
                                 final File[] contents = file.listFiles();
@@ -182,15 +164,30 @@
                                     }
                                 }
                             }
+                            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                         }
                     });
         }
     }
 
+    void addOnBackPressedActivity(ActivityRecord ar) {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
+        mOnBackPressedActivities.add(ar);
+    }
+
+    void clearOnBackPressedActivities() {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
+        mOnBackPressedActivities.clear();
+    }
+
     /**
-     * Prepare to handle on transition start. Clear all temporary fields.
+     * Prepare to collect any change for snapshots processing. Clear all temporary fields.
      */
-    void preTransitionStart() {
+    void beginSnapshotProcess() {
         if (shouldDisableSnapshots()) {
             return;
         }
@@ -198,18 +195,22 @@
     }
 
     /**
-     * on transition start has notified, start process data.
+     * End collect any change for snapshots processing, start process data.
      */
-    void postTransitionStart() {
+    void endSnapshotProcess() {
         if (shouldDisableSnapshots()) {
             return;
         }
-        onCommitTransition();
+        for (int i = mOnBackPressedActivities.size() - 1; i >= 0; --i) {
+            handleActivityTransition(mOnBackPressedActivities.valueAt(i));
+        }
+        mOnBackPressedActivities.clear();
+        mTmpTransitionParticipants.clear();
+        postProcess();
     }
 
     @VisibleForTesting
     void resetTmpFields() {
-        mPendingCaptureActivity.clear();
         mPendingRemoveActivity.clear();
         mPendingDeleteActivity.clear();
         mPendingLoadActivity.clear();
@@ -218,31 +219,13 @@
     /**
      * Start process all pending activities for a transition.
      */
-    private void onCommitTransition() {
+    private void postProcess() {
         if (DEBUG) {
-            Slog.d(TAG, "ActivitySnapshotController#onCommitTransition result:"
-                    + " capture " + mPendingCaptureActivity
+            Slog.d(TAG, "ActivitySnapshotController#postProcess result:"
                     + " remove " + mPendingRemoveActivity
                     + " delete " + mPendingDeleteActivity
                     + " load " + mPendingLoadActivity);
         }
-        // task snapshots
-        for (int i = mPendingCaptureActivity.size() - 1; i >= 0; i--) {
-            recordSnapshot(mPendingCaptureActivity.valueAt(i));
-        }
-        // 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);
-        }
-        // 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);
-        }
         // load snapshot to cache
         for (int i = mPendingLoadActivity.size() - 1; i >= 0; i--) {
             final ActivityRecord ar = mPendingLoadActivity.valueAt(i);
@@ -258,6 +241,8 @@
                             new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) {
                                 @Override
                                 void write() {
+                                    Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
+                                            "load_activity_snapshot");
                                     final TaskSnapshot snapshot = mSnapshotLoader.loadTask(code,
                                             userId, false /* loadLowResolutionBitmap */);
                                     synchronized (mService.getWindowManagerLock()) {
@@ -265,16 +250,36 @@
                                             mCache.putSnapshot(ar, snapshot);
                                         }
                                     }
+                                    Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                                 }
                             });
                 }
             }
         }
+        // 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);
+        }
+        // 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);
+        }
         // don't keep any reference
         resetTmpFields();
     }
 
-    private void recordSnapshot(ActivityRecord activity) {
+    void recordSnapshot(ActivityRecord activity) {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "ActivitySnapshotController#recordSnapshot " + activity);
+        }
         final TaskSnapshot snapshot = recordSnapshotInner(activity, false /* allowSnapshotHome */);
         if (snapshot != null) {
             final int code = getSystemHashCode(activity);
@@ -285,15 +290,20 @@
     /**
      * Called when the visibility of an app changes outside the regular app transition flow.
      */
-    void notifyAppVisibilityChanged(ActivityRecord appWindowToken, boolean visible) {
+    void notifyAppVisibilityChanged(ActivityRecord ar, boolean visible) {
         if (shouldDisableSnapshots()) {
             return;
         }
+        final Task task = ar.getTask();
+        if (task == null) {
+            return;
+        }
+        // Doesn't need to capture activity snapshot when it converts from translucent.
         if (!visible) {
             resetTmpFields();
-            addBelowTopActivityIfExist(appWindowToken.getTask(), mPendingRemoveActivity,
+            addBelowActivityIfExist(ar, mPendingRemoveActivity, false,
                     "remove-snapshot");
-            onCommitTransition();
+            postProcess();
         }
     }
 
@@ -301,65 +311,146 @@
         return System.identityHashCode(activity);
     }
 
-    void handleOpenActivityTransition(TransitionState<ActivityRecord> transitionState) {
-        ArraySet<ActivityRecord> participant = transitionState.getParticipant(false /* open */);
-        for (ActivityRecord ar : participant) {
-            mPendingCaptureActivity.add(ar);
-            // remove the snapshot for the one below close
-            final ActivityRecord below = ar.getTask().getActivityBelow(ar);
-            if (below != null) {
-                mPendingRemoveActivity.add(below);
+    @VisibleForTesting
+    void handleTransitionFinish(@NonNull ArrayList<WindowContainer> windows) {
+        mTmpTransitionParticipants.clear();
+        mTmpTransitionParticipants.addAll(windows);
+        for (int i = mTmpTransitionParticipants.size() - 1; i >= 0; --i) {
+            final WindowContainer next = mTmpTransitionParticipants.get(i);
+            if (next.asTask() != null) {
+                handleTaskTransition(next.asTask());
+            } else if (next.asTaskFragment() != null) {
+                final TaskFragment tf = next.asTaskFragment();
+                final ActivityRecord ar = tf.getTopMostActivity();
+                if (ar != null) {
+                    handleActivityTransition(ar);
+                }
+            } else if (next.asActivityRecord() != null) {
+                handleActivityTransition(next.asActivityRecord());
             }
         }
     }
 
-    void handleCloseActivityTransition(TransitionState<ActivityRecord> transitionState) {
-        ArraySet<ActivityRecord> participant = transitionState.getParticipant(true /* open */);
-        for (ActivityRecord ar : participant) {
+    private void handleActivityTransition(@NonNull ActivityRecord ar) {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
+        if (ar.isVisibleRequested()) {
             mPendingDeleteActivity.add(ar);
             // load next one if exists.
-            final ActivityRecord below = ar.getTask().getActivityBelow(ar);
-            if (below != null) {
-                mPendingLoadActivity.add(below);
-            }
+            addBelowActivityIfExist(ar, mPendingLoadActivity, true, "load-snapshot");
+        } else {
+            // remove the snapshot for the one below close
+            addBelowActivityIfExist(ar, mPendingRemoveActivity, true, "remove-snapshot");
         }
     }
 
-    void handleCloseTaskTransition(TransitionState<Task> closeTaskTransitionRecord) {
-        ArraySet<Task> participant = closeTaskTransitionRecord.getParticipant(false /* open */);
-        for (Task close : participant) {
-            // this is close task transition
-            // remove the N - 1 from cache
-            addBelowTopActivityIfExist(close, mPendingRemoveActivity, "remove-snapshot");
+    private void handleTaskTransition(Task task) {
+        if (shouldDisableSnapshots()) {
+            return;
         }
-    }
-
-    void handleOpenTaskTransition(TransitionState<Task> openTaskTransitionRecord) {
-        ArraySet<Task> participant = openTaskTransitionRecord.getParticipant(true /* open */);
-        for (Task open : participant) {
-            // this is close task transition
-            // remove the N - 1 from cache
-            addBelowTopActivityIfExist(open, mPendingLoadActivity, "load-snapshot");
+        final ActivityRecord topActivity = task.getTopMostActivity();
+        if (topActivity == null) {
+            return;
+        }
+        if (task.isVisibleRequested()) {
+            // this is open task transition
+            // load the N - 1 to cache
+            addBelowActivityIfExist(topActivity, mPendingLoadActivity, true, "load-snapshot");
             // Move the activities to top of mSavedFilesInOrder, so when purge happen, there
             // will trim the persisted files from the most non-accessed.
-            adjustSavedFileOrder(open);
+            adjustSavedFileOrder(task);
+        } else {
+            // this is close task transition
+            // remove the N - 1 from cache
+            addBelowActivityIfExist(topActivity, mPendingRemoveActivity, true, "remove-snapshot");
         }
     }
 
-    // Add the top -1 activity to a set if it exists.
-    private void addBelowTopActivityIfExist(Task task, ArraySet<ActivityRecord> set,
-            String debugMessage) {
-        final ActivityRecord topActivity = task.getTopMostActivity();
-        if (topActivity != null) {
-            final ActivityRecord below = task.getActivityBelow(topActivity);
-            if (below != null) {
-                set.add(below);
-                if (DEBUG) {
-                    Slog.d(TAG, "ActivitySnapshotController#addBelowTopActivityIfExist "
-                            + below + " from " + debugMessage);
-                }
+    /**
+     * Add the top -1 activity to a set if it exists.
+     * @param inTransition true if the activity must participant in transition.
+     */
+    private void addBelowActivityIfExist(ActivityRecord currentActivity,
+            ArraySet<ActivityRecord> set, boolean inTransition, String debugMessage) {
+        getActivityBelow(currentActivity, inTransition, mTmpBelowActivities);
+        for (int i = mTmpBelowActivities.size() - 1; i >= 0; --i) {
+            set.add(mTmpBelowActivities.get(i));
+            if (DEBUG) {
+                Slog.d(TAG, "ActivitySnapshotController#addBelowTopActivityIfExist "
+                        + mTmpBelowActivities.get(i) + " from " + debugMessage);
             }
         }
+        mTmpBelowActivities.clear();
+    }
+
+    private void getActivityBelow(ActivityRecord currentActivity, boolean inTransition,
+            ArrayList<ActivityRecord> result) {
+        final Task currentTask = currentActivity.getTask();
+        if (currentTask == null) {
+            return;
+        }
+        final ActivityRecord initPrev = currentTask.getActivityBelow(currentActivity);
+        if (initPrev == null) {
+            return;
+        }
+        final TaskFragment currTF = currentActivity.getTaskFragment();
+        final TaskFragment prevTF = initPrev.getTaskFragment();
+        final TaskFragment prevAdjacentTF = prevTF != null
+                ? prevTF.getAdjacentTaskFragment() : null;
+        if (currTF == prevTF && currTF != null || prevAdjacentTF == null) {
+            // Current activity and previous one is in the same task fragment, or
+            // previous activity is not in a task fragment, or
+            // previous activity's task fragment doesn't adjacent to any others.
+            if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) {
+                result.add(initPrev);
+            }
+            return;
+        }
+
+        if (prevAdjacentTF == currTF) {
+            // previous activity A is adjacent to current activity B.
+            // Try to find anyone below previous activityA, which are C and D if exists.
+            // A | B
+            // C (| D)
+            getActivityBelow(initPrev, inTransition, result);
+        } else {
+            // previous activity C isn't adjacent to current activity A.
+            // A
+            // B | C
+            final Task prevAdjacentTask = prevAdjacentTF.getTask();
+            if (prevAdjacentTask == currentTask) {
+                final int currentIndex = currTF != null
+                        ? currentTask.mChildren.indexOf(currTF)
+                        : currentTask.mChildren.indexOf(currentActivity);
+                final int prevAdjacentIndex =
+                        prevAdjacentTask.mChildren.indexOf(prevAdjacentTF);
+                // prevAdjacentTF already above currentActivity
+                if (prevAdjacentIndex > currentIndex) {
+                    return;
+                }
+            }
+            if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) {
+                result.add(initPrev);
+            }
+            // prevAdjacentTF is adjacent to another one
+            final ActivityRecord prevAdjacentActivity = prevAdjacentTF.getTopMostActivity();
+            if (prevAdjacentActivity != null && (!inTransition
+                    || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) {
+                result.add(prevAdjacentActivity);
+            }
+        }
+    }
+
+    static boolean isInParticipant(ActivityRecord ar,
+            ArrayList<WindowContainer> transitionParticipants) {
+        for (int i = transitionParticipants.size() - 1; i >= 0; --i) {
+            final WindowContainer wc = transitionParticipants.get(i);
+            if (ar == wc || ar.isDescendantOf(wc)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     private void adjustSavedFileOrder(Task nextTopTask) {
@@ -376,6 +467,9 @@
 
     @Override
     void onAppRemoved(ActivityRecord activity) {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
         super.onAppRemoved(activity);
         final int code = getSystemHashCode(activity);
         removeIfUserSavedFileExist(code, activity.mUserId);
@@ -386,6 +480,9 @@
 
     @Override
     void onAppDied(ActivityRecord activity) {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
         super.onAppDied(activity);
         final int code = getSystemHashCode(activity);
         removeIfUserSavedFileExist(code, activity.mUserId);
@@ -440,7 +537,7 @@
     private void removeIfUserSavedFileExist(int code, int userId) {
         final UserSavedFile usf = getUserFiles(userId).get(code);
         if (usf != null) {
-            mUserSavedFiles.remove(code);
+            mUserSavedFiles.get(userId).remove(code);
             mSavedFilesInOrder.remove(usf);
             mPersister.removeSnap(code, userId);
         }
@@ -490,11 +587,13 @@
                     new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) {
                         @Override
                         void write() {
+                            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "activity_remove_files");
                             for (int i = files.size() - 1; i >= 0; --i) {
                                 final UserSavedFile usf = files.get(i);
                                 mSnapshotPersistQueue.deleteSnapshot(
                                         usf.mFileId, usf.mUserId, mPersistInfoProvider);
                             }
+                            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                         }
                     });
         }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 78da5de..fcf6587 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3019,9 +3019,11 @@
         // Set to activity manager directly to make sure the state can be seen by the subsequent
         // update of scheduling group.
         proc.setRunningAnimationUnsafe();
-        mH.removeMessages(H.UPDATE_PROCESS_ANIMATING_STATE, proc);
-        mH.sendMessageDelayed(mH.obtainMessage(H.UPDATE_PROCESS_ANIMATING_STATE, proc),
+        mH.sendMessage(mH.obtainMessage(H.ADD_WAKEFULNESS_ANIMATING_REASON, proc));
+        mH.removeMessages(H.REMOVE_WAKEFULNESS_ANIMATING_REASON, proc);
+        mH.sendMessageDelayed(mH.obtainMessage(H.REMOVE_WAKEFULNESS_ANIMATING_REASON, proc),
                 DOZE_ANIMATING_STATE_RETAIN_TIME_MS);
+        Trace.instant(TRACE_TAG_WINDOW_MANAGER, "requestWakefulnessAnimating");
     }
 
     @Override
@@ -5646,9 +5648,10 @@
 
     final class H extends Handler {
         static final int REPORT_TIME_TRACKER_MSG = 1;
-        static final int UPDATE_PROCESS_ANIMATING_STATE = 2;
         static final int END_POWER_MODE_UNKNOWN_VISIBILITY_MSG = 3;
         static final int RESUME_FG_APP_SWITCH_MSG = 4;
+        static final int ADD_WAKEFULNESS_ANIMATING_REASON = 5;
+        static final int REMOVE_WAKEFULNESS_ANIMATING_REASON = 6;
 
         static final int FIRST_ACTIVITY_TASK_MSG = 100;
         static final int FIRST_SUPERVISOR_TASK_MSG = 200;
@@ -5665,13 +5668,23 @@
                     tracker.deliverResult(mContext);
                 }
                 break;
-                case UPDATE_PROCESS_ANIMATING_STATE: {
+                case ADD_WAKEFULNESS_ANIMATING_REASON: {
                     final WindowProcessController proc = (WindowProcessController) msg.obj;
                     synchronized (mGlobalLock) {
-                        proc.updateRunningRemoteOrRecentsAnimation();
+                        proc.addAnimatingReason(
+                                WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE);
                     }
                 }
                 break;
+                case REMOVE_WAKEFULNESS_ANIMATING_REASON: {
+                    final WindowProcessController proc = (WindowProcessController) msg.obj;
+                    synchronized (mGlobalLock) {
+                        proc.removeAnimatingReason(
+                                WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE);
+                    }
+                    Trace.instant(TRACE_TAG_WINDOW_MANAGER, "finishWakefulnessAnimating");
+                }
+                break;
                 case END_POWER_MODE_UNKNOWN_VISIBILITY_MSG: {
                     synchronized (mGlobalLock) {
                         mRetainPowerModeAndTopProcessState = false;
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index 4ce21bd..2eceecc 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -33,6 +33,7 @@
 
 import com.android.internal.R;
 
+import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.function.Consumer;
@@ -224,7 +225,15 @@
      * operation directly to avoid waiting until timeout.
      */
     void updateTargetWindows() {
-        if (mTransitionOp == OP_LEGACY || !mIsStartTransactionCommitted) return;
+        if (mTransitionOp == OP_LEGACY) return;
+        if (!mIsStartTransactionCommitted) {
+            if (mTimeoutRunnable == null && !mDisplayContent.hasTopFixedRotationLaunchingApp()
+                    && !mDisplayContent.isRotationChanging() && !mDisplayContent.inTransition()) {
+                Slog.d(TAG, "Cancel for no change");
+                mDisplayContent.finishAsyncRotationIfPossible();
+            }
+            return;
+        }
         for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
             final Operation op = mTargetWindowTokens.valueAt(i);
             if (op.mIsCompletionPending || op.mAction == Operation.ACTION_SEAMLESS) {
@@ -608,6 +617,16 @@
         return op.mAction != Operation.ACTION_SEAMLESS;
     }
 
+    void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "AsyncRotationController");
+        prefix += "  ";
+        pw.println(prefix + "mTransitionOp=" + mTransitionOp);
+        pw.println(prefix + "mIsStartTransactionCommitted=" + mIsStartTransactionCommitted);
+        pw.println(prefix + "mIsSyncDrawRequested=" + mIsSyncDrawRequested);
+        pw.println(prefix + "mOriginalRotation=" + mOriginalRotation);
+        pw.println(prefix + "mTargetWindowTokens=" + mTargetWindowTokens);
+    }
+
     /** The operation to control the rotation appearance associated with window token. */
     private static class Operation {
         @Retention(RetentionPolicy.SOURCE)
@@ -635,5 +654,10 @@
         Operation(@Action int action) {
             mAction = action;
         }
+
+        @Override
+        public String toString() {
+            return "Operation{a=" + mAction + " pending=" + mIsCompletionPending + '}';
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 976641b..993c016 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1177,6 +1177,8 @@
                 if (!composeAnimations(mCloseTarget, mOpenTarget, openActivity)) {
                     return null;
                 }
+                mCloseTarget.mTransitionController.mSnapshotController
+                        .mActivitySnapshotController.clearOnBackPressedActivities();
                 applyPreviewStrategy(mOpenAdaptor, openActivity);
 
                 final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback();
@@ -1222,6 +1224,8 @@
             // Call it again to make sure the activity could be visible while handling the pending
             // animation.
             activity.commitVisibility(true, true);
+            activity.mTransitionController.mSnapshotController
+                    .mActivitySnapshotController.addOnBackPressedActivity(activity);
         }
         activity.mLaunchTaskBehind = true;
 
@@ -1248,6 +1252,9 @@
         // Restore the launch-behind state.
         activity.mTaskSupervisor.scheduleLaunchTaskBehindComplete(activity.token);
         activity.mLaunchTaskBehind = false;
+        // Ignore all change
+        activity.mTransitionController.mSnapshotController
+                .mActivitySnapshotController.clearOnBackPressedActivities();
         ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
                 "Setting Activity.mLauncherTaskBehind to false. Activity=%s",
                 activity);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 64c2c5d..ee3014c 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -3454,6 +3454,9 @@
                 if (mFixedRotationLaunchingApp != null) {
                     setSeamlessTransitionForFixedRotation(controller.getCollectingTransition());
                 }
+            } else if (mAsyncRotationController != null && !isRotationChanging()) {
+                Slog.i(TAG, "Finish AsyncRotation for previous intermediate change");
+                finishAsyncRotationIfPossible();
             }
             return;
         }
@@ -3627,6 +3630,9 @@
         if (mFixedRotationLaunchingApp != null) {
             pw.println("  mFixedRotationLaunchingApp=" + mFixedRotationLaunchingApp);
         }
+        if (mAsyncRotationController != null) {
+            mAsyncRotationController.dump(pw, prefix);
+        }
 
         pw.println();
         pw.print(prefix + "mHoldScreenWindow="); pw.print(mHoldScreenWindow);
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index 3551370..f9fa9e6 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -20,7 +20,6 @@
 import static android.view.SurfaceControl.HIDDEN;
 import static android.window.TaskConstants.TASK_CHILD_LAYER_LETTERBOX_BACKGROUND;
 
-import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -256,11 +255,11 @@
         private final GestureDetector mDoubleTapDetector;
         private final DoubleTapListener mDoubleTapListener;
 
-        TapEventReceiver(InputChannel inputChannel, Context context) {
+        TapEventReceiver(InputChannel inputChannel, WindowManagerService wmService) {
             super(inputChannel, UiThread.getHandler().getLooper());
-            mDoubleTapListener = new DoubleTapListener();
+            mDoubleTapListener = new DoubleTapListener(wmService);
             mDoubleTapDetector = new GestureDetector(
-                    context, mDoubleTapListener, UiThread.getHandler());
+                    wmService.mContext, mDoubleTapListener, UiThread.getHandler());
         }
 
         @Override
@@ -271,14 +270,24 @@
     }
 
     private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
+        private final WindowManagerService mWmService;
+
+        private DoubleTapListener(WindowManagerService wmService) {
+            mWmService = wmService;
+        }
+
         @Override
         public boolean onDoubleTapEvent(MotionEvent e) {
-            if (e.getAction() == MotionEvent.ACTION_UP) {
-                mDoubleTapCallbackX.accept((int) e.getRawX());
-                mDoubleTapCallbackY.accept((int) e.getRawY());
-                return true;
+            synchronized (mWmService.mGlobalLock) {
+                // This check prevents late events to be handled in case the Letterbox has been
+                // already destroyed and so mOuter.isEmpty() is true.
+                if (!mOuter.isEmpty() && e.getAction() == MotionEvent.ACTION_UP) {
+                    mDoubleTapCallbackX.accept((int) e.getRawX());
+                    mDoubleTapCallbackY.accept((int) e.getRawY());
+                    return true;
+                }
+                return false;
             }
-            return false;
         }
     }
 
@@ -294,7 +303,7 @@
             mWmService = win.mWmService;
             final String name = namePrefix + (win.mActivityRecord != null ? win.mActivityRecord : win);
             mClientChannel = mWmService.mInputManager.createInputChannel(name);
-            mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService.mContext);
+            mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService);
 
             mToken = mClientChannel.getToken();
 
diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java
index badcfa9..37f9730 100644
--- a/services/core/java/com/android/server/wm/SnapshotController.java
+++ b/services/core/java/com/android/server/wm/SnapshotController.java
@@ -16,185 +16,36 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
-import android.annotation.IntDef;
-import android.util.ArraySet;
-import android.util.Slog;
-import android.util.SparseArray;
+import android.os.Trace;
 import android.view.WindowManager;
 
-import com.android.internal.annotations.VisibleForTesting;
-
 import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
-import java.util.function.Consumer;
 
 /**
  * Integrates common functionality from TaskSnapshotController and ActivitySnapshotController.
  */
 class SnapshotController {
-    private static final boolean DEBUG = false;
-    private static final String TAG = AbsAppSnapshotController.TAG;
-
-    static final int ACTIVITY_OPEN = 1;
-    static final int ACTIVITY_CLOSE = 2;
-    static final int TASK_OPEN = 4;
-    static final int TASK_CLOSE = 8;
-    /** @hide */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(
-            value = {ACTIVITY_OPEN,
-                    ACTIVITY_CLOSE,
-                    TASK_OPEN,
-                    TASK_CLOSE})
-    @interface TransitionStateType {}
-
     private final SnapshotPersistQueue mSnapshotPersistQueue;
     final TaskSnapshotController mTaskSnapshotController;
     final ActivitySnapshotController mActivitySnapshotController;
 
-    private final ArraySet<Task> mTmpCloseTasks = new ArraySet<>();
-    private final ArraySet<Task> mTmpOpenTasks = new ArraySet<>();
-
-    private final SparseArray<TransitionState> mTmpOpenCloseRecord = new SparseArray<>();
-    private final ArraySet<Integer> mTmpAnalysisRecord = new ArraySet<>();
-    private final SparseArray<ArrayList<Consumer<TransitionState>>> mTransitionStateConsumer =
-            new SparseArray<>();
-    private int mActivatedType;
-
-    private final ActivityOrderCheck mActivityOrderCheck = new ActivityOrderCheck();
-    private final ActivityOrderCheck.AnalysisResult mResultHandler = (type, close, open) -> {
-        addTransitionRecord(type, true/* open */, open);
-        addTransitionRecord(type, false/* open */, close);
-    };
-
-    private static class ActivityOrderCheck {
-        private ActivityRecord mOpenActivity;
-        private ActivityRecord mCloseActivity;
-        private int mOpenIndex = -1;
-        private int mCloseIndex = -1;
-
-        private void reset() {
-            mOpenActivity = null;
-            mCloseActivity = null;
-            mOpenIndex = -1;
-            mCloseIndex = -1;
-        }
-
-        private void setTarget(boolean open, ActivityRecord ar, int index) {
-            if (open) {
-                mOpenActivity = ar;
-                mOpenIndex = index;
-            } else {
-                mCloseActivity = ar;
-                mCloseIndex = index;
-            }
-        }
-
-        void analysisOrder(ArraySet<ActivityRecord> closeApps,
-                ArraySet<ActivityRecord> openApps, Task task, AnalysisResult result) {
-            for (int j = closeApps.size() - 1; j >= 0; j--) {
-                final ActivityRecord ar = closeApps.valueAt(j);
-                if (ar.getTask() == task) {
-                    setTarget(false, ar, task.mChildren.indexOf(ar));
-                    break;
-                }
-            }
-            for (int j = openApps.size() - 1; j >= 0; j--) {
-                final ActivityRecord ar = openApps.valueAt(j);
-                if (ar.getTask() == task) {
-                    setTarget(true, ar, task.mChildren.indexOf(ar));
-                    break;
-                }
-            }
-            if (mOpenIndex > mCloseIndex && mCloseIndex != -1) {
-                result.onCheckResult(ACTIVITY_OPEN, mCloseActivity, mOpenActivity);
-            } else if (mOpenIndex < mCloseIndex && mOpenIndex != -1) {
-                result.onCheckResult(ACTIVITY_CLOSE, mCloseActivity, mOpenActivity);
-            }
-            reset();
-        }
-        private interface AnalysisResult {
-            void onCheckResult(@TransitionStateType int type,
-                    ActivityRecord close, ActivityRecord open);
-        }
-    }
-
-    private void addTransitionRecord(int type, boolean open, WindowContainer target) {
-        TransitionState record = mTmpOpenCloseRecord.get(type);
-        if (record == null) {
-            record =  new TransitionState();
-            mTmpOpenCloseRecord.set(type, record);
-        }
-        record.addParticipant(target, open);
-        mTmpAnalysisRecord.add(type);
-    }
-
-    private void clearRecord() {
-        mTmpOpenCloseRecord.clear();
-        mTmpAnalysisRecord.clear();
-    }
-
-    static class TransitionState<TYPE extends WindowContainer> {
-        private final ArraySet<TYPE> mOpenParticipant = new ArraySet<>();
-        private final ArraySet<TYPE> mCloseParticipant = new ArraySet<>();
-
-        void addParticipant(TYPE target, boolean open) {
-            final ArraySet<TYPE> participant = open
-                    ? mOpenParticipant : mCloseParticipant;
-            participant.add(target);
-        }
-
-        ArraySet<TYPE> getParticipant(boolean open) {
-            return open ? mOpenParticipant : mCloseParticipant;
-        }
-    }
-
     SnapshotController(WindowManagerService wms) {
         mSnapshotPersistQueue = new SnapshotPersistQueue();
         mTaskSnapshotController = new TaskSnapshotController(wms, mSnapshotPersistQueue);
         mActivitySnapshotController = new ActivitySnapshotController(wms, mSnapshotPersistQueue);
     }
 
-    void registerTransitionStateConsumer(@TransitionStateType int type,
-            Consumer<TransitionState> consumer) {
-        ArrayList<Consumer<TransitionState>> consumers = mTransitionStateConsumer.get(type);
-        if (consumers == null) {
-            consumers = new ArrayList<>();
-            mTransitionStateConsumer.set(type, consumers);
-        }
-        if (!consumers.contains(consumer)) {
-            consumers.add(consumer);
-        }
-        mActivatedType |= type;
-    }
-
-    void unregisterTransitionStateConsumer(int type, Consumer<TransitionState> consumer) {
-        final ArrayList<Consumer<TransitionState>> consumers = mTransitionStateConsumer.get(type);
-        if (consumers == null) {
-            return;
-        }
-        consumers.remove(consumer);
-        if (consumers.size() == 0) {
-            mActivatedType &= ~type;
-        }
-    }
-
-    private boolean hasTransitionStateConsumer(@TransitionStateType int type) {
-        return (mActivatedType & type) != 0;
-    }
-
     void systemReady() {
         mSnapshotPersistQueue.systemReady();
-        mTaskSnapshotController.systemReady();
-        mActivitySnapshotController.systemReady();
     }
 
     void setPause(boolean paused) {
@@ -212,47 +63,69 @@
     }
 
     void notifyAppVisibilityChanged(ActivityRecord appWindowToken, boolean visible) {
-        if (!visible && hasTransitionStateConsumer(TASK_CLOSE)) {
-            final Task task = appWindowToken.getTask();
-            if (task == null || task.isVisibleRequested()) {
-                return;
+        mActivitySnapshotController.notifyAppVisibilityChanged(appWindowToken, visible);
+    }
+
+    // For legacy transition, which won't support activity snapshot
+    void onTransitionStarting(DisplayContent displayContent) {
+        mTaskSnapshotController.handleClosingApps(displayContent.mClosingApps);
+    }
+
+    // For shell transition, record snapshots before transaction start.
+    void onTransactionReady(@WindowManager.TransitionType int type,
+            ArrayList<Transition.ChangeInfo> changeInfos) {
+        final boolean isTransitionOpen = isTransitionOpen(type);
+        final boolean isTransitionClose = isTransitionClose(type);
+        if (!isTransitionOpen && !isTransitionClose && type < TRANSIT_FIRST_CUSTOM) {
+            return;
+        }
+        for (int i = changeInfos.size() - 1; i >= 0; --i) {
+            Transition.ChangeInfo info = changeInfos.get(i);
+            // Intentionally skip record snapshot for changes originated from PiP.
+            if (info.mWindowingMode == WINDOWING_MODE_PINNED) continue;
+            if (info.mContainer.asTask() != null && !info.mContainer.isVisibleRequested()) {
+                mTaskSnapshotController.recordSnapshot(info.mContainer.asTask(),
+                        false /* allowSnapshotHome */);
             }
-            // close task transition
-            addTransitionRecord(TASK_CLOSE, false /*open*/, task);
-            mActivitySnapshotController.preTransitionStart();
-            notifyTransition(TASK_CLOSE);
-            mActivitySnapshotController.postTransitionStart();
-            clearRecord();
+            // Won't need to capture activity snapshot in close transition.
+            if (isTransitionClose) {
+                continue;
+            }
+            if (info.mContainer.asActivityRecord() != null
+                    || info.mContainer.asTaskFragment() != null) {
+                final TaskFragment tf = info.mContainer.asTaskFragment();
+                final ActivityRecord ar = tf != null ? tf.getTopMostActivity()
+                        : info.mContainer.asActivityRecord();
+                final boolean taskVis = ar != null && ar.getTask().isVisibleRequested();
+                if (ar != null && !ar.isVisibleRequested() && taskVis) {
+                    mActivitySnapshotController.recordSnapshot(ar);
+                }
+            }
         }
     }
 
-    // For legacy transition
-    void onTransitionStarting(DisplayContent displayContent) {
-        handleAppTransition(displayContent.mClosingApps, displayContent.mOpeningApps);
-    }
-
-    // For shell transition, adapt to legacy transition.
-    void onTransitionReady(@WindowManager.TransitionType int type,
-            ArraySet<WindowContainer> participants) {
+    void onTransitionFinish(@WindowManager.TransitionType int type,
+            ArrayList<Transition.ChangeInfo> changeInfos) {
         final boolean isTransitionOpen = isTransitionOpen(type);
         final boolean isTransitionClose = isTransitionClose(type);
         if (!isTransitionOpen && !isTransitionClose && type < TRANSIT_FIRST_CUSTOM
-                || (mActivatedType == 0)) {
+                || (changeInfos.isEmpty())) {
             return;
         }
-        final ArraySet<ActivityRecord> openingApps = new ArraySet<>();
-        final ArraySet<ActivityRecord> closingApps = new ArraySet<>();
-
-        for (int i = participants.size() - 1; i >= 0; --i) {
-            final ActivityRecord ar = participants.valueAt(i).asActivityRecord();
-            if (ar == null || ar.getTask() == null) continue;
-            if (ar.isVisibleRequested()) {
-                openingApps.add(ar);
-            } else {
-                closingApps.add(ar);
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "SnapshotController_analysis");
+        mActivitySnapshotController.beginSnapshotProcess();
+        final ArrayList<WindowContainer> windows = new ArrayList<>();
+        for (int i = changeInfos.size() - 1; i >= 0; --i) {
+            final WindowContainer wc = changeInfos.get(i).mContainer;
+            if (wc.asTask() == null && wc.asTaskFragment() == null
+                    && wc.asActivityRecord() == null) {
+                continue;
             }
+            windows.add(wc);
         }
-        handleAppTransition(closingApps, openingApps);
+        mActivitySnapshotController.handleTransitionFinish(windows);
+        mActivitySnapshotController.endSnapshotProcess();
+        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
     }
 
     private static boolean isTransitionOpen(int type) {
@@ -262,78 +135,6 @@
         return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
     }
 
-    @VisibleForTesting
-    void handleAppTransition(ArraySet<ActivityRecord> closingApps,
-            ArraySet<ActivityRecord> openApps) {
-        if (mActivatedType == 0) {
-            return;
-        }
-        analysisTransition(closingApps, openApps);
-        mActivitySnapshotController.preTransitionStart();
-        for (Integer transitionType : mTmpAnalysisRecord) {
-            notifyTransition(transitionType);
-        }
-        mActivitySnapshotController.postTransitionStart();
-        clearRecord();
-    }
-
-    private void notifyTransition(int transitionType) {
-        final TransitionState record = mTmpOpenCloseRecord.get(transitionType);
-        final ArrayList<Consumer<TransitionState>> consumers =
-                mTransitionStateConsumer.get(transitionType);
-        for (Consumer<TransitionState> consumer : consumers) {
-            consumer.accept(record);
-        }
-    }
-
-    private void analysisTransition(ArraySet<ActivityRecord> closingApps,
-            ArraySet<ActivityRecord> openingApps) {
-        getParticipantTasks(closingApps, mTmpCloseTasks, false /* isOpen */);
-        getParticipantTasks(openingApps, mTmpOpenTasks, true /* isOpen */);
-        if (DEBUG) {
-            Slog.d(TAG, "AppSnapshotController#analysisTransition participants"
-                    + " mTmpCloseTasks " + mTmpCloseTasks
-                    + " mTmpOpenTasks " + mTmpOpenTasks);
-        }
-        for (int i = mTmpCloseTasks.size() - 1; i >= 0; i--) {
-            final Task closeTask = mTmpCloseTasks.valueAt(i);
-            if (mTmpOpenTasks.contains(closeTask)) {
-                if (hasTransitionStateConsumer(ACTIVITY_OPEN)
-                        || hasTransitionStateConsumer(ACTIVITY_CLOSE)) {
-                    mActivityOrderCheck.analysisOrder(closingApps, openingApps, closeTask,
-                            mResultHandler);
-                }
-            } else if (hasTransitionStateConsumer(TASK_CLOSE)) {
-                // close task transition
-                addTransitionRecord(TASK_CLOSE, false /*open*/, closeTask);
-            }
-        }
-        if (hasTransitionStateConsumer(TASK_OPEN)) {
-            for (int i = mTmpOpenTasks.size() - 1; i >= 0; i--) {
-                final Task openTask = mTmpOpenTasks.valueAt(i);
-                if (!mTmpCloseTasks.contains(openTask)) {
-                    // this is open task transition
-                    addTransitionRecord(TASK_OPEN, true /*open*/, openTask);
-                }
-            }
-        }
-        mTmpCloseTasks.clear();
-        mTmpOpenTasks.clear();
-    }
-
-    private void getParticipantTasks(ArraySet<ActivityRecord> activityRecords, ArraySet<Task> tasks,
-            boolean isOpen) {
-        for (int i = activityRecords.size() - 1; i >= 0; i--) {
-            final ActivityRecord activity = activityRecords.valueAt(i);
-            final Task task = activity.getTask();
-            if (task == null) continue;
-
-            if (isOpen == activity.isVisibleRequested()) {
-                tasks.add(task);
-            }
-        }
-    }
-
     void dump(PrintWriter pw, String prefix) {
         mTaskSnapshotController.dump(pw, prefix);
         mActivitySnapshotController.dump(pw, prefix);
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index 58e1c54..f4f641f 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.graphics.Bitmap.CompressFormat.JPEG;
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -26,6 +27,7 @@
 import android.graphics.Bitmap;
 import android.os.Process;
 import android.os.SystemClock;
+import android.os.Trace;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.window.TaskSnapshot;
@@ -249,6 +251,7 @@
 
         @Override
         void write() {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "StoreWriteQueueItem");
             if (!mPersistInfoProvider.createDirectory(mUserId)) {
                 Slog.e(TAG, "Unable to create snapshot directory for user dir="
                         + mPersistInfoProvider.getDirectory(mUserId));
@@ -263,6 +266,7 @@
             if (failed) {
                 deleteSnapshot(mId, mUserId, mPersistInfoProvider);
             }
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         }
 
         boolean writeProto() {
@@ -373,7 +377,9 @@
 
         @Override
         void write() {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "DeleteWriteQueueItem");
             deleteSnapshot(mId, mUserId, mPersistInfoProvider);
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 2b22d75..34806bd 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -65,6 +65,9 @@
     /** Whether to prepare the removal animation. */
     boolean mPrepareRemoveAnimation;
 
+    /** Non-zero if this starting window is added in a collecting transition. */
+    int mTransitionId;
+
     protected StartingData(WindowManagerService service, int typeParams) {
         mService = service;
         mTypeParams = typeParams;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 6caccdd..8fbaac2 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2878,8 +2878,8 @@
             // No need to check if allowed if it's leaving dragResize
             if (dragResizing
                     && !(getRootTask().getWindowingMode() == WINDOWING_MODE_FREEFORM)) {
-                throw new IllegalArgumentException("Drag resize not allow for root task id="
-                        + getRootTaskId());
+                Slog.e(TAG, "Drag resize isn't allowed for root task id=" + getRootTaskId());
+                return;
             }
             mDragResizing = dragResizing;
             resetDragResizingChangeReported();
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index c747c09..4eb4290 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.wm;
 
-import static com.android.server.wm.SnapshotController.TASK_CLOSE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
@@ -77,13 +76,6 @@
         setSnapshotEnabled(snapshotEnabled);
     }
 
-    void systemReady() {
-        if (!shouldDisableSnapshots()) {
-            mService.mSnapshotController.registerTransitionStateConsumer(TASK_CLOSE,
-                    this::handleTaskClose);
-        }
-    }
-
     static PersistInfoProvider createPersistInfoProvider(WindowManagerService service,
             BaseAppSnapshotPersister.DirectoryResolver resolver) {
         final float highResTaskSnapshotScale = service.mContext.getResources().getFloat(
@@ -116,20 +108,23 @@
                 enableLowResSnapshots, lowResScaleFactor, use16BitFormat);
     }
 
-    void handleTaskClose(SnapshotController.TransitionState<Task> closeTaskTransitionRecord) {
+    // Still needed for legacy transition.(AppTransitionControllerTest)
+    void handleClosingApps(ArraySet<ActivityRecord> closingApps) {
         if (shouldDisableSnapshots()) {
             return;
         }
+        // We need to take a snapshot of the task if and only if all activities of the task are
+        // either closing or hidden.
         mTmpTasks.clear();
-        final ArraySet<Task> tasks = closeTaskTransitionRecord.getParticipant(false /* open */);
-        if (mService.mAtmService.getTransitionController().isShellTransitionsEnabled()) {
-            mTmpTasks.addAll(tasks);
-        } else {
-            for (Task task : tasks) {
-                getClosingTasksInner(task, mTmpTasks);
-            }
+        for (int i = closingApps.size() - 1; i >= 0; i--) {
+            final ActivityRecord activity = closingApps.valueAt(i);
+            final Task task = activity.getTask();
+            if (task == null) continue;
+
+            getClosingTasksInner(task, mTmpTasks);
         }
         snapshotTasks(mTmpTasks);
+        mTmpTasks.clear();
         mSkipClosingAppSnapshotTasks.clear();
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
index cd15119..3e8c017 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
@@ -16,6 +16,9 @@
 
 package com.android.server.wm;
 
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+
+import android.os.Trace;
 import android.util.ArraySet;
 import android.window.TaskSnapshot;
 
@@ -102,6 +105,7 @@
 
         @Override
         void write() {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "RemoveObsoleteFilesQueueItem");
             final ArraySet<Integer> newPersistedTaskIds;
             synchronized (mLock) {
                 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
@@ -120,6 +124,7 @@
                     }
                 }
             }
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         }
 
         @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index fd25edf..f1fb17b 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1191,8 +1191,6 @@
                                         "  Skipping post-transition snapshot for task %d",
                                         task.mTaskId);
                             }
-                            snapController.mActivitySnapshotController
-                                    .notifyAppVisibilityChanged(ar, false /* visible */);
                         }
                         ar.commitVisibility(false /* visible */, false /* performLayout */,
                                 true /* fromTransition */);
@@ -1389,6 +1387,7 @@
         // Handle back animation if it's already started.
         mController.mAtm.mBackNavigationController.onTransitionFinish(mTargets, this);
         mController.mFinishingTransition = null;
+        mController.mSnapshotController.onTransitionFinish(mType, mTargets);
     }
 
     void abort() {
@@ -1593,16 +1592,7 @@
         // transferred. If transition is transient, IME won't be moved during the transition and
         // the tasks are still live, so we take the snapshot at the end of the transition instead.
         if (mTransientLaunches == null) {
-            for (int i = mParticipants.size() - 1; i >= 0; --i) {
-                final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
-                if (ar == null || ar.getTask() == null
-                        || ar.getTask().isVisibleRequested()) continue;
-                final ChangeInfo change = mChanges.get(ar);
-                // Intentionally skip record snapshot for changes originated from PiP.
-                if (change != null && change.mWindowingMode == WINDOWING_MODE_PINNED) continue;
-                mController.mSnapshotController.mTaskSnapshotController.recordSnapshot(
-                        ar.getTask(), false /* allowSnapshotHome */);
-            }
+            mController.mSnapshotController.onTransactionReady(mType, mTargets);
         }
 
         // This is non-null only if display has changes. It handles the visible windows that don't
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index af8fb02..c59d2d3 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -145,6 +145,27 @@
         }
     }
 
+    void logRemovingStartingWindow(@NonNull StartingData startingData) {
+        if (startingData.mTransitionId == 0) {
+            return;
+        }
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID,
+                    startingData.mTransitionId);
+            outputStream.write(
+                    com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS,
+                    SystemClock.elapsedRealtimeNanos());
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
     private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream,
             Transition transition, ArrayList<ChangeInfo> targets) {
         Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto");
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 0388496..9375b29 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1657,9 +1657,18 @@
     }
 
     private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) {
-        final TaskFragment root1 = WindowContainer.fromBinder(hop.getContainer()).asTaskFragment();
-        final TaskFragment root2 =
-                WindowContainer.fromBinder(hop.getAdjacentRoot()).asTaskFragment();
+        final WindowContainer wc1 = WindowContainer.fromBinder(hop.getContainer());
+        if (wc1 == null || !wc1.isAttached()) {
+            Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc1);
+            return TRANSACT_EFFECTS_NONE;
+        }
+        final TaskFragment root1 = wc1.asTaskFragment();
+        final WindowContainer wc2 = WindowContainer.fromBinder(hop.getAdjacentRoot());
+        if (wc2 == null || !wc2.isAttached()) {
+            Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc2);
+            return TRANSACT_EFFECTS_NONE;
+        }
+        final TaskFragment root2 = wc2.asTaskFragment();
         if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) {
             throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by"
                     + " organizer root1=" + root1 + " root2=" + root2);
@@ -1672,7 +1681,12 @@
     }
 
     private int clearAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) {
-        final TaskFragment root = WindowContainer.fromBinder(hop.getContainer()).asTaskFragment();
+        final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer());
+        if (wc == null || !wc.isAttached()) {
+            Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc);
+            return TRANSACT_EFFECTS_NONE;
+        }
+        final TaskFragment root = wc.asTaskFragment();
         if (!root.mCreatedByOrganizer) {
             throw new IllegalArgumentException("clearAdjacentRootsHierarchyOp: Not created by"
                     + " organizer root=" + root);
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index d7d2b4e..caec45c5 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -48,6 +48,7 @@
 import static java.util.Objects.requireNonNull;
 
 import android.Manifest;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -75,7 +76,6 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
-import android.view.IRemoteAnimationRunner;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -87,6 +87,8 @@
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -249,11 +251,30 @@
     @Nullable
     private ArrayMap<ActivityRecord, int[]> mRemoteActivities;
 
-    /** Whether our process is currently running a {@link RecentsAnimation} */
-    private boolean mRunningRecentsAnimation;
+    /**
+     * It can be set for a running transition player ({@link android.window.ITransitionPlayer}) or
+     * remote animators (running {@link android.window.IRemoteTransition}).
+     */
+    static final int ANIMATING_REASON_REMOTE_ANIMATION = 1;
+    /** It is set for wakefulness transition. */
+    static final int ANIMATING_REASON_WAKEFULNESS_CHANGE = 1 << 1;
+    /** Whether the legacy {@link RecentsAnimation} is running. */
+    static final int ANIMATING_REASON_LEGACY_RECENT_ANIMATION = 1 << 2;
 
-    /** Whether our process is currently running a {@link IRemoteAnimationRunner} */
-    private boolean mRunningRemoteAnimation;
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            ANIMATING_REASON_REMOTE_ANIMATION,
+            ANIMATING_REASON_WAKEFULNESS_CHANGE,
+            ANIMATING_REASON_LEGACY_RECENT_ANIMATION,
+    })
+    @interface AnimatingReason {}
+
+    /**
+     * Non-zero if this process is currently running an important animation. This should be never
+     * set for system server.
+     */
+    @AnimatingReason
+    private int mAnimatingReasons;
 
     // The bits used for mActivityStateFlags.
     private static final int ACTIVITY_STATE_FLAG_IS_VISIBLE = 1 << 16;
@@ -1847,30 +1868,45 @@
     }
 
     void setRunningRecentsAnimation(boolean running) {
-        if (mRunningRecentsAnimation == running) {
-            return;
+        if (running) {
+            addAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION);
+        } else {
+            removeAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION);
         }
-        mRunningRecentsAnimation = running;
-        updateRunningRemoteOrRecentsAnimation();
     }
 
     void setRunningRemoteAnimation(boolean running) {
-        if (mRunningRemoteAnimation == running) {
-            return;
+        if (running) {
+            addAnimatingReason(ANIMATING_REASON_REMOTE_ANIMATION);
+        } else {
+            removeAnimatingReason(ANIMATING_REASON_REMOTE_ANIMATION);
         }
-        mRunningRemoteAnimation = running;
-        updateRunningRemoteOrRecentsAnimation();
     }
 
-    void updateRunningRemoteOrRecentsAnimation() {
+    void addAnimatingReason(@AnimatingReason int reason) {
+        final int prevReasons = mAnimatingReasons;
+        mAnimatingReasons |= reason;
+        if (prevReasons == 0) {
+            setAnimating(true);
+        }
+    }
+
+    void removeAnimatingReason(@AnimatingReason int reason) {
+        final int prevReasons = mAnimatingReasons;
+        mAnimatingReasons &= ~reason;
+        if (prevReasons != 0 && mAnimatingReasons == 0) {
+            setAnimating(false);
+        }
+    }
+
+    /** Applies the animating state to activity manager for updating process priority. */
+    private void setAnimating(boolean animating) {
         // Posting on handler so WM lock isn't held when we call into AM.
-        mAtm.mH.sendMessage(PooledLambda.obtainMessage(
-                WindowProcessListener::setRunningRemoteAnimation, mListener,
-                isRunningRemoteTransition()));
+        mAtm.mH.post(() -> mListener.setRunningRemoteAnimation(animating));
     }
 
     boolean isRunningRemoteTransition() {
-        return mRunningRecentsAnimation || mRunningRemoteAnimation;
+        return (mAnimatingReasons & ANIMATING_REASON_REMOTE_ANIMATION) != 0;
     }
 
     /** Adjusts scheduling group for animation. This method MUST NOT be called inside WM lock. */
@@ -1924,6 +1960,21 @@
         pw.println(prefix + " mLastReportedConfiguration=" + (mHasCachedConfiguration
                 ? ("(cached) " + mLastReportedConfiguration) : mLastReportedConfiguration));
 
+        final int animatingReasons = mAnimatingReasons;
+        if (animatingReasons != 0) {
+            pw.print(prefix + " mAnimatingReasons=");
+            if ((animatingReasons & ANIMATING_REASON_REMOTE_ANIMATION) != 0) {
+                pw.print("remote-animation|");
+            }
+            if ((animatingReasons & ANIMATING_REASON_WAKEFULNESS_CHANGE) != 0) {
+                pw.print("wakefulness|");
+            }
+            if ((animatingReasons & ANIMATING_REASON_LEGACY_RECENT_ANIMATION) != 0) {
+                pw.print("legacy-recents");
+            }
+            pw.println();
+        }
+
         final int stateFlags = mActivityStateFlags;
         if (stateFlags != ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER) {
             pw.print(prefix + " mActivityStateFlags=");
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 8faeebc..c09e6a3 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -2412,7 +2412,7 @@
         ProtoLog.v(WM_DEBUG_ADD_REMOVE,
                 "removeIfPossible: %s callers=%s", this, Debug.getCallers(5));
 
-        final boolean startingWindow = mAttrs.type == TYPE_APPLICATION_STARTING;
+        final boolean startingWindow = mStartingData != null;
         if (startingWindow) {
             ProtoLog.d(WM_DEBUG_STARTING_WINDOW, "Starting window removed %s", this);
             // Cancel the remove starting window animation on shell. The main window might changed
@@ -2426,6 +2426,7 @@
                     return false;
                 }, true);
             }
+            mTransitionController.mTransitionTracer.logRemovingStartingWindow(mStartingData);
         } else if (mAttrs.type == TYPE_BASE_APPLICATION
                 && isSelfAnimating(0, ANIMATION_TYPE_STARTING_REVEAL)) {
             // Cancel the remove starting window animation in case the binder dead before remove
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 0cfddd3..769be17 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -351,6 +351,8 @@
         assertEquals(startFingerprintNow ? BiometricSensor.STATE_AUTHENTICATING
                         : BiometricSensor.STATE_COOKIE_RETURNED,
                 session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+        verify(mBiometricContext).updateContext((OperationContextExt) anyObject(),
+                eq(session.isCrypto()));
 
         // start fingerprint sensor if it was delayed
         if (!startFingerprintNow) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
new file mode 100644
index 0000000..99d66c5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class AuthenticationStatsCollectorTest {
+
+    private AuthenticationStatsCollector mAuthenticationStatsCollector;
+    private static final float FRR_THRESHOLD = 0.2f;
+    private static final int USER_ID_1 = 1;
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1))
+                .thenReturn(FRR_THRESHOLD);
+
+        mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+                0 /* modality */);
+    }
+
+
+    @Test
+    public void authenticate_authenticationSucceeded_mapShouldBeUpdated() {
+        // Assert that the user doesn't exist in the map initially.
+        assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1));
+
+        mAuthenticationStatsCollector.authenticate(USER_ID_1, true /* authenticated*/);
+
+        AuthenticationStats authenticationStats =
+                mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1);
+        assertEquals(USER_ID_1, authenticationStats.getUserId());
+        assertEquals(1, authenticationStats.getTotalAttempts());
+        assertEquals(0, authenticationStats.getRejectedAttempts());
+        assertEquals(0, authenticationStats.getEnrollmentNotifications());
+    }
+
+    @Test
+    public void authenticate_authenticationFailed_mapShouldBeUpdated() {
+        // Assert that the user doesn't exist in the map initially.
+        assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1));
+
+        mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated*/);
+
+        AuthenticationStats authenticationStats =
+                mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1);
+        assertEquals(USER_ID_1, authenticationStats.getUserId());
+        assertEquals(1, authenticationStats.getTotalAttempts());
+        assertEquals(1, authenticationStats.getRejectedAttempts());
+        assertEquals(0, authenticationStats.getEnrollmentNotifications());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
new file mode 100644
index 0000000..e8e72cb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class AuthenticationStatsTest {
+
+    @Test
+    public void authenticate_statsShouldBeUpdated() {
+        AuthenticationStats authenticationStats =
+                new AuthenticationStats(1 /* userId */ , 0 /* totalAttempts */,
+                        0 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+                        0 /* modality */);
+
+        authenticationStats.authenticate(true /* authenticated */);
+
+        assertEquals(authenticationStats.getTotalAttempts(), 1);
+        assertEquals(authenticationStats.getRejectedAttempts(), 0);
+
+        authenticationStats.authenticate(false /* authenticated */);
+
+        assertEquals(authenticationStats.getTotalAttempts(), 2);
+        assertEquals(authenticationStats.getRejectedAttempts(), 1);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
index 612f717..a508718 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
@@ -39,6 +39,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.biometrics.AuthenticationStatsCollector;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 
 import org.junit.Before;
@@ -65,6 +66,8 @@
     @Mock
     private BiometricFrameworkStatsLogger mSink;
     @Mock
+    private AuthenticationStatsCollector mAuthenticationStatsCollector;
+    @Mock
     private SensorManager mSensorManager;
     @Mock
     private BaseClientMonitor mClient;
@@ -87,7 +90,8 @@
     }
 
     private BiometricLogger createLogger(int statsModality, int statsAction, int statsClient) {
-        return new BiometricLogger(statsModality, statsAction, statsClient, mSink, mSensorManager);
+        return new BiometricLogger(statsModality, statsAction, statsClient, mSink,
+                mAuthenticationStatsCollector, mSensorManager);
     }
 
     @Test
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 d1d6e9d..f43120d 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
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.hardware.biometrics.common.CommonProps;
 import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
@@ -39,6 +40,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
@@ -59,11 +61,15 @@
 
     private static final String TAG = "FaceProviderTest";
 
+    private static final float FRR_THRESHOLD = 0.2f;
+
     @Mock
     private Context mContext;
     @Mock
     private UserManager mUserManager;
     @Mock
+    private Resources mResources;
+    @Mock
     private IFace mDaemon;
     @Mock
     private BiometricContext mBiometricContext;
@@ -86,6 +92,10 @@
         when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>());
         when(mDaemon.createSession(anyInt(), anyInt(), any())).thenReturn(mock(ISession.class));
 
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1))
+                .thenReturn(FRR_THRESHOLD);
+
         final SensorProps sensor1 = new SensorProps();
         sensor1.commonProps = new CommonProps();
         sensor1.commonProps.sensorId = 0;
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
index d174533..e558c4d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.face.FaceSensorProperties;
@@ -41,6 +42,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -65,12 +67,15 @@
     private static final String TAG = "Face10Test";
     private static final int SENSOR_ID = 1;
     private static final int USER_ID = 20;
+    private static final float FRR_THRESHOLD = 0.2f;
 
     @Mock
     private Context mContext;
     @Mock
     private UserManager mUserManager;
     @Mock
+    private Resources mResources;
+    @Mock
     private BiometricScheduler mScheduler;
     @Mock
     private BiometricContext mBiometricContext;
@@ -93,6 +98,10 @@
         when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
         when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>());
 
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1))
+                .thenReturn(FRR_THRESHOLD);
+
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
         final int maxEnrollmentsPerUser = 1;
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 0eca8c9..98f1843 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
@@ -18,6 +18,9 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
 import static org.junit.Assert.assertEquals;
 
 import android.platform.test.annotations.Presubmit;
@@ -28,6 +31,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+
 /**
  * Test class for {@link ActivitySnapshotController}.
  *
@@ -42,13 +47,13 @@
     private ActivitySnapshotController mActivitySnapshotController;
     @Before
     public void setUp() throws Exception {
+        spyOn(mWm.mSnapshotController.mActivitySnapshotController);
         mActivitySnapshotController = mWm.mSnapshotController.mActivitySnapshotController;
+        doReturn(false).when(mActivitySnapshotController).shouldDisableSnapshots();
         mActivitySnapshotController.resetTmpFields();
     }
     @Test
     public void testOpenActivityTransition() {
-        final SnapshotController.TransitionState transitionState =
-                new SnapshotController.TransitionState();
         final Task task = createTask(mDisplayContent);
         // note for createAppWindow: the new child is added at index 0
         final WindowState openingWindow = createAppWindow(task,
@@ -59,14 +64,12 @@
                 "closingWindow");
         closingWindow.mActivityRecord.commitVisibility(
                 false /* visible */, true /* performLayout */);
-        transitionState.addParticipant(closingWindow.mActivityRecord, false);
-        transitionState.addParticipant(openingWindow.mActivityRecord, true);
-        mActivitySnapshotController.handleOpenActivityTransition(transitionState);
+        final ArrayList<WindowContainer> windows = new ArrayList<>();
+        windows.add(openingWindow.mActivityRecord);
+        windows.add(closingWindow.mActivityRecord);
+        mActivitySnapshotController.handleTransitionFinish(windows);
 
-        assertEquals(1, mActivitySnapshotController.mPendingCaptureActivity.size());
         assertEquals(0, mActivitySnapshotController.mPendingRemoveActivity.size());
-        assertEquals(closingWindow.mActivityRecord,
-                mActivitySnapshotController.mPendingCaptureActivity.valueAt(0));
         mActivitySnapshotController.resetTmpFields();
 
         // simulate three activity
@@ -74,19 +77,15 @@
                 "belowClose");
         belowClose.mActivityRecord.commitVisibility(
                 false /* visible */, true /* performLayout */);
-        mActivitySnapshotController.handleOpenActivityTransition(transitionState);
-        assertEquals(1, mActivitySnapshotController.mPendingCaptureActivity.size());
+        windows.add(belowClose.mActivityRecord);
+        mActivitySnapshotController.handleTransitionFinish(windows);
         assertEquals(1, mActivitySnapshotController.mPendingRemoveActivity.size());
-        assertEquals(closingWindow.mActivityRecord,
-                mActivitySnapshotController.mPendingCaptureActivity.valueAt(0));
         assertEquals(belowClose.mActivityRecord,
                 mActivitySnapshotController.mPendingRemoveActivity.valueAt(0));
     }
 
     @Test
     public void testCloseActivityTransition() {
-        final SnapshotController.TransitionState transitionState =
-                new SnapshotController.TransitionState();
         final Task task = createTask(mDisplayContent);
         // note for createAppWindow: the new child is added at index 0
         final WindowState closingWindow = createAppWindow(task, ACTIVITY_TYPE_STANDARD,
@@ -97,10 +96,10 @@
                 ACTIVITY_TYPE_STANDARD, "openingWindow");
         openingWindow.mActivityRecord.commitVisibility(
                 true /* visible */, true /* performLayout */);
-        transitionState.addParticipant(closingWindow.mActivityRecord, false);
-        transitionState.addParticipant(openingWindow.mActivityRecord, true);
-        mActivitySnapshotController.handleCloseActivityTransition(transitionState);
-        assertEquals(0, mActivitySnapshotController.mPendingCaptureActivity.size());
+        final ArrayList<WindowContainer> windows = new ArrayList<>();
+        windows.add(openingWindow.mActivityRecord);
+        windows.add(closingWindow.mActivityRecord);
+        mActivitySnapshotController.handleTransitionFinish(windows);
         assertEquals(1, mActivitySnapshotController.mPendingDeleteActivity.size());
         assertEquals(openingWindow.mActivityRecord,
                 mActivitySnapshotController.mPendingDeleteActivity.valueAt(0));
@@ -111,8 +110,8 @@
                 "belowOpen");
         belowOpen.mActivityRecord.commitVisibility(
                 false /* visible */, true /* performLayout */);
-        mActivitySnapshotController.handleCloseActivityTransition(transitionState);
-        assertEquals(0, mActivitySnapshotController.mPendingCaptureActivity.size());
+        windows.add(belowOpen.mActivityRecord);
+        mActivitySnapshotController.handleTransitionFinish(windows);
         assertEquals(1, mActivitySnapshotController.mPendingDeleteActivity.size());
         assertEquals(1, mActivitySnapshotController.mPendingLoadActivity.size());
         assertEquals(openingWindow.mActivityRecord,
@@ -123,10 +122,6 @@
 
     @Test
     public void testTaskTransition() {
-        final SnapshotController.TransitionState taskCloseTransition =
-                new SnapshotController.TransitionState();
-        final SnapshotController.TransitionState taskOpenTransition =
-                new SnapshotController.TransitionState();
         final Task closeTask = createTask(mDisplayContent);
         // note for createAppWindow: the new child is added at index 0
         final WindowState closingWindow = createAppWindow(closeTask, ACTIVITY_TYPE_STANDARD,
@@ -147,10 +142,10 @@
                 "openingWindowBelow");
         openingWindowBelow.mActivityRecord.commitVisibility(
                 false /* visible */, true /* performLayout */);
-        taskCloseTransition.addParticipant(closeTask, false);
-        taskOpenTransition.addParticipant(openTask, true);
-        mActivitySnapshotController.handleCloseTaskTransition(taskCloseTransition);
-        mActivitySnapshotController.handleOpenTaskTransition(taskOpenTransition);
+        final ArrayList<WindowContainer> windows = new ArrayList<>();
+        windows.add(closeTask);
+        windows.add(openTask);
+        mActivitySnapshotController.handleTransitionFinish(windows);
 
         assertEquals(1, mActivitySnapshotController.mPendingRemoveActivity.size());
         assertEquals(closingWindowBelow.mActivityRecord,
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppSnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/AppSnapshotControllerTests.java
deleted file mode 100644
index 83af1814..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/AppSnapshotControllerTests.java
+++ /dev/null
@@ -1,171 +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.wm;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-
-import static com.android.server.wm.SnapshotController.ACTIVITY_CLOSE;
-import static com.android.server.wm.SnapshotController.ACTIVITY_OPEN;
-import static com.android.server.wm.SnapshotController.TASK_CLOSE;
-import static com.android.server.wm.SnapshotController.TASK_OPEN;
-
-import static org.junit.Assert.assertTrue;
-
-import android.platform.test.annotations.Presubmit;
-import android.util.ArraySet;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-
-/**
- * Test class for {@link SnapshotController}.
- *
- * Build/Install/Run:
- *  *  atest WmTests:AppSnapshotControllerTests
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class AppSnapshotControllerTests extends WindowTestsBase {
-    final ArraySet<ActivityRecord> mClosingApps = new ArraySet<>();
-    final ArraySet<ActivityRecord> mOpeningApps = new ArraySet<>();
-
-    final TransitionMonitor mOpenActivityMonitor = new TransitionMonitor();
-    final TransitionMonitor mCloseActivityMonitor = new TransitionMonitor();
-    final TransitionMonitor mOpenTaskMonitor = new TransitionMonitor();
-    final TransitionMonitor mCloseTaskMonitor = new TransitionMonitor();
-
-    @Before
-    public void setUp() throws Exception {
-        resetStatus();
-        mWm.mSnapshotController.registerTransitionStateConsumer(
-                ACTIVITY_CLOSE, mCloseActivityMonitor::handleTransition);
-        mWm.mSnapshotController.registerTransitionStateConsumer(
-                ACTIVITY_OPEN, mOpenActivityMonitor::handleTransition);
-        mWm.mSnapshotController.registerTransitionStateConsumer(
-                TASK_CLOSE, mCloseTaskMonitor::handleTransition);
-        mWm.mSnapshotController.registerTransitionStateConsumer(
-                TASK_OPEN, mOpenTaskMonitor::handleTransition);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mWm.mSnapshotController.unregisterTransitionStateConsumer(
-                ACTIVITY_CLOSE, mCloseActivityMonitor::handleTransition);
-        mWm.mSnapshotController.unregisterTransitionStateConsumer(
-                ACTIVITY_OPEN, mOpenActivityMonitor::handleTransition);
-        mWm.mSnapshotController.unregisterTransitionStateConsumer(
-                TASK_CLOSE, mCloseTaskMonitor::handleTransition);
-        mWm.mSnapshotController.unregisterTransitionStateConsumer(
-                TASK_OPEN, mOpenTaskMonitor::handleTransition);
-    }
-
-    private static class TransitionMonitor {
-        private final ArraySet<WindowContainer> mOpenParticipant = new ArraySet<>();
-        private final ArraySet<WindowContainer> mCloseParticipant = new ArraySet<>();
-        void handleTransition(SnapshotController.TransitionState<ActivityRecord> state) {
-            mOpenParticipant.addAll(state.getParticipant(true /* open */));
-            mCloseParticipant.addAll(state.getParticipant(false /* close */));
-        }
-        void reset() {
-            mOpenParticipant.clear();
-            mCloseParticipant.clear();
-        }
-    }
-
-    private void resetStatus() {
-        mClosingApps.clear();
-        mOpeningApps.clear();
-        mOpenActivityMonitor.reset();
-        mCloseActivityMonitor.reset();
-        mOpenTaskMonitor.reset();
-        mCloseTaskMonitor.reset();
-    }
-
-    @Test
-    public void testHandleAppTransition_openActivityTransition() {
-        final Task task = createTask(mDisplayContent);
-        // note for createAppWindow: the new child is added at index 0
-        final WindowState openingWindow = createAppWindow(task,
-                ACTIVITY_TYPE_STANDARD, "openingWindow");
-        openingWindow.mActivityRecord.commitVisibility(
-                true /* visible */, true /* performLayout */);
-        final WindowState closingWindow = createAppWindow(task, ACTIVITY_TYPE_STANDARD,
-                "closingWindow");
-        closingWindow.mActivityRecord.commitVisibility(
-                false /* visible */, true /* performLayout */);
-        mClosingApps.add(closingWindow.mActivityRecord);
-        mOpeningApps.add(openingWindow.mActivityRecord);
-        mWm.mSnapshotController.handleAppTransition(mClosingApps, mOpeningApps);
-        assertTrue(mOpenActivityMonitor.mCloseParticipant.contains(closingWindow.mActivityRecord));
-        assertTrue(mOpenActivityMonitor.mOpenParticipant.contains(openingWindow.mActivityRecord));
-    }
-
-    @Test
-    public void testHandleAppTransition_closeActivityTransition() {
-        final Task task = createTask(mDisplayContent);
-        // note for createAppWindow: the new child is added at index 0
-        final WindowState closingWindow = createAppWindow(task, ACTIVITY_TYPE_STANDARD,
-                "closingWindow");
-        closingWindow.mActivityRecord.commitVisibility(
-                false /* visible */, true /* performLayout */);
-        final WindowState openingWindow = createAppWindow(task,
-                ACTIVITY_TYPE_STANDARD, "openingWindow");
-        openingWindow.mActivityRecord.commitVisibility(
-                true /* visible */, true /* performLayout */);
-        mClosingApps.add(closingWindow.mActivityRecord);
-        mOpeningApps.add(openingWindow.mActivityRecord);
-        mWm.mSnapshotController.handleAppTransition(mClosingApps, mOpeningApps);
-        assertTrue(mCloseActivityMonitor.mCloseParticipant.contains(closingWindow.mActivityRecord));
-        assertTrue(mCloseActivityMonitor.mOpenParticipant.contains(openingWindow.mActivityRecord));
-    }
-
-    @Test
-    public void testHandleAppTransition_TaskTransition() {
-        final Task closeTask = createTask(mDisplayContent);
-        // note for createAppWindow: the new child is added at index 0
-        final WindowState closingWindow = createAppWindow(closeTask, ACTIVITY_TYPE_STANDARD,
-                "closingWindow");
-        closingWindow.mActivityRecord.commitVisibility(
-                false /* visible */, true /* performLayout */);
-        final WindowState closingWindowBelow = createAppWindow(closeTask, ACTIVITY_TYPE_STANDARD,
-                "closingWindowBelow");
-        closingWindowBelow.mActivityRecord.commitVisibility(
-                false /* visible */, true /* performLayout */);
-
-        final Task openTask = createTask(mDisplayContent);
-        final WindowState openingWindow = createAppWindow(openTask, ACTIVITY_TYPE_STANDARD,
-                "openingWindow");
-        openingWindow.mActivityRecord.commitVisibility(
-                true /* visible */, true /* performLayout */);
-        final WindowState openingWindowBelow = createAppWindow(openTask, ACTIVITY_TYPE_STANDARD,
-                "openingWindowBelow");
-        openingWindowBelow.mActivityRecord.commitVisibility(
-                false /* visible */, true /* performLayout */);
-
-        mClosingApps.add(closingWindow.mActivityRecord);
-        mOpeningApps.add(openingWindow.mActivityRecord);
-        mWm.mSnapshotController.handleAppTransition(mClosingApps, mOpeningApps);
-        assertTrue(mCloseTaskMonitor.mCloseParticipant.contains(closeTask));
-        assertTrue(mOpenTaskMonitor.mOpenParticipant.contains(openTask));
-    }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index bdd178b..9cc4117 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2054,6 +2054,17 @@
         assertNotEquals(testPlayer.mLastReady.getChange(dcToken).getEndRotation(),
                 testPlayer.mLastReady.getChange(dcToken).getStartRotation());
         testPlayer.finish();
+
+        // The AsyncRotationController should only exist if there is an ongoing rotation change.
+        dc.finishAsyncRotationIfPossible();
+        dc.setLastHasContent();
+        doReturn(dr.getRotation() + 1).when(dr).rotationForOrientation(anyInt(), anyInt());
+        dr.updateRotationUnchecked(true /* forceUpdate */);
+        assertNotNull(dc.getAsyncRotationController());
+        doReturn(dr.getRotation() - 1).when(dr).rotationForOrientation(anyInt(), anyInt());
+        dr.updateRotationUnchecked(true /* forceUpdate */);
+        assertNull("Cancel AsyncRotationController for the intermediate rotation changes 0->1->0",
+                dc.getAsyncRotationController());
     }
 
     @Test
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 ca5d8fe..b4f1176 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -46,7 +46,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wm.SnapshotController.TASK_CLOSE;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static org.junit.Assert.assertEquals;
@@ -1383,8 +1382,6 @@
     @Test
     public void testTransientLaunch() {
         spyOn(mWm.mSnapshotController.mTaskSnapshotController);
-        mWm.mSnapshotController.registerTransitionStateConsumer(TASK_CLOSE,
-                mWm.mSnapshotController.mTaskSnapshotController::handleTaskClose);
         final ArrayList<ActivityRecord> enteringAnimReports = new ArrayList<>();
         final TransitionController controller = new TestTransitionController(mAtm) {
             @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index cf83981..ebe40b0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -170,9 +170,13 @@
     }
 
     @Test
-    public void testSetRunningRecentsAnimation() {
-        mWpc.setRunningRecentsAnimation(true);
-        mWpc.setRunningRecentsAnimation(false);
+    public void testSetAnimatingReason() {
+        mWpc.addAnimatingReason(WindowProcessController.ANIMATING_REASON_REMOTE_ANIMATION);
+        assertTrue(mWpc.isRunningRemoteTransition());
+        mWpc.addAnimatingReason(WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE);
+        mWpc.removeAnimatingReason(WindowProcessController.ANIMATING_REASON_REMOTE_ANIMATION);
+        assertFalse(mWpc.isRunningRemoteTransition());
+        mWpc.removeAnimatingReason(WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE);
         waitHandlerIdle(mAtm.mH);
 
         InOrder orderVerifier = Mockito.inOrder(mMockListener);
@@ -201,7 +205,7 @@
         waitHandlerIdle(mAtm.mH);
 
         InOrder orderVerifier = Mockito.inOrder(mMockListener);
-        orderVerifier.verify(mMockListener, times(3)).setRunningRemoteAnimation(eq(true));
+        orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(true));
         orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(false));
         orderVerifier.verifyNoMoreInteractions();
     }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index ccc4ac2..58da4b43 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -368,29 +368,29 @@
     /**
      * This method is only used by VisualQueryDetector.
      */
-    void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
+    boolean startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
         if (DEBUG) {
             Slog.d(TAG, "startPerceivingLocked");
         }
         final VisualQueryDetectorSession session = getVisualQueryDetectorSessionLocked();
         if (session == null) {
-            return;
+            return false;
         }
-        session.startPerceivingLocked(callback);
+        return session.startPerceivingLocked(callback);
     }
 
     /**
      * This method is only used by VisaulQueryDetector.
      */
-    void stopPerceivingLocked() {
+    boolean stopPerceivingLocked() {
         if (DEBUG) {
             Slog.d(TAG, "stopPerceivingLocked");
         }
         final VisualQueryDetectorSession session = getVisualQueryDetectorSessionLocked();
         if (session == null) {
-            return;
+            return false;
         }
-        session.stopPerceivingLocked();
+        return session.stopPerceivingLocked();
     }
 
     public void startListeningFromExternalSourceLocked(
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
index 2e05e20..4720d27 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
@@ -95,7 +95,7 @@
     }
 
     @SuppressWarnings("GuardedBy")
-    void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
+    boolean startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
         if (DEBUG) {
             Slog.d(TAG, "startPerceivingLocked");
         }
@@ -198,15 +198,16 @@
                 mQueryStreaming = false;
             }
         };
-        mRemoteDetectionService.run(service -> service.detectWithVisualSignals(internalCallback));
+        return mRemoteDetectionService.run(
+                service -> service.detectWithVisualSignals(internalCallback));
     }
 
     @SuppressWarnings("GuardedBy")
-    void stopPerceivingLocked() {
+    boolean stopPerceivingLocked() {
         if (DEBUG) {
             Slog.d(TAG, "stopPerceivingLocked");
         }
-        mRemoteDetectionService.run(ISandboxedDetectionService::stopDetection);
+        return mRemoteDetectionService.run(ISandboxedDetectionService::stopDetection);
     }
 
     @Override
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 3574ef8..0de9255 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -23,7 +23,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
-import android.app.ActivityThread;
 import android.app.AppGlobals;
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
@@ -51,7 +50,6 @@
 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
 import android.media.AudioFormat;
 import android.media.permission.Identity;
-import android.media.permission.IdentityContext;
 import android.media.permission.PermissionUtil;
 import android.media.permission.SafeCloseable;
 import android.os.Binder;
@@ -61,7 +59,6 @@
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
-import android.os.Process;
 import android.os.RemoteCallback;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -88,6 +85,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.internal.app.IVisualQueryRecognitionStatusListener;
 import com.android.internal.app.IVoiceActionCheckCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.internal.app.IVoiceInteractionSessionListener;
@@ -139,6 +137,7 @@
 
     private final RemoteCallbackList<IVoiceInteractionSessionListener>
             mVoiceInteractionSessionListeners = new RemoteCallbackList<>();
+    private IVisualQueryRecognitionStatusListener mVisualQueryRecognitionStatusListener;
 
     public VoiceInteractionManagerService(Context context) {
         super(context);
@@ -1346,6 +1345,17 @@
         @android.annotation.EnforcePermission(
                 android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
         @Override
+        public void subscribeVisualQueryRecognitionStatus(IVisualQueryRecognitionStatusListener
+                listener) {
+            super.subscribeVisualQueryRecognitionStatus_enforcePermission();
+            synchronized (this) {
+                mVisualQueryRecognitionStatusListener = listener;
+            }
+        }
+
+        @android.annotation.EnforcePermission(
+                android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
+        @Override
         public void enableVisualQueryDetection(
                 IVisualQueryDetectionAttentionListener listener) {
             super.enableVisualQueryDetection_enforcePermission();
@@ -1391,7 +1401,10 @@
                 }
                 final long caller = Binder.clearCallingIdentity();
                 try {
-                    mImpl.startPerceivingLocked(callback);
+                    boolean success = mImpl.startPerceivingLocked(callback);
+                    if (success && mVisualQueryRecognitionStatusListener != null) {
+                        mVisualQueryRecognitionStatusListener.onStartPerceiving();
+                    }
                 } finally {
                     Binder.restoreCallingIdentity(caller);
                 }
@@ -1409,7 +1422,10 @@
                 }
                 final long caller = Binder.clearCallingIdentity();
                 try {
-                    mImpl.stopPerceivingLocked();
+                    boolean success = mImpl.stopPerceivingLocked();
+                    if (success && mVisualQueryRecognitionStatusListener != null) {
+                        mVisualQueryRecognitionStatusListener.onStopPerceiving();
+                    }
                 } finally {
                     Binder.restoreCallingIdentity(caller);
                 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 5d88a65..471acc1 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -784,30 +784,30 @@
         mHotwordDetectionConnection.setVisualQueryDetectionAttentionListenerLocked(listener);
     }
 
-    public void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
+    public boolean startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
         if (DEBUG) {
             Slog.d(TAG, "startPerceivingLocked");
         }
 
         if (mHotwordDetectionConnection == null) {
             // TODO: callback.onError();
-            return;
+            return false;
         }
 
-        mHotwordDetectionConnection.startPerceivingLocked(callback);
+        return mHotwordDetectionConnection.startPerceivingLocked(callback);
     }
 
-    public void stopPerceivingLocked() {
+    public boolean stopPerceivingLocked() {
         if (DEBUG) {
             Slog.d(TAG, "stopPerceivingLocked");
         }
 
         if (mHotwordDetectionConnection == null) {
             Slog.w(TAG, "stopPerceivingLocked() called but connection isn't established");
-            return;
+            return false;
         }
 
-        mHotwordDetectionConnection.stopPerceivingLocked();
+        return mHotwordDetectionConnection.stopPerceivingLocked();
     }
 
     public void startListeningFromMicLocked(