Merge "Override applyLayoutFeatures for ConstraintHelper" into udc-dev
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
index a9f720a..515ddc8 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
@@ -80,7 +80,7 @@
 
     private void prepareForNextRun() {
         SystemClock.sleep(COOL_OFF_PERIOD_MS);
-        ShellHelper.runShellCommand("am wait-for-broadcast-idle");
+        ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
         mStartTimeNs = System.nanoTime();
         mPausedDurationNs = 0;
     }
@@ -102,7 +102,7 @@
      * to avoid unnecessary waiting.
      */
     public void resumeTiming() {
-        ShellHelper.runShellCommand("am wait-for-broadcast-idle");
+        ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
         resumeTimer();
     }
 
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index 19a4766..6dba5b3 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -1541,7 +1541,8 @@
 
     private void waitForBroadcastIdle() {
         try {
-            ShellHelper.runShellCommandWithTimeout("am wait-for-broadcast-idle", TIMEOUT_IN_SECOND);
+            ShellHelper.runShellCommandWithTimeout(
+                    "am wait-for-broadcast-idle --flush-broadcast-loopers", TIMEOUT_IN_SECOND);
         } catch (TimeoutException e) {
             Log.e(TAG, "Ending waitForBroadcastIdle because it is taking too long", e);
         }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index df9257c..5c1b3ee 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5006,12 +5006,6 @@
             return mUserExtras;
         }
 
-        private Bundle getAllExtras() {
-            final Bundle saveExtras = (Bundle) mUserExtras.clone();
-            saveExtras.putAll(mN.extras);
-            return saveExtras;
-        }
-
         /**
          * Add an action to this notification. Actions are typically displayed by
          * the system as a button adjacent to the notification content.
@@ -6617,9 +6611,16 @@
                                 + " vs bubble: " + mN.mBubbleMetadata.getShortcutId());
             }
 
-            // first, add any extras from the calling code
+            // Adds any new extras provided by the user.
             if (mUserExtras != null) {
-                mN.extras = getAllExtras();
+                final Bundle saveExtras = (Bundle) mUserExtras.clone();
+                if (SystemProperties.getBoolean(
+                        "persist.sysui.notification.builder_extras_override", false)) {
+                    mN.extras.putAll(saveExtras);
+                } else {
+                    saveExtras.putAll(mN.extras);
+                    mN.extras = saveExtras;
+                }
             }
 
             mN.creationTime = System.currentTimeMillis();
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 776e34b..385fd50 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -24,9 +24,11 @@
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
+import android.annotation.UserIdInt;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
+import android.compat.annotation.LoggingOnly;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -49,6 +51,7 @@
 import android.view.KeyEvent;
 import android.view.View;
 
+import com.android.internal.compat.IPlatformCompat;
 import com.android.internal.statusbar.AppClipsServiceConnector;
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.IStatusBarService;
@@ -170,6 +173,8 @@
     public @interface Disable2Flags {}
     // LINT.ThenChange(frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt)
 
+    private static final String TAG = "StatusBarManager";
+
     /**
      * Default disable flags for setup
      *
@@ -572,13 +577,13 @@
     private static final long MEDIA_CONTROL_SESSION_ACTIONS = 203800354L;
 
     /**
-     * Media controls based on {@link android.app.Notification.MediaStyle} notifications will be
-     * required to include a non-empty title, either in the {@link android.media.MediaMetadata} or
+     * Media controls based on {@link android.app.Notification.MediaStyle} notifications should
+     * include a non-empty title, either in the {@link android.media.MediaMetadata} or
      * notification title.
      */
     @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    private static final long MEDIA_CONTROL_REQUIRES_TITLE = 274775190L;
+    @LoggingOnly
+    private static final long MEDIA_CONTROL_BLANK_TITLE = 274775190L;
 
     @UnsupportedAppUsage
     private Context mContext;
@@ -586,6 +591,9 @@
     @UnsupportedAppUsage
     private IBinder mToken = new Binder();
 
+    private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface(
+            ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+
     @UnsupportedAppUsage
     StatusBarManager(Context context) {
         mContext = context;
@@ -597,7 +605,7 @@
             mService = IStatusBarService.Stub.asInterface(
                     ServiceManager.getService(Context.STATUS_BAR_SERVICE));
             if (mService == null) {
-                Slog.w("StatusBarManager", "warning: no STATUS_BAR_SERVICE");
+                Slog.w(TAG, "warning: no STATUS_BAR_SERVICE");
             }
         }
         return mService;
@@ -1226,18 +1234,22 @@
     }
 
     /**
-     * Checks whether the given package must include a non-empty title for its media controls.
+     * Log that the given package has posted media controls with a blank title
      *
      * @param packageName App posting media controls
-     * @param user Current user handle
-     * @return true if the app is required to provide a non-empty title
+     * @param userId Current user ID
+     * @throws RuntimeException if there is an error reporting the change
      *
      * @hide
      */
-    @RequiresPermission(allOf = {android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
-            android.Manifest.permission.LOG_COMPAT_CHANGE})
-    public static boolean isMediaTitleRequiredForApp(String packageName, UserHandle user) {
-        return CompatChanges.isChangeEnabled(MEDIA_CONTROL_REQUIRES_TITLE, packageName, user);
+    public void logBlankMediaTitle(String packageName, @UserIdInt int userId)
+            throws RuntimeException {
+        try {
+            mPlatformCompat.reportChangeByPackageName(MEDIA_CONTROL_BLANK_TITLE, packageName,
+                        userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 7f19897..8fafb18 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2883,6 +2883,20 @@
             "android.software.car.templates_host";
 
     /**
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:If this
+     * feature is supported, the device should also declare {@link #FEATURE_AUTOMOTIVE} and show
+     * a UI that can display multiple tasks at the same time on a single display. The user can
+     * perform multiple actions on different tasks simultaneously. Apps open in split screen mode
+     * by default, instead of full screen. Unlike Android's multi-window mode, where users can
+     * choose how to display apps, the device determines how apps are shown.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_CAR_SPLITSCREEN_MULTITASKING =
+            "android.software.car.splitscreen_multitasking";
+
+    /**
      * Feature for {@link #getSystemAvailableFeatures} and
      * {@link #hasSystemFeature(String, int)}: If this feature is supported, the device supports
      * {@link android.security.identity.IdentityCredentialStore} implemented in secure hardware
diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index b601275..3ffbe1d 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -1,223 +1,170 @@
 {
-  "imports": [
-    {
-      "path": "frameworks/base/core/tests/coretests/src/android/content/pm"
-    },
-    {
-      "path": "frameworks/base/services/tests/PackageManagerServiceTests"
-    },
-    {
-      "path": "frameworks/base/services/tests/PackageManager"
-    },
-    {
-      "path": "frameworks/base/services/tests/PackageManagerComponentOverrideTests"
-    },
-    {
-      "path": "frameworks/base/services/tests/servicestests/src/com/android/server/pm"
-    },
-    {
-      "path": "cts/tests/tests/packageinstaller"
-    },
-    {
-      "path": "cts/hostsidetests/stagedinstall"
-    },
-    {
-      "path": "cts/hostsidetests/packagemanager"
-    },
-    {
-      "path": "cts/hostsidetests/os/test_mappings/packagemanager"
-    },
-    {
-      "path": "cts/hostsidetests/appsearch"
-    },
-    {
-      "path": "system/apex/tests"
-    },
-    {
-      "path": "cts/tests/tests/content/pm/SecureFrp"
-    }
-  ],
-  "presubmit": [
-    {
-      "name": "CtsInstantAppTests",
-      "file_patterns": ["(/|^)InstantApp[^/]*"]
-    },
-    {
-      "name": "CarrierAppIntegrationTestCases"
-    },
-    {
-      "name": "ApkVerityTest"
-    },
-    {
-      "name": "CtsSilentUpdateHostTestCases"
-    },
-    {
-      "name": "CtsSuspendAppsTestCases"
-    },
-    {
-      "name": "CtsAppFgsTestCases",
-      "file_patterns": ["(/|^)ServiceInfo[^/]*"],
-      "options": [
+    "imports":[
         {
-          "include-annotation": "android.platform.test.annotations.Presubmit"
+            "path":"frameworks/base/core/tests/coretests/src/android/content/pm"
         },
         {
-          "exclude-annotation": "androidx.test.filters.LargeTest"
+            "path":"frameworks/base/services/tests/PackageManagerServiceTests"
         },
         {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
+            "path":"frameworks/base/services/tests/PackageManager"
+        },
+        {
+            "path":"frameworks/base/services/tests/PackageManagerComponentOverrideTests"
+        },
+        {
+            "path":"frameworks/base/services/tests/servicestests/src/com/android/server/pm"
+        },
+        {
+            "path":"cts/tests/tests/packageinstaller"
+        },
+        {
+            "path":"cts/hostsidetests/stagedinstall"
+        },
+        {
+            "path":"cts/hostsidetests/packagemanager"
+        },
+        {
+            "path":"cts/hostsidetests/os/test_mappings/packagemanager"
+        },
+        {
+            "path":"cts/hostsidetests/appsearch"
+        },
+        {
+            "path":"system/apex/tests"
+        },
+        {
+            "path":"cts/tests/tests/content/pm/SecureFrp"
         }
-      ]
-    },
-    {
-      "name": "CtsShortFgsTestCases",
-      "file_patterns": ["(/|^)ServiceInfo[^/]*"],
-      "options": [
+    ],
+    "presubmit":[
         {
-          "include-annotation": "android.platform.test.annotations.Presubmit"
+            "name":"CtsInstantAppTests",
+            "file_patterns":[
+                "(/|^)InstantApp[^/]*"
+            ]
         },
         {
-          "exclude-annotation": "androidx.test.filters.LargeTest"
+            "name":"CarrierAppIntegrationTestCases"
         },
         {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
+            "name":"ApkVerityTest"
+        },
+        {
+            "name":"CtsSilentUpdateHostTestCases"
+        },
+        {
+            "name":"CtsSuspendAppsTestCases"
+        },
+        {
+            "name":"CtsAppFgsTestCases",
+            "file_patterns":[
+                "(/|^)ServiceInfo[^/]*"
+            ],
+            "options":[
+                {
+                    "include-annotation":"android.platform.test.annotations.Presubmit"
+                },
+                {
+                    "exclude-annotation":"androidx.test.filters.LargeTest"
+                },
+                {
+                    "exclude-annotation":"androidx.test.filters.FlakyTest"
+                }
+            ]
+        },
+        {
+            "name":"CtsShortFgsTestCases",
+            "file_patterns":[
+                "(/|^)ServiceInfo[^/]*"
+            ],
+            "options":[
+                {
+                    "include-annotation":"android.platform.test.annotations.Presubmit"
+                },
+                {
+                    "exclude-annotation":"androidx.test.filters.LargeTest"
+                },
+                {
+                    "exclude-annotation":"androidx.test.filters.FlakyTest"
+                }
+            ]
+        },
+        {
+            "name":"CtsIncrementalInstallHostTestCases",
+            "options":[
+                {
+                    "include-filter":"android.incrementalinstall.cts.IncrementalFeatureTest"
+                }
+            ]
         }
-      ]
-    },
-    {
-      "name": "CtsIncrementalInstallHostTestCases",
-      "options": [
+    ],
+    "presubmit-large":[
         {
-          "include-filter": "android.incrementalinstall.cts.IncrementalFeatureTest"
-        }
-      ]
-    }
-  ],
-  "presubmit-large": [
-    {
-      "name": "CtsContentTestCases",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
+            "name":"CtsContentTestCases",
+            "options":[
+                {
+                    "exclude-annotation":"androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation":"org.junit.Ignore"
+                },
+                {
+                    "include-filter":"android.content.pm.cts"
+                }
+            ]
         },
         {
-          "exclude-annotation": "org.junit.Ignore"
+            "name":"CtsUsesNativeLibraryTest",
+            "options":[
+                {
+                    "exclude-annotation":"androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation":"org.junit.Ignore"
+                }
+            ]
         },
         {
-          "include-filter": "android.content.pm.cts"
-        }
-      ]
-    },
-    {
-      "name": "CtsUsesNativeLibraryTest",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
+            "name":"CtsSuspendAppsPermissionTestCases",
+            "options":[
+                {
+                    "exclude-annotation":"androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation":"org.junit.Ignore"
+                }
+            ]
         },
         {
-          "exclude-annotation": "org.junit.Ignore"
+            "name":"CtsAppSecurityHostTestCases",
+            "options":[
+                {
+                    "include-annotation":"android.platform.test.annotations.Presubmit"
+                },
+                {
+                    "exclude-annotation":"android.platform.test.annotations.Postsubmit"
+                },
+                {
+                    "exclude-annotation":"androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation":"org.junit.Ignore"
+                }
+            ]
         }
-      ]
-    },
-    {
-      "name": "CtsSuspendAppsPermissionTestCases",
-      "options": [
+    ],
+    "postsubmit":[
         {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
+            "name":"CtsAppSecurityHostTestCases",
+            "options":[
+                {
+                    "include-filter":"android.appsecurity.cts.AppSecurityTests#testPermissionDiffCert"
+                }
+            ]
         },
         {
-          "exclude-annotation": "org.junit.Ignore"
+            "name":"CtsInstallHostTestCases"
         }
-      ]
-    },
-    {
-      "name": "CtsAppSecurityHostTestCases",
-      "options": [
-        {
-          "include-annotation": "android.platform.test.annotations.Presubmit"
-        },
-        {
-          "exclude-annotation": "android.platform.test.annotations.Postsubmit"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
-    }
-  ],
-  "postsubmit": [
-    {
-      "name": "CtsAppSecurityHostTestCases",
-      "options": [
-        {
-          "include-filter": "android.appsecurity.cts.AppSecurityTests#testPermissionDiffCert"
-        }
-      ]
-    },
-    {
-      "name": "CtsInstallHostTestCases"
-    }
-  ],
-  "staged-platinum-postsubmit": [
-    {
-      "name": "CtsIncrementalInstallHostTestCases"
-    },
-    {
-      "name": "CtsAppSecurityHostTestCases",
-      "options": [
-        {
-          "include-filter": "android.appsecurity.cts.SplitTests"
-        },
-        {
-          "include-filter": "android.appsecurity.cts.EphemeralTest"
-        }
-      ]
-    },
-    {
-      "name": "CtsContentTestCases",
-      "options": [
-        {
-          "include-filter": "android.content.cts.IntentFilterTest"
-        }
-      ]
-    }
-  ],
-  "platinum-postsubmit": [
-      {
-        "name": "CtsIncrementalInstallHostTestCases",
-        "options": [
-            {
-                "exclude-annotation": "androidx.test.filters.FlakyTest"
-            }
-        ]
-      },
-      {
-        "name": "CtsAppSecurityHostTestCases",
-        "options": [
-            {
-                "include-filter": "android.appsecurity.cts.SplitTests"
-            },
-            {
-                "include-filter": "android.appsecurity.cts.EphemeralTest"
-            },
-            {
-                "exclude-annotation": "androidx.test.filters.FlakyTest"
-            }
-        ]
-      },
-      {
-        "name": "CtsContentTestCases",
-        "options":[
-            {
-                "include-filter": "android.content.cts.IntentFilterTest"
-            },
-            {
-                "exclude-annotation": "androidx.test.filters.FlakyTest"
-            }
-        ]
-      }
-  ]
-}
+    ]
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 6b044fc..82694ee 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -97,27 +97,6 @@
     public @interface BiometricError {}
 
     /**
-     * Single sensor or unspecified multi-sensor behavior (prefer an explicit choice if the
-     * device is multi-sensor).
-     * @hide
-     */
-    public static final int BIOMETRIC_MULTI_SENSOR_DEFAULT = 0;
-
-    /**
-     * Use face and fingerprint sensors together.
-     * @hide
-     */
-    public static final int BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE = 1;
-
-    /**
-     * @hide
-     */
-    @IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT,
-            BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface BiometricMultiSensorMode {}
-
-    /**
      * Types of authenticators, defined at a level of granularity supported by
      * {@link BiometricManager} and {@link BiometricPrompt}.
      *
diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
index 450c5ce..45f1c8a 100644
--- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
@@ -29,5 +29,7 @@
     // Notifies the client that an internal event, e.g. back button has occurred.
     void onSystemEvent(int event);
     // Notifies that the dialog has finished animating.
-    void onDialogAnimatedIn();
+    void onDialogAnimatedIn(boolean startFingerprintNow);
+    // Notifies that the fingerprint should start now (after onDialogAnimatedIn(false)).
+    void onStartFingerprintNow();
 }
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index 6910501..78388ef 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -246,8 +246,7 @@
         @Override
         public void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
                 @NonNull ViewTreeObserver.InternalInsetsInfo dest) {
-            if (!mImeDrawsImeNavBar || mNavigationBarFrame == null
-                    || mService.isExtractViewShown()) {
+            if (!mImeDrawsImeNavBar || mNavigationBarFrame == null) {
                 return;
             }
 
@@ -255,53 +254,58 @@
             if (systemInsets != null) {
                 final Window window = mService.mWindow.getWindow();
                 final View decor = window.getDecorView();
-                Region touchableRegion = null;
-                final View inputFrame = mService.mInputFrame;
-                switch (originalInsets.touchableInsets) {
-                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME:
-                        if (inputFrame.getVisibility() == View.VISIBLE) {
-                            inputFrame.getLocationInWindow(mTempPos);
-                            mTempRect.set(mTempPos[0], mTempPos[1],
-                                    mTempPos[0] + inputFrame.getWidth(),
-                                    mTempPos[1] + inputFrame.getHeight());
-                            touchableRegion = new Region(mTempRect);
-                        }
-                        break;
-                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT:
-                        if (inputFrame.getVisibility() == View.VISIBLE) {
-                            inputFrame.getLocationInWindow(mTempPos);
-                            mTempRect.set(mTempPos[0], originalInsets.contentTopInsets,
-                                    mTempPos[0] + inputFrame.getWidth() ,
-                                    mTempPos[1] + inputFrame.getHeight());
-                            touchableRegion = new Region(mTempRect);
-                        }
-                        break;
-                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE:
-                        if (inputFrame.getVisibility() == View.VISIBLE) {
-                            inputFrame.getLocationInWindow(mTempPos);
-                            mTempRect.set(mTempPos[0], originalInsets.visibleTopInsets,
-                                    mTempPos[0] + inputFrame.getWidth(),
-                                    mTempPos[1] + inputFrame.getHeight());
-                            touchableRegion = new Region(mTempRect);
-                        }
-                        break;
-                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION:
-                        touchableRegion = new Region();
-                        touchableRegion.set(originalInsets.touchableRegion);
-                        break;
-                }
-                // Hereafter "mTempRect" means a navigation bar rect.
-                mTempRect.set(decor.getLeft(), decor.getBottom() - systemInsets.bottom,
-                        decor.getRight(), decor.getBottom());
-                if (touchableRegion == null) {
-                    touchableRegion = new Region(mTempRect);
-                } else {
-                    touchableRegion.union(mTempRect);
-                }
 
-                dest.touchableRegion.set(touchableRegion);
-                dest.setTouchableInsets(
-                        ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+                // If the extract view is shown, everything is touchable, so no need to update
+                // touchable insets, but we still update normal insets below.
+                if (!mService.isExtractViewShown()) {
+                    Region touchableRegion = null;
+                    final View inputFrame = mService.mInputFrame;
+                    switch (originalInsets.touchableInsets) {
+                        case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME:
+                            if (inputFrame.getVisibility() == View.VISIBLE) {
+                                inputFrame.getLocationInWindow(mTempPos);
+                                mTempRect.set(mTempPos[0], mTempPos[1],
+                                        mTempPos[0] + inputFrame.getWidth(),
+                                        mTempPos[1] + inputFrame.getHeight());
+                                touchableRegion = new Region(mTempRect);
+                            }
+                            break;
+                        case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT:
+                            if (inputFrame.getVisibility() == View.VISIBLE) {
+                                inputFrame.getLocationInWindow(mTempPos);
+                                mTempRect.set(mTempPos[0], originalInsets.contentTopInsets,
+                                        mTempPos[0] + inputFrame.getWidth(),
+                                        mTempPos[1] + inputFrame.getHeight());
+                                touchableRegion = new Region(mTempRect);
+                            }
+                            break;
+                        case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE:
+                            if (inputFrame.getVisibility() == View.VISIBLE) {
+                                inputFrame.getLocationInWindow(mTempPos);
+                                mTempRect.set(mTempPos[0], originalInsets.visibleTopInsets,
+                                        mTempPos[0] + inputFrame.getWidth(),
+                                        mTempPos[1] + inputFrame.getHeight());
+                                touchableRegion = new Region(mTempRect);
+                            }
+                            break;
+                        case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION:
+                            touchableRegion = new Region();
+                            touchableRegion.set(originalInsets.touchableRegion);
+                            break;
+                    }
+                    // Hereafter "mTempRect" means a navigation bar rect.
+                    mTempRect.set(decor.getLeft(), decor.getBottom() - systemInsets.bottom,
+                            decor.getRight(), decor.getBottom());
+                    if (touchableRegion == null) {
+                        touchableRegion = new Region(mTempRect);
+                    } else {
+                        touchableRegion.union(mTempRect);
+                    }
+
+                    dest.touchableRegion.set(touchableRegion);
+                    dest.setTouchableInsets(
+                            ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+                }
 
                 // TODO(b/215443343): See if we can use View#OnLayoutChangeListener().
                 // TODO(b/215443343): See if we can replace DecorView#mNavigationColorViewState.view
diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java
index 6f2a915..3f40139 100644
--- a/core/java/android/preference/SeekBarVolumizer.java
+++ b/core/java/android/preference/SeekBarVolumizer.java
@@ -37,7 +37,6 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.preference.VolumePreference.VolumeStore;
-import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.provider.Settings.System;
@@ -47,7 +46,6 @@
 import android.widget.SeekBar.OnSeekBarChangeListener;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.os.SomeArgs;
 
 import java.util.concurrent.TimeUnit;
@@ -295,14 +293,8 @@
         if (zenMuted) {
             mSeekBar.setProgress(mLastAudibleStreamVolume, true);
         } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
-            /**
-             * the first variable above is preserved and the conditions below are made explicit
-             * so that when user attempts to slide the notification seekbar out of vibrate the
-             * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased
-             */
-            if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
-                    || mStreamType == AudioManager.STREAM_RING
+            // For ringer-mode affected streams, show volume as zero when ringermode is vibrate
+            if (mStreamType == AudioManager.STREAM_RING
                     || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) {
                 mSeekBar.setProgress(0, true);
             }
@@ -397,9 +389,7 @@
         // set the time of stop volume
         if ((mStreamType == AudioManager.STREAM_VOICE_CALL
                 || mStreamType == AudioManager.STREAM_RING
-                || (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
-                && mStreamType == AudioManager.STREAM_NOTIFICATION)
+                || mStreamType == AudioManager.STREAM_NOTIFICATION
                 || mStreamType == AudioManager.STREAM_ALARM)) {
             sStopVolumeTime = java.lang.System.currentTimeMillis();
         }
@@ -686,10 +676,7 @@
         }
 
         private void updateVolumeSlider(int streamType, int streamValue) {
-            final boolean streamMatch =  !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
-                    && mNotificationOrRing ? isNotificationOrRing(streamType) :
-                    streamType == mStreamType;
+            final boolean streamMatch = (streamType == mStreamType);
             if (mSeekBar != null && streamMatch && streamValue != -1) {
                 final boolean muted = mAudioManager.isStreamMute(mStreamType)
                         || streamValue == 0;
diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java
index 095189a..67ac811 100644
--- a/core/java/android/view/SurfaceControlRegistry.java
+++ b/core/java/android/view/SurfaceControlRegistry.java
@@ -62,7 +62,6 @@
     private static class DefaultReporter implements Reporter {
         public void onMaxLayersExceeded(WeakHashMap<SurfaceControl, Long> surfaceControls,
                 int limit, PrintWriter pw) {
-            final int size = Math.min(surfaceControls.size(), limit);
             final long now = SystemClock.elapsedRealtime();
             final ArrayList<Map.Entry<SurfaceControl, Long>> entries = new ArrayList<>();
             for (Map.Entry<SurfaceControl, Long> entry : surfaceControls.entrySet()) {
@@ -71,6 +70,7 @@
             // Sort entries by time registered when dumping
             // TODO: Or should it sort by name?
             entries.sort((o1, o2) -> (int) (o1.getValue() - o2.getValue()));
+            final int size = Math.min(entries.size(), limit);
 
             pw.println("SurfaceControlRegistry");
             pw.println("----------------------");
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 441636d..f1cde3b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -7734,13 +7734,14 @@
         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
 
         boolean handled = false;
-        final ListenerInfo li = mListenerInfo;
+        final OnLongClickListener listener =
+                mListenerInfo == null ? null : mListenerInfo.mOnLongClickListener;
         boolean shouldPerformHapticFeedback = true;
-        if (li != null && li.mOnLongClickListener != null) {
-            handled = li.mOnLongClickListener.onLongClick(View.this);
+        if (listener != null) {
+            handled = listener.onLongClick(View.this);
             if (handled) {
-                shouldPerformHapticFeedback =
-                        li.mOnLongClickListener.onLongClickUseDefaultHapticFeedback(View.this);
+                shouldPerformHapticFeedback = listener.onLongClickUseDefaultHapticFeedback(
+                        View.this);
             }
         }
         if (!handled) {
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 34e6e49..5525336 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -2598,6 +2598,11 @@
         public int getActionTag() {
             return VIEW_GROUP_ACTION_ADD_TAG;
         }
+
+        @Override
+        public final void visitUris(@NonNull Consumer<Uri> visitor) {
+            mNestedViews.visitUris(visitor);
+        }
     }
 
     /**
diff --git a/core/java/android/window/TaskSnapshot.java b/core/java/android/window/TaskSnapshot.java
index b7bb608..41b6d31 100644
--- a/core/java/android/window/TaskSnapshot.java
+++ b/core/java/android/window/TaskSnapshot.java
@@ -28,6 +28,7 @@
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SystemClock;
 import android.view.Surface;
 import android.view.WindowInsetsController;
 
@@ -38,6 +39,9 @@
 public class TaskSnapshot implements Parcelable {
     // Identifier of this snapshot
     private final long mId;
+    // The elapsed real time (in nanoseconds) when this snapshot was captured, not intended for use outside the
+    // process in which the snapshot was taken (ie. this is not parceled)
+    private final long mCaptureTime;
     // Top activity in task when snapshot was taken
     private final ComponentName mTopActivityComponent;
     private final HardwareBuffer mSnapshot;
@@ -65,7 +69,7 @@
     // Must be one of the named color spaces, otherwise, always use SRGB color space.
     private final ColorSpace mColorSpace;
 
-    public TaskSnapshot(long id,
+    public TaskSnapshot(long id, long captureTime,
             @NonNull ComponentName topActivityComponent, HardwareBuffer snapshot,
             @NonNull ColorSpace colorSpace, int orientation, int rotation, Point taskSize,
             Rect contentInsets, Rect letterboxInsets, boolean isLowResolution,
@@ -73,6 +77,7 @@
             @WindowInsetsController.Appearance int appearance, boolean isTranslucent,
             boolean hasImeSurface) {
         mId = id;
+        mCaptureTime = captureTime;
         mTopActivityComponent = topActivityComponent;
         mSnapshot = snapshot;
         mColorSpace = colorSpace.getId() < 0
@@ -92,6 +97,7 @@
 
     private TaskSnapshot(Parcel source) {
         mId = source.readLong();
+        mCaptureTime = SystemClock.elapsedRealtimeNanos();
         mTopActivityComponent = ComponentName.readFromParcel(source);
         mSnapshot = source.readTypedObject(HardwareBuffer.CREATOR);
         int colorSpaceId = source.readInt();
@@ -119,6 +125,14 @@
     }
 
     /**
+     * @return The elapsed real time (in nanoseconds) when this snapshot was captured. This time is
+     * only valid in the process where this snapshot was taken.
+     */
+    public long getCaptureTime() {
+        return mCaptureTime;
+    }
+
+    /**
      * @return The top activity component for the task at the point this snapshot was taken.
      */
     public ComponentName getTopActivityComponent() {
@@ -268,6 +282,7 @@
         final int height = mSnapshot != null ? mSnapshot.getHeight() : 0;
         return "TaskSnapshot{"
                 + " mId=" + mId
+                + " mCaptureTime=" + mCaptureTime
                 + " mTopActivityComponent=" + mTopActivityComponent.flattenToShortString()
                 + " mSnapshot=" + mSnapshot + " (" + width + "x" + height + ")"
                 + " mColorSpace=" + mColorSpace.toString()
@@ -296,6 +311,7 @@
     /** Builder for a {@link TaskSnapshot} object */
     public static final class Builder {
         private long mId;
+        private long mCaptureTime;
         private ComponentName mTopActivity;
         private HardwareBuffer mSnapshot;
         private ColorSpace mColorSpace;
@@ -317,6 +333,11 @@
             return this;
         }
 
+        public Builder setCaptureTime(long captureTime) {
+            mCaptureTime = captureTime;
+            return this;
+        }
+
         public Builder setTopActivityComponent(ComponentName name) {
             mTopActivity = name;
             return this;
@@ -400,6 +421,7 @@
         public TaskSnapshot build() {
             return new TaskSnapshot(
                     mId,
+                    mCaptureTime,
                     mTopActivity,
                     mSnapshot,
                     mColorSpace,
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index c0370cc..8c05130 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -493,6 +493,9 @@
         if ((flags & FLAG_FIRST_CUSTOM) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
         }
+        if ((flags & FLAG_MOVED_TO_TOP) != 0) {
+            sb.append(sb.length() == 0 ? "" : "|").append("MOVE_TO_TOP");
+        }
         return sb.toString();
     }
 
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 7ad2a68..8135f9c 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -549,11 +549,6 @@
             "task_manager_inform_job_scheduler_of_pending_app_stop";
 
     /**
-     * (boolean) Whether to show notification volume control slider separate from ring.
-     */
-    public static final String VOLUME_SEPARATE_NOTIFICATION = "volume_separate_notification";
-
-    /**
      * (boolean) Whether widget provider info would be saved to / loaded from system persistence
      * layer as opposed to individual manifests in respective apps.
      */
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index ae58626..d2564fb 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -157,7 +157,7 @@
     */
     void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver,
             in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId,
-            long operationId, String opPackageName, long requestId, int multiSensorConfig);
+            long operationId, String opPackageName, long requestId);
     /**
     * Used to notify the authentication dialog that a biometric has been authenticated.
     */
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 3708859..3977666 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -123,8 +123,7 @@
     // Used to show the authentication dialog (Biometrics, Device Credential)
     void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver,
             in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
-            int userId, long operationId, String opPackageName, long requestId,
-            int multiSensorConfig);
+            int userId, long operationId, String opPackageName, long requestId);
 
     // Used to notify the authentication dialog that a biometric has been authenticated
     void onBiometricAuthenticated(int modality);
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index f277635..116c301c 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -367,28 +367,42 @@
      * using a single static object.
      */
     @VisibleForTesting
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public void startListeningForLatencyTrackerConfigChanges() {
         final Context context = ActivityThread.currentApplication();
-        if (context != null
-                && context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) == PERMISSION_GRANTED) {
-            // Post initialization to the background in case we're running on the main thread.
-            BackgroundThread.getHandler().post(() -> this.updateProperties(
-                    DeviceConfig.getProperties(NAMESPACE_LATENCY_TRACKER)));
-            DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_LATENCY_TRACKER,
-                    BackgroundThread.getExecutor(), mOnPropertiesChangedListener);
-        } else {
+        if (context == null) {
             if (DEBUG) {
-                if (context == null) {
-                    Log.d(TAG, "No application for " + ActivityThread.currentActivityThread());
-                } else {
-                    synchronized (mLock) {
-                        Log.d(TAG, "Initialized the LatencyTracker."
-                                + " (No READ_DEVICE_CONFIG permission to change configs)"
-                                + " enabled=" + mEnabled + ", package=" + context.getPackageName());
-                    }
+                Log.d(TAG, "No application for package: " + ActivityThread.currentPackageName());
+            }
+            return;
+        }
+        if (context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) != PERMISSION_GRANTED) {
+            if (DEBUG) {
+                synchronized (mLock) {
+                    Log.d(TAG, "Initialized the LatencyTracker."
+                            + " (No READ_DEVICE_CONFIG permission to change configs)"
+                            + " enabled=" + mEnabled + ", package=" + context.getPackageName());
                 }
             }
+            return;
         }
+
+        // Post initialization to the background in case we're running on the main thread.
+        BackgroundThread.getHandler().post(() -> {
+            try {
+                this.updateProperties(
+                        DeviceConfig.getProperties(NAMESPACE_LATENCY_TRACKER));
+                DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_LATENCY_TRACKER,
+                        BackgroundThread.getExecutor(), mOnPropertiesChangedListener);
+            } catch (SecurityException ex) {
+                // In case of running tests that the main thread passes the check,
+                // but the background thread doesn't have necessary permissions.
+                // Swallow it since it's ok to ignore device config changes in the tests.
+                Log.d(TAG, "Can't get properties: READ_DEVICE_CONFIG granted="
+                        + context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
+                        + ", package=" + context.getPackageName());
+            }
+        });
     }
 
     /**
diff --git a/core/proto/android/os/system_properties.proto b/core/proto/android/os/system_properties.proto
index 84c82e0..10f07ac 100644
--- a/core/proto/android/os/system_properties.proto
+++ b/core/proto/android/os/system_properties.proto
@@ -434,9 +434,8 @@
             optional string vibrator = 37;
             optional string virtual_device = 38;
             optional string vulkan = 39;
-            optional string egl_legacy = 40;
 
-            // Next Tag: 41
+            // Next Tag: 40
         }
         optional Hardware hardware = 27;
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 91fbf6b..fb3acbe 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5410,6 +5410,9 @@
     <!-- Title for button to see application detail in app store which it came from - it may allow user to update to newer version. [CHAR LIMIT=50] -->
     <string name="deprecated_target_sdk_app_store">Check for update</string>
 
+    <!-- Message displayed in dialog when app is 32 bit on a 64 bit system. [CHAR LIMIT=NONE] -->
+    <string name="deprecated_abi_message">This app isn\'t compatible with the latest version of Android. Check for an update or contact the app\'s developer.</string>
+
     <!-- Notification title shown when new SMS/MMS is received while the device is locked [CHAR LIMIT=NONE] -->
     <string name="new_sms_notification_title">You have new messages</string>
     <!-- Notification content shown when new SMS/MMS is received while the device is locked [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f35e32b..6ab671a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3156,6 +3156,8 @@
   <java-symbol type="string" name="deprecated_target_sdk_message" />
   <java-symbol type="string" name="deprecated_target_sdk_app_store" />
 
+  <java-symbol type="string" name="deprecated_abi_message" />
+
   <!-- New SMS notification while phone is locked. -->
   <java-symbol type="string" name="new_sms_notification_title" />
   <java-symbol type="string" name="new_sms_notification_content" />
@@ -5129,4 +5131,5 @@
   <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" />
 
   <java-symbol type="drawable" name="focus_event_pressed_key_background" />
+  <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" />
 </resources>
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index c5b00c9..eba7f58 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -33,6 +33,8 @@
 import static android.app.Notification.EXTRA_PEOPLE_LIST;
 import static android.app.Notification.EXTRA_PICTURE;
 import static android.app.Notification.EXTRA_PICTURE_ICON;
+import static android.app.Notification.EXTRA_SUMMARY_TEXT;
+import static android.app.Notification.EXTRA_TITLE;
 import static android.app.Notification.MessagingStyle.Message.KEY_DATA_URI;
 import static android.app.Notification.MessagingStyle.Message.KEY_SENDER_PERSON;
 import static android.app.Notification.MessagingStyle.Message.KEY_TEXT;
@@ -76,6 +78,7 @@
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SystemProperties;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
@@ -111,6 +114,9 @@
     @Before
     public void setUp() {
         mContext = InstrumentationRegistry.getContext();
+        // TODO(b/169435530): remove this flag set once resolved.
+        SystemProperties.set("persist.sysui.notification.builder_extras_override",
+                Boolean.toString(false));
     }
 
     @Test
@@ -1481,6 +1487,107 @@
         Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
     }
 
+    // Ensures that extras in a Notification Builder can be updated.
+    @Test
+    public void testExtras_cachedExtrasOverwrittenByUserProvided() {
+        // Sets the flag to new state.
+        // TODO(b/169435530): remove this set value once resolved.
+        SystemProperties.set("persist.sysui.notification.builder_extras_override",
+                Boolean.toString(true));
+        Bundle extras = new Bundle();
+        extras.putCharSequence(EXTRA_TITLE, "test title");
+        extras.putCharSequence(EXTRA_SUMMARY_TEXT, "summary text");
+
+        Notification.Builder builder = new Notification.Builder(mContext, "test id")
+                .addExtras(extras);
+
+        Notification notification = builder.build();
+        assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+                "test title");
+        assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+                "summary text");
+
+        extras.putCharSequence(EXTRA_TITLE, "new title");
+        builder.addExtras(extras);
+        notification = builder.build();
+        assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+                "new title");
+        assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+                "summary text");
+    }
+
+    // Ensures that extras in a Notification Builder can be updated by an extender.
+    @Test
+    public void testExtras_cachedExtrasOverwrittenByExtender() {
+        // Sets the flag to new state.
+        // TODO(b/169435530): remove this set value once resolved.
+        SystemProperties.set("persist.sysui.notification.builder_extras_override",
+                Boolean.toString(true));
+        Notification.CarExtender extender = new Notification.CarExtender().setColor(1234);
+
+        Notification notification = new Notification.Builder(mContext, "test id")
+                .extend(extender).build();
+
+        extender.setColor(5678);
+
+        Notification.Builder.recoverBuilder(mContext, notification).extend(extender).build();
+
+        Notification.CarExtender recoveredExtender = new Notification.CarExtender(notification);
+        assertThat(recoveredExtender.getColor()).isEqualTo(5678);
+    }
+
+    // Validates pre-flag flip behavior, that extras in a Notification Builder cannot be updated.
+    // TODO(b/169435530): remove this test once resolved.
+    @Test
+    public void testExtras_cachedExtrasOverwrittenByUserProvidedOld() {
+        // Sets the flag to old state.
+        SystemProperties.set("persist.sysui.notification.builder_extras_override",
+                Boolean.toString(false));
+
+        Bundle extras = new Bundle();
+        extras.putCharSequence(EXTRA_TITLE, "test title");
+        extras.putCharSequence(EXTRA_SUMMARY_TEXT, "summary text");
+
+        Notification.Builder builder = new Notification.Builder(mContext, "test id")
+                .addExtras(extras);
+
+        Notification notification = builder.build();
+        assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+                "test title");
+        assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+                "summary text");
+
+        extras.putCharSequence(EXTRA_TITLE, "new title");
+        builder.addExtras(extras);
+        notification = builder.build();
+        assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+                "test title");
+        assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+                "summary text");
+    }
+
+    // Validates pre-flag flip behavior, that extras in a Notification Builder cannot be updated
+    // by an extender.
+    // TODO(b/169435530): remove this test once resolved.
+    @Test
+    public void testExtras_cachedExtrasOverwrittenByExtenderOld() {
+        // Sets the flag to old state.
+        SystemProperties.set("persist.sysui.notification.builder_extras_override",
+                Boolean.toString(false));
+
+        Notification.CarExtender extender = new Notification.CarExtender().setColor(1234);
+
+        Notification notification = new Notification.Builder(mContext, "test id")
+                .extend(extender).build();
+
+        extender.setColor(5678);
+
+        Notification.Builder.recoverBuilder(mContext, notification).extend(extender).build();
+
+        Notification.CarExtender recoveredExtender = new Notification.CarExtender(notification);
+        assertThat(recoveredExtender.getColor()).isEqualTo(1234);
+    }
+
     private void assertValid(Notification.Colors c) {
         // Assert that all colors are populated
         assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
diff --git a/core/tests/coretests/src/android/content/res/TEST_MAPPING b/core/tests/coretests/src/android/content/res/TEST_MAPPING
index ab14950..4ea6e40 100644
--- a/core/tests/coretests/src/android/content/res/TEST_MAPPING
+++ b/core/tests/coretests/src/android/content/res/TEST_MAPPING
@@ -39,18 +39,5 @@
         }
       ]
     }
-  ],
-  "ironwood-postsubmit": [
-    {
-      "name": "FrameworksCoreTests",
-      "options":[
-        {
-            "include-annotation": "android.platform.test.annotations.IwTest"
-        },
-        {
-            "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
-    }
   ]
 }
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 4672226..7879801 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -716,6 +716,30 @@
     }
 
     @Test
+    public void visitUris_nestedViews() {
+        final RemoteViews outer = new RemoteViews(mPackage, R.layout.remote_views_test);
+
+        final RemoteViews inner = new RemoteViews(mPackage, 33);
+        final Uri imageUriI = Uri.parse("content://inner/image");
+        final Icon icon1 = Icon.createWithContentUri("content://inner/icon1");
+        final Icon icon2 = Icon.createWithContentUri("content://inner/icon2");
+        final Icon icon3 = Icon.createWithContentUri("content://inner/icon3");
+        final Icon icon4 = Icon.createWithContentUri("content://inner/icon4");
+        inner.setImageViewUri(R.id.image, imageUriI);
+        inner.setTextViewCompoundDrawables(R.id.text, icon1, icon2, icon3, icon4);
+
+        outer.addView(R.id.layout, inner);
+
+        Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
+        outer.visitUris(visitor);
+        verify(visitor, times(1)).accept(eq(imageUriI));
+        verify(visitor, times(1)).accept(eq(icon1.getUri()));
+        verify(visitor, times(1)).accept(eq(icon2.getUri()));
+        verify(visitor, times(1)).accept(eq(icon3.getUri()));
+        verify(visitor, times(1)).accept(eq(icon4.getUri()));
+    }
+
+    @Test
     public void visitUris_separateOrientation() {
         final RemoteViews landscape = new RemoteViews(mPackage, R.layout.remote_views_test);
         final Uri imageUriL = Uri.parse("content://landscape/image");
diff --git a/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java b/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java
index 281d677..6764ac8 100644
--- a/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java
+++ b/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java
@@ -88,6 +88,7 @@
                 1, HardwareBuffer.USAGE_CPU_READ_RARELY);
         return new TaskSnapshot(
                 System.currentTimeMillis(),
+                0 /* captureTime */,
                 new ComponentName("", ""), buffer,
                 ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
                 Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */,
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 4b4e722..e4defcf 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1135,6 +1135,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimation.java"
     },
+    "-1060529098": {
+      "message": "  Skipping post-transition snapshot for task %d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "-1060365734": {
       "message": "Attempted to add QS dialog window with bad token %s.  Aborting.",
       "level": "WARN",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING b/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING
deleted file mode 100644
index 8dd1369..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "ironwood-postsubmit": [
-    {
-      "name": "WMShellFlickerTests",
-      "options": [
-        {
-          "include-annotation": "android.platform.test.annotations.IwTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
-    }
-  ]
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index fbdbd3e..7b37d59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.activityembedding;
 
+import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION;
 import static android.window.TransitionInfo.FLAG_FILLS_TASK;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 
@@ -111,6 +112,11 @@
         if (containsNonEmbeddedChange && !handleNonEmbeddedChanges(changes)) {
             return false;
         }
+        final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+        if (options != null && options.getType() == ANIM_SCENE_TRANSITION) {
+            // Scene-transition will be handled by app side.
+            return false;
+        }
 
         // Start ActivityEmbedding animation.
         mTransitionCallbacks.put(transition, finishCallback);
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 91c7cc0..68fea41 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
@@ -2518,6 +2518,7 @@
             mExpandedAnimationController.expandFromStack(() -> {
                 updatePointerPosition(false /* forIme */);
                 afterExpandedViewAnimation();
+                mExpandedViewContainer.setVisibility(VISIBLE);
                 mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
             } /* after */);
             return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
index ac6e4c2..53683c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
@@ -54,14 +54,6 @@
         DevicePostureController.OnDevicePostureChangedListener,
         DisplayController.OnDisplaysChangedListener {
     /**
-     * When {@code true}, floating windows like PiP would auto move to the position
-     * specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode.
-     */
-    private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP =
-            SystemProperties.getBoolean(
-                    "persist.wm.debug.enable_move_floating_window_in_tabletop", true);
-
-    /**
      * Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled,
      * {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise.
      * See also {@link #getPreferredHalfInTabletopMode()}.
@@ -162,14 +154,6 @@
         }
     }
 
-    /**
-     * @return {@code true} if floating windows like PiP would auto move to the position
-     * specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode.
-     */
-    public boolean enableMoveFloatingWindowInTabletop() {
-        return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP;
-    }
-
     /** @return Preferred half for floating windows like PiP when in tabletop mode. */
     @PreferredTabletopHalf
     public int getPreferredHalfInTabletopMode() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 753dfa7..a9ccdf6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -293,6 +293,9 @@
         }
 
         if (mResizingIconView == null) {
+            if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+                animFinishedCallback.accept(false);
+            }
             return;
         }
 
@@ -311,6 +314,9 @@
                         releaseDecor(finishT);
                         finishT.apply();
                         finishT.close();
+                        if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+                            animFinishedCallback.accept(true);
+                        }
                     }
                 });
                 return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
index 65a12d6..2590cab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
@@ -252,13 +252,16 @@
     // It can be removed when min_sdk of the app is set to 31 or greater.
     @SuppressLint("NewApi")
     private List<RemoteAction> getMediaActions() {
-        if (mMediaController == null || mMediaController.getPlaybackState() == null) {
+        // Cache the PlaybackState since it's a Binder call.
+        final PlaybackState playbackState;
+        if (mMediaController == null
+                || (playbackState = mMediaController.getPlaybackState()) == null) {
             return Collections.emptyList();
         }
 
         ArrayList<RemoteAction> mediaActions = new ArrayList<>();
-        boolean isPlaying = mMediaController.getPlaybackState().isActive();
-        long actions = mMediaController.getPlaybackState().getActions();
+        boolean isPlaying = playbackState.isActive();
+        long actions = playbackState.getActions();
 
         // Prev action
         mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index bfc1fb9..3c7ce3c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -249,11 +249,6 @@
                     finishTransaction);
         }
 
-        // Fade in the fadeout PIP when the fixed rotation is finished.
-        if (mPipTransitionState.isInPip() && !mInFixedRotation && mHasFadeOut) {
-            fadeExistingPip(true /* show */);
-        }
-
         return false;
     }
 
@@ -1056,6 +1051,12 @@
                 .crop(finishTransaction, leash, destBounds)
                 .round(finishTransaction, leash, isInPip)
                 .shadow(finishTransaction, leash, isInPip);
+        // Make sure the PiP keeps invisible if it was faded out. If it needs to fade in, that will
+        // be handled by onFixedRotationFinished().
+        if (isInPip && mHasFadeOut) {
+            startTransaction.setAlpha(leash, 0f);
+            finishTransaction.setAlpha(leash, 0f);
+        }
     }
 
     /** Hides and shows the existing PIP during fixed rotation transition of other activities. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index ed8dc7de..fc674a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -65,9 +65,18 @@
         }
         Rect pipBounds = new Rect(startingBounds);
 
-        // move PiP towards corner if user hasn't moved it manually or the flag is on
-        if (mKeepClearAreaGravityEnabled
-                || (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip())) {
+        boolean shouldApplyGravity = false;
+        // if PiP is outside of screen insets, reposition using gravity
+        if (!insets.contains(pipBounds)) {
+            shouldApplyGravity = true;
+        }
+        // if user has not interacted with PiP, reposition using gravity
+        if (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip()) {
+            shouldApplyGravity = true;
+        }
+
+        // apply gravity that will position PiP in bottom left or bottom right corner within insets
+        if (mKeepClearAreaGravityEnabled || shouldApplyGravity) {
             float snapFraction = pipBoundsAlgorithm.getSnapFraction(startingBounds);
             int verticalGravity = Gravity.BOTTOM;
             int horizontalGravity;
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 63181da..6a861ce 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
@@ -677,7 +677,6 @@
                 });
 
         mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> {
-            if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return;
             final String tag = "tabletop-mode";
             if (!isInTabletopMode) {
                 mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 5c9709c..f35eda6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -371,7 +371,8 @@
      * Find the background task that match the given component.
      */
     @Nullable
-    public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName) {
+    public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName,
+            int userId) {
         if (componentName == null) {
             return null;
         }
@@ -383,7 +384,7 @@
             if (task.isVisible) {
                 continue;
             }
-            if (componentName.equals(task.baseIntent.getComponent())) {
+            if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) {
                 return task;
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index a9ad3c9..c286959 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -264,21 +264,18 @@
         void cancel(String reason) {
             // restoring (to-home = false) involves submitting more WM changes, so by default, use
             // toHome = true when canceling.
-            cancel(true /* toHome */, reason);
+            cancel(true /* toHome */, false /* withScreenshots */, reason);
         }
 
-        void cancel(boolean toHome, String reason) {
+        void cancel(boolean toHome, boolean withScreenshots, String reason) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                     "[%d] RecentsController.cancel: toHome=%b reason=%s",
                     mInstanceId, toHome, reason);
             if (mListener != null) {
-                try {
-                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                            "[%d] RecentsController.cancel: calling onAnimationCanceled",
-                            mInstanceId);
-                    mListener.onAnimationCanceled(null, null);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Error canceling recents animation", e);
+                if (withScreenshots) {
+                    sendCancelWithSnapshots();
+                } else {
+                    sendCancel(null, null);
                 }
             }
             if (mFinishCB != null) {
@@ -300,24 +297,34 @@
                 snapshots = new TaskSnapshot[mPausingTasks.size()];
                 try {
                     for (int i = 0; i < mPausingTasks.size(); ++i) {
+                        TaskState state = mPausingTasks.get(0);
                         snapshots[i] = ActivityTaskManager.getService().takeTaskSnapshot(
-                                mPausingTasks.get(0).mTaskInfo.taskId, false /* updateCache */);
+                                state.mTaskInfo.taskId, true /* updateCache */);
                     }
                 } catch (RemoteException e) {
                     taskIds = null;
                     snapshots = null;
                 }
             }
+            return sendCancel(taskIds, snapshots);
+        }
+
+        /**
+         * Sends a cancel message to the recents animation.
+         */
+        private boolean sendCancel(@Nullable int[] taskIds,
+                @Nullable TaskSnapshot[] taskSnapshots) {
             try {
+                final String cancelDetails = taskSnapshots != null ? " with snapshots" : "";
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "[%d] RecentsController.cancel: calling onAnimationCanceled with snapshots",
-                        mInstanceId);
-                mListener.onAnimationCanceled(taskIds, snapshots);
+                        "[%d] RecentsController.cancel: calling onAnimationCanceled %s",
+                        mInstanceId, cancelDetails);
+                mListener.onAnimationCanceled(taskIds, taskSnapshots);
+                return true;
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error canceling recents animation", e);
                 return false;
             }
-            return true;
         }
 
         void cleanUp() {
@@ -519,7 +526,7 @@
                     // Finish recents animation if the display is changed, so the default
                     // transition handler can play the animation such as rotation effect.
                     if (change.hasFlags(TransitionInfo.FLAG_IS_DISPLAY)) {
-                        cancel(mWillFinishToHome, "display change");
+                        cancel(mWillFinishToHome, true /* withScreenshots */, "display change");
                         return;
                     }
                     // Don't consider order-only changes as changing apps.
@@ -633,7 +640,7 @@
                         + foundRecentsClosing);
                 if (foundRecentsClosing) {
                     mWillFinishToHome = false;
-                    cancel(false /* toHome */, "didn't merge");
+                    cancel(false /* toHome */, false /* withScreenshots */, "didn't merge");
                 }
                 return;
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 34701f1..ea33a1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -723,7 +723,7 @@
                 // in the background with priority.
                 final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
                         .map(recentTasks -> recentTasks.findTaskInBackground(
-                                intent.getIntent().getComponent()))
+                                intent.getIntent().getComponent(), userId1))
                         .orElse(null);
                 if (taskInfo != null) {
                     startTask(taskInfo.taskId, position, options);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 14ea86a..d2b0e28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -87,6 +87,14 @@
         mStageCoordinator = stageCoordinator;
     }
 
+    private void initTransition(@NonNull IBinder transition,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        mAnimatingTransition = transition;
+        mFinishTransaction = finishTransaction;
+        mFinishCallback = finishCallback;
+    }
+
     /** Play animation for enter transition or dismiss transition. */
     void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
@@ -94,9 +102,7 @@
             @NonNull Transitions.TransitionFinishCallback finishCallback,
             @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
             @NonNull WindowContainerToken topRoot) {
-        mFinishCallback = finishCallback;
-        mAnimatingTransition = transition;
-        mFinishTransaction = finishTransaction;
+        initTransition(transition, finishTransaction, finishCallback);
 
         final TransitSession pendingTransition = getPendingTransition(transition);
         if (pendingTransition != null) {
@@ -220,6 +226,45 @@
         onFinish(null /* wct */, null /* wctCB */);
     }
 
+    /** Play animation for drag divider dismiss transition. */
+    void playDragDismissAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull WindowContainerToken toTopRoot, @NonNull SplitDecorManager toTopDecor,
+            @NonNull WindowContainerToken topRoot) {
+        initTransition(transition, finishTransaction, finishCallback);
+
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            final SurfaceControl leash = change.getLeash();
+
+            if (toTopRoot.equals(change.getContainer())) {
+                startTransaction.setAlpha(leash, 1.f);
+                startTransaction.show(leash);
+
+                ValueAnimator va = new ValueAnimator();
+                mAnimations.add(va);
+
+                toTopDecor.onResized(startTransaction, animated -> {
+                    mAnimations.remove(va);
+                    if (animated) {
+                        mTransitions.getMainExecutor().execute(() -> {
+                            onFinish(null /* wct */, null /* wctCB */);
+                        });
+                    }
+                });
+            } else if (topRoot.equals(change.getContainer())) {
+                // Ensure it on top of all changes in transition.
+                startTransaction.setLayer(leash, Integer.MAX_VALUE);
+                startTransaction.setAlpha(leash, 1.f);
+                startTransaction.show(leash);
+            }
+        }
+        startTransaction.apply();
+        onFinish(null /* wct */, null /* wctCB */);
+    }
+
     /** Play animation for resize transition. */
     void playResizeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
@@ -227,9 +272,7 @@
             @NonNull Transitions.TransitionFinishCallback finishCallback,
             @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
             @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) {
-        mFinishCallback = finishCallback;
-        mAnimatingTransition = transition;
-        mFinishTransaction = finishTransaction;
+        initTransition(transition, finishTransaction, finishCallback);
 
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index bf20567..e0fffff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1328,8 +1328,6 @@
             mIsExiting = true;
             childrenToTop.resetBounds(wct);
             wct.reorder(childrenToTop.mRootTaskInfo.token, true);
-            wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token,
-                    SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
         }
         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
                 false /* reparentLeafTaskIfRelaunch */);
@@ -1517,6 +1515,10 @@
 
     void finishEnterSplitScreen(SurfaceControl.Transaction t) {
         mSplitLayout.update(t);
+        mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash,
+                getMainStageBounds());
+        mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash,
+                getSideStageBounds());
         setDividerVisibility(true, t);
         // Ensure divider surface are re-parented back into the hierarchy at the end of the
         // transition. See Transition#buildFinishTransaction for more detail.
@@ -1989,13 +1991,15 @@
         final boolean mainStageToTop =
                 bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
                         : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
+        final StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage;
         if (!ENABLE_SHELL_TRANSITIONS) {
-            exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, reason);
+            exitSplitScreen(toTopStage, reason);
             return;
         }
 
         final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
         final WindowContainerTransaction wct = new WindowContainerTransaction();
+        toTopStage.resetBounds(wct);
         prepareExitSplitScreen(dismissTop, wct);
         if (mRootTaskInfo != null) {
             wct.setDoNotPip(mRootTaskInfo.token);
@@ -2531,8 +2535,17 @@
             shouldAnimate = startPendingEnterAnimation(
                     mSplitTransitions.mPendingEnter, info, startTransaction, finishTransaction);
         } else if (mSplitTransitions.isPendingDismiss(transition)) {
+            final SplitScreenTransitions.DismissSession dismiss = mSplitTransitions.mPendingDismiss;
             shouldAnimate = startPendingDismissAnimation(
-                    mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction);
+                    dismiss, info, startTransaction, finishTransaction);
+            if (shouldAnimate && dismiss.mReason == EXIT_REASON_DRAG_DIVIDER) {
+                final StageTaskListener toTopStage =
+                        dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage;
+                mSplitTransitions.playDragDismissAnimation(transition, info, startTransaction,
+                        finishTransaction, finishCallback, toTopStage.mRootTaskInfo.token,
+                        toTopStage.getSplitDecorManager(), mRootTaskInfo.token);
+                return true;
+            }
         } else if (mSplitTransitions.isPendingResize(transition)) {
             mSplitTransitions.playResizeAnimation(transition, info, startTransaction,
                     finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token,
@@ -2787,6 +2800,10 @@
             mSplitTransitions.mPendingDismiss = null;
             return false;
         }
+        dismissTransition.setFinishedCallback((callbackWct, callbackT) -> {
+            mMainStage.getSplitDecorManager().release(callbackT);
+            mSideStage.getSplitDecorManager().release(callbackT);
+        });
 
         addDividerBarToTransition(info, false /* show */);
         return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index e2e9270..da7d186 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
@@ -201,7 +202,7 @@
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
         if (mRootTaskInfo.taskId == taskInfo.taskId) {
             // Inflates split decor view only when the root task is visible.
-            if (mRootTaskInfo.isVisible != taskInfo.isVisible) {
+            if (!ENABLE_SHELL_TRANSITIONS && mRootTaskInfo.isVisible != taskInfo.isVisible) {
                 if (taskInfo.isVisible) {
                     mSplitDecorManager.inflate(mContext, mRootLeash,
                             taskInfo.configuration.windowConfiguration.getBounds());
@@ -385,6 +386,7 @@
     void resetBounds(WindowContainerTransaction wct) {
         wct.setBounds(mRootTaskInfo.token, null);
         wct.setAppBounds(mRootTaskInfo.token, null);
+        wct.setSmallestScreenWidthDp(mRootTaskInfo.token, SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
     }
 
     void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index f33b077..3b306e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -191,6 +191,12 @@
      */
     private static final int SYNC_ALLOWANCE_MS = 120;
 
+    /**
+     * Keyguard gets a more generous timeout to finish its animations, because we are always holding
+     * a sleep token during occlude/unocclude transitions and we want them to finish playing cleanly
+     */
+    private static final int SYNC_ALLOWANCE_KEYGUARD_MS = 2000;
+
     /** For testing only. Disables the force-finish timeout on sync. */
     private boolean mDisableForceSync = false;
 
@@ -492,6 +498,10 @@
                 finishT.show(leash);
             } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
                 finishT.hide(leash);
+            } else if (isOpening && mode == TRANSIT_CHANGE) {
+                // Just in case there is a race with another animation (eg. recents finish()).
+                // Changes are visible->visible so it's a problem if it isn't visible.
+                t.show(leash);
             }
         }
     }
@@ -669,7 +679,7 @@
                 // Sleep starts a process of forcing all prior transitions to finish immediately
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                         "Start finish-for-sync track %d", i);
-                finishForSync(i, null /* forceFinish */);
+                finishForSync(active, i, null /* forceFinish */);
             }
             if (hadPreceding) {
                 return false;
@@ -1017,6 +1027,9 @@
         for (int i = 0; i < mPendingTransitions.size(); ++i) {
             if (mPendingTransitions.get(i).mToken == token) return true;
         }
+        for (int i = 0; i < mReadyDuringSync.size(); ++i) {
+            if (mReadyDuringSync.get(i).mToken == token) return true;
+        }
         for (int t = 0; t < mTracks.size(); ++t) {
             final Track tr = mTracks.get(t);
             for (int i = 0; i < tr.mReadyTransitions.size(); ++i) {
@@ -1103,10 +1116,17 @@
      *
      * This is then repeated until there are no more pending sleep transitions.
      *
+     * @param reason The SLEEP transition that triggered this round of finishes. We will continue
+     *               looping round finishing transitions as long as this is still waiting.
      * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge
      *                    signal to -- so it will be force-finished if it's still running.
      */
-    private void finishForSync(int trackIdx, @Nullable ActiveTransition forceFinish) {
+    private void finishForSync(ActiveTransition reason,
+            int trackIdx, @Nullable ActiveTransition forceFinish) {
+        if (!isTransitionKnown(reason.mToken)) {
+            Log.d(TAG, "finishForSleep: already played sync transition " + reason);
+            return;
+        }
         final Track track = mTracks.get(trackIdx);
         if (forceFinish != null) {
             final Track trk = mTracks.get(forceFinish.getTrack());
@@ -1150,8 +1170,11 @@
             if (track.mActiveTransition == playing) {
                 if (!mDisableForceSync) {
                     // Give it a short amount of time to process it before forcing.
-                    mMainExecutor.executeDelayed(() -> finishForSync(trackIdx, playing),
-                            SYNC_ALLOWANCE_MS);
+                    final int tolerance = KeyguardTransitionHandler.handles(playing.mInfo)
+                            ? SYNC_ALLOWANCE_KEYGUARD_MS
+                            : SYNC_ALLOWANCE_MS;
+                    mMainExecutor.executeDelayed(
+                            () -> finishForSync(reason, trackIdx, playing), tolerance);
                 }
                 break;
             }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 9189d3d..fb17d87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -223,7 +223,7 @@
         doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
         // Put the same component into a task in the background
         ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
-        doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
+        doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
                 SPLIT_POSITION_TOP_OR_LEFT, null);
@@ -247,7 +247,7 @@
                 SPLIT_POSITION_BOTTOM_OR_RIGHT);
         // Put the same component into a task in the background
         doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
-                .findTaskInBackground(any());
+                .findTaskInBackground(any(), anyInt());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
                 SPLIT_POSITION_TOP_OR_LEFT, null);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index ae69b3d..4e446c6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -44,11 +44,15 @@
 
     static SplitLayout createMockSplitLayout() {
         final Rect dividerBounds = new Rect(48, 0, 52, 100);
+        final Rect bounds1 = new Rect(0, 0, 40, 100);
+        final Rect bounds2 = new Rect(60, 0, 100, 100);
         final SurfaceControl leash = createMockSurface();
         SplitLayout out = mock(SplitLayout.class);
         doReturn(dividerBounds).when(out).getDividerBounds();
         doReturn(dividerBounds).when(out).getRefDividerBounds();
         doReturn(leash).when(out).getDividerLeash();
+        doReturn(bounds1).when(out).getBounds1();
+        doReturn(bounds2).when(out).getBounds2();
         return out;
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 8038453..60c0e55 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -42,6 +42,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -72,6 +73,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.transition.Transitions;
 
@@ -117,13 +119,13 @@
         doReturn(mockExecutor).when(mTransitions).getAnimExecutor();
         doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
         mSplitLayout = SplitTestUtils.createMockSplitLayout();
-        mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
+        mMainStage = spy(new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
-                mIconProvider);
+                mIconProvider));
         mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
-        mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
+        mSideStage = spy(new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
-                mIconProvider);
+                mIconProvider));
         mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
@@ -137,6 +139,8 @@
                 .setParentTaskId(mMainStage.mRootTaskInfo.taskId).build();
         mSideChild = new TestRunningTaskInfoBuilder()
                 .setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
+        doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
+        doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 6621ab8..66b6c62 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -68,6 +68,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
 import com.android.wm.shell.sysui.ShellController;
@@ -145,6 +146,8 @@
 
         mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
         mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
+        doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
+        doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index 8115a5d..ee9f886 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -364,6 +364,7 @@
                 1, HardwareBuffer.USAGE_CPU_READ_RARELY);
         return new TaskSnapshot(
                 System.currentTimeMillis(),
+                0 /* captureTime */,
                 new ComponentName("", ""), buffer,
                 ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
                 Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */,
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 0db88af..0c1b793 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -51,6 +51,7 @@
     CsipDeviceManager mCsipDeviceManager;
     BluetoothDevice mOngoingSetMemberPair;
     boolean mIsLateBonding;
+    int mGroupIdOfLateBonding;
 
     public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
         mContext = context;
@@ -213,6 +214,14 @@
      * @return The name, or if unavailable, the address.
      */
     public String getName(BluetoothDevice device) {
+        if (isOngoingPairByCsip(device)) {
+            CachedBluetoothDevice firstDevice =
+                    mCsipDeviceManager.getFirstMemberDevice(mGroupIdOfLateBonding);
+            if (firstDevice != null && firstDevice.getName() != null) {
+                return firstDevice.getName();
+            }
+        }
+
         CachedBluetoothDevice cachedDevice = findDevice(device);
         if (cachedDevice != null && cachedDevice.getName() != null) {
             return cachedDevice.getName();
@@ -314,6 +323,7 @@
             // To clear the SetMemberPair flag when the Bluetooth is turning off.
             mOngoingSetMemberPair = null;
             mIsLateBonding = false;
+            mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
         }
     }
 
@@ -426,6 +436,7 @@
             return false;
         }
 
+        Log.d(TAG, "isLateBonding: " + mIsLateBonding);
         return mIsLateBonding;
     }
 
@@ -444,11 +455,13 @@
         Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " groupId=" + groupId + " by CSIP ");
         mOngoingSetMemberPair = device;
         mIsLateBonding = checkLateBonding(groupId);
+        mGroupIdOfLateBonding = groupId;
         syncConfigFromMainDevice(device, groupId);
         if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) {
             Log.d(TAG, "Bonding could not be started");
             mOngoingSetMemberPair = null;
             mIsLateBonding = false;
+            mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
         }
     }
 
@@ -494,6 +507,7 @@
 
         mOngoingSetMemberPair = null;
         mIsLateBonding = false;
+        mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
         if (bondState != BluetoothDevice.BOND_NONE) {
             if (findDevice(device) == null) {
                 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index c4f09ce..f911d35 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -111,6 +111,9 @@
     public static final int COMPLICATION_TYPE_SMARTSPACE = 7;
     public static final int COMPLICATION_TYPE_MEDIA_ENTRY = 8;
 
+    private static final int SCREENSAVER_HOME_CONTROLS_ENABLED_DEFAULT = 1;
+    private static final int LOCKSCREEN_SHOW_CONTROLS_DEFAULT = 0;
+
     private final Context mContext;
     private final IDreamManager mDreamManager;
     private final DreamInfoComparator mComparator;
@@ -311,8 +314,14 @@
 
     /** Gets whether home controls button is enabled on the dream */
     private boolean getHomeControlsEnabled() {
-        return Settings.Secure.getInt(mContext.getContentResolver(),
-                Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, 1) == 1;
+        return Settings.Secure.getInt(
+                mContext.getContentResolver(),
+                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+                LOCKSCREEN_SHOW_CONTROLS_DEFAULT) == 1
+                && Settings.Secure.getInt(
+                        mContext.getContentResolver(),
+                        Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
+                        SCREENSAVER_HOME_CONTROLS_ENABLED_DEFAULT) == 1;
     }
 
     /**
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
index 22ec12d..2edf403 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
@@ -28,6 +28,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
+import android.provider.Settings;
 
 import org.junit.After;
 import org.junit.Before;
@@ -84,6 +85,7 @@
 
     @Test
     public void testComplicationsEnabledByDefault() {
+        setControlsEnabledOnLockscreen(true);
         assertThat(mBackend.getComplicationsEnabled()).isTrue();
         assertThat(mBackend.getEnabledComplications()).containsExactlyElementsIn(
                 SUPPORTED_DREAM_COMPLICATIONS_LIST);
@@ -91,6 +93,7 @@
 
     @Test
     public void testEnableComplicationExplicitly() {
+        setControlsEnabledOnLockscreen(true);
         mBackend.setComplicationsEnabled(true);
         assertThat(mBackend.getEnabledComplications()).containsExactlyElementsIn(
                 SUPPORTED_DREAM_COMPLICATIONS_LIST);
@@ -99,6 +102,7 @@
 
     @Test
     public void testDisableComplications() {
+        setControlsEnabledOnLockscreen(true);
         mBackend.setComplicationsEnabled(false);
         assertThat(mBackend.getEnabledComplications())
                 .containsExactly(COMPLICATION_TYPE_HOME_CONTROLS);
@@ -107,6 +111,7 @@
 
     @Test
     public void testHomeControlsDisabled_ComplicationsEnabled() {
+        setControlsEnabledOnLockscreen(true);
         mBackend.setComplicationsEnabled(true);
         mBackend.setHomeControlsEnabled(false);
         // Home controls should not be enabled, only date and time.
@@ -118,6 +123,7 @@
 
     @Test
     public void testHomeControlsDisabled_ComplicationsDisabled() {
+        setControlsEnabledOnLockscreen(true);
         mBackend.setComplicationsEnabled(false);
         mBackend.setHomeControlsEnabled(false);
         assertThat(mBackend.getEnabledComplications()).isEmpty();
@@ -125,9 +131,9 @@
 
     @Test
     public void testHomeControlsEnabled_ComplicationsDisabled() {
+        setControlsEnabledOnLockscreen(true);
         mBackend.setComplicationsEnabled(false);
         mBackend.setHomeControlsEnabled(true);
-        // Home controls should not be enabled, only date and time.
         final List<Integer> enabledComplications =
                 Collections.singletonList(COMPLICATION_TYPE_HOME_CONTROLS);
         assertThat(mBackend.getEnabledComplications())
@@ -136,9 +142,9 @@
 
     @Test
     public void testHomeControlsEnabled_ComplicationsEnabled() {
+        setControlsEnabledOnLockscreen(true);
         mBackend.setComplicationsEnabled(true);
         mBackend.setHomeControlsEnabled(true);
-        // Home controls should not be enabled, only date and time.
         final List<Integer> enabledComplications =
                 Arrays.asList(
                         COMPLICATION_TYPE_HOME_CONTROLS,
@@ -148,4 +154,26 @@
         assertThat(mBackend.getEnabledComplications())
                 .containsExactlyElementsIn(enabledComplications);
     }
+
+    @Test
+    public void testHomeControlsEnabled_lockscreenDisabled() {
+        setControlsEnabledOnLockscreen(false);
+        mBackend.setComplicationsEnabled(true);
+        mBackend.setHomeControlsEnabled(true);
+        // Home controls should not be enabled, only date and time.
+        final List<Integer> enabledComplications =
+                Arrays.asList(
+                        COMPLICATION_TYPE_DATE,
+                        COMPLICATION_TYPE_TIME
+                );
+        assertThat(mBackend.getEnabledComplications())
+                .containsExactlyElementsIn(enabledComplications);
+    }
+
+    private void setControlsEnabledOnLockscreen(boolean enabled) {
+        Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+                enabled ? 1 : 0);
+    }
 }
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index bdd941d..aee829d 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -90,63 +90,6 @@
   //
   // If you don't use @Postsubmit, your new test will immediately
   // block presubmit, which is probably not what you want!
-  "sysui-platinum-postsubmit": [
-    {
-      "name": "PlatformScenarioTests",
-      "options": [
-        {
-            "include-filter": "android.platform.test.scenario.sysui"
-        },
-        {
-            "include-annotation": "android.platform.test.scenario.annotation.Scenario"
-        },
-        {
-            "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-            "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
-        }
-      ]
-    }
-  ],
-  "sysui-staged-platinum-postsubmit": [
-    {
-      "name": "PlatformScenarioTests",
-      "options": [
-        {
-            "include-filter": "android.platform.test.scenario.sysui"
-        },
-        {
-            "include-annotation": "android.platform.test.scenario.annotation.Scenario"
-        },
-        {
-            "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
-    }
-  ],
-  "ironwood-postsubmit": [
-    {
-      "name": "PlatformScenarioTests",
-      "options": [
-        {
-            "include-annotation": "android.platform.test.annotations.IwTest"
-        },
-        {
-            "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-            "include-filter": "android.platform.test.scenario.sysui"
-        },
-        {
-            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
-        }
-      ]
-    }
-  ],
   "auto-end-to-end-postsubmit": [
     {
       "name": "AndroidAutomotiveHomeTests",
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 8dd2c39..465b73e 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -272,6 +272,7 @@
             color = lockScreenColor,
             animate = isAnimationEnabled,
             duration = APPEAR_ANIM_DURATION,
+            interpolator = Interpolators.EMPHASIZED_DECELERATE,
             delay = 0,
             onAnimationEnd = null
         )
@@ -562,7 +563,7 @@
         private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
         private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
         private const val DOZE_ANIM_DURATION: Long = 300
-        private const val APPEAR_ANIM_DURATION: Long = 350
+        private const val APPEAR_ANIM_DURATION: Long = 833
         private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
         private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
         private const val COLOR_ANIM_DURATION: Long = 400
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index cad2c16..4b79689 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -96,6 +96,7 @@
 
     <!-- additional offset for clock switch area items -->
     <dimen name="small_clock_height">114dp</dimen>
+    <dimen name="small_clock_padding_top">28dp</dimen>
     <dimen name="clock_padding_start">28dp</dimen>
     <dimen name="below_clock_padding_start">32dp</dimen>
     <dimen name="below_clock_padding_end">16dp</dimen>
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml
index b3b40f3..8169189 100644
--- a/packages/SystemUI/res/layout/auth_biometric_contents.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml
@@ -13,7 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
+<!-- TODO(b/251476085): inline in biometric_prompt_layout after Biometric*Views are un-flagged -->
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <TextView
diff --git a/packages/SystemUI/res/layout/auth_biometric_face_view.xml b/packages/SystemUI/res/layout/auth_biometric_face_view.xml
index be30f21..e3d0732 100644
--- a/packages/SystemUI/res/layout/auth_biometric_face_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_face_view.xml
@@ -13,7 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
+<!-- TODO(b/251476085): remove after BiometricFaceView is un-flagged -->
 <com.android.systemui.biometrics.AuthBiometricFaceView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
index 05ca2a7..896d836 100644
--- a/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
@@ -13,7 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
+<!-- TODO(b/251476085): remove after BiometricFingerprintAndFaceView is un-flagged -->
 <com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
index 01ea31f..e36f9796 100644
--- a/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
@@ -13,7 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
+<!-- TODO(b/251476085): remove after BiometricFingerprintView is un-flagged -->
 <com.android.systemui.biometrics.AuthBiometricFingerprintView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
new file mode 100644
index 0000000..05ff1b1
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -0,0 +1,176 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.android.systemui.biometrics.ui.BiometricPromptLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contents"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="@integer/biometric_dialog_text_gravity"
+        android:singleLine="true"
+        android:marqueeRepeatLimit="1"
+        android:ellipsize="marquee"
+        android:importantForAccessibility="no"
+        style="@style/TextAppearance.AuthCredential.Title"/>
+
+    <TextView
+        android:id="@+id/subtitle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="@integer/biometric_dialog_text_gravity"
+        android:singleLine="true"
+        android:marqueeRepeatLimit="1"
+        android:ellipsize="marquee"
+        style="@style/TextAppearance.AuthCredential.Subtitle"/>
+
+    <TextView
+        android:id="@+id/description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:scrollbars ="vertical"
+        android:importantForAccessibility="no"
+        style="@style/TextAppearance.AuthCredential.Description"/>
+
+    <Space android:id="@+id/space_above_icon"
+        android:layout_width="match_parent"
+        android:layout_height="48dp" />
+
+    <FrameLayout
+        android:id="@+id/biometric_icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center">
+
+        <com.airbnb.lottie.LottieAnimationView
+            android:id="@+id/biometric_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:contentDescription="@null"
+            android:scaleType="fitXY" />
+
+        <com.airbnb.lottie.LottieAnimationView
+            android:id="@+id/biometric_icon_overlay"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:contentDescription="@null"
+            android:scaleType="fitXY" />
+    </FrameLayout>
+
+    <!-- For sensors such as UDFPS, this view is used during custom measurement/layout to add extra
+         padding so that the biometric icon is always in the right physical position. -->
+    <Space android:id="@+id/space_below_icon"
+        android:layout_width="match_parent"
+        android:layout_height="12dp" />
+
+    <TextView
+        android:id="@+id/indicator"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingHorizontal="24dp"
+        android:textSize="12sp"
+        android:gravity="center_horizontal"
+        android:accessibilityLiveRegion="polite"
+        android:singleLine="true"
+        android:ellipsize="marquee"
+        android:marqueeRepeatLimit="marquee_forever"
+        android:scrollHorizontally="true"
+        android:fadingEdge="horizontal"
+        android:textColor="@color/biometric_dialog_gray"/>
+
+    <LinearLayout
+        android:id="@+id/button_bar"
+        android:layout_width="match_parent"
+        android:layout_height="88dp"
+        style="?android:attr/buttonBarStyle"
+        android:orientation="horizontal"
+        android:paddingTop="24dp">
+
+        <Space android:id="@+id/leftSpacer"
+            android:layout_width="8dp"
+            android:layout_height="match_parent"
+            android:visibility="visible" />
+
+        <!-- Negative Button, reserved for app -->
+        <Button android:id="@+id/button_negative"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            android:layout_gravity="center_vertical"
+            android:ellipsize="end"
+            android:maxLines="2"
+            android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+            android:visibility="gone"/>
+        <!-- Cancel Button, replaces negative button when biometric is accepted -->
+        <Button android:id="@+id/button_cancel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            android:layout_gravity="center_vertical"
+            android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+            android:text="@string/cancel"
+            android:visibility="gone"/>
+        <!-- "Use Credential" Button, replaces if device credential is allowed -->
+        <Button android:id="@+id/button_use_credential"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            android:layout_gravity="center_vertical"
+            android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+            android:visibility="gone"/>
+
+        <Space android:id="@+id/middleSpacer"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:visibility="visible"/>
+
+        <!-- Positive Button -->
+        <Button android:id="@+id/button_confirm"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@*android:style/Widget.DeviceDefault.Button.Colored"
+            android:layout_gravity="center_vertical"
+            android:ellipsize="end"
+            android:maxLines="2"
+            android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
+            android:text="@string/biometric_dialog_confirm"
+            android:visibility="gone"/>
+        <!-- Try Again Button -->
+        <Button android:id="@+id/button_try_again"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@*android:style/Widget.DeviceDefault.Button.Colored"
+            android:layout_gravity="center_vertical"
+            android:ellipsize="end"
+            android:maxLines="2"
+            android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
+            android:text="@string/biometric_dialog_try_again"
+            android:visibility="gone"/>
+
+        <Space android:id="@+id/rightSpacer"
+            android:layout_width="8dp"
+            android:layout_height="match_parent"
+            android:visibility="visible" />
+    </LinearLayout>
+
+</com.android.systemui.biometrics.ui.BiometricPromptLayout>
diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml
index ae052502..bbf3adf 100644
--- a/packages/SystemUI/res/layout/screen_record_dialog.xml
+++ b/packages/SystemUI/res/layout/screen_record_dialog.xml
@@ -73,7 +73,7 @@
                         android:tint="?android:attr/textColorSecondary"
                         android:layout_gravity="center"
                         android:layout_weight="0"
-                        android:layout_marginRight="@dimen/screenrecord_option_padding"/>
+                        android:layout_marginEnd="@dimen/screenrecord_option_padding"/>
                     <Spinner
                         android:id="@+id/screen_recording_options"
                         android:layout_width="0dp"
@@ -106,7 +106,7 @@
                         android:src="@drawable/ic_touch"
                         android:tint="?android:attr/textColorSecondary"
                         android:layout_gravity="center"
-                        android:layout_marginRight="@dimen/screenrecord_option_padding"/>
+                        android:layout_marginEnd="@dimen/screenrecord_option_padding"/>
                     <TextView
                         android:layout_width="0dp"
                         android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/volume_ringer_drawer.xml b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
index 1112bcd..9b1fa23 100644
--- a/packages/SystemUI/res/layout/volume_ringer_drawer.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
@@ -85,7 +85,7 @@
                     android:layout_height="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_gravity="center"
                     android:tint="?android:attr/textColorPrimary"
-                    android:src="@drawable/ic_volume_ringer_mute" />
+                    android:src="@drawable/ic_speaker_mute" />
 
             </FrameLayout>
 
@@ -102,7 +102,7 @@
                     android:layout_height="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_gravity="center"
                     android:tint="?android:attr/textColorPrimary"
-                    android:src="@drawable/ic_volume_ringer" />
+                    android:src="@drawable/ic_speaker_on" />
 
             </FrameLayout>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a62dead..8d3ba36 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -716,7 +716,7 @@
     <!-- Minimum margin between clock and status bar -->
     <dimen name="keyguard_clock_top_margin">18dp</dimen>
     <!-- The amount to shift the clocks during a small/large transition -->
-    <dimen name="keyguard_clock_switch_y_shift">10dp</dimen>
+    <dimen name="keyguard_clock_switch_y_shift">14dp</dimen>
     <!-- When large clock is showing, offset the smartspace by this amount -->
     <dimen name="keyguard_smartspace_top_offset">12dp</dimen>
     <!-- With the large clock, move up slightly from the center -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 003f9b0..67fdb4c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -353,7 +353,7 @@
     <!-- Message shown when a biometric is authenticated, waiting for the user to confirm authentication [CHAR LIMIT=40]-->
     <string name="biometric_dialog_tap_confirm">Tap Confirm to complete</string>
     <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
-    <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face. Press the unlock icon to continue.</string>
+    <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face.</string>
     <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
     <string name="biometric_dialog_tap_confirm_with_face_alt_1">Unlocked by face. Press to continue.</string>
     <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index e54d473..d9d64ad 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -1,5 +1,8 @@
 package com.android.keyguard;
 
+import static android.view.View.ALPHA;
+import static android.view.View.TRANSLATION_Y;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -35,11 +38,12 @@
 
     private static final String TAG = "KeyguardClockSwitch";
 
-    private static final long CLOCK_OUT_MILLIS = 150;
-    private static final long CLOCK_IN_MILLIS = 200;
-    public static final long CLOCK_IN_START_DELAY_MILLIS = CLOCK_OUT_MILLIS / 2;
-    private static final long STATUS_AREA_START_DELAY_MILLIS = 50;
-    private static final long STATUS_AREA_MOVE_MILLIS = 350;
+    private static final long CLOCK_OUT_MILLIS = 133;
+    private static final long CLOCK_IN_MILLIS = 167;
+    public static final long CLOCK_IN_START_DELAY_MILLIS = 133;
+    private static final long STATUS_AREA_START_DELAY_MILLIS = 0;
+    private static final long STATUS_AREA_MOVE_UP_MILLIS = 967;
+    private static final long STATUS_AREA_MOVE_DOWN_MILLIS = 467;
 
     @IntDef({LARGE, SMALL})
     @Retention(RetentionPolicy.SOURCE)
@@ -66,6 +70,17 @@
                 top + targetHeight);
     }
 
+    /** Returns a region for the small clock to position itself, based on the given parent. */
+    public static Rect getSmallClockRegion(ViewGroup parent) {
+        int targetHeight = parent.getResources()
+                .getDimensionPixelSize(R.dimen.small_clock_text_size);
+        return new Rect(
+                parent.getLeft(),
+                parent.getTop(),
+                parent.getRight(),
+                parent.getTop() + targetHeight);
+    }
+
     /**
      * Frame for small/large clocks
      */
@@ -90,7 +105,7 @@
 
     @VisibleForTesting AnimatorSet mClockInAnim = null;
     @VisibleForTesting AnimatorSet mClockOutAnim = null;
-    private ObjectAnimator mStatusAreaAnim = null;
+    private AnimatorSet mStatusAreaAnim = null;
 
     private int mClockSwitchYAmount;
     @VisibleForTesting boolean mChildrenAreLaidOut = false;
@@ -172,13 +187,8 @@
     void updateClockTargetRegions() {
         if (mClock != null) {
             if (mSmallClockFrame.isLaidOut()) {
-                int targetHeight =  getResources()
-                        .getDimensionPixelSize(R.dimen.small_clock_text_size);
-                mClock.getSmallClock().getEvents().onTargetRegionChanged(new Rect(
-                        mSmallClockFrame.getLeft(),
-                        mSmallClockFrame.getTop(),
-                        mSmallClockFrame.getRight(),
-                        mSmallClockFrame.getTop() + targetHeight));
+                Rect targetRegion = getSmallClockRegion(mSmallClockFrame);
+                mClock.getSmallClock().getEvents().onTargetRegionChanged(targetRegion);
             }
 
             if (mLargeClockFrame.isLaidOut()) {
@@ -220,39 +230,44 @@
         mStatusAreaAnim = null;
 
         View in, out;
-        int direction = 1;
-        float statusAreaYTranslation;
+        float statusAreaYTranslation, clockInYTranslation, clockOutYTranslation;
         if (useLargeClock) {
             out = mSmallClockFrame;
             in = mLargeClockFrame;
             if (indexOfChild(in) == -1) addView(in, 0);
-            direction = -1;
             statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
                     + mSmartspaceTopOffset;
+            clockInYTranslation = 0;
+            clockOutYTranslation = 0; // Small clock translation is handled with statusArea
         } else {
             in = mSmallClockFrame;
             out = mLargeClockFrame;
             statusAreaYTranslation = 0f;
+            clockInYTranslation = 0f;
+            clockOutYTranslation = mClockSwitchYAmount * -1f;
 
-            // Must remove in order for notifications to appear in the proper place
+            // Must remove in order for notifications to appear in the proper place, ideally this
+            // would happen after the out animation runs, but we can't guarantee that the
+            // nofications won't enter only after the out animation runs.
             removeView(out);
         }
 
         if (!animate) {
             out.setAlpha(0f);
+            out.setTranslationY(clockOutYTranslation);
             in.setAlpha(1f);
-            in.setVisibility(VISIBLE);
+            in.setTranslationY(clockInYTranslation);
+            in.setVisibility(View.VISIBLE);
             mStatusArea.setTranslationY(statusAreaYTranslation);
             return;
         }
 
         mClockOutAnim = new AnimatorSet();
         mClockOutAnim.setDuration(CLOCK_OUT_MILLIS);
-        mClockOutAnim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+        mClockOutAnim.setInterpolator(Interpolators.LINEAR);
         mClockOutAnim.playTogether(
-                ObjectAnimator.ofFloat(out, View.ALPHA, 0f),
-                ObjectAnimator.ofFloat(out, View.TRANSLATION_Y, 0,
-                        direction * -mClockSwitchYAmount));
+                ObjectAnimator.ofFloat(out, ALPHA, 0f),
+                ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation));
         mClockOutAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
                 mClockOutAnim = null;
@@ -264,8 +279,9 @@
         mClockInAnim = new AnimatorSet();
         mClockInAnim.setDuration(CLOCK_IN_MILLIS);
         mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-        mClockInAnim.playTogether(ObjectAnimator.ofFloat(in, View.ALPHA, 1f),
-                ObjectAnimator.ofFloat(in, View.TRANSLATION_Y, direction * mClockSwitchYAmount, 0));
+        mClockInAnim.playTogether(
+                ObjectAnimator.ofFloat(in, ALPHA, 1f),
+                ObjectAnimator.ofFloat(in, TRANSLATION_Y, clockInYTranslation));
         mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS);
         mClockInAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
@@ -273,19 +289,22 @@
             }
         });
 
-        mClockInAnim.start();
-        mClockOutAnim.start();
-
-        mStatusAreaAnim = ObjectAnimator.ofFloat(mStatusArea, View.TRANSLATION_Y,
-                statusAreaYTranslation);
-        mStatusAreaAnim.setStartDelay(useLargeClock ? STATUS_AREA_START_DELAY_MILLIS : 0L);
-        mStatusAreaAnim.setDuration(STATUS_AREA_MOVE_MILLIS);
-        mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mStatusAreaAnim = new AnimatorSet();
+        mStatusAreaAnim.setStartDelay(STATUS_AREA_START_DELAY_MILLIS);
+        mStatusAreaAnim.setDuration(
+                useLargeClock ? STATUS_AREA_MOVE_UP_MILLIS : STATUS_AREA_MOVE_DOWN_MILLIS);
+        mStatusAreaAnim.setInterpolator(Interpolators.EMPHASIZED);
+        mStatusAreaAnim.playTogether(
+                ObjectAnimator.ofFloat(mStatusArea, TRANSLATION_Y, statusAreaYTranslation),
+                ObjectAnimator.ofFloat(mSmallClockFrame, TRANSLATION_Y, statusAreaYTranslation));
         mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
                 mStatusAreaAnim = null;
             }
         });
+
+        mClockInAnim.start();
+        mClockOutAnim.start();
         mStatusAreaAnim.start();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 9d9a87d..c684dc5 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -51,6 +51,12 @@
      */
     val isBypassEnabled: StateFlow<Boolean>
 
+    /**
+     * Number of consecutively failed authentication attempts. This resets to `0` when
+     * authentication succeeds.
+     */
+    val failedAuthenticationAttempts: StateFlow<Int>
+
     /** See [isUnlocked]. */
     fun setUnlocked(isUnlocked: Boolean)
 
@@ -59,6 +65,9 @@
 
     /** See [isBypassEnabled]. */
     fun setBypassEnabled(isBypassEnabled: Boolean)
+
+    /** See [failedAuthenticationAttempts]. */
+    fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int)
 }
 
 class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
@@ -75,6 +84,10 @@
     private val _isBypassEnabled = MutableStateFlow(false)
     override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
 
+    private val _failedAuthenticationAttempts = MutableStateFlow(0)
+    override val failedAuthenticationAttempts: StateFlow<Int> =
+        _failedAuthenticationAttempts.asStateFlow()
+
     override fun setUnlocked(isUnlocked: Boolean) {
         _isUnlocked.value = isUnlocked
     }
@@ -86,6 +99,10 @@
     override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
         _authenticationMethod.value = authenticationMethod
     }
+
+    override fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) {
+        _failedAuthenticationAttempts.value = failedAuthenticationAttempts
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5aea930..3984627 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -75,6 +75,12 @@
      */
     val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
 
+    /**
+     * Number of consecutively failed authentication attempts. This resets to `0` when
+     * authentication succeeds.
+     */
+    val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
+
     init {
         // UNLOCKS WHEN AUTH METHOD REMOVED.
         //
@@ -130,7 +136,12 @@
             }
 
         if (isSuccessful) {
+            repository.setFailedAuthenticationAttempts(0)
             repository.setUnlocked(true)
+        } else {
+            repository.setFailedAuthenticationAttempts(
+                repository.failedAuthenticationAttempts.value + 1
+            )
         }
 
         return isSuccessful
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
index 83250b6..6f008c3 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -36,8 +36,10 @@
 
     data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
 
-    data class Pattern(val coordinates: List<PatternCoordinate>) :
-        AuthenticationMethodModel(isSecure = true) {
+    data class Pattern(
+        val coordinates: List<PatternCoordinate>,
+        val isPatternVisible: Boolean = true,
+    ) : AuthenticationMethodModel(isSecure = true) {
 
         data class PatternCoordinate(
             val x: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
index 1404053..682888f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
@@ -21,42 +21,43 @@
 import com.airbnb.lottie.LottieAnimationView
 import com.android.systemui.R
 import com.android.systemui.biometrics.AuthBiometricView.BiometricState
-import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
 import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
 import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
 import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
 
 /** Face/Fingerprint combined icon animator for BiometricPrompt. */
-class AuthBiometricFingerprintAndFaceIconController(
-        context: Context,
-        iconView: LottieAnimationView,
-        iconViewOverlay: LottieAnimationView
+open class AuthBiometricFingerprintAndFaceIconController(
+    context: Context,
+    iconView: LottieAnimationView,
+    iconViewOverlay: LottieAnimationView,
 ) : AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) {
 
     override val actsAsConfirmButton: Boolean = true
 
     override fun shouldAnimateIconViewForTransition(
-            @BiometricState oldState: Int,
-            @BiometricState newState: Int
+        @BiometricState oldState: Int,
+        @BiometricState newState: Int
     ): Boolean = when (newState) {
         STATE_PENDING_CONFIRMATION -> true
-        STATE_AUTHENTICATED -> false
         else -> super.shouldAnimateIconViewForTransition(oldState, newState)
     }
 
     @RawRes
     override fun getAnimationForTransition(
-            @BiometricState oldState: Int,
-            @BiometricState newState: Int
+        @BiometricState oldState: Int,
+        @BiometricState newState: Int
     ): Int? = when (newState) {
         STATE_PENDING_CONFIRMATION -> {
             if (oldState == STATE_ERROR || oldState == STATE_HELP) {
                 R.raw.fingerprint_dialogue_error_to_unlock_lottie
+            } else if (oldState == STATE_PENDING_CONFIRMATION) {
+                // TODO(jbolinger): missing asset for this transition
+                // (unlocked icon to success checkmark)
+                R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie
             } else {
                 R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie
             }
         }
-        STATE_AUTHENTICATED -> null
         else -> super.getAnimationForTransition(oldState, newState)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
index 57ffd24..7ce74db 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
@@ -40,11 +40,11 @@
     override fun ignoreUnsuccessfulEventsFrom(@Modality modality: Int, unsuccessfulReason: String) =
         modality == TYPE_FACE && !(isFaceClass3 && isLockoutErrorString(unsuccessfulReason))
 
-    override fun onPointerDown(failedModalities: Set<Int>) = failedModalities.contains(TYPE_FACE)
-
     override fun createIconController(): AuthIconController =
         AuthBiometricFingerprintAndFaceIconController(mContext, mIconView, mIconViewOverlay)
 
+    override fun isCoex() = true
+
     private fun isLockoutErrorString(unsuccessfulReason: String) =
         unsuccessfulReason == FaceManager.getErrorString(
             mContext,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index f04fdfff..9807b9e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -122,7 +122,9 @@
         if (shouldAnimateIconViewForTransition(lastState, newState)) {
             iconView.playAnimation()
         }
-        LottieColorUtils.applyDynamicColors(context, iconView)
+        if (isSideFps) {
+            LottieColorUtils.applyDynamicColors(context, iconView)
+        }
     }
 
     override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 4db371b..fb160f2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -56,43 +56,42 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 
 /**
  * Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers.
  */
-public abstract class AuthBiometricView extends LinearLayout {
+public abstract class AuthBiometricView extends LinearLayout implements AuthBiometricViewAdapter {
 
     private static final String TAG = "AuthBiometricView";
 
     /**
      * Authentication hardware idle.
      */
-    protected static final int STATE_IDLE = 0;
+    public static final int STATE_IDLE = 0;
     /**
      * UI animating in, authentication hardware active.
      */
-    protected static final int STATE_AUTHENTICATING_ANIMATING_IN = 1;
+    public static final int STATE_AUTHENTICATING_ANIMATING_IN = 1;
     /**
      * UI animated in, authentication hardware active.
      */
-    protected static final int STATE_AUTHENTICATING = 2;
+    public static final int STATE_AUTHENTICATING = 2;
     /**
      * UI animated in, authentication hardware active.
      */
-    protected static final int STATE_HELP = 3;
+    public static final int STATE_HELP = 3;
     /**
      * Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle.
      */
-    protected static final int STATE_ERROR = 4;
+    public static final int STATE_ERROR = 4;
     /**
      * Authenticated, waiting for user confirmation. Authentication hardware idle.
      */
-    protected static final int STATE_PENDING_CONFIRMATION = 5;
+    public static final int STATE_PENDING_CONFIRMATION = 5;
     /**
      * Authenticated, dialog animating away soon.
      */
-    protected static final int STATE_AUTHENTICATED = 6;
+    public static final int STATE_AUTHENTICATED = 6;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_HELP,
@@ -102,13 +101,14 @@
     /**
      * Callback to the parent when a user action has occurred.
      */
-    interface Callback {
+    public interface Callback {
         int ACTION_AUTHENTICATED = 1;
         int ACTION_USER_CANCELED = 2;
         int ACTION_BUTTON_NEGATIVE = 3;
         int ACTION_BUTTON_TRY_AGAIN = 4;
         int ACTION_ERROR = 5;
         int ACTION_USE_DEVICE_CREDENTIAL = 6;
+        int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7;
 
         /**
          * When an action has occurred. The caller will only invoke this when the callback should
@@ -268,6 +268,27 @@
     /** Create the controller for managing the icons transitions during the prompt.*/
     @NonNull
     protected abstract AuthIconController createIconController();
+
+    @Override
+    public AuthIconController getLegacyIconController() {
+        return mIconController;
+    }
+
+    @Override
+    public void cancelAnimation() {
+        animate().cancel();
+    }
+
+    @Override
+    public View asView() {
+        return this;
+    }
+
+    @Override
+    public boolean isCoex() {
+        return false;
+    }
+
     void setPanelController(AuthPanelController panelController) {
         mPanelController = panelController;
     }
@@ -544,12 +565,12 @@
         mState = newState;
     }
 
-    void onOrientationChanged() {
+    public void onOrientationChanged() {
         // Update padding and AuthPanel outline by calling updateSize when the orientation changed.
         updateSize(mSize);
     }
 
-    public void onDialogAnimatedIn() {
+    public void onDialogAnimatedIn(boolean fingerprintWasStarted) {
         updateState(STATE_AUTHENTICATING);
     }
 
@@ -597,18 +618,6 @@
     }
 
     /**
-     * Fingerprint pointer down event. This does nothing by default and will not be called if the
-     * device does not have an appropriate sensor (UDFPS), but it may be used as an alternative
-     * to the "retry" button when fingerprint is used with other modalities.
-     *
-     * @param failedModalities the set of modalities that have failed
-     * @return true if a retry was initiated as a result of this event
-     */
-    public boolean onPointerDown(Set<Integer> failedModalities) {
-        return false;
-    }
-
-    /**
      * Show a help message to the user.
      *
      * @param modality sensor modality
@@ -752,7 +761,8 @@
     /**
      * Kicks off the animation process and invokes the callback.
      */
-    void startTransitionToCredentialUI() {
+    @Override
+    public void startTransitionToCredentialUI() {
         updateSize(AuthDialog.SIZE_LARGE);
         mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt
new file mode 100644
index 0000000..631511c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.hardware.biometrics.BiometricAuthenticator
+import android.os.Bundle
+import android.view.View
+
+/** TODO(b/251476085): Temporary interface while legacy biometric prompt is around. */
+@Deprecated("temporary adapter while migrating biometric prompt - do not expand")
+interface AuthBiometricViewAdapter {
+    val legacyIconController: AuthIconController?
+
+    fun onDialogAnimatedIn(fingerprintWasStarted: Boolean)
+
+    fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int)
+
+    fun onAuthenticationFailed(
+        @BiometricAuthenticator.Modality modality: Int,
+        failureReason: String
+    )
+
+    fun onError(@BiometricAuthenticator.Modality modality: Int, error: String)
+
+    fun onHelp(@BiometricAuthenticator.Modality modality: Int, help: String)
+
+    fun startTransitionToCredentialUI()
+
+    fun requestLayout()
+
+    fun onSaveState(bundle: Bundle?)
+
+    fun restoreState(bundle: Bundle?)
+
+    fun onOrientationChanged()
+
+    fun cancelAnimation()
+
+    fun isCoex(): Boolean
+
+    fun asView(): View
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index e775c2e..49ac264 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -16,15 +16,13 @@
 
 package com.android.systemui.biometrics;
 
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
-import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
 import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
 
 import android.animation.Animator;
-import android.annotation.DurationMillisLong;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -66,12 +64,19 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.R;
 import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
+import com.android.systemui.biometrics.domain.model.BiometricModalities;
+import com.android.systemui.biometrics.ui.BiometricPromptLayout;
 import com.android.systemui.biometrics.ui.CredentialView;
 import com.android.systemui.biometrics.ui.binder.AuthBiometricFingerprintViewBinder;
+import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
 import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
@@ -84,6 +89,8 @@
 
 import javax.inject.Provider;
 
+import kotlinx.coroutines.CoroutineScope;
+
 /**
  * Top level container/controller for the BiometricPrompt UI.
  */
@@ -126,16 +133,20 @@
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
     private final InteractionJankMonitor mInteractionJankMonitor;
+    private final CoroutineScope mApplicationCoroutineScope;
 
     // TODO: these should be migrated out once ready
-    private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
+    private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
     private final Provider<AuthBiometricFingerprintViewModel>
             mAuthBiometricFingerprintViewModelProvider;
+    private final @NonNull Provider<PromptSelectorInteractor> mPromptSelectorInteractorProvider;
+    // TODO(b/251476085): these should be migrated out of the view
     private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+    private final PromptViewModel mPromptViewModel;
 
     @VisibleForTesting final BiometricCallback mBiometricCallback;
 
-    @Nullable private AuthBiometricView mBiometricView;
+    @Nullable private AuthBiometricViewAdapter mBiometricView;
     @Nullable private View mCredentialView;
     private final AuthPanelController mPanelController;
     private final FrameLayout mFrameLayout;
@@ -154,7 +165,8 @@
     // HAT received from LockSettingsService when credential is verified.
     @Nullable private byte[] mCredentialAttestation;
 
-    @VisibleForTesting
+    // TODO(b/251476085): remove when legacy prompt is replaced
+    @Deprecated
     static class Config {
         Context mContext;
         AuthDialogCallback mCallback;
@@ -167,96 +179,9 @@
         long mOperationId;
         long mRequestId = -1;
         boolean mSkipAnimation = false;
-        @BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
         ScaleFactorProvider mScaleProvider;
     }
 
-    public static class Builder {
-        Config mConfig;
-
-        public Builder(Context context) {
-            mConfig = new Config();
-            mConfig.mContext = context;
-        }
-
-        public Builder setCallback(AuthDialogCallback callback) {
-            mConfig.mCallback = callback;
-            return this;
-        }
-
-        public Builder setPromptInfo(PromptInfo promptInfo) {
-            mConfig.mPromptInfo = promptInfo;
-            return this;
-        }
-
-        public Builder setRequireConfirmation(boolean requireConfirmation) {
-            mConfig.mRequireConfirmation = requireConfirmation;
-            return this;
-        }
-
-        public Builder setUserId(int userId) {
-            mConfig.mUserId = userId;
-            return this;
-        }
-
-        public Builder setOpPackageName(String opPackageName) {
-            mConfig.mOpPackageName = opPackageName;
-            return this;
-        }
-
-        public Builder setSkipIntro(boolean skip) {
-            mConfig.mSkipIntro = skip;
-            return this;
-        }
-
-        public Builder setOperationId(@DurationMillisLong long operationId) {
-            mConfig.mOperationId = operationId;
-            return this;
-        }
-
-        /** Unique id for this request. */
-        public Builder setRequestId(long requestId) {
-            mConfig.mRequestId = requestId;
-            return this;
-        }
-
-        @VisibleForTesting
-        public Builder setSkipAnimationDuration(boolean skip) {
-            mConfig.mSkipAnimation = skip;
-            return this;
-        }
-
-        /** The multi-sensor mode. */
-        public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
-            mConfig.mMultiSensorConfig = multiSensorConfig;
-            return this;
-        }
-
-        public Builder setScaleFactorProvider(ScaleFactorProvider scaleProvider) {
-            mConfig.mScaleProvider = scaleProvider;
-            return this;
-        }
-
-        public AuthContainerView build(@Background DelayableExecutor bgExecutor, int[] sensorIds,
-                @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
-                @Nullable List<FaceSensorPropertiesInternal> faceProps,
-                @NonNull WakefulnessLifecycle wakefulnessLifecycle,
-                @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
-                @NonNull UserManager userManager,
-                @NonNull LockPatternUtils lockPatternUtils,
-                @NonNull InteractionJankMonitor jankMonitor,
-                @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
-                @NonNull Provider<AuthBiometricFingerprintViewModel>
-                        authBiometricFingerprintViewModelProvider,
-                @NonNull Provider<CredentialViewModel> credentialViewModelProvider) {
-            mConfig.mSensorIds = sensorIds;
-            return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
-                    panelInteractionDetector, userManager, lockPatternUtils, jankMonitor,
-                    biometricPromptInteractor, authBiometricFingerprintViewModelProvider,
-                    credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor);
-        }
-    }
-
     @VisibleForTesting
     final class BiometricCallback implements AuthBiometricView.Callback {
         @Override
@@ -285,6 +210,9 @@
                         addCredentialView(false /* animatePanel */, true /* animateContents */);
                     }, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
                     break;
+                case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR:
+                    mConfig.mCallback.onStartFingerprintNow(getRequestId());
+                    break;
                 default:
                     Log.e(TAG, "Unhandled action: " + action);
             }
@@ -336,8 +264,10 @@
         alertDialog.show();
     }
 
-    @VisibleForTesting
-    AuthContainerView(Config config,
+    // TODO(b/251476085): remove Config and further decompose these properties out of view classes
+    AuthContainerView(@NonNull Config config,
+            @NonNull FeatureFlags featureFlags,
+            @NonNull CoroutineScope applicationCoroutineScope,
             @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
             @Nullable List<FaceSensorPropertiesInternal> faceProps,
             @NonNull WakefulnessLifecycle wakefulnessLifecycle,
@@ -345,9 +275,36 @@
             @NonNull UserManager userManager,
             @NonNull LockPatternUtils lockPatternUtils,
             @NonNull InteractionJankMonitor jankMonitor,
-            @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
             @NonNull Provider<AuthBiometricFingerprintViewModel>
                     authBiometricFingerprintViewModelProvider,
+            @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractor,
+            @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
+            @NonNull PromptViewModel promptViewModel,
+            @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
+            @NonNull @Background DelayableExecutor bgExecutor) {
+        this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps,
+                wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
+                jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor,
+                promptCredentialInteractor, promptViewModel, credentialViewModelProvider,
+                new Handler(Looper.getMainLooper()), bgExecutor);
+    }
+
+    @VisibleForTesting
+    AuthContainerView(@NonNull Config config,
+            @NonNull FeatureFlags featureFlags,
+            @NonNull CoroutineScope applicationCoroutineScope,
+            @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
+            @Nullable List<FaceSensorPropertiesInternal> faceProps,
+            @NonNull WakefulnessLifecycle wakefulnessLifecycle,
+            @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
+            @NonNull UserManager userManager,
+            @NonNull LockPatternUtils lockPatternUtils,
+            @NonNull InteractionJankMonitor jankMonitor,
+            @NonNull Provider<AuthBiometricFingerprintViewModel>
+                    authBiometricFingerprintViewModelProvider,
+            @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
+            @NonNull Provider<PromptCredentialInteractor> credentialInteractor,
+            @NonNull PromptViewModel promptViewModel,
             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
             @NonNull Handler mainHandler,
             @NonNull @Background DelayableExecutor bgExecutor) {
@@ -360,6 +317,7 @@
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mPanelInteractionDetector = panelInteractionDetector;
+        mApplicationCoroutineScope = applicationCoroutineScope;
 
         mTranslationY = getResources()
                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
@@ -376,10 +334,70 @@
         mPanelController = new AuthPanelController(mContext, mPanelView);
         mBackgroundExecutor = bgExecutor;
         mInteractionJankMonitor = jankMonitor;
-        mBiometricPromptInteractor = biometricPromptInteractor;
+        mPromptCredentialInteractor = credentialInteractor;
         mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
+        mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
         mCredentialViewModelProvider = credentialViewModelProvider;
+        mPromptViewModel = promptViewModel;
 
+        if (featureFlags.isEnabled(Flags.BIOMETRIC_BP_STRONG)) {
+            showPrompt(config, layoutInflater, promptViewModel,
+                    Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
+                    Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
+        } else {
+            showLegacyPrompt(config, layoutInflater, fpProps, faceProps);
+        }
+
+        // TODO: De-dupe the logic with AuthCredentialPasswordView
+        setOnKeyListener((v, keyCode, event) -> {
+            if (keyCode != KeyEvent.KEYCODE_BACK) {
+                return false;
+            }
+            if (event.getAction() == KeyEvent.ACTION_UP) {
+                onBackInvoked();
+            }
+            return true;
+        });
+
+        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+        setFocusableInTouchMode(true);
+        requestFocus();
+    }
+
+    private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
+            @NonNull PromptViewModel viewModel,
+            @Nullable FingerprintSensorPropertiesInternal fpProps,
+            @Nullable FaceSensorPropertiesInternal faceProps) {
+        if (Utils.isBiometricAllowed(config.mPromptInfo)) {
+            mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication(
+                    config.mPromptInfo,
+                    config.mRequireConfirmation,
+                    config.mUserId,
+                    config.mOperationId,
+                    new BiometricModalities(fpProps, faceProps));
+
+            final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
+                    R.layout.biometric_prompt_layout, null, false);
+            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);
+
+            // TODO(b/251476085): migrate these dependencies
+            if (fpProps != null && fpProps.isAnyUdfpsType()) {
+                view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
+                        config.mScaleProvider);
+            }
+        } else {
+            mPromptSelectorInteractorProvider.get().resetPrompt();
+        }
+    }
+
+    // TODO(b/251476085): remove entirely
+    private void showLegacyPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
+            @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
+            @Nullable List<FaceSensorPropertiesInternal> faceProps
+    ) {
         // Inflate biometric view only if necessary.
         if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
             final FingerprintSensorPropertiesInternal fpProperties =
@@ -421,31 +439,18 @@
 
         // init view before showing
         if (mBiometricView != null) {
-            mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
-            mBiometricView.setPanelController(mPanelController);
-            mBiometricView.setPromptInfo(mConfig.mPromptInfo);
-            mBiometricView.setCallback(mBiometricCallback);
-            mBiometricView.setBackgroundView(mBackgroundView);
-            mBiometricView.setUserId(mConfig.mUserId);
-            mBiometricView.setEffectiveUserId(mEffectiveUserId);
-            mBiometricView.setJankListener(getJankListener(mBiometricView, TRANSIT,
+            final AuthBiometricView view = (AuthBiometricView) mBiometricView;
+            view.setRequireConfirmation(mConfig.mRequireConfirmation);
+            view.setPanelController(mPanelController);
+            view.setPromptInfo(mConfig.mPromptInfo);
+            view.setCallback(mBiometricCallback);
+            view.setBackgroundView(mBackgroundView);
+            view.setUserId(mConfig.mUserId);
+            view.setEffectiveUserId(mEffectiveUserId);
+            // TODO(b/201510778): This uses the wrong timeout in some cases (remove w/ above)
+            view.setJankListener(getJankListener(view, TRANSIT,
                     AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS));
         }
-
-        // TODO: De-dupe the logic with AuthCredentialPasswordView
-        setOnKeyListener((v, keyCode, event) -> {
-            if (keyCode != KeyEvent.KEYCODE_BACK) {
-                return false;
-            }
-            if (event.getAction() == KeyEvent.ACTION_UP) {
-                onBackInvoked();
-            }
-            return true;
-        });
-
-        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
-        setFocusableInTouchMode(true);
-        requestFocus();
     }
 
     private void onBackInvoked() {
@@ -495,7 +500,7 @@
         mBackgroundView.setOnClickListener(null);
         mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
 
-        mBiometricPromptInteractor.get().useCredentialsForAuthentication(
+        mPromptSelectorInteractorProvider.get().useCredentialsForAuthentication(
                 mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId);
         final CredentialViewModel vm = mCredentialViewModelProvider.get();
         vm.setAnimateContents(animateContents);
@@ -527,7 +532,7 @@
                 () -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
 
         if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
-            mBiometricScrollView.addView(mBiometricView);
+            mBiometricScrollView.addView(mBiometricView.asView());
         } else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
             addCredentialView(true /* animatePanel */, false /* animateContents */);
         } else {
@@ -601,9 +606,13 @@
     }
 
     private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
+        // TODO(b/251476085): legacy view (delete when removed)
         if (view instanceof AuthBiometricFingerprintView) {
             return ((AuthBiometricFingerprintView) view).isUdfps();
         }
+        if (view instanceof BiometricPromptLayout) {
+            return ((BiometricPromptLayout) view).isUdfps();
+        }
 
         return false;
     }
@@ -613,7 +622,7 @@
         if (display == null) {
             return false;
         }
-        if (!shouldUpdatePositionForUdfps(mBiometricView)) {
+        if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) {
             return false;
         }
 
@@ -626,12 +635,12 @@
 
             case Surface.ROTATION_90:
                 mPanelController.setPosition(AuthPanelController.POSITION_RIGHT);
-                setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+                setScrollViewGravity(Gravity.BOTTOM | Gravity.RIGHT);
                 break;
 
             case Surface.ROTATION_270:
                 mPanelController.setPosition(AuthPanelController.POSITION_LEFT);
-                setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
+                setScrollViewGravity(Gravity.BOTTOM | Gravity.LEFT);
                 break;
 
             case Surface.ROTATION_180:
@@ -689,7 +698,7 @@
                 mCredentialView.animate().cancel();
             }
             mPanelView.animate().cancel();
-            mBiometricView.animate().cancel();
+            mBiometricView.cancelAnimation();
             animate().cancel();
             onDialogAnimatedIn();
         }
@@ -750,8 +759,9 @@
     @Override
     public void onPointerDown() {
         if (mBiometricView != null) {
-            if (mBiometricView.onPointerDown(mFailedModalities)) {
+            if (mFailedModalities.contains(TYPE_FACE)) {
                 Log.d(TAG, "retrying failed modalities (pointer down)");
+                mFailedModalities.remove(TYPE_FACE);
                 mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
             }
         } else {
@@ -885,11 +895,17 @@
         }
         mContainerState = STATE_SHOWING;
         if (mBiometricView != null) {
-            mConfig.mCallback.onDialogAnimatedIn(getRequestId());
-            mBiometricView.onDialogAnimatedIn();
+            final boolean delayFingerprint = mBiometricView.isCoex() && !mConfig.mRequireConfirmation;
+            mConfig.mCallback.onDialogAnimatedIn(getRequestId(), !delayFingerprint);
+            mBiometricView.onDialogAnimatedIn(!delayFingerprint);
         }
     }
 
+    @Override
+    public PromptViewModel getViewModel() {
+        return mPromptViewModel;
+    }
+
     @VisibleForTesting
     static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
         final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
@@ -922,26 +938,5 @@
         if (mConfig != null) {
             pw.println("    config.sensorIds exist=" + (mConfig.mSensorIds != null));
         }
-        final AuthBiometricView biometricView = mBiometricView;
-        pw.println("    scrollView=" + findViewById(R.id.biometric_scrollview));
-        pw.println("      biometricView=" + biometricView);
-        if (biometricView != null) {
-            int[] ids = {
-                    R.id.title,
-                    R.id.subtitle,
-                    R.id.description,
-                    R.id.biometric_icon_frame,
-                    R.id.biometric_icon,
-                    R.id.indicator,
-                    R.id.button_bar,
-                    R.id.button_negative,
-                    R.id.button_use_credential,
-                    R.id.button_confirm,
-                    R.id.button_try_again
-            };
-            for (final int id: ids) {
-                pw.println("        " + biometricView.findViewById(id));
-            }
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index fd9cee0..57f1928 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -37,7 +37,6 @@
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.IBiometricContextListener;
@@ -71,14 +70,18 @@
 import com.android.settingslib.udfps.UdfpsOverlayParams;
 import com.android.settingslib.udfps.UdfpsUtils;
 import com.android.systemui.CoreStartable;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
 import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.data.repository.BiometricType;
 import com.android.systemui.statusbar.CommandQueue;
@@ -86,8 +89,6 @@
 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,6 +102,9 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
+import kotlin.Unit;
+import kotlinx.coroutines.CoroutineScope;
+
 /**
  * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
  * appropriate biometric UI (e.g. BiometricDialogView).
@@ -109,7 +113,7 @@
  * {@link com.android.keyguard.KeyguardUpdateMonitor}
  */
 @SysUISingleton
-public class AuthController implements CoreStartable,  CommandQueue.Callbacks,
+public class AuthController implements CoreStartable, CommandQueue.Callbacks,
         AuthDialogCallback, DozeReceiver {
 
     private static final String TAG = "AuthController";
@@ -118,6 +122,7 @@
 
     private final Handler mHandler;
     private final Context mContext;
+    private final FeatureFlags mFeatureFlags;
     private final Execution mExecution;
     private final CommandQueue mCommandQueue;
     private final ActivityTaskManager mActivityTaskManager;
@@ -125,13 +130,15 @@
     @Nullable private final FaceManager mFaceManager;
     private final Provider<UdfpsController> mUdfpsControllerFactory;
     private final Provider<SideFpsController> mSidefpsControllerFactory;
+    private final CoroutineScope mApplicationCoroutineScope;
 
     // TODO: these should be migrated out once ready
-    @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
-
     @NonNull private final Provider<AuthBiometricFingerprintViewModel>
             mAuthBiometricFingerprintViewModelProvider;
+    @NonNull private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
+    @NonNull private final Provider<PromptSelectorInteractor> mPromptSelectorInteractor;
     @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+    @NonNull private final Provider<PromptViewModel> mPromptViewModelProvider;
     @NonNull private final LogContextInteractor mLogContextInteractor;
 
     private final Display mDisplay;
@@ -461,7 +468,7 @@
     }
 
     @Override
-    public void onDialogAnimatedIn(long requestId) {
+    public void onDialogAnimatedIn(long requestId, boolean startFingerprintNow) {
         final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId);
         if (receiver == null) {
             Log.w(TAG, "Skip onDialogAnimatedIn");
@@ -469,7 +476,22 @@
         }
 
         try {
-            receiver.onDialogAnimatedIn();
+            receiver.onDialogAnimatedIn(startFingerprintNow);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
+        }
+    }
+
+    @Override
+    public void onStartFingerprintNow(long requestId) {
+        final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId);
+        if (receiver == null) {
+            Log.e(TAG, "onStartUdfpsNow: Receiver is null");
+            return;
+        }
+
+        try {
+            receiver.onStartFingerprintNow();
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
         }
@@ -728,6 +750,8 @@
     }
     @Inject
     public AuthController(Context context,
+            @NonNull FeatureFlags featureFlags,
+            @Application CoroutineScope applicationCoroutineScope,
             Execution execution,
             CommandQueue commandQueue,
             ActivityTaskManager activityTaskManager,
@@ -743,16 +767,19 @@
             @NonNull LockPatternUtils lockPatternUtils,
             @NonNull UdfpsLogger udfpsLogger,
             @NonNull LogContextInteractor logContextInteractor,
-            @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
             @NonNull Provider<AuthBiometricFingerprintViewModel>
                     authBiometricFingerprintViewModelProvider,
+            @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractorProvider,
+            @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
+            @NonNull Provider<PromptViewModel> promptViewModelProvider,
             @NonNull InteractionJankMonitor jankMonitor,
             @Main Handler handler,
             @Background DelayableExecutor bgExecutor,
             @NonNull VibratorHelper vibrator,
             @NonNull UdfpsUtils udfpsUtils) {
         mContext = context;
+        mFeatureFlags = featureFlags;
         mExecution = execution;
         mUserManager = userManager;
         mLockPatternUtils = lockPatternUtils;
@@ -773,10 +800,13 @@
         mFaceEnrolledForUser = new SparseBooleanArray();
         mVibratorHelper = vibrator;
         mUdfpsUtils = udfpsUtils;
+        mApplicationCoroutineScope = applicationCoroutineScope;
 
         mLogContextInteractor = logContextInteractor;
-        mBiometricPromptInteractor = biometricPromptInteractor;
         mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
+        mPromptSelectorInteractor = promptSelectorInteractorProvider;
+        mPromptCredentialInteractor = promptCredentialInteractorProvider;
+        mPromptViewModelProvider = promptViewModelProvider;
         mCredentialViewModelProvider = credentialViewModelProvider;
 
         mOrientationListener = new BiometricDisplayListener(
@@ -913,8 +943,7 @@
     @Override
     public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
             int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
-            int userId, long operationId, String opPackageName, long requestId,
-            @BiometricMultiSensorMode int multiSensorConfig) {
+            int userId, long operationId, String opPackageName, long requestId) {
         @Authenticators.Types final int authenticators = promptInfo.getAuthenticators();
 
         if (DEBUG) {
@@ -927,8 +956,7 @@
                     + ", credentialAllowed: " + credentialAllowed
                     + ", requireConfirmation: " + requireConfirmation
                     + ", operationId: " + operationId
-                    + ", requestId: " + requestId
-                    + ", multiSensorConfig: " + multiSensorConfig);
+                    + ", requestId: " + requestId);
         }
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = promptInfo;
@@ -940,7 +968,6 @@
         args.arg6 = opPackageName;
         args.argl1 = operationId;
         args.argl2 = requestId;
-        args.argi2 = multiSensorConfig;
 
         boolean skipAnimation = false;
         if (mCurrentDialog != null) {
@@ -948,7 +975,7 @@
             skipAnimation = true;
         }
 
-        showDialog(args, skipAnimation, null /* savedState */);
+        showDialog(args, skipAnimation, null /* savedState */, mPromptViewModelProvider.get());
     }
 
     /**
@@ -1171,7 +1198,8 @@
         return mFpEnrolledForUser.getOrDefault(userId, false);
     }
 
-    private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
+    private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState,
+            @Nullable PromptViewModel viewModel) {
         mCurrentDialogArgs = args;
 
         final PromptInfo promptInfo = (PromptInfo) args.arg1;
@@ -1182,7 +1210,6 @@
         final String opPackageName = (String) args.arg6;
         final long operationId = args.argl1;
         final long requestId = args.argl2;
-        @BiometricMultiSensorMode final int multiSensorConfig = args.argi2;
 
         // Create a new dialog but do not replace the current one yet.
         final AuthDialog newDialog = buildDialog(
@@ -1195,11 +1222,11 @@
                 skipAnimation,
                 operationId,
                 requestId,
-                multiSensorConfig,
                 mWakefulnessLifecycle,
                 mPanelInteractionDetector,
                 mUserManager,
-                mLockPatternUtils);
+                mLockPatternUtils,
+                viewModel);
 
         if (newDialog == null) {
             Log.e(TAG, "Unsupported type configuration");
@@ -1253,6 +1280,7 @@
 
         // Save the state of the current dialog (buttons showing, etc)
         if (mCurrentDialog != null) {
+            final PromptViewModel viewModel = mCurrentDialog.getViewModel();
             final Bundle savedState = new Bundle();
             mCurrentDialog.onSaveState(savedState);
             mCurrentDialog.dismissWithoutCallback(false /* animate */);
@@ -1271,7 +1299,7 @@
                     promptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL);
                 }
 
-                showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
+                showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState, viewModel);
             }
         }
     }
@@ -1286,26 +1314,28 @@
     protected AuthDialog buildDialog(@Background DelayableExecutor bgExecutor,
             PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds,
             String opPackageName, boolean skipIntro, long operationId, long requestId,
-            @BiometricMultiSensorMode int multiSensorConfig,
             @NonNull WakefulnessLifecycle wakefulnessLifecycle,
             @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
             @NonNull UserManager userManager,
-            @NonNull LockPatternUtils lockPatternUtils) {
-        return new AuthContainerView.Builder(mContext)
-                .setCallback(this)
-                .setPromptInfo(promptInfo)
-                .setRequireConfirmation(requireConfirmation)
-                .setUserId(userId)
-                .setOpPackageName(opPackageName)
-                .setSkipIntro(skipIntro)
-                .setOperationId(operationId)
-                .setRequestId(requestId)
-                .setMultiSensorConfig(multiSensorConfig)
-                .setScaleFactorProvider(() -> getScaleFactor())
-                .build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle,
-                        panelInteractionDetector, userManager, lockPatternUtils,
-                        mInteractionJankMonitor, mBiometricPromptInteractor,
-                        mAuthBiometricFingerprintViewModelProvider, mCredentialViewModelProvider);
+            @NonNull LockPatternUtils lockPatternUtils,
+            @NonNull PromptViewModel viewModel) {
+        final AuthContainerView.Config config = new AuthContainerView.Config();
+        config.mContext = mContext;
+        config.mCallback = this;
+        config.mPromptInfo = promptInfo;
+        config.mRequireConfirmation = requireConfirmation;
+        config.mUserId = userId;
+        config.mOpPackageName = opPackageName;
+        config.mSkipIntro = skipIntro;
+        config.mOperationId = operationId;
+        config.mRequestId = requestId;
+        config.mSensorIds = sensorIds;
+        config.mScaleProvider = this::getScaleFactor;
+        return new AuthContainerView(config, mFeatureFlags, mApplicationCoroutineScope, mFpProps, mFaceProps,
+                wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
+                mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider,
+                mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel,
+                mCredentialViewModelProvider, bgExecutor);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index 51f39b3..b6eabfa 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -24,13 +24,17 @@
 import android.view.WindowManager;
 
 import com.android.systemui.Dumpable;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
  * Interface for the biometric dialog UI.
+ *
+ * TODO(b/251476085): remove along with legacy controller once flag is removed
  */
+@Deprecated
 public interface AuthDialog extends Dumpable {
 
     String KEY_CONTAINER_GOING_AWAY = "container_going_away";
@@ -70,10 +74,10 @@
      * {@link AuthPanelController}.
      */
     class LayoutParams {
-        final int mMediumHeight;
-        final int mMediumWidth;
+        public final int mMediumHeight;
+        public final int mMediumWidth;
 
-        LayoutParams(int mediumWidth, int mediumHeight) {
+        public LayoutParams(int mediumWidth, int mediumHeight) {
             mMediumWidth = mediumWidth;
             mMediumHeight = mediumHeight;
         }
@@ -172,4 +176,6 @@
      * must remain fixed on the physical sensor location.
      */
     void onOrientationChanged();
+
+    PromptViewModel getViewModel();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index bbe461a..9a21940 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
@@ -70,5 +70,10 @@
     /**
      * Notifies when the dialog has finished animating.
      */
-    void onDialogAnimatedIn(long requestId);
+    void onDialogAnimatedIn(long requestId, boolean startFingerprintNow);
+
+    /**
+     * Notifies that the fingerprint sensor should be started now.
+     */
+    void onStartFingerprintNow(long requestId);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
index f5f4640..f56bb88 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
@@ -84,9 +84,6 @@
         }
     }
 
-    /** If the icon should act as a "retry" button in the [currentState]. */
-    fun iconTapSendsRetryWhen(@BiometricState currentState: Int): Boolean = false
-
     /** Call during [updateState] if the controller is not [deactivated]. */
     abstract fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int)
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index ad10071..acdde34 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -114,16 +114,7 @@
     }
 
     private int getTopBound(@Position int position) {
-        switch (position) {
-            case POSITION_BOTTOM:
-                return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin);
-            case POSITION_LEFT:
-            case POSITION_RIGHT:
-                return Math.max((mContainerHeight - mContentHeight) / 2, mMargin);
-            default:
-                Log.e(TAG, "Unrecognized position: " + position);
-                return getTopBound(POSITION_BOTTOM);
-        }
+        return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin);
     }
 
     public void setContainerDimensions(int containerWidth, int containerHeight) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
index 43745bf..16dc42a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
@@ -63,7 +63,7 @@
     }
 
     @NonNull
-    AuthDialog.LayoutParams onMeasureInternal(
+    public AuthDialog.LayoutParams onMeasureInternal(
             int width, int height, @NonNull AuthDialog.LayoutParams layoutParams,
             float scaleFactor) {
 
@@ -86,7 +86,7 @@
      * too cleanly support this case. So, let's have the onLayout code translate the sensor location
      * instead.
      */
-    int getBottomSpacerHeight() {
+    public int getBottomSpacerHeight() {
         return mBottomSpacerHeight;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index 096d941..ddf1457 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -31,6 +31,8 @@
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl
 import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
 import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.util.concurrency.ThreadFactory
 import dagger.Binds
@@ -57,6 +59,11 @@
 
     @Binds
     @SysUISingleton
+    fun providesPromptSelectorInteractor(impl: PromptSelectorInteractorImpl):
+            PromptSelectorInteractor
+
+    @Binds
+    @SysUISingleton
     fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
index 92a13cf..b4dc272 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
@@ -2,7 +2,7 @@
 
 import android.hardware.biometrics.PromptInfo
 import com.android.systemui.biometrics.AuthController
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -35,12 +35,20 @@
     /** The kind of credential to use (biometric, pin, pattern, etc.). */
     val kind: StateFlow<PromptKind>
 
+    /**
+     * If explicit confirmation is required.
+     *
+     * Note: overlaps/conflicts with [PromptInfo.isConfirmationRequested], which needs clean up.
+     */
+    val isConfirmationRequired: StateFlow<Boolean>
+
     /** Update the prompt configuration, which should be set before [isShowing]. */
     fun setPrompt(
         promptInfo: PromptInfo,
         userId: Int,
         gatekeeperChallenge: Long?,
-        kind: PromptKind = PromptKind.ANY_BIOMETRIC,
+        kind: PromptKind,
+        requireConfirmation: Boolean = false,
     )
 
     /** Unset the prompt info. */
@@ -74,29 +82,35 @@
     private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null)
     override val userId = _userId.asStateFlow()
 
-    private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+    private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric())
     override val kind = _kind.asStateFlow()
 
+    private val _isConfirmationRequired: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isConfirmationRequired = _isConfirmationRequired.asStateFlow()
+
     override fun setPrompt(
         promptInfo: PromptInfo,
         userId: Int,
         gatekeeperChallenge: Long?,
         kind: PromptKind,
+        requireConfirmation: Boolean,
     ) {
         _kind.value = kind
         _userId.value = userId
         _challenge.value = gatekeeperChallenge
         _promptInfo.value = promptInfo
+        _isConfirmationRequired.value = requireConfirmation
     }
 
     override fun unsetPrompt() {
         _promptInfo.value = null
         _userId.value = null
         _challenge.value = null
-        _kind.value = PromptKind.ANY_BIOMETRIC
+        _kind.value = PromptKind.Biometric()
+        _isConfirmationRequired.value = false
     }
 
     companion object {
-        private const val TAG = "BiometricPromptRepository"
+        private const val TAG = "PromptRepositoryImpl"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
index 6362c2f..d92c217 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
@@ -1,14 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.biometrics.domain.interactor
 
 import android.hardware.biometrics.PromptInfo
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
 import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.data.model.PromptKind
 import com.android.systemui.biometrics.data.repository.PromptRepository
 import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
 import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
 import com.android.systemui.dagger.qualifiers.Background
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -24,8 +40,16 @@
 /**
  * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users
  * PIN, pattern, or password credential instead of a biometric.
+ *
+ * This is used to cache the calling app's options that were given to the underlying authenticate
+ * APIs and should be set before any UI is shown to the user.
+ *
+ * There can be at most one request active at a given time. Use [resetPrompt] when no request is
+ * active to clear the cache.
+ *
+ * Views that use any biometric should use [PromptSelectorInteractor] instead.
  */
-class BiometricPromptCredentialInteractor
+class PromptCredentialInteractor
 @Inject
 constructor(
     @Background private val bgDispatcher: CoroutineDispatcher,
@@ -36,7 +60,7 @@
     val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing
 
     /** Metadata about the current credential prompt, including app-supplied preferences. */
-    val prompt: Flow<BiometricPromptRequest?> =
+    val prompt: Flow<BiometricPromptRequest.Credential?> =
         combine(
                 biometricPromptRepository.promptInfo,
                 biometricPromptRepository.challenge,
@@ -48,20 +72,20 @@
                 }
 
                 when (kind) {
-                    PromptKind.PIN ->
+                    PromptKind.Pin ->
                         BiometricPromptRequest.Credential.Pin(
                             info = promptInfo,
                             userInfo = userInfo(userId),
                             operationInfo = operationInfo(challenge)
                         )
-                    PromptKind.PATTERN ->
+                    PromptKind.Pattern ->
                         BiometricPromptRequest.Credential.Pattern(
                             info = promptInfo,
                             userInfo = userInfo(userId),
                             operationInfo = operationInfo(challenge),
                             stealthMode = credentialInteractor.isStealthModeActive(userId)
                         )
-                    PromptKind.PASSWORD ->
+                    PromptKind.Password ->
                         BiometricPromptRequest.Credential.Password(
                             info = promptInfo,
                             userInfo = userInfo(userId),
@@ -182,8 +206,8 @@
 /** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
 private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
     when (this) {
-        Utils.CREDENTIAL_PIN -> PromptKind.PIN
-        Utils.CREDENTIAL_PASSWORD -> PromptKind.PASSWORD
-        Utils.CREDENTIAL_PATTERN -> PromptKind.PATTERN
-        else -> PromptKind.ANY_BIOMETRIC
+        Utils.CREDENTIAL_PIN -> PromptKind.Pin
+        Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+        Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+        else -> PromptKind.Biometric()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
new file mode 100644
index 0000000..e6e07f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.hardware.biometrics.PromptInfo
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.Utils.getCredentialType
+import com.android.systemui.biometrics.Utils.isDeviceCredentialAllowed
+import com.android.systemui.biometrics.data.repository.PromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/**
+ * Business logic for BiometricPrompt's biometric view variants (face, fingerprint, coex, etc.).
+ *
+ * This is used to cache the calling app's options that were given to the underlying authenticate
+ * APIs and should be set before any UI is shown to the user.
+ *
+ * There can be at most one request active at a given time. Use [resetPrompt] when no request is
+ * active to clear the cache.
+ *
+ * Views that use credential fallback should use [PromptCredentialInteractor] instead.
+ */
+interface PromptSelectorInteractor {
+
+    /** Static metadata about the current prompt. */
+    val prompt: Flow<BiometricPromptRequest.Biometric?>
+
+    /** If using a credential is allowed. */
+    val isCredentialAllowed: Flow<Boolean>
+
+    /**
+     * The kind of credential the user may use as a fallback or [PromptKind.Biometric] if unknown or
+     * not [isCredentialAllowed].
+     */
+    val credentialKind: Flow<PromptKind>
+
+    /** If the API caller requested explicit confirmation after successful authentication. */
+    val isConfirmationRequested: Flow<Boolean>
+
+    /** Use biometrics for authentication. */
+    fun useBiometricsForAuthentication(
+        promptInfo: PromptInfo,
+        requireConfirmation: Boolean,
+        userId: Int,
+        challenge: Long,
+        modalities: BiometricModalities,
+    )
+
+    /** Use credential-based authentication instead of biometrics. */
+    fun useCredentialsForAuthentication(
+        promptInfo: PromptInfo,
+        @Utils.CredentialType kind: Int,
+        userId: Int,
+        challenge: Long,
+    )
+
+    /** Unset the current authentication request. */
+    fun resetPrompt()
+}
+
+@SysUISingleton
+class PromptSelectorInteractorImpl
+@Inject
+constructor(
+    private val promptRepository: PromptRepository,
+    lockPatternUtils: LockPatternUtils,
+) : PromptSelectorInteractor {
+
+    override val prompt: Flow<BiometricPromptRequest.Biometric?> =
+        combine(
+            promptRepository.promptInfo,
+            promptRepository.challenge,
+            promptRepository.userId,
+            promptRepository.kind
+        ) { promptInfo, challenge, userId, kind ->
+            if (promptInfo == null || userId == null || challenge == null) {
+                return@combine null
+            }
+
+            when (kind) {
+                is PromptKind.Biometric ->
+                    BiometricPromptRequest.Biometric(
+                        info = promptInfo,
+                        userInfo = BiometricUserInfo(userId = userId),
+                        operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge),
+                        modalities = kind.activeModalities,
+                    )
+                else -> null
+            }
+        }
+
+    override val isConfirmationRequested: Flow<Boolean> =
+        promptRepository.promptInfo
+            .map { info -> info?.isConfirmationRequested ?: false }
+            .distinctUntilChanged()
+
+    override val isCredentialAllowed: Flow<Boolean> =
+        promptRepository.promptInfo
+            .map { info -> if (info != null) isDeviceCredentialAllowed(info) else false }
+            .distinctUntilChanged()
+
+    override val credentialKind: Flow<PromptKind> =
+        combine(prompt, isCredentialAllowed) { prompt, isAllowed ->
+            if (prompt != null && isAllowed) {
+                when (
+                    getCredentialType(lockPatternUtils, prompt.userInfo.deviceCredentialOwnerId)
+                ) {
+                    Utils.CREDENTIAL_PIN -> PromptKind.Pin
+                    Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+                    Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+                    else -> PromptKind.Biometric()
+                }
+            } else {
+                PromptKind.Biometric()
+            }
+        }
+
+    override fun useBiometricsForAuthentication(
+        promptInfo: PromptInfo,
+        requireConfirmation: Boolean,
+        userId: Int,
+        challenge: Long,
+        modalities: BiometricModalities
+    ) {
+        promptRepository.setPrompt(
+            promptInfo = promptInfo,
+            userId = userId,
+            gatekeeperChallenge = challenge,
+            kind = PromptKind.Biometric(modalities),
+            requireConfirmation = requireConfirmation,
+        )
+    }
+
+    override fun useCredentialsForAuthentication(
+        promptInfo: PromptInfo,
+        @Utils.CredentialType kind: Int,
+        userId: Int,
+        challenge: Long,
+    ) {
+        promptRepository.setPrompt(
+            promptInfo = promptInfo,
+            userId = userId,
+            gatekeeperChallenge = challenge,
+            kind = kind.asBiometricPromptCredential(),
+        )
+    }
+
+    override fun resetPrompt() {
+        promptRepository.unsetPrompt()
+    }
+}
+
+// TODO(b/251476085): remove along with Utils.CredentialType
+/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
+private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
+    when (this) {
+        Utils.CREDENTIAL_PIN -> PromptKind.Pin
+        Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+        Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+        else -> PromptKind.Biometric()
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt
new file mode 100644
index 0000000..274f58a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.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.biometrics.domain.model
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+
+/** The available modalities for an operation. */
+data class BiometricModalities(
+    val fingerprintProperties: FingerprintSensorPropertiesInternal? = null,
+    val faceProperties: FaceSensorPropertiesInternal? = null,
+) {
+    /** If there are no available modalities. */
+    val isEmpty: Boolean
+        get() = !hasFingerprint && !hasFace
+
+    /** If fingerprint authentication is available (and [fingerprintProperties] is non-null). */
+    val hasFingerprint: Boolean
+        get() = fingerprintProperties != null
+
+    /** If fingerprint authentication is available (and [faceProperties] is non-null). */
+    val hasFace: Boolean
+        get() = faceProperties != null
+
+    /** If only face authentication is enabled. */
+    val hasFaceOnly: Boolean
+        get() = hasFace && !hasFingerprint
+
+    /** If only fingerprint authentication is enabled. */
+    val hasFingerprintOnly: Boolean
+        get() = hasFingerprint && !hasFace
+
+    /** If face & fingerprint authentication is enabled (coex). */
+    val hasFaceAndFingerprint: Boolean
+        get() = hasFingerprint && hasFace
+
+    /** If [hasFace] and it is configured as a STRONG class 3 biometric. */
+    val isFaceStrong: Boolean
+        get() = faceProperties?.sensorStrength == SensorProperties.STRENGTH_STRONG
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt
new file mode 100644
index 0000000..3197c09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.model
+
+import android.hardware.biometrics.BiometricAuthenticator
+
+/** Shadows [BiometricAuthenticator.Modality] for Kotlin use within SysUI. */
+enum class BiometricModality {
+    None,
+    Fingerprint,
+    Face,
+}
+
+/** Convert a framework [BiometricAuthenticator.Modality] to a SysUI [BiometricModality]. */
+@BiometricAuthenticator.Modality
+fun Int.asBiometricModality(): BiometricModality =
+    when (this) {
+        BiometricAuthenticator.TYPE_FINGERPRINT -> BiometricModality.Fingerprint
+        BiometricAuthenticator.TYPE_FACE -> BiometricModality.Face
+        else -> BiometricModality.None
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index 5ee0381..75de47d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -21,6 +21,7 @@
         info: PromptInfo,
         userInfo: BiometricUserInfo,
         operationInfo: BiometricOperationInfo,
+        val modalities: BiometricModalities,
     ) :
         BiometricPromptRequest(
             title = info.title?.toString() ?: "",
@@ -28,7 +29,9 @@
             description = info.description?.toString() ?: "",
             userInfo = userInfo,
             operationInfo = operationInfo
-        )
+        ) {
+        val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
+    }
 
     /** Prompt using a credential (pin, pattern, password). */
     sealed class Credential(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
index e82646f..416fc64 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,15 +14,19 @@
  * limitations under the License.
  */
 
-package com.android.systemui.biometrics.data.model
+package com.android.systemui.biometrics.shared.model
 
 import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.model.BiometricModalities
 
 // TODO(b/251476085): this should eventually replace Utils.CredentialType
 /** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */
-enum class PromptKind {
-    ANY_BIOMETRIC,
-    PIN,
-    PATTERN,
-    PASSWORD,
+sealed interface PromptKind {
+    data class Biometric(
+        val activeModalities: BiometricModalities = BiometricModalities(),
+    ) : PromptKind
+
+    object Pin : PromptKind
+    object Pattern : PromptKind
+    object Password : PromptKind
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
new file mode 100644
index 0000000..fb246cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Insets;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.biometrics.AuthBiometricFingerprintIconController;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthDialog;
+import com.android.systemui.biometrics.UdfpsDialogMeasureAdapter;
+
+import kotlin.Pair;
+
+/**
+ * Contains the Biometric views (title, subtitle, icon, buttons, etc.).
+ *
+ * TODO(b/251476085): get the udfps junk out of here, at a minimum. Likely can be replaced with a
+ * normal LinearLayout.
+ */
+public class BiometricPromptLayout extends LinearLayout {
+
+    private static final String TAG = "BiometricPromptLayout";
+
+    @NonNull
+    private final WindowManager mWindowManager;
+    @Nullable
+    private AuthController.ScaleFactorProvider mScaleFactorProvider;
+    @Nullable
+    private UdfpsDialogMeasureAdapter mUdfpsAdapter;
+
+    private final boolean mUseCustomBpSize;
+    private final int mCustomBpWidth;
+    private final int mCustomBpHeight;
+
+    public BiometricPromptLayout(Context context) {
+        this(context, null);
+    }
+
+    public BiometricPromptLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mWindowManager = context.getSystemService(WindowManager.class);
+
+        mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size);
+        mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width);
+        mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height);
+    }
+
+    @Deprecated
+    public void setUdfpsAdapter(@NonNull UdfpsDialogMeasureAdapter adapter,
+            @NonNull AuthController.ScaleFactorProvider scaleProvider) {
+        mUdfpsAdapter = adapter;
+        mScaleFactorProvider = scaleProvider != null ? scaleProvider : () -> 1.0f;
+    }
+
+    @Deprecated
+    public boolean isUdfps() {
+        return mUdfpsAdapter != null;
+    }
+
+    @Deprecated
+    public void updateFingerprintAffordanceSize(
+            @NonNull AuthBiometricFingerprintIconController iconController) {
+        if (mUdfpsAdapter != null) {
+            final int sensorDiameter = mUdfpsAdapter.getSensorDiameter(
+                    mScaleFactorProvider.provide());
+            iconController.setIconLayoutParamSize(new Pair(sensorDiameter, sensorDiameter));
+        }
+    }
+
+    @NonNull
+    private AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
+        int totalHeight = 0;
+        final int numChildren = getChildCount();
+        for (int i = 0; i < numChildren; i++) {
+            final View child = getChildAt(i);
+
+            if (child.getId() == R.id.space_above_icon
+                    || child.getId() == R.id.space_below_icon
+                    || child.getId() == R.id.button_bar) {
+                child.measure(
+                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
+                                MeasureSpec.EXACTLY));
+            } else if (child.getId() == R.id.biometric_icon_frame) {
+                final View iconView = findViewById(R.id.biometric_icon);
+                child.measure(
+                        MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width,
+                                MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height,
+                                MeasureSpec.EXACTLY));
+            } else if (child.getId() == R.id.biometric_icon) {
+                child.measure(
+                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+            } else {
+                child.measure(
+                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+            }
+
+            if (child.getVisibility() != View.GONE) {
+                totalHeight += child.getMeasuredHeight();
+            }
+        }
+
+        final AuthDialog.LayoutParams params = new AuthDialog.LayoutParams(width, totalHeight);
+        if (mUdfpsAdapter != null) {
+            return mUdfpsAdapter.onMeasureInternal(width, height, params,
+                    (mScaleFactorProvider != null) ? mScaleFactorProvider.provide() : 1.0f);
+        } else {
+            return params;
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        if (mUseCustomBpSize) {
+            width = mCustomBpWidth;
+            height = mCustomBpHeight;
+        } else {
+            width = Math.min(width, height);
+        }
+
+        // add nav bar insets since the parent AuthContainerView
+        // uses LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+        final Insets insets = mWindowManager.getMaximumWindowMetrics().getWindowInsets()
+                .getInsets(WindowInsets.Type.navigationBars());
+        final AuthDialog.LayoutParams params = onMeasureInternal(width, height);
+        setMeasuredDimension(params.mMediumWidth + insets.left + insets.right,
+                params.mMediumHeight + insets.bottom);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        if (mUdfpsAdapter != null) {
+            // Move the UDFPS icon and indicator text if necessary. This probably only needs to
+            // happen for devices where the UDFPS sensor is too low.
+            // TODO(b/201510778): Update this logic to support cases where the sensor or text
+            // overlap the button bar area.
+            final float bottomSpacerHeight = mUdfpsAdapter.getBottomSpacerHeight();
+            Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight);
+            if (bottomSpacerHeight < 0) {
+                final FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame);
+                iconFrame.setTranslationY(-bottomSpacerHeight);
+                final TextView indicator = findViewById(R.id.indicator);
+                indicator.setTranslationY(-bottomSpacerHeight);
+            }
+        }
+    }
+}
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 ede62ac..a3f34ce 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
@@ -68,15 +68,15 @@
         var inputTopBound: Int
         var headerRightBound = right
         var headerTopBounds = top
+        var headerBottomBounds = bottom
         val subTitleBottom: Int = if (subtitleView.isGone) titleView.bottom else subtitleView.bottom
         val descBottom = if (descriptionView.isGone) subTitleBottom else descriptionView.bottom
         if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
             inputTopBound = (bottom - credentialInput.height) / 2
             inputLeftBound = (right - left) / 2
             headerRightBound = inputLeftBound
-            headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset)
-
-            if (descriptionView.bottom > bottomInset) {
+            if (descriptionView.bottom > headerBottomBounds) {
+                headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset)
                 credentialHeader.layout(left, headerTopBounds, headerRightBound, bottom)
             }
         } else {
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
new file mode 100644
index 0000000..8486c3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -0,0 +1,622 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.binder
+
+import android.animation.Animator
+import android.content.Context
+import android.hardware.biometrics.BiometricAuthenticator
+import android.hardware.biometrics.BiometricConstants
+import android.hardware.biometrics.BiometricPrompt
+import android.hardware.face.FaceManager
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.util.Log
+import android.view.View
+import android.view.accessibility.AccessibilityManager
+import android.widget.Button
+import android.widget.TextView
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.airbnb.lottie.LottieAnimationView
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthBiometricFaceIconController
+import com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceIconController
+import com.android.systemui.biometrics.AuthBiometricFingerprintIconController
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.AuthBiometricView.Callback
+import com.android.systemui.biometrics.AuthBiometricViewAdapter
+import com.android.systemui.biometrics.AuthIconController
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.domain.model.asBiometricModality
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
+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.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+private const val TAG = "BiometricViewBinder"
+
+/** Top-most view binder for BiometricPrompt views. */
+object BiometricViewBinder {
+
+    /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */
+    @JvmStatic
+    fun bind(
+        view: BiometricPromptLayout,
+        viewModel: PromptViewModel,
+        panelViewController: AuthPanelController,
+        jankListener: BiometricJankListener,
+        backgroundView: View,
+        legacyCallback: Callback,
+        applicationScope: CoroutineScope,
+    ): AuthBiometricViewAdapter {
+        val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
+        fun notifyAccessibilityChanged() {
+            Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+        }
+
+        val textColorError =
+            view.resources.getColor(R.color.biometric_dialog_error, view.context.theme)
+        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)
+
+        // set selected for marquee
+        titleView.isSelected = true
+        subtitleView.isSelected = true
+        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)
+
+        // 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)
+
+        // Positive-side (right) buttons
+        val confirmationButton = view.findViewById<Button>(R.id.button_confirm)
+        val retryButton = view.findViewById<Button>(R.id.button_try_again)
+
+        // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers
+        val adapter =
+            Spaghetti(
+                view = view,
+                viewModel = viewModel,
+                applicationContext = view.context.applicationContext,
+                applicationScope = applicationScope,
+            )
+
+        // bind to prompt
+        var boundSize = false
+        view.repeatWhenAttached {
+            // these do not change and need to be set before any size transitions
+            val modalities = viewModel.modalities.first()
+            titleView.text = viewModel.title.first()
+            descriptionView.text = viewModel.description.first()
+            subtitleView.text = viewModel.subtitle.first()
+
+            // set button listeners
+            negativeButton.setOnClickListener {
+                legacyCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE)
+            }
+            cancelButton.setOnClickListener {
+                legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
+            }
+            credentialFallbackButton.setOnClickListener {
+                viewModel.onSwitchToCredential()
+                legacyCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
+            }
+            confirmationButton.setOnClickListener { viewModel.confirmAuthenticated() }
+            retryButton.setOnClickListener {
+                viewModel.showAuthenticating(isRetry = true)
+                legacyCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN)
+            }
+
+            // TODO(b/251476085): migrate legacy icon controllers and remove
+            var legacyState: Int = viewModel.legacyState.value
+            val iconController =
+                modalities.asIconController(
+                    view.context,
+                    iconView,
+                    iconViewOverlay,
+                )
+            adapter.attach(this, iconController, modalities, legacyCallback)
+            if (iconController is AuthBiometricFingerprintIconController) {
+                view.updateFingerprintAffordanceSize(iconController)
+            }
+            if (iconController is HackyCoexIconController) {
+                iconController.faceMode = !viewModel.isConfirmationRequested.first()
+            }
+
+            // the icon controller must be created before this happens for the legacy
+            // sizing code in BiometricPromptLayout to work correctly. Simplify this
+            // when those are also migrated. (otherwise the icon size may not be set to
+            // a pixel value before the view is measured and WRAP_CONTENT will be incorrectly
+            // used as part of the measure spec)
+            if (!boundSize) {
+                boundSize = true
+                BiometricViewSizeBinder.bind(
+                    view = view,
+                    viewModel = viewModel,
+                    viewsToHideWhenSmall =
+                        listOf(
+                            titleView,
+                            subtitleView,
+                            descriptionView,
+                        ),
+                    viewsToFadeInOnSizeChange =
+                        listOf(
+                            titleView,
+                            subtitleView,
+                            descriptionView,
+                            indicatorMessageView,
+                            negativeButton,
+                            cancelButton,
+                            retryButton,
+                            confirmationButton,
+                            credentialFallbackButton,
+                        ),
+                    panelViewController = panelViewController,
+                    jankListener = jankListener,
+                )
+            }
+
+            // TODO(b/251476085): migrate legacy icon controllers and remove
+            // The fingerprint sensor is started by the legacy
+            // AuthContainerView#onDialogAnimatedIn in all cases but the implicit coex flow
+            // (delayed mode). In that case, start it on the first transition to delayed
+            // which will be triggered by any auth failure.
+            lifecycleScope.launch {
+                val oldMode = viewModel.fingerprintStartMode.first()
+                viewModel.fingerprintStartMode.collect { newMode ->
+                    // trigger sensor to start
+                    if (
+                        oldMode == FingerprintStartMode.Pending &&
+                            newMode == FingerprintStartMode.Delayed
+                    ) {
+                        legacyCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)
+                    }
+
+                    if (newMode.isStarted) {
+                        // do wonky switch from implicit to explicit flow
+                        (iconController as? HackyCoexIconController)?.faceMode = false
+                        viewModel.showAuthenticating(
+                            modalities.asDefaultHelpMessage(view.context),
+                        )
+                    }
+                }
+            }
+
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // handle background clicks
+                launch {
+                    combine(viewModel.isAuthenticated, viewModel.size) { (authenticated, _), size ->
+                            when {
+                                authenticated -> false
+                                size == PromptSize.SMALL -> false
+                                size == PromptSize.LARGE -> false
+                                else -> true
+                            }
+                        }
+                        .collect { dismissOnClick ->
+                            backgroundView.setOnClickListener {
+                                if (dismissOnClick) {
+                                    legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
+                                } else {
+                                    Log.w(TAG, "Ignoring background click")
+                                }
+                            }
+                        }
+                }
+
+                // set messages
+                launch {
+                    viewModel.isIndicatorMessageVisible.collect { show ->
+                        indicatorMessageView.visibility = show.asVisibleOrHidden()
+                    }
+                }
+
+                // configure & hide/disable buttons
+                launch {
+                    viewModel.credentialKind
+                        .map { kind ->
+                            when (kind) {
+                                PromptKind.Pin ->
+                                    view.resources.getString(R.string.biometric_dialog_use_pin)
+                                PromptKind.Password ->
+                                    view.resources.getString(R.string.biometric_dialog_use_password)
+                                PromptKind.Pattern ->
+                                    view.resources.getString(R.string.biometric_dialog_use_pattern)
+                                else -> ""
+                            }
+                        }
+                        .collect { credentialFallbackButton.text = it }
+                }
+                launch { viewModel.negativeButtonText.collect { negativeButton.text = it } }
+                launch {
+                    viewModel.isConfirmButtonVisible.collect { show ->
+                        confirmationButton.visibility = show.asVisibleOrGone()
+                    }
+                }
+                launch {
+                    viewModel.isCancelButtonVisible.collect { show ->
+                        cancelButton.visibility = show.asVisibleOrGone()
+                    }
+                }
+                launch {
+                    viewModel.isNegativeButtonVisible.collect { show ->
+                        negativeButton.visibility = show.asVisibleOrGone()
+                    }
+                }
+                launch {
+                    viewModel.isTryAgainButtonVisible.collect { show ->
+                        retryButton.visibility = show.asVisibleOrGone()
+                    }
+                }
+                launch {
+                    viewModel.isCredentialButtonVisible.collect { show ->
+                        credentialFallbackButton.visibility = show.asVisibleOrGone()
+                    }
+                }
+
+                // reuse the icon as a confirm button
+                launch {
+                    viewModel.isConfirmButtonVisible
+                        .map { isPending ->
+                            when {
+                                isPending && iconController.actsAsConfirmButton ->
+                                    View.OnClickListener { viewModel.confirmAuthenticated() }
+                                else -> null
+                            }
+                        }
+                        .collect { onClick ->
+                            iconViewOverlay.setOnClickListener(onClick)
+                            iconView.setOnClickListener(onClick)
+                        }
+                }
+
+                // TODO(b/251476085): remove w/ legacy icon controllers
+                // set icon affordance using legacy states
+                // like the old code, this causes animations to repeat on config changes :(
+                // but keep behavior for now as no one has complained...
+                launch {
+                    viewModel.legacyState.collect { newState ->
+                        iconController.updateState(legacyState, newState)
+                        legacyState = newState
+                    }
+                }
+
+                // not sure why this is here, but the legacy code did it probably needed?
+                launch {
+                    viewModel.isAuthenticating.collect { isAuthenticating ->
+                        if (isAuthenticating) {
+                            notifyAccessibilityChanged()
+                        }
+                    }
+                }
+
+                // dismiss prompt when authenticated and confirmed
+                launch {
+                    viewModel.isAuthenticated.collect { authState ->
+                        if (authState.isAuthenticatedAndConfirmed) {
+                            view.announceForAccessibility(
+                                view.resources.getString(R.string.biometric_dialog_authenticated)
+                            )
+                            notifyAccessibilityChanged()
+
+                            launch {
+                                delay(authState.delay)
+                                legacyCallback.onAction(Callback.ACTION_AUTHENTICATED)
+                            }
+                        }
+                    }
+                }
+
+                // show error & help messages
+                launch {
+                    viewModel.message.collect { promptMessage ->
+                        val isError = promptMessage is PromptMessage.Error
+
+                        indicatorMessageView.text = promptMessage.message
+                        indicatorMessageView.setTextColor(
+                            if (isError) textColorError else textColorHint
+                        )
+
+                        // select to enable marquee unless a screen reader is enabled
+                        // TODO(wenhuiy): this may have recently changed per UX - verify and remove
+                        indicatorMessageView.isSelected =
+                            !accessibilityManager.isEnabled ||
+                                !accessibilityManager.isTouchExplorationEnabled
+
+                        notifyAccessibilityChanged()
+                    }
+                }
+            }
+        }
+
+        return adapter
+    }
+}
+
+/**
+ * Adapter for legacy events. Remove once legacy controller can be replaced by flagged code.
+ *
+ * These events can be dispatched when the view is being recreated so they need to be delivered to
+ * the view model (which will be retained) via the application scope.
+ *
+ * Do not reference the [view] for anything other than [asView].
+ *
+ * TODO(b/251476085): remove after replacing AuthContainerView
+ */
+private class Spaghetti(
+    private val view: View,
+    private val viewModel: PromptViewModel,
+    private val applicationContext: Context,
+    private val applicationScope: CoroutineScope,
+) : AuthBiometricViewAdapter {
+
+    private var lifecycleScope: CoroutineScope? = null
+    private var modalities: BiometricModalities = BiometricModalities()
+    private var faceFailedAtLeastOnce = false
+    private var legacyCallback: Callback? = null
+
+    override var legacyIconController: AuthIconController? = null
+        private set
+
+    // hacky way to suppress lockout errors
+    private val lockoutErrorStrings =
+        listOf(
+                BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
+                BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
+            )
+            .map { FaceManager.getErrorString(applicationContext, it, 0 /* vendorCode */) }
+
+    fun attach(
+        lifecycleOwner: LifecycleOwner,
+        iconController: AuthIconController,
+        activeModalities: BiometricModalities,
+        callback: Callback,
+    ) {
+        modalities = activeModalities
+        legacyIconController = iconController
+        legacyCallback = callback
+
+        lifecycleOwner.lifecycle.addObserver(
+            object : DefaultLifecycleObserver {
+                override fun onCreate(owner: LifecycleOwner) {
+                    lifecycleScope = owner.lifecycleScope
+                    iconController.deactivated = false
+                }
+
+                override fun onDestroy(owner: LifecycleOwner) {
+                    lifecycleScope = null
+                    iconController.deactivated = true
+                }
+            }
+        )
+    }
+
+    override fun onDialogAnimatedIn(fingerprintWasStarted: Boolean) {
+        if (fingerprintWasStarted) {
+            viewModel.ensureFingerprintHasStarted(isDelayed = false)
+            viewModel.showAuthenticating(modalities.asDefaultHelpMessage(applicationContext))
+        } else {
+            viewModel.showAuthenticating()
+        }
+    }
+
+    override fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int) {
+        applicationScope.launch {
+            val authenticatedModality = modality.asBiometricModality()
+            val msgId = getHelpForSuccessfulAuthentication(authenticatedModality)
+            viewModel.showAuthenticated(
+                modality = authenticatedModality,
+                dismissAfterDelay = 500,
+                helpMessage = if (msgId != null) applicationContext.getString(msgId) else ""
+            )
+        }
+    }
+
+    private suspend fun getHelpForSuccessfulAuthentication(
+        authenticatedModality: BiometricModality,
+    ): Int? =
+        when {
+            // for coex, show a message when face succeeds after fingerprint has also started
+            modalities.hasFaceAndFingerprint &&
+                (viewModel.fingerprintStartMode.first() != FingerprintStartMode.Pending) &&
+                (authenticatedModality == BiometricModality.Face) ->
+                R.string.biometric_dialog_tap_confirm_with_face
+            else -> null
+        }
+
+    override fun onAuthenticationFailed(
+        @BiometricAuthenticator.Modality modality: Int,
+        failureReason: String,
+    ) {
+        val failedModality = modality.asBiometricModality()
+        viewModel.ensureFingerprintHasStarted(isDelayed = true)
+
+        applicationScope.launch {
+            val suppress =
+                modalities.hasFaceAndFingerprint &&
+                    (failedModality == BiometricModality.Face) &&
+                    faceFailedAtLeastOnce
+            if (failedModality == BiometricModality.Face) {
+                faceFailedAtLeastOnce = true
+            }
+
+            viewModel.showTemporaryError(
+                failureReason,
+                messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
+                authenticateAfterError = modalities.hasFingerprint,
+                suppressIfErrorShowing = suppress,
+                failedModality = failedModality,
+            )
+        }
+    }
+
+    override fun onError(modality: Int, error: String) {
+        val errorModality = modality.asBiometricModality()
+        if (ignoreUnsuccessfulEventsFrom(errorModality, error)) {
+            return
+        }
+
+        applicationScope.launch {
+            val suppress =
+                modalities.hasFaceAndFingerprint && (errorModality == BiometricModality.Face)
+            viewModel.showTemporaryError(
+                error,
+                suppressIfErrorShowing = suppress,
+            )
+            delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+            legacyCallback?.onAction(Callback.ACTION_ERROR)
+        }
+    }
+
+    override fun onHelp(modality: Int, help: String) {
+        if (ignoreUnsuccessfulEventsFrom(modality.asBiometricModality(), "")) {
+            return
+        }
+
+        applicationScope.launch {
+            viewModel.showTemporaryHelp(
+                help,
+                messageAfterHelp = modalities.asDefaultHelpMessage(applicationContext),
+            )
+        }
+    }
+
+    private fun ignoreUnsuccessfulEventsFrom(modality: BiometricModality, message: String) =
+        when {
+            modalities.hasFaceAndFingerprint ->
+                (modality == BiometricModality.Face) &&
+                    !(modalities.isFaceStrong && lockoutErrorStrings.contains(message))
+            else -> false
+        }
+
+    override fun startTransitionToCredentialUI() {
+        applicationScope.launch {
+            viewModel.onSwitchToCredential()
+            legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
+        }
+    }
+
+    override fun requestLayout() {
+        // nothing, for legacy view...
+    }
+
+    override fun restoreState(bundle: Bundle?) {
+        // nothing, for legacy view...
+    }
+
+    override fun onSaveState(bundle: Bundle?) {
+        // nothing, for legacy view...
+    }
+
+    override fun onOrientationChanged() {
+        // nothing, for legacy view...
+    }
+
+    override fun cancelAnimation() {
+        view.animate()?.cancel()
+    }
+
+    override fun isCoex() = modalities.hasFaceAndFingerprint
+
+    override fun asView() = view
+}
+
+private fun BiometricModalities.asDefaultHelpMessage(context: Context): String =
+    when {
+        hasFingerprint -> context.getString(R.string.fingerprint_dialog_touch_sensor)
+        else -> ""
+    }
+
+private fun BiometricModalities.asIconController(
+    context: Context,
+    iconView: LottieAnimationView,
+    iconViewOverlay: LottieAnimationView,
+): AuthIconController =
+    when {
+        hasFaceAndFingerprint -> HackyCoexIconController(context, iconView, iconViewOverlay)
+        hasFingerprint -> AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay)
+        hasFace -> AuthBiometricFaceIconController(context, iconView)
+        else -> throw IllegalStateException("unexpected view type :$this")
+    }
+
+private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE
+
+private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE
+
+// TODO(b/251476085): proper type?
+typealias BiometricJankListener = Animator.AnimatorListener
+
+// TODO(b/251476085): delete - temporary until the legacy icon controllers are replaced
+private class HackyCoexIconController(
+    context: Context,
+    iconView: LottieAnimationView,
+    iconViewOverlay: LottieAnimationView,
+) : AuthBiometricFingerprintAndFaceIconController(context, iconView, iconViewOverlay) {
+
+    private var state: Int? = null
+    private val faceController = AuthBiometricFaceIconController(context, iconView)
+
+    var faceMode: Boolean = true
+        set(value) {
+            if (field != value) {
+                field = value
+
+                faceController.deactivated = !value
+                iconView.setImageIcon(null)
+                iconViewOverlay.setImageIcon(null)
+                state?.let { updateIcon(AuthBiometricView.STATE_IDLE, it) }
+            }
+        }
+
+    override fun updateIcon(lastState: Int, newState: Int) {
+        if (deactivated) {
+            return
+        }
+
+        if (faceMode) {
+            faceController.updateIcon(lastState, newState)
+        } else {
+            super.updateIcon(lastState, newState)
+        }
+
+        state = newState
+    }
+}
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
new file mode 100644
index 0000000..e4c4e9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.binder
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityManager
+import android.widget.TextView
+import androidx.core.animation.addListener
+import androidx.core.view.doOnLayout
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthDialog
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptSize
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.biometrics.ui.viewmodel.isLarge
+import com.android.systemui.biometrics.ui.viewmodel.isMedium
+import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
+import com.android.systemui.biometrics.ui.viewmodel.isSmall
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.launch
+
+/** Helper for [BiometricViewBinder] to handle resize transitions. */
+object BiometricViewSizeBinder {
+
+    /** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
+    fun bind(
+        view: BiometricPromptLayout,
+        viewModel: PromptViewModel,
+        viewsToHideWhenSmall: List<TextView>,
+        viewsToFadeInOnSizeChange: List<View>,
+        panelViewController: AuthPanelController,
+        jankListener: BiometricJankListener,
+    ) {
+        val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
+        fun notifyAccessibilityChanged() {
+            Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+        }
+
+        fun startMonitoredAnimation(animators: List<Animator>) {
+            with(AnimatorSet()) {
+                addListener(jankListener)
+                addListener(onEnd = { notifyAccessibilityChanged() })
+                play(animators.first()).apply { animators.drop(1).forEach { next -> with(next) } }
+                start()
+            }
+        }
+
+        val iconHolderView = view.findViewById<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)
+
+        // cache the original position of the icon view (as done in legacy view)
+        // this must happen before any size changes can be made
+        var iconHolderOriginalY = 0f
+        view.doOnLayout {
+            iconHolderOriginalY = iconHolderView.y
+
+            // bind to prompt
+            // TODO(b/251476085): migrate the legacy panel controller and simplify this
+            view.repeatWhenAttached {
+                var currentSize: PromptSize? = null
+                lifecycleScope.launch {
+                    viewModel.size.collect { size ->
+                        // prepare for animated size transitions
+                        for (v in viewsToHideWhenSmall) {
+                            v.showTextOrHide(forceHide = size.isSmall)
+                        }
+                        if (currentSize == null && size.isSmall) {
+                            iconHolderView.alpha = 0f
+                        }
+                        if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
+                            viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
+                        }
+
+                        // propagate size changes to legacy panel controller and animate transitions
+                        view.doOnLayout {
+                            val width = view.measuredWidth
+                            val height = view.measuredHeight
+
+                            when {
+                                size.isSmall -> {
+                                    iconHolderView.alpha = 1f
+                                    iconHolderView.y =
+                                        view.height - iconHolderView.height - iconPadding
+                                    val newHeight =
+                                        iconHolderView.height + 2 * iconPadding.toInt() -
+                                            iconHolderView.paddingTop -
+                                            iconHolderView.paddingBottom
+                                    panelViewController.updateForContentDimensions(
+                                        width,
+                                        newHeight,
+                                        0, /* animateDurationMs */
+                                    )
+                                }
+                                size.isMedium && currentSize.isSmall -> {
+                                    val duration = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
+                                    panelViewController.updateForContentDimensions(
+                                        width,
+                                        height,
+                                        duration,
+                                    )
+                                    startMonitoredAnimation(
+                                        listOf(
+                                            iconHolderView.asVerticalAnimator(
+                                                duration = duration.toLong(),
+                                                toY = iconHolderOriginalY,
+                                            ),
+                                            viewsToFadeInOnSizeChange.asFadeInAnimator(
+                                                duration = duration.toLong(),
+                                                delay = duration.toLong(),
+                                            ),
+                                        )
+                                    )
+                                }
+                                size.isMedium && currentSize.isNullOrNotSmall -> {
+                                    panelViewController.updateForContentDimensions(
+                                        width,
+                                        height,
+                                        0, /* animateDurationMs */
+                                    )
+                                }
+                                size.isLarge -> {
+                                    val duration = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
+                                    panelViewController.setUseFullScreen(true)
+                                    panelViewController.updateForContentDimensions(
+                                        panelViewController.containerWidth,
+                                        panelViewController.containerHeight,
+                                        duration,
+                                    )
+
+                                    startMonitoredAnimation(
+                                        listOf(
+                                            view.asVerticalAnimator(
+                                                duration.toLong() * 2 / 3,
+                                                toY = view.y - fullSizeYOffset
+                                            ),
+                                            listOf(view)
+                                                .asFadeInAnimator(
+                                                    duration = duration.toLong() / 2,
+                                                    delay = duration.toLong(),
+                                                ),
+                                        )
+                                    )
+                                    // TODO(b/251476085): clean up (copied from legacy)
+                                    if (view.isAttachedToWindow) {
+                                        val parent = view.parent as? ViewGroup
+                                        parent?.removeView(view)
+                                    }
+                                }
+                            }
+
+                            currentSize = size
+                            notifyAccessibilityChanged()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun TextView.showTextOrHide(forceHide: Boolean = false) {
+    visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
+}
+
+private fun View.asVerticalAnimator(
+    duration: Long,
+    toY: Float,
+    fromY: Float = this.y
+): ValueAnimator {
+    val animator = ValueAnimator.ofFloat(fromY, toY)
+    animator.duration = duration
+    animator.addUpdateListener { y = it.animatedValue as Float }
+    return animator
+}
+
+private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator {
+    forEach { it.alpha = 0f }
+    val animator = ValueAnimator.ofFloat(0f, 1f)
+    animator.duration = duration
+    animator.startDelay = delay
+    animator.addUpdateListener {
+        val alpha = it.animatedValue as Float
+        forEach { view -> view.alpha = alpha }
+    }
+    return animator
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
index ba23f1c..a64798c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
@@ -4,7 +4,7 @@
 import android.os.UserHandle
 
 /** View model for the top-level header / info area of BiometricPrompt. */
-interface HeaderViewModel {
+interface CredentialHeaderViewModel {
     val user: UserHandle
     val title: String
     val subtitle: String
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
index 84bbceb..9d7b940 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -7,8 +7,8 @@
 import com.android.internal.widget.LockPatternView
 import com.android.systemui.R
 import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
 import com.android.systemui.biometrics.domain.interactor.CredentialStatus
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
 import com.android.systemui.dagger.qualifiers.Application
 import javax.inject.Inject
@@ -27,11 +27,11 @@
 @Inject
 constructor(
     @Application private val applicationContext: Context,
-    private val credentialInteractor: BiometricPromptCredentialInteractor,
+    private val credentialInteractor: PromptCredentialInteractor,
 ) {
 
     /** Top level information about the prompt. */
-    val header: Flow<HeaderViewModel> =
+    val header: Flow<CredentialHeaderViewModel> =
         credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map {
             request ->
             BiometricPromptHeaderViewModelImpl(
@@ -109,12 +109,14 @@
     }
 
     /** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */
-    suspend fun checkCredential(text: CharSequence, header: HeaderViewModel) =
+    suspend fun checkCredential(text: CharSequence, header: CredentialHeaderViewModel) =
         checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text))
 
     /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */
-    suspend fun checkCredential(pattern: List<LockPatternView.Cell>, header: HeaderViewModel) =
-        checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
+    suspend fun checkCredential(
+        pattern: List<LockPatternView.Cell>,
+        header: CredentialHeaderViewModel
+    ) = checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
 
     private suspend fun checkCredential(result: CredentialStatus) {
         when (result) {
@@ -172,7 +174,7 @@
     override val subtitle: String,
     override val description: String,
     override val icon: Drawable,
-) : HeaderViewModel
+) : CredentialHeaderViewModel
 
-private fun HeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
+private fun CredentialHeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
     (this as BiometricPromptHeaderViewModelImpl).request
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt
new file mode 100644
index 0000000..9cb91b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+import com.android.systemui.biometrics.domain.model.BiometricModality
+
+/**
+ * The authenticated state with the [authenticatedModality] (when [isAuthenticated]) with an
+ * optional [delay] to keep the UI showing before dismissing when [needsUserConfirmation] is not
+ * required.
+ */
+data class PromptAuthState(
+    val isAuthenticated: Boolean,
+    val authenticatedModality: BiometricModality = BiometricModality.None,
+    val needsUserConfirmation: Boolean = false,
+    val delay: Long = 0,
+) {
+    /** If authentication was successful and the user has confirmed (or does not need to). */
+    val isAuthenticatedAndConfirmed: Boolean
+        get() = isAuthenticated && !needsUserConfirmation
+
+    /** If a successful authentication has not occurred. */
+    val isNotAuthenticated: Boolean
+        get() = !isAuthenticated
+
+    /** If a authentication has succeeded and it was done by face (may need confirmation). */
+    val isAuthenticatedByFace: Boolean
+        get() = isAuthenticated && authenticatedModality == BiometricModality.Face
+
+    /** If a authentication has succeeded and it was done by fingerprint (may need confirmation). */
+    val isAuthenticatedByFingerprint: Boolean
+        get() = isAuthenticated && authenticatedModality == BiometricModality.Fingerprint
+
+    /** Copies this state, but toggles [needsUserConfirmation] to false. */
+    fun asConfirmed(): PromptAuthState =
+        PromptAuthState(
+            isAuthenticated = isAuthenticated,
+            authenticatedModality = authenticatedModality,
+            needsUserConfirmation = false,
+            delay = delay,
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt
new file mode 100644
index 0000000..219da71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+/**
+ * A help, hint, or error message to show.
+ *
+ * These typically correspond to the same category of help/error callbacks from the underlying HAL
+ * that runs the biometric operation, but may be customized by the framework.
+ */
+sealed interface PromptMessage {
+
+    /** The message to show the user or the empty string. */
+    val message: String
+        get() =
+            when (this) {
+                is Error -> errorMessage
+                is Help -> helpMessage
+                else -> ""
+            }
+
+    /** If this is an [Error] or [Help] message. */
+    val isErrorOrHelp: Boolean
+        get() = this is Error || this is Help
+
+    /** An error message. */
+    data class Error(val errorMessage: String) : PromptMessage
+
+    /** A help message. */
+    data class Help(val helpMessage: String) : PromptMessage
+
+    /** No message. */
+    object Empty : PromptMessage
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt
new file mode 100644
index 0000000..d779062
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+/** The size of a biometric prompt. */
+enum class PromptSize {
+    /** Minimal UI, showing only biometric icon. */
+    SMALL,
+    /** Normal-sized biometric UI, showing title, icon, buttons, etc. */
+    MEDIUM,
+    /** Full-screen credential UI. */
+    LARGE,
+}
+
+val PromptSize?.isSmall: Boolean
+    get() = this != null && this == PromptSize.SMALL
+
+val PromptSize?.isNotSmall: Boolean
+    get() = this != null && this != PromptSize.SMALL
+
+val PromptSize?.isNullOrNotSmall: Boolean
+    get() = this == null || this != PromptSize.SMALL
+
+val PromptSize?.isMedium: Boolean
+    get() = this != null && this == PromptSize.MEDIUM
+
+val PromptSize?.isLarge: Boolean
+    get() = this != null && this == PromptSize.LARGE
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
new file mode 100644
index 0000000..2f8ed09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.hardware.biometrics.BiometricPrompt
+import android.util.Log
+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.domain.model.BiometricModality
+import com.android.systemui.biometrics.shared.model.PromptKind
+import javax.inject.Inject
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** ViewModel for BiometricPrompt. */
+class PromptViewModel
+@Inject
+constructor(
+    private val interactor: PromptSelectorInteractor,
+) {
+    /** The set of modalities available for this prompt */
+    val modalities: Flow<BiometricModalities> =
+        interactor.prompt.map { it?.modalities ?: BiometricModalities() }.distinctUntilChanged()
+
+    // TODO(b/251476085): remove after icon controllers are migrated - do not keep this state
+    private var _legacyState = MutableStateFlow(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+    val legacyState: StateFlow<Int> = _legacyState.asStateFlow()
+
+    private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+    /** If the user is currently authenticating (i.e. at least one biometric is scanning). */
+    val isAuthenticating: Flow<Boolean> = _isAuthenticating.asStateFlow()
+
+    private val _isAuthenticated: MutableStateFlow<PromptAuthState> =
+        MutableStateFlow(PromptAuthState(false))
+
+    /** If the user has successfully authenticated and confirmed (when explicitly required). */
+    val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow()
+
+    /** If the API caller requested explicit confirmation after successful authentication. */
+    val isConfirmationRequested: Flow<Boolean> = interactor.isConfirmationRequested
+
+    /** The kind of credential the user has. */
+    val credentialKind: Flow<PromptKind> = interactor.credentialKind
+
+    /** The label to use for the cancel button. */
+    val negativeButtonText: Flow<String> = interactor.prompt.map { it?.negativeButtonText ?: "" }
+
+    private val _message: MutableStateFlow<PromptMessage> = MutableStateFlow(PromptMessage.Empty)
+
+    /** A message to show the user, if there is an error, hint, or help to show. */
+    val message: Flow<PromptMessage> = _message.asStateFlow()
+
+    private val isRetrySupported: Flow<Boolean> = modalities.map { it.hasFace }
+
+    private val _fingerprintStartMode = MutableStateFlow(FingerprintStartMode.Pending)
+
+    /** Fingerprint sensor state. */
+    val fingerprintStartMode: Flow<FingerprintStartMode> = _fingerprintStartMode.asStateFlow()
+
+    private val _forceLargeSize = MutableStateFlow(false)
+    private val _forceMediumSize = MutableStateFlow(false)
+
+    /** The size of the prompt. */
+    val size: Flow<PromptSize> =
+        combine(
+                _forceLargeSize,
+                _forceMediumSize,
+                modalities,
+                interactor.isConfirmationRequested,
+                fingerprintStartMode,
+            ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode ->
+                when {
+                    forceLarge -> PromptSize.LARGE
+                    forceMedium -> PromptSize.MEDIUM
+                    modalities.hasFaceOnly && !confirmationRequired -> PromptSize.SMALL
+                    modalities.hasFaceAndFingerprint &&
+                        !confirmationRequired &&
+                        fpStartMode == FingerprintStartMode.Pending -> PromptSize.SMALL
+                    else -> PromptSize.MEDIUM
+                }
+            }
+            .distinctUntilChanged()
+
+    /** Title for the prompt. */
+    val title: Flow<String> = interactor.prompt.map { it?.title ?: "" }.distinctUntilChanged()
+
+    /** Subtitle for the prompt. */
+    val subtitle: Flow<String> = interactor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
+
+    /** Description for the prompt. */
+    val description: Flow<String> =
+        interactor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
+
+    /** If the indicator (help, error) message should be shown. */
+    val isIndicatorMessageVisible: Flow<Boolean> =
+        combine(
+                size,
+                message,
+            ) { size, message ->
+                size.isNotSmall && message.message.isNotBlank()
+            }
+            .distinctUntilChanged()
+
+    /** If the auth is pending confirmation and the confirm button should be shown. */
+    val isConfirmButtonVisible: Flow<Boolean> =
+        combine(
+                size,
+                isAuthenticated,
+            ) { size, authState ->
+                size.isNotSmall && authState.isAuthenticated && authState.needsUserConfirmation
+            }
+            .distinctUntilChanged()
+
+    /** If the negative button should be shown. */
+    val isNegativeButtonVisible: Flow<Boolean> =
+        combine(
+                size,
+                isAuthenticated,
+                interactor.isCredentialAllowed,
+            ) { size, authState, credentialAllowed ->
+                size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed
+            }
+            .distinctUntilChanged()
+
+    /** If the cancel button should be shown (. */
+    val isCancelButtonVisible: Flow<Boolean> =
+        combine(
+                size,
+                isAuthenticated,
+                isNegativeButtonVisible,
+                isConfirmButtonVisible,
+            ) { size, authState, showNegativeButton, showConfirmButton ->
+                size.isNotSmall &&
+                    authState.isAuthenticated &&
+                    !showNegativeButton &&
+                    showConfirmButton
+            }
+            .distinctUntilChanged()
+
+    private val _canTryAgainNow = MutableStateFlow(false)
+    /**
+     * If authentication can be manually restarted via the try again button or touching a
+     * fingerprint sensor.
+     */
+    val canTryAgainNow: Flow<Boolean> =
+        combine(
+                _canTryAgainNow,
+                size,
+                isAuthenticated,
+                isRetrySupported,
+            ) { readyToTryAgain, size, authState, supportsRetry ->
+                readyToTryAgain && size.isNotSmall && supportsRetry && authState.isNotAuthenticated
+            }
+            .distinctUntilChanged()
+
+    /** If the try again button show be shown (only the button, see [canTryAgainNow]). */
+    val isTryAgainButtonVisible: Flow<Boolean> =
+        combine(
+                canTryAgainNow,
+                modalities,
+            ) { tryAgainIsPossible, modalities ->
+                tryAgainIsPossible && modalities.hasFaceOnly
+            }
+            .distinctUntilChanged()
+
+    /** If the credential fallback button show be shown. */
+    val isCredentialButtonVisible: Flow<Boolean> =
+        combine(
+                size,
+                isAuthenticated,
+                interactor.isCredentialAllowed,
+            ) { size, authState, credentialAllowed ->
+                size.isNotSmall && authState.isNotAuthenticated && credentialAllowed
+            }
+            .distinctUntilChanged()
+
+    private var messageJob: Job? = null
+
+    /**
+     * Show a temporary error [message] associated with an optional [failedModality].
+     *
+     * An optional [messageAfterError] will be shown via [showAuthenticating] when
+     * [authenticateAfterError] is set (or via [showHelp] when not set) after the error is
+     * dismissed.
+     *
+     * The error is ignored if the user has already authenticated and it is treated as
+     * [onSilentError] if [suppressIfErrorShowing] is set and an error message is already showing.
+     */
+    suspend fun showTemporaryError(
+        message: String,
+        messageAfterError: String = "",
+        authenticateAfterError: Boolean = false,
+        suppressIfErrorShowing: Boolean = false,
+        failedModality: BiometricModality = BiometricModality.None,
+    ) = coroutineScope {
+        if (_isAuthenticated.value.isAuthenticated) {
+            return@coroutineScope
+        }
+        if (_message.value.isErrorOrHelp && suppressIfErrorShowing) {
+            onSilentError(failedModality)
+            return@coroutineScope
+        }
+
+        _isAuthenticating.value = false
+        _isAuthenticated.value = PromptAuthState(false)
+        _forceMediumSize.value = true
+        _canTryAgainNow.value = supportsRetry(failedModality)
+        _message.value = PromptMessage.Error(message)
+        _legacyState.value = AuthBiometricView.STATE_ERROR
+
+        messageJob?.cancel()
+        messageJob = launch {
+            delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+            if (authenticateAfterError) {
+                showAuthenticating(messageAfterError)
+            } else {
+                showHelp(messageAfterError)
+            }
+        }
+    }
+
+    /**
+     * Call instead of [showTemporaryError] if an error from the HAL should be silently ignored to
+     * enable retry (if the [failedModality] supports retrying).
+     *
+     * Ignored if the user has already authenticated.
+     */
+    private fun onSilentError(failedModality: BiometricModality = BiometricModality.None) {
+        if (_isAuthenticated.value.isNotAuthenticated) {
+            _canTryAgainNow.value = supportsRetry(failedModality)
+        }
+    }
+
+    /**
+     * Call to ensure the fingerprint sensor has started. Either when the dialog is first shown
+     * (most cases) or when it should be enabled after a first error (coex implicit flow).
+     */
+    fun ensureFingerprintHasStarted(isDelayed: Boolean) {
+        if (_fingerprintStartMode.value == FingerprintStartMode.Pending) {
+            _fingerprintStartMode.value =
+                if (isDelayed) FingerprintStartMode.Delayed else FingerprintStartMode.Normal
+        }
+    }
+
+    // enable retry only when face fails (fingerprint runs constantly)
+    private fun supportsRetry(failedModality: BiometricModality) =
+        failedModality == BiometricModality.Face
+
+    /**
+     * Show a persistent help message.
+     *
+     * Will be show even if the user has already authenticated.
+     */
+    suspend fun showHelp(message: String) {
+        val alreadyAuthenticated = _isAuthenticated.value.isAuthenticated
+        if (!alreadyAuthenticated) {
+            _isAuthenticating.value = false
+            _isAuthenticated.value = PromptAuthState(false)
+        }
+
+        _message.value =
+            if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
+        _forceMediumSize.value = true
+        _legacyState.value =
+            if (alreadyAuthenticated) {
+                AuthBiometricView.STATE_PENDING_CONFIRMATION
+            } else {
+                AuthBiometricView.STATE_HELP
+            }
+
+        messageJob?.cancel()
+        messageJob = null
+    }
+
+    /**
+     * Show a temporary help message and transition back to a fixed message.
+     *
+     * Ignored if the user has already authenticated.
+     */
+    suspend fun showTemporaryHelp(
+        message: String,
+        messageAfterHelp: String = "",
+    ) = coroutineScope {
+        if (_isAuthenticated.value.isAuthenticated) {
+            return@coroutineScope
+        }
+
+        _isAuthenticating.value = false
+        _isAuthenticated.value = PromptAuthState(false)
+        _message.value =
+            if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
+        _forceMediumSize.value = true
+        _legacyState.value = AuthBiometricView.STATE_HELP
+
+        messageJob?.cancel()
+        messageJob = launch {
+            delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+            showAuthenticating(messageAfterHelp)
+        }
+    }
+
+    /** Show the user that biometrics are actively running and set [isAuthenticating]. */
+    fun showAuthenticating(message: String = "", isRetry: Boolean = false) {
+        if (_isAuthenticated.value.isAuthenticated) {
+            // TODO(jbolinger): convert to go/tex-apc?
+            Log.w(TAG, "Cannot show authenticating after authenticated")
+            return
+        }
+
+        _isAuthenticating.value = true
+        _isAuthenticated.value = PromptAuthState(false)
+        _message.value = if (message.isBlank()) PromptMessage.Empty else PromptMessage.Help(message)
+        _legacyState.value = AuthBiometricView.STATE_AUTHENTICATING
+
+        // reset the try again button(s) after the user attempts a retry
+        if (isRetry) {
+            _canTryAgainNow.value = false
+        }
+
+        messageJob?.cancel()
+        messageJob = null
+    }
+
+    /**
+     * Show successfully authentication, set [isAuthenticated], and dismiss the prompt after a
+     * [dismissAfterDelay] or prompt for explicit confirmation (if required).
+     */
+    suspend fun showAuthenticated(
+        modality: BiometricModality,
+        dismissAfterDelay: Long,
+        helpMessage: String = "",
+    ) {
+        if (_isAuthenticated.value.isAuthenticated) {
+            // TODO(jbolinger): convert to go/tex-apc?
+            Log.w(TAG, "Cannot show authenticated after authenticated")
+            return
+        }
+
+        _isAuthenticating.value = false
+        val needsUserConfirmation = needsExplicitConfirmation(modality)
+        _isAuthenticated.value =
+            PromptAuthState(true, modality, needsUserConfirmation, dismissAfterDelay)
+        _message.value = PromptMessage.Empty
+        _legacyState.value =
+            if (needsUserConfirmation) {
+                AuthBiometricView.STATE_PENDING_CONFIRMATION
+            } else {
+                AuthBiometricView.STATE_AUTHENTICATED
+            }
+
+        messageJob?.cancel()
+        messageJob = null
+
+        if (helpMessage.isNotBlank()) {
+            showHelp(helpMessage)
+        }
+    }
+
+    private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
+        val availableModalities = modalities.first()
+        val confirmationRequested = interactor.isConfirmationRequested.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
+            if (modality == BiometricModality.Face) {
+                return (fingerprintStartMode.first() != FingerprintStartMode.Pending) ||
+                    confirmationRequested
+            }
+        }
+        if (availableModalities.hasFaceOnly) {
+            return confirmationRequested
+        }
+        // fingerprint only never requires confirmation
+        return false
+    }
+
+    /**
+     * Set the prompt's auth state to authenticated and confirmed.
+     *
+     * This should only be used after [showAuthenticated] when the operation requires explicit user
+     * confirmation.
+     */
+    fun confirmAuthenticated() {
+        val authState = _isAuthenticated.value
+        if (authState.isNotAuthenticated) {
+            "Cannot show authenticated after authenticated"
+            Log.w(TAG, "Cannot confirm authenticated when not authenticated")
+            return
+        }
+
+        _isAuthenticated.value = authState.asConfirmed()
+        _message.value = PromptMessage.Empty
+        _legacyState.value = AuthBiometricView.STATE_AUTHENTICATED
+
+        messageJob?.cancel()
+        messageJob = null
+    }
+
+    /**
+     * Switch to the credential view.
+     *
+     * TODO(b/251476085): this should be decoupled from the shared panel controller
+     */
+    fun onSwitchToCredential() {
+        _forceLargeSize.value = true
+    }
+
+    companion object {
+        private const val TAG = "PromptViewModel"
+    }
+}
+
+/** How the fingerprint sensor was started for the prompt. */
+enum class FingerprintStartMode {
+    /** Fingerprint sensor has not started. */
+    Pending,
+
+    /** Fingerprint sensor started immediately when prompt was displayed. */
+    Normal,
+
+    /** Fingerprint sensor started after the first failure of another passive modality. */
+    Delayed;
+
+    /** If this is [Normal] or [Delayed]. */
+    val isStarted: Boolean
+        get() = this == Normal || this == Delayed
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
index 4c817b2..49a0a3c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bouncer.data.repo
 
+import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -29,7 +30,15 @@
     /** The user-facing message to show in the bouncer. */
     val message: StateFlow<String?> = _message.asStateFlow()
 
+    private val _throttling = MutableStateFlow<AuthenticationThrottledModel?>(null)
+    /** The current authentication throttling state. If `null`, there's no throttling. */
+    val throttling: StateFlow<AuthenticationThrottledModel?> = _throttling.asStateFlow()
+
     fun setMessage(message: String?) {
         _message.value = message
     }
+
+    fun setThrottling(throttling: AuthenticationThrottledModel?) {
+        _throttling.value = throttling
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 8264fed..e462e2f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -17,10 +17,12 @@
 package com.android.systemui.bouncer.domain.interactor
 
 import android.content.Context
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneKey
@@ -29,8 +31,11 @@
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /** Encapsulates business logic and application state accessing use-cases. */
@@ -46,7 +51,22 @@
 ) {
 
     /** The user-facing message to show in the bouncer. */
-    val message: StateFlow<String?> = repository.message
+    val message: StateFlow<String?> =
+        combine(
+                repository.message,
+                repository.throttling,
+            ) { message, throttling ->
+                messageOrThrottlingMessage(message, throttling)
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue =
+                    messageOrThrottlingMessage(
+                        repository.message.value,
+                        repository.throttling.value,
+                    )
+            )
 
     /**
      * The currently-configured authentication method. This determines how the authentication
@@ -55,6 +75,9 @@
     val authenticationMethod: StateFlow<AuthenticationMethodModel> =
         authenticationInteractor.authenticationMethod
 
+    /** The current authentication throttling state. If `null`, there's no throttling. */
+    val throttling: StateFlow<AuthenticationThrottledModel?> = repository.throttling
+
     init {
         applicationScope.launch {
             combine(
@@ -129,14 +152,39 @@
     fun authenticate(
         input: List<Any>,
     ) {
+        if (repository.throttling.value != null) {
+            return
+        }
+
         val isAuthenticated = authenticationInteractor.authenticate(input)
-        if (isAuthenticated) {
-            sceneInteractor.setCurrentScene(
-                containerName = containerName,
-                scene = SceneModel(SceneKey.Gone),
-            )
-        } else {
-            repository.setMessage(errorMessage(authenticationMethod.value))
+        val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
+        when {
+            isAuthenticated -> {
+                repository.setThrottling(null)
+                sceneInteractor.setCurrentScene(
+                    containerName = containerName,
+                    scene = SceneModel(SceneKey.Gone),
+                )
+            }
+            failedAttempts >= THROTTLE_AGGRESSIVELY_AFTER || failedAttempts % THROTTLE_EVERY == 0 ->
+                applicationScope.launch {
+                    var remainingDurationSec = THROTTLE_DURATION_SEC
+                    while (remainingDurationSec > 0) {
+                        repository.setThrottling(
+                            AuthenticationThrottledModel(
+                                failedAttemptCount = failedAttempts,
+                                totalDurationSec = THROTTLE_DURATION_SEC,
+                                remainingDurationSec = remainingDurationSec,
+                            )
+                        )
+                        remainingDurationSec--
+                        delay(1000)
+                    }
+
+                    repository.setThrottling(null)
+                    clearMessage()
+                }
+            else -> repository.setMessage(errorMessage(authenticationMethod.value))
         }
     }
 
@@ -163,10 +211,31 @@
         }
     }
 
+    private fun messageOrThrottlingMessage(
+        message: String?,
+        throttling: AuthenticationThrottledModel?,
+    ): String {
+        return when {
+            throttling != null ->
+                applicationContext.getString(
+                    com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown,
+                    throttling.remainingDurationSec,
+                )
+            message != null -> message
+            else -> ""
+        }
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(
             containerName: String,
         ): BouncerInteractor
     }
+
+    companion object {
+        @VisibleForTesting const val THROTTLE_DURATION_SEC = 30
+        @VisibleForTesting const val THROTTLE_AGGRESSIVELY_AFTER = 15
+        @VisibleForTesting const val THROTTLE_EVERY = 5
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt
new file mode 100644
index 0000000..cbea635
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.shared.model
+
+/**
+ * Models application state for when further authentication attempts are being throttled due to too
+ * many consecutive failed authentication attempts.
+ */
+data class AuthenticationThrottledModel(
+    /** Total number of failed attempts so far. */
+    val failedAttemptCount: Int,
+    /** Total amount of time the user has to wait before attempting again. */
+    val totalDurationSec: Int,
+    /** Remaining amount of time the user has to wait before attempting again. */
+    val remainingDurationSec: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index ebefb78..774a559 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -16,4 +16,14 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
-sealed interface AuthMethodBouncerViewModel
+import kotlinx.coroutines.flow.StateFlow
+
+sealed interface AuthMethodBouncerViewModel {
+    /**
+     * Whether user input is enabled.
+     *
+     * If `false`, user input should be completely ignored in the UI as the user is "locked out" of
+     * being able to attempt to unlock the device.
+     */
+    val isInputEnabled: StateFlow<Boolean>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index c6528d0..02991bd 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.Context
+import com.android.systemui.R
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.dagger.qualifiers.Application
@@ -24,10 +25,14 @@
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input on bouncer UIs. */
 class BouncerViewModel
@@ -40,16 +45,42 @@
 ) {
     private val interactor: BouncerInteractor = interactorFactory.create(containerName)
 
+    /**
+     * Whether updates to the message should be cross-animated from one message to another.
+     *
+     * If `false`, no animation should be applied, the message text should just be replaced
+     * instantly.
+     */
+    val isMessageUpdateAnimationsEnabled: StateFlow<Boolean> =
+        interactor.throttling
+            .map { it == null }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = interactor.throttling.value == null,
+            )
+
+    private val isInputEnabled: StateFlow<Boolean> =
+        interactor.throttling
+            .map { it == null }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = interactor.throttling.value == null,
+            )
+
     private val pin: PinBouncerViewModel by lazy {
         PinBouncerViewModel(
             applicationScope = applicationScope,
             interactor = interactor,
+            isInputEnabled = isInputEnabled,
         )
     }
 
     private val password: PasswordBouncerViewModel by lazy {
         PasswordBouncerViewModel(
             interactor = interactor,
+            isInputEnabled = isInputEnabled,
         )
     }
 
@@ -58,6 +89,7 @@
             applicationContext = applicationContext,
             applicationScope = applicationScope,
             interactor = interactor,
+            isInputEnabled = isInputEnabled,
         )
     }
 
@@ -81,11 +113,59 @@
                 initialValue = interactor.message.value ?: "",
             )
 
+    private val _throttlingDialogMessage = MutableStateFlow<String?>(null)
+    /**
+     * A message for a throttling dialog to show when the user has attempted the wrong credential
+     * too many times and now must wait a while before attempting again.
+     *
+     * If `null`, no dialog should be shown.
+     *
+     * Once the dialog is shown, the UI should call [onThrottlingDialogDismissed] when the user
+     * dismisses this dialog.
+     */
+    val throttlingDialogMessage: StateFlow<String?> = _throttlingDialogMessage.asStateFlow()
+
+    init {
+        applicationScope.launch {
+            interactor.throttling
+                .map { model ->
+                    model?.let {
+                        when (interactor.authenticationMethod.value) {
+                            is AuthenticationMethodModel.PIN ->
+                                R.string.kg_too_many_failed_pin_attempts_dialog_message
+                            is AuthenticationMethodModel.Password ->
+                                R.string.kg_too_many_failed_password_attempts_dialog_message
+                            is AuthenticationMethodModel.Pattern ->
+                                R.string.kg_too_many_failed_pattern_attempts_dialog_message
+                            else -> null
+                        }?.let { stringResourceId ->
+                            applicationContext.getString(
+                                stringResourceId,
+                                model.failedAttemptCount,
+                                model.totalDurationSec,
+                            )
+                        }
+                    }
+                }
+                .distinctUntilChanged()
+                .collect { dialogMessageOrNull ->
+                    if (dialogMessageOrNull != null) {
+                        _throttlingDialogMessage.value = dialogMessageOrNull
+                    }
+                }
+        }
+    }
+
     /** Notifies that the emergency services button was clicked. */
     fun onEmergencyServicesButtonClicked() {
         // TODO(b/280877228): implement this
     }
 
+    /** Notifies that a throttling dialog has been dismissed by the user. */
+    fun onThrottlingDialogDismissed() {
+        _throttlingDialogMessage.value = null
+    }
+
     private fun toViewModel(
         authMethod: AuthenticationMethodModel,
     ): AuthMethodBouncerViewModel? {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 730d4e8..c38fcaa 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -24,6 +24,7 @@
 /** Holds UI state and handles user input for the password bouncer UI. */
 class PasswordBouncerViewModel(
     private val interactor: BouncerInteractor,
+    override val isInputEnabled: StateFlow<Boolean>,
 ) : AuthMethodBouncerViewModel {
 
     private val _password = MutableStateFlow("")
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index eb1b457..1b0b38e 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -37,6 +37,7 @@
     private val applicationContext: Context,
     applicationScope: CoroutineScope,
     private val interactor: BouncerInteractor,
+    override val isInputEnabled: StateFlow<Boolean>,
 ) : AuthMethodBouncerViewModel {
 
     /** The number of columns in the dot grid. */
@@ -63,6 +64,16 @@
     /** All dots on the grid. */
     val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow()
 
+    /** Whether the pattern itself should be rendered visibly. */
+    val isPatternVisible: StateFlow<Boolean> =
+        interactor.authenticationMethod
+            .map { authMethod -> isPatternVisible(authMethod) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = isPatternVisible(interactor.authenticationMethod.value),
+            )
+
     /** Notifies that the UI has been shown to the user. */
     fun onShown() {
         interactor.resetMessage()
@@ -146,6 +157,10 @@
         _selectedDots.value = linkedSetOf()
     }
 
+    private fun isPatternVisible(authMethodModel: AuthenticationMethodModel): Boolean {
+        return (authMethodModel as? AuthenticationMethodModel.Pattern)?.isPatternVisible ?: false
+    }
+
     private fun defaultDots(): List<PatternDotViewModel> {
         return buildList {
             (0 until columnCount).forEach { x ->
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index f9223cb..2a733d9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -33,6 +33,7 @@
 class PinBouncerViewModel(
     private val applicationScope: CoroutineScope,
     private val interactor: BouncerInteractor,
+    override val isInputEnabled: StateFlow<Boolean>,
 ) : AuthMethodBouncerViewModel {
 
     private val entered = MutableStateFlow<List<Int>>(emptyList())
diff --git a/packages/SystemUI/src/com/android/systemui/complication/ComplicationTypesUpdater.java b/packages/SystemUI/src/com/android/systemui/complication/ComplicationTypesUpdater.java
index a334c1e..0bdc7f1 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/ComplicationTypesUpdater.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/ComplicationTypesUpdater.java
@@ -77,6 +77,10 @@
                 Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
                 settingsObserver,
                 UserHandle.myUserId());
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+                settingsObserver,
+                UserHandle.myUserId());
         settingsObserver.onChange(false);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e118fdf..19810b3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -104,6 +104,16 @@
     val SENSITIVE_REVEAL_ANIM =
         unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
 
+    // TODO(b/280783617): Tracking Bug
+    @Keep
+    @JvmField
+    val BUILDER_EXTRAS_OVERRIDE =
+            sysPropBooleanFlag(
+                    128,
+                    "persist.sysui.notification.builder_extras_override",
+                    default = false
+            )
+
     // 200 - keyguard/lockscreen
     // ** Flag retired **
     // public static final BooleanFlag KEYGUARD_LAYOUT =
@@ -136,7 +146,7 @@
      * the digits when the clock moves.
      */
     @JvmField
-    val STEP_CLOCK_ANIMATION = unreleasedFlag(212, "step_clock_animation", teamfood = true)
+    val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
 
     /**
      * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
@@ -167,7 +177,7 @@
      */
     // TODO(b/281655028): Tracking bug
     @JvmField
-    val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = true)
+    val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false)
 
     /** Flag to control the migration of face auth to modern architecture. */
     // TODO(b/262838215): Tracking bug
@@ -241,7 +251,7 @@
     /** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
     // TODO(b/279794160): Tracking bug.
     @JvmField
-    val DELAY_BOUNCER = releasedFlag(235, "delay_bouncer")
+    val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer")
 
     /** Migrate the indication area to the new keyguard root view. */
     // TODO(b/280067944): Tracking bug.
@@ -525,13 +535,6 @@
     val ENABLE_PIP_APP_ICON_OVERLAY =
         sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = true)
 
-    // TODO(b/272110828): Tracking bug
-    @Keep
-    @JvmField
-    val ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP =
-        sysPropBooleanFlag(
-            1116, "persist.wm.debug.enable_move_floating_window_in_tabletop", default = true)
-
     // TODO(b/273443374): Tracking Bug
     @Keep
     @JvmField val LOCKSCREEN_LIVE_WALLPAPER =
@@ -615,8 +618,6 @@
         unreleasedFlag(1401, "quick_tap_flow_framework", teamfood = false)
 
     // 1500 - chooser aka sharesheet
-    // TODO(b/254512507): Tracking Bug
-    val CHOOSER_UNBUNDLED = releasedFlag(1500, "chooser_unbundled")
 
     // 1700 - clipboard
     @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 54da680..b8d3121 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -19,7 +19,6 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
@@ -30,13 +29,11 @@
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
 import static android.view.WindowManager.TRANSIT_OLD_NONE;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.TransitionFlags;
 import static android.view.WindowManager.TransitionOldType;
 import static android.view.WindowManager.TransitionType;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.Service;
@@ -65,6 +62,7 @@
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.TransitionInfo;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.policy.IKeyguardDrawnCallback;
 import com.android.internal.policy.IKeyguardExitCallback;
@@ -116,6 +114,14 @@
             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
             final int taskId = taskInfo != null ? change.getTaskInfo().taskId : -1;
 
+            if (taskId != -1 && change.getParent() != null) {
+                final TransitionInfo.Change parentChange = info.getChange(change.getParent());
+                if (parentChange != null && parentChange.getTaskInfo() != null) {
+                    // Only adding the root task as the animation target.
+                    continue;
+                }
+            }
+
             final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
                     // wallpapers go into the "below" layer space
                     info.getChanges().size() - i,
@@ -123,13 +129,6 @@
                     (change.getFlags() & TransitionInfo.FLAG_SHOW_WALLPAPER) != 0,
                     info, t, leashMap);
 
-            // Use hasAnimatingParent to mark the anything below root task
-            if (taskId != -1 && change.getParent() != null) {
-                final TransitionInfo.Change parentChange = info.getChange(change.getParent());
-                if (parentChange != null && parentChange.getTaskInfo() != null) {
-                    target.hasAnimatingParent = true;
-                }
-            }
             out.add(target);
         }
         return out.toArray(new RemoteAnimationTarget[out.size()]);
@@ -158,71 +157,83 @@
     // Note: Also used for wrapping occlude by Dream animation. It works (with some redundancy).
     public static IRemoteTransition wrap(IRemoteAnimationRunner runner) {
         return new IRemoteTransition.Stub() {
-            final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks =
-                    new ArrayMap<>();
+
             private final ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = new ArrayMap<>();
 
+            @GuardedBy("mLeashMap")
+            private IRemoteTransitionFinishedCallback mFinishCallback = null;
+
             @Override
             public void startAnimation(IBinder transition, TransitionInfo info,
                     SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
                     throws RemoteException {
                 Slog.d(TAG, "Starts IRemoteAnimationRunner: info=" + info);
-                final RemoteAnimationTarget[] apps =
-                        wrap(info, false /* wallpapers */, t, mLeashMap);
-                final RemoteAnimationTarget[] wallpapers =
-                        wrap(info, true /* wallpapers */, t, mLeashMap);
-                final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0];
 
-                // Sets the alpha to 0 for the opening root task for fade in animation. And since
-                // the fade in animation can only apply on the first opening app, so set alpha to 1
-                // for anything else.
-                for (RemoteAnimationTarget target : apps) {
-                    if (target.taskId != -1
-                            && target.mode == RemoteAnimationTarget.MODE_OPENING
-                            && !target.hasAnimatingParent) {
-                        t.setAlpha(target.leash, 0.0f);
-                    } else {
-                        t.setAlpha(target.leash, 1.0f);
-                    }
-                }
-                t.apply();
-                synchronized (mFinishCallbacks) {
-                    mFinishCallbacks.put(transition, finishCallback);
-                }
-                runner.onAnimationStart(getTransitionOldType(info.getType(), info.getFlags(), apps),
-                        apps, wallpapers, nonApps,
-                        new IRemoteAnimationFinishedCallback.Stub() {
-                            @Override
-                            public void onAnimationFinished() throws RemoteException {
-                                synchronized (mFinishCallbacks) {
-                                    if (mFinishCallbacks.remove(transition) == null) return;
-                                }
-                                info.releaseAllSurfaces();
-                                Slog.d(TAG, "Finish IRemoteAnimationRunner.");
-                                finishCallback.onTransitionFinished(null /* wct */, null /* t */);
-                            }
+                synchronized (mLeashMap) {
+                    final RemoteAnimationTarget[] apps =
+                            wrap(info, false /* wallpapers */, t, mLeashMap);
+                    final RemoteAnimationTarget[] wallpapers =
+                            wrap(info, true /* wallpapers */, t, mLeashMap);
+                    final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0];
+
+                    // Set alpha back to 1 for the independent changes because we will be animating
+                    // children instead.
+                    for (TransitionInfo.Change chg : info.getChanges()) {
+                        if (TransitionInfo.isIndependent(chg, info)) {
+                            t.setAlpha(chg.getLeash(), 1.f);
                         }
-                );
+                    }
+                    initAlphaForAnimationTargets(t, apps);
+                    initAlphaForAnimationTargets(t, wallpapers);
+                    t.apply();
+                    mFinishCallback = finishCallback;
+                    runner.onAnimationStart(
+                            getTransitionOldType(info.getType(), info.getFlags(), apps),
+                            apps, wallpapers, nonApps,
+                            new IRemoteAnimationFinishedCallback.Stub() {
+                                @Override
+                                public void onAnimationFinished() throws RemoteException {
+                                    synchronized (mLeashMap) {
+                                        Slog.d(TAG, "Finish IRemoteAnimationRunner.");
+                                        finish();
+                                    }
+                                }
+                            }
+                    );
+                }
             }
 
             public void mergeAnimation(IBinder candidateTransition, TransitionInfo candidateInfo,
                     SurfaceControl.Transaction candidateT, IBinder currentTransition,
-                    IRemoteTransitionFinishedCallback candidateFinishCallback) {
+                    IRemoteTransitionFinishedCallback candidateFinishCallback)
+                    throws RemoteException {
                 try {
-                    final IRemoteTransitionFinishedCallback currentFinishCB;
-                    synchronized (mFinishCallbacks) {
-                        currentFinishCB = mFinishCallbacks.remove(currentTransition);
+                    synchronized (mLeashMap) {
+                        runner.onAnimationCancelled();
+                        finish();
                     }
-                    if (currentFinishCB == null) {
-                        Slog.e(TAG, "Called mergeAnimation, but finish callback is missing");
-                        return;
-                    }
-                    runner.onAnimationCancelled();
-                    currentFinishCB.onTransitionFinished(null /* wct */, null /* t */);
                 } catch (RemoteException e) {
                     // nothing, we'll just let it finish on its own I guess.
                 }
             }
+
+            private static void initAlphaForAnimationTargets(@NonNull SurfaceControl.Transaction t,
+                    @NonNull RemoteAnimationTarget[] targets) {
+                for (RemoteAnimationTarget target : targets) {
+                    if (target.mode != MODE_OPENING) continue;
+                    t.setAlpha(target.leash, 0.f);
+                }
+            }
+
+            @GuardedBy("mLeashMap")
+            private void finish() throws RemoteException {
+                mLeashMap.clear();
+                final IRemoteTransitionFinishedCallback finishCallback = mFinishCallback;
+                if (finishCallback != null) {
+                    mFinishCallback = null;
+                    finishCallback.onTransitionFinished(null /* wct */, null /* t */);
+                }
+            }
         };
     }
 
@@ -333,15 +344,29 @@
     };
 
     private final IKeyguardService.Stub mBinder = new IKeyguardService.Stub() {
+        private static final String TRACK_NAME = "IKeyguardService";
+
+        /**
+         * Helper for tracing the most-recent call on the IKeyguardService interface.
+         * IKeyguardService is oneway, so we are most interested in the order of the calls as they
+         * are received. We use an async track to make it easier to visualize in the trace.
+         * @param name name of the trace section
+         */
+        private static void trace(String name) {
+            Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0);
+            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, name, 0);
+        }
 
         @Override // Binder interface
         public void addStateMonitorCallback(IKeyguardStateCallback callback) {
+            trace("addStateMonitorCallback");
             checkPermission();
             mKeyguardViewMediator.addStateMonitorCallback(callback);
         }
 
         @Override // Binder interface
         public void verifyUnlock(IKeyguardExitCallback callback) {
+            trace("verifyUnlock");
             Trace.beginSection("KeyguardService.mBinder#verifyUnlock");
             checkPermission();
             mKeyguardViewMediator.verifyUnlock(callback);
@@ -350,6 +375,7 @@
 
         @Override // Binder interface
         public void setOccluded(boolean isOccluded, boolean animate) {
+            trace("setOccluded isOccluded=" + isOccluded + " animate=" + animate);
             Log.d(TAG, "setOccluded(" + isOccluded + ")");
 
             Trace.beginSection("KeyguardService.mBinder#setOccluded");
@@ -360,24 +386,28 @@
 
         @Override // Binder interface
         public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+            trace("dismiss message=" + message);
             checkPermission();
             mKeyguardViewMediator.dismiss(callback, message);
         }
 
         @Override // Binder interface
         public void onDreamingStarted() {
+            trace("onDreamingStarted");
             checkPermission();
             mKeyguardViewMediator.onDreamingStarted();
         }
 
         @Override // Binder interface
         public void onDreamingStopped() {
+            trace("onDreamingStopped");
             checkPermission();
             mKeyguardViewMediator.onDreamingStopped();
         }
 
         @Override // Binder interface
         public void onStartedGoingToSleep(@PowerManager.GoToSleepReason int pmSleepReason) {
+            trace("onStartedGoingToSleep pmSleepReason=" + pmSleepReason);
             checkPermission();
             mKeyguardViewMediator.onStartedGoingToSleep(
                     WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason));
@@ -388,6 +418,8 @@
         @Override // Binder interface
         public void onFinishedGoingToSleep(
                 @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) {
+            trace("onFinishedGoingToSleep pmSleepReason=" + pmSleepReason
+                    + " cameraGestureTriggered=" + cameraGestureTriggered);
             checkPermission();
             mKeyguardViewMediator.onFinishedGoingToSleep(
                     WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason),
@@ -399,6 +431,8 @@
         @Override // Binder interface
         public void onStartedWakingUp(
                 @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
+            trace("onStartedWakingUp pmWakeReason=" + pmWakeReason
+                    + " cameraGestureTriggered=" + cameraGestureTriggered);
             Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp");
             checkPermission();
             mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
@@ -409,6 +443,7 @@
 
         @Override // Binder interface
         public void onFinishedWakingUp() {
+            trace("onFinishedWakingUp");
             Trace.beginSection("KeyguardService.mBinder#onFinishedWakingUp");
             checkPermission();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.FINISHED_WAKING_UP);
@@ -417,6 +452,7 @@
 
         @Override // Binder interface
         public void onScreenTurningOn(IKeyguardDrawnCallback callback) {
+            trace("onScreenTurningOn");
             Trace.beginSection("KeyguardService.mBinder#onScreenTurningOn");
             checkPermission();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNING_ON,
@@ -451,6 +487,7 @@
 
         @Override // Binder interface
         public void onScreenTurnedOn() {
+            trace("onScreenTurnedOn");
             Trace.beginSection("KeyguardService.mBinder#onScreenTurnedOn");
             checkPermission();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_ON);
@@ -460,12 +497,14 @@
 
         @Override // Binder interface
         public void onScreenTurningOff() {
+            trace("onScreenTurningOff");
             checkPermission();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNING_OFF);
         }
 
         @Override // Binder interface
         public void onScreenTurnedOff() {
+            trace("onScreenTurnedOff");
             checkPermission();
             mKeyguardViewMediator.onScreenTurnedOff();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_OFF);
@@ -474,12 +513,14 @@
 
         @Override // Binder interface
         public void setKeyguardEnabled(boolean enabled) {
+            trace("setKeyguardEnabled enabled" + enabled);
             checkPermission();
             mKeyguardViewMediator.setKeyguardEnabled(enabled);
         }
 
         @Override // Binder interface
         public void onSystemReady() {
+            trace("onSystemReady");
             Trace.beginSection("KeyguardService.mBinder#onSystemReady");
             checkPermission();
             mKeyguardViewMediator.onSystemReady();
@@ -488,24 +529,28 @@
 
         @Override // Binder interface
         public void doKeyguardTimeout(Bundle options) {
+            trace("doKeyguardTimeout");
             checkPermission();
             mKeyguardViewMediator.doKeyguardTimeout(options);
         }
 
         @Override // Binder interface
         public void setSwitchingUser(boolean switching) {
+            trace("setSwitchingUser switching=" + switching);
             checkPermission();
             mKeyguardViewMediator.setSwitchingUser(switching);
         }
 
         @Override // Binder interface
         public void setCurrentUser(int userId) {
+            trace("setCurrentUser userId=" + userId);
             checkPermission();
             mKeyguardViewMediator.setCurrentUser(userId);
         }
 
-        @Override
+        @Override // Binder interface
         public void onBootCompleted() {
+            trace("onBootCompleted");
             checkPermission();
             mKeyguardViewMediator.onBootCompleted();
         }
@@ -515,28 +560,33 @@
          * {@code IRemoteAnimationRunner#onAnimationStart} instead.
          */
         @Deprecated
-        @Override
+        @Override // Binder interface
         public void startKeyguardExitAnimation(long startTime, long fadeoutDuration) {
+            trace("startKeyguardExitAnimation startTime=" + startTime
+                    + " fadeoutDuration=" + fadeoutDuration);
             Trace.beginSection("KeyguardService.mBinder#startKeyguardExitAnimation");
             checkPermission();
             mKeyguardViewMediator.startKeyguardExitAnimation(startTime, fadeoutDuration);
             Trace.endSection();
         }
 
-        @Override
+        @Override // Binder interface
         public void onShortPowerPressedGoHome() {
+            trace("onShortPowerPressedGoHome");
             checkPermission();
             mKeyguardViewMediator.onShortPowerPressedGoHome();
         }
 
-        @Override
+        @Override // Binder interface
         public void dismissKeyguardToLaunch(Intent intentToLaunch) {
+            trace("dismissKeyguardToLaunch");
             checkPermission();
             mKeyguardViewMediator.dismissKeyguardToLaunch(intentToLaunch);
         }
 
-        @Override
+        @Override // Binder interface
         public void onSystemKeyPressed(int keycode) {
+            trace("onSystemKeyPressed keycode=" + keycode);
             checkPermission();
             mKeyguardViewMediator.onSystemKeyPressed(keycode);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index f96f337..122e259 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -812,8 +812,8 @@
 
             // Translate up from the bottom.
             surfaceBehindMatrix.setTranslate(
-                    surfaceBehindRemoteAnimationTarget.localBounds.left.toFloat(),
-                    surfaceBehindRemoteAnimationTarget.localBounds.top.toFloat() +
+                    surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(),
+                    surfaceBehindRemoteAnimationTarget.screenSpaceBounds.top.toFloat() +
                             surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount)
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index d8affa4..1978b3d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -139,7 +139,8 @@
         if (dozeDisabledAndScreenOff || dozeEnabledAndDozeAnimationCompleted) {
             Trace.beginSection("ResourceTrimmer#trimMemory")
             Log.d(LOG_TAG, "SysUI asleep, trimming memory.")
-            globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+            globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+            globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
             Trace.endSection()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
new file mode 100644
index 0000000..641e20b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class KeyguardClockRepository
+@Inject
+constructor(
+    private val secureSettings: SecureSettings,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+
+    val selectedClockSize: Flow<SettingsClockSize> =
+        secureSettings
+            .observerFlow(
+                names = arrayOf(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
+                userId = UserHandle.USER_SYSTEM,
+            )
+            .onStart { emit(Unit) } // Forces an initial update.
+            .map { getClockSize() }
+
+    private suspend fun getClockSize(): SettingsClockSize {
+        return withContext(backgroundDispatcher) {
+            if (
+                secureSettings.getIntForUser(
+                    Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
+                    1,
+                    UserHandle.USER_CURRENT
+                ) == 1
+            ) {
+                SettingsClockSize.DYNAMIC
+            } else {
+                SettingsClockSize.SMALL
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
new file mode 100644
index 0000000..98f445c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -0,0 +1,34 @@
+/*
+ *  Copyright (C) 2023 The Android Open Source Project
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardClockRepository
+import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business-logic related to the keyguard clock. */
+@SysUISingleton
+class KeyguardClockInteractor
+@Inject
+constructor(
+    repository: KeyguardClockRepository,
+) {
+    val selectedClockSize: Flow<SettingsClockSize> = repository.selectedClockSize
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SettingsClockSize.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SettingsClockSize.kt
new file mode 100644
index 0000000..c6b0f58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SettingsClockSize.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.shared.model
+
+enum class SettingsClockSize {
+    DYNAMIC,
+    SMALL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockSmartspaceViewBinder.kt
new file mode 100644
index 0000000..57c32b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockSmartspaceViewBinder.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.ui.binder
+
+import android.view.View
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockSmartspaceViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.flow.collect
+
+/** Binder for the small clock view, large clock view and smartspace. */
+object KeyguardPreviewClockSmartspaceViewBinder {
+
+    @JvmStatic
+    fun bind(
+        largeClockHostView: View,
+        smallClockHostView: View,
+        smartspace: View?,
+        viewModel: KeyguardPreviewClockSmartspaceViewModel,
+    ) {
+        largeClockHostView.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.isLargeClockVisible.collect { largeClockHostView.isVisible = it }
+            }
+        }
+
+        smallClockHostView.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.isSmallClockVisible.collect { smallClockHostView.isVisible = it }
+            }
+        }
+
+        smartspace?.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.smartSpaceTopPadding.collect { smartspace.setTopPadding(it) }
+            }
+        }
+    }
+
+    private fun View.setTopPadding(padding: Int) {
+        setPaddingRelative(paddingStart, padding, paddingEnd, paddingBottom)
+    }
+}
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 555a09b..4308d84 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
@@ -22,6 +22,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.content.res.Resources
 import android.graphics.Rect
 import android.hardware.display.DisplayManager
 import android.os.Bundle
@@ -33,6 +34,7 @@
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.widget.FrameLayout
+import androidx.core.view.isInvisible
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.R
@@ -40,7 +42,10 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockSmartspaceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockSmartspaceViewModel
+import com.android.systemui.plugins.ClockController
 import com.android.systemui.shared.clocks.ClockRegistry
 import com.android.systemui.shared.clocks.DefaultClockController
 import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
@@ -60,6 +65,7 @@
     @Application private val context: Context,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Main private val mainHandler: Handler,
+    private val clockSmartspaceViewModel: KeyguardPreviewClockSmartspaceViewModel,
     private val bottomAreaViewModel: KeyguardBottomAreaViewModel,
     displayManager: DisplayManager,
     private val windowManager: WindowManager,
@@ -79,6 +85,7 @@
             KeyguardPreviewConstants.KEY_HIGHLIGHT_QUICK_AFFORDANCES,
             false,
         )
+    /** [shouldHideClock] here means that we never create and bind the clock views */
     private val shouldHideClock: Boolean =
         bundle.getBoolean(ClockPreviewConstants.KEY_HIDE_CLOCK, false)
 
@@ -87,7 +94,8 @@
     val surfacePackage: SurfaceControlViewHost.SurfacePackage
         get() = host.surfacePackage
 
-    private var clockView: View? = null
+    private lateinit var largeClockHostView: FrameLayout
+    private lateinit var smallClockHostView: FrameLayout
     private var smartSpaceView: View? = null
     private var colorOverride: Int? = null
 
@@ -126,6 +134,12 @@
 
             if (!shouldHideClock) {
                 setUpClock(rootView)
+                KeyguardPreviewClockSmartspaceViewBinder.bind(
+                    largeClockHostView,
+                    smallClockHostView,
+                    smartSpaceView,
+                    clockSmartspaceViewModel,
+                )
             }
 
             rootView.measure(
@@ -205,11 +219,9 @@
         smartSpaceView = lockscreenSmartspaceController.buildAndConnectDateView(parentView)
 
         val topPadding: Int =
-            with(context.resources) {
-                getDimensionPixelSize(R.dimen.status_bar_header_height_keyguard) +
-                    getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
-                    getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
-            }
+            KeyguardPreviewClockSmartspaceViewModel.getLargeClockSmartspaceTopPadding(
+                context.resources
+            )
 
         val startPadding: Int =
             with(context.resources) {
@@ -284,10 +296,19 @@
     }
 
     private fun setUpClock(parentView: ViewGroup) {
+        largeClockHostView = createLargeClockHostView()
+        largeClockHostView.isInvisible = true
+        parentView.addView(largeClockHostView)
+
+        smallClockHostView = createSmallClockHostView(parentView.resources)
+        smallClockHostView.isInvisible = true
+        parentView.addView(smallClockHostView)
+
+        // TODO (b/283465254): Move the listeners to KeyguardClockRepository
         val clockChangeListener =
             object : ClockRegistry.ClockChangeListener {
                 override fun onCurrentClockChanged() {
-                    onClockChanged(parentView)
+                    onClockChanged()
                 }
             }
         clockRegistry.registerClockChangeListener(clockChangeListener)
@@ -317,62 +338,89 @@
         disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
 
         val layoutChangeListener =
-            object : View.OnLayoutChangeListener {
-                override fun onLayoutChange(
-                    v: View,
-                    left: Int,
-                    top: Int,
-                    right: Int,
-                    bottom: Int,
-                    oldLeft: Int,
-                    oldTop: Int,
-                    oldRight: Int,
-                    oldBottom: Int
-                ) {
-                    if (clockController.clock !is DefaultClockController) {
-                        clockController.clock
-                            ?.largeClock
-                            ?.events
-                            ?.onTargetRegionChanged(
-                                KeyguardClockSwitch.getLargeClockRegion(parentView)
-                            )
-                    }
+            View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+                if (clockController.clock !is DefaultClockController) {
+                    clockController.clock
+                        ?.largeClock
+                        ?.events
+                        ?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView))
                 }
             }
-
         parentView.addOnLayoutChangeListener(layoutChangeListener)
-
         disposables.add(
             DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
         )
 
-        onClockChanged(parentView)
+        onClockChanged()
     }
 
-    private fun onClockChanged(parentView: ViewGroup) {
+    private fun createLargeClockHostView(): FrameLayout {
+        val hostView = FrameLayout(context)
+        hostView.layoutParams =
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT,
+            )
+        return hostView
+    }
+
+    private fun createSmallClockHostView(resources: Resources): FrameLayout {
+        val hostView = FrameLayout(context)
+        val layoutParams =
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.WRAP_CONTENT,
+                resources.getDimensionPixelSize(R.dimen.small_clock_height)
+            )
+        layoutParams.topMargin =
+            KeyguardPreviewClockSmartspaceViewModel.getStatusBarHeight(resources) +
+                resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+        hostView.layoutParams = layoutParams
+
+        hostView.setPaddingRelative(
+            resources.getDimensionPixelSize(R.dimen.clock_padding_start),
+            0,
+            0,
+            0
+        )
+        hostView.clipChildren = false
+        return hostView
+    }
+
+    private fun onClockChanged() {
         val clock = clockRegistry.createCurrentClock()
         clockController.clock = clock
 
         colorOverride?.let { clock.events.onSeedColorChanged(it) }
 
-        clock.largeClock.events.onTargetRegionChanged(
-            KeyguardClockSwitch.getLargeClockRegion(parentView)
-        )
-
-        clockView?.let { parentView.removeView(it) }
-        clockView =
-            clock.largeClock.view.apply {
-                if (shouldHighlightSelectedAffordance) {
-                    alpha = DIM_ALPHA
-                }
-                parentView.addView(this)
-                visibility = View.VISIBLE
-            }
+        updateLargeClock(clock)
+        updateSmallClock(clock)
 
         // Hide smart space if the clock has weather display; otherwise show it
         hideSmartspace(clock.largeClock.config.hasCustomWeatherDataDisplay)
     }
 
+    private fun updateLargeClock(clock: ClockController) {
+        clock.largeClock.events.onTargetRegionChanged(
+            KeyguardClockSwitch.getLargeClockRegion(largeClockHostView)
+        )
+        if (shouldHighlightSelectedAffordance) {
+            clock.largeClock.view.alpha = DIM_ALPHA
+        }
+        largeClockHostView.removeAllViews()
+        largeClockHostView.addView(clock.largeClock.view)
+    }
+
+    private fun updateSmallClock(clock: ClockController) {
+        clock.smallClock.events.onTargetRegionChanged(
+            KeyguardClockSwitch.getSmallClockRegion(smallClockHostView)
+        )
+        if (shouldHighlightSelectedAffordance) {
+            clock.smallClock.view.alpha = DIM_ALPHA
+        }
+        smallClockHostView.removeAllViews()
+        smallClockHostView.addView(clock.smallClock.view)
+    }
+
     companion object {
         private const val KEY_HOST_TOKEN = "host_token"
         private const val KEY_VIEW_WIDTH = "width"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockSmartspaceViewModel.kt
new file mode 100644
index 0000000..00c603b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockSmartspaceViewModel.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Resources
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** View model for the small clock view, large clock view and smartspace. */
+class KeyguardPreviewClockSmartspaceViewModel
+@Inject
+constructor(
+    @Application private val context: Context,
+    interactor: KeyguardClockInteractor,
+) {
+
+    val isLargeClockVisible: Flow<Boolean> =
+        interactor.selectedClockSize.map { it == SettingsClockSize.DYNAMIC }
+
+    val isSmallClockVisible: Flow<Boolean> =
+        interactor.selectedClockSize.map { it == SettingsClockSize.SMALL }
+
+    val smartSpaceTopPadding: Flow<Int> =
+        interactor.selectedClockSize.map {
+            when (it) {
+                SettingsClockSize.DYNAMIC -> getLargeClockSmartspaceTopPadding(context.resources)
+                SettingsClockSize.SMALL -> getSmallClockSmartspaceTopPadding(context.resources)
+            }
+        }
+
+    companion object {
+        fun getLargeClockSmartspaceTopPadding(resources: Resources): Int {
+            return with(resources) {
+                getDimensionPixelSize(R.dimen.status_bar_header_height_keyguard) +
+                    getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
+                    getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
+            }
+        }
+
+        fun getSmallClockSmartspaceTopPadding(resources: Resources): Int {
+            return with(resources) {
+                getStatusBarHeight(this) +
+                    getDimensionPixelSize(R.dimen.small_clock_padding_top) +
+                    getDimensionPixelSize(R.dimen.small_clock_height)
+            }
+        }
+
+        fun getStatusBarHeight(resource: Resources): Int {
+            var result = 0
+            val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android")
+            if (resourceId > 0) {
+                result = resource.getDimensionPixelSize(resourceId)
+            }
+            return result
+        }
+    }
+}
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 1469d96..bce3346 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
@@ -16,10 +16,12 @@
 
 package com.android.systemui.media.controls.pipeline
 
+import android.annotation.SuppressLint
 import android.app.BroadcastOptions
 import android.app.Notification
 import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
 import android.app.PendingIntent
+import android.app.StatusBarManager
 import android.app.smartspace.SmartspaceConfig
 import android.app.smartspace.SmartspaceManager
 import android.app.smartspace.SmartspaceSession
@@ -43,7 +45,6 @@
 import android.net.Uri
 import android.os.Parcelable
 import android.os.Process
-import android.os.RemoteException
 import android.os.UserHandle
 import android.provider.Settings
 import android.service.notification.StatusBarNotification
@@ -53,7 +54,6 @@
 import android.util.Pair as APair
 import androidx.media.utils.MediaConstants
 import com.android.internal.logging.InstanceId
-import com.android.internal.statusbar.IStatusBarService
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.Dumpable
 import com.android.systemui.R
@@ -185,7 +185,6 @@
     private val logger: MediaUiEventLogger,
     private val smartspaceManager: SmartspaceManager,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val statusBarService: IStatusBarService,
 ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
 
     companion object {
@@ -230,6 +229,10 @@
     private val artworkHeight =
         context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
 
+    @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
+    private val statusBarManager =
+        context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
+
     /** Check whether this notification is an RCN */
     private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
         return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
@@ -257,7 +260,6 @@
         mediaFlags: MediaFlags,
         logger: MediaUiEventLogger,
         smartspaceManager: SmartspaceManager,
-        statusBarService: IStatusBarService,
         keyguardUpdateMonitor: KeyguardUpdateMonitor,
     ) : this(
         context,
@@ -283,7 +285,6 @@
         logger,
         smartspaceManager,
         keyguardUpdateMonitor,
-        statusBarService,
     )
 
     private val appChangeReceiver =
@@ -793,27 +794,12 @@
             song = HybridGroupManager.resolveTitle(notif)
         }
         if (song.isNullOrBlank()) {
-            if (mediaFlags.isMediaTitleRequired(sbn.packageName, sbn.user)) {
-                // App is required to provide a title: cancel the underlying notification
-                try {
-                    statusBarService.onNotificationError(
-                        sbn.packageName,
-                        sbn.tag,
-                        sbn.id,
-                        sbn.uid,
-                        sbn.initialPid,
-                        MEDIA_TITLE_ERROR_MESSAGE,
-                        sbn.user.identifier
-                    )
-                } catch (e: RemoteException) {
-                    Log.e(TAG, "cancelNotification failed: $e")
-                }
-                // Only add log for media removed if active media is updated with invalid title.
-                foregroundExecutor.execute { removeEntry(key, !isNewlyActiveEntry) }
-                return
-            } else {
-                // For apps that don't have the title requirement yet, add a placeholder
-                song = context.getString(R.string.controls_media_empty_title, appName)
+            // For apps that don't include a title, log and add a placeholder
+            song = context.getString(R.string.controls_media_empty_title, appName)
+            try {
+                statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
+            } catch (e: RuntimeException) {
+                Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 3751c60..9bc66f6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -64,9 +64,4 @@
 
     /** Check whether we allow remote media to generate resume controls */
     fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME)
-
-    /** Check whether app is required to provide a non-empty media title */
-    fun isMediaTitleRequired(packageName: String, user: UserHandle): Boolean {
-        return StatusBarManager.isMediaTitleRequiredForApp(packageName, user)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 5818fd0..e524189 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -38,6 +38,7 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.hardware.input.InputManager;
+import android.icu.text.SimpleDateFormat;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -99,7 +100,9 @@
 import java.io.PrintWriter;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.Executor;
@@ -281,6 +284,8 @@
     private LogArray mPredictionLog = new LogArray(MAX_NUM_LOGGED_PREDICTIONS);
     private LogArray mGestureLogInsideInsets = new LogArray(MAX_NUM_LOGGED_GESTURES);
     private LogArray mGestureLogOutsideInsets = new LogArray(MAX_NUM_LOGGED_GESTURES);
+    private SimpleDateFormat mLogDateFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.US);
+    private Date mTmpLogDate = new Date();
 
     private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver;
 
@@ -969,11 +974,17 @@
             }
 
             // For debugging purposes, only log edge points
+            long curTime = System.currentTimeMillis();
+            mTmpLogDate.setTime(curTime);
+            String curTimeStr = mLogDateFormat.format(mTmpLogDate);
             (isWithinInsets ? mGestureLogInsideInsets : mGestureLogOutsideInsets).log(String.format(
-                    "Gesture [%d,alw=%B,%B,%B,%B,%B,%B,disp=%s,wl=%d,il=%d,wr=%d,ir=%d,excl=%s]",
-                    System.currentTimeMillis(), isTrackpadMultiFingerSwipe, mAllowGesture,
+                    "Gesture [%d [%s],alw=%B, mltf=%B, left=%B, defLeft=%B, backAlw=%B, disbld=%B,"
+                            + " qsDisbld=%b, blkdAct=%B, pip=%B,"
+                            + " disp=%s, wl=%d, il=%d, wr=%d, ir=%d, excl=%s]",
+                    curTime, curTimeStr, mAllowGesture, isTrackpadMultiFingerSwipe,
                     mIsOnLeftEdge, mDeferSetIsOnLeftEdge, mIsBackGestureAllowed,
-                    QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisplaySize,
+                    QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisabledForQuickstep,
+                    mGestureBlockingActivityRunning, mIsInPip, mDisplaySize,
                     mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion));
         } else if (mAllowGesture || mLogGesture) {
             if (!mThresholdCrossed) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index fb4feb8..a532195 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -33,7 +33,6 @@
 import android.content.Context;
 import android.graphics.drawable.Icon;
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
@@ -317,7 +316,7 @@
                 IBiometricSysuiReceiver receiver,
                 int[] sensorIds, boolean credentialAllowed,
                 boolean requireConfirmation, int userId, long operationId, String opPackageName,
-                long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
+                long requestId) {
         }
 
         /** @see IStatusBar#onBiometricAuthenticated(int) */
@@ -956,8 +955,7 @@
     @Override
     public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
             int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
-            int userId, long operationId, String opPackageName, long requestId,
-            @BiometricMultiSensorMode int multiSensorConfig) {
+            int userId, long operationId, String opPackageName, long requestId) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = promptInfo;
@@ -969,7 +967,6 @@
             args.arg6 = opPackageName;
             args.argl1 = operationId;
             args.argl2 = requestId;
-            args.argi2 = multiSensorConfig;
             mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args)
                     .sendToTarget();
         }
@@ -1573,8 +1570,7 @@
                                 someArgs.argi1 /* userId */,
                                 someArgs.argl1 /* operationId */,
                                 (String) someArgs.arg6 /* opPackageName */,
-                                someArgs.argl2 /* requestId */,
-                                someArgs.argi2 /* multiSensorConfig */);
+                                someArgs.argl2 /* requestId */);
                     }
                     someArgs.recycle();
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index 993c3801..b956207 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -354,7 +354,11 @@
 
     @Override
     protected void snapChild(final View animView, final float targetLeft, float velocity) {
-        superSnapChild(animView, targetLeft, velocity);
+        if (animView instanceof SwipeableView) {
+            // only perform the snapback animation on views that are swipeable inside the shade.
+            superSnapChild(animView, targetLeft, velocity);
+        }
+
         mCallback.onDragCancelled(animView);
         if (targetLeft == 0) {
             handleMenuCoveredOrDismissed();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index a6b2bd8..f26a84b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -30,6 +30,8 @@
 import android.view.ViewDebug;
 import android.view.WindowInsetsController.Appearance;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.colorextraction.ColorExtractor.GradientColors;
 import com.android.internal.view.AppearanceRegion;
 import com.android.systemui.Dumpable;
@@ -46,7 +48,6 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Date;
 
 import javax.inject.Inject;
 
@@ -57,7 +58,8 @@
 public class LightBarController implements BatteryController.BatteryStateChangeCallback, Dumpable {
 
     private static final String TAG = "LightBarController";
-    private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean DEBUG_NAVBAR = Compile.IS_DEBUG;
+    private static final boolean DEBUG_LOGS = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
 
     private static final float NAV_BAR_INVERSION_SCRIM_ALPHA_THRESHOLD = 0.1f;
 
@@ -113,6 +115,7 @@
 
     private String mLastSetScrimStateLog;
     private String mLastNavigationBarAppearanceChangedLog;
+    private StringBuilder mLogStringBuilder = null;
 
     @Inject
     public LightBarController(
@@ -193,35 +196,43 @@
                 final boolean darkForTop = darkForQs || mGlobalActionsVisible;
                 mNavigationLight =
                         ((mHasLightNavigationBar && !darkForScrim) || lightForScrim) && !darkForTop;
-                mLastNavigationBarAppearanceChangedLog = "onNavigationBarAppearanceChanged()"
-                        + " appearance=" + appearance
-                        + " nbModeChanged=" + nbModeChanged
-                        + " navigationBarMode=" + navigationBarMode
-                        + " navbarColorManagedByIme=" + navbarColorManagedByIme
-                        + " mHasLightNavigationBar=" + mHasLightNavigationBar
-                        + " ignoreScrimForce=" + ignoreScrimForce
-                        + " darkForScrim=" + darkForScrim
-                        + " lightForScrim=" + lightForScrim
-                        + " darkForQs=" + darkForQs
-                        + " darkForTop=" + darkForTop
-                        + " mNavigationLight=" + mNavigationLight
-                        + " last=" + last
-                        + " timestamp=" + new Date();
-                if (DEBUG) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+                if (DEBUG_NAVBAR) {
+                    mLastNavigationBarAppearanceChangedLog = getLogStringBuilder()
+                            .append("onNavigationBarAppearanceChanged()")
+                            .append(" appearance=").append(appearance)
+                            .append(" nbModeChanged=").append(nbModeChanged)
+                            .append(" navigationBarMode=").append(navigationBarMode)
+                            .append(" navbarColorManagedByIme=").append(navbarColorManagedByIme)
+                            .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+                            .append(" ignoreScrimForce=").append(ignoreScrimForce)
+                            .append(" darkForScrim=").append(darkForScrim)
+                            .append(" lightForScrim=").append(lightForScrim)
+                            .append(" darkForQs=").append(darkForQs)
+                            .append(" darkForTop=").append(darkForTop)
+                            .append(" mNavigationLight=").append(mNavigationLight)
+                            .append(" last=").append(last)
+                            .append(" timestamp=").append(System.currentTimeMillis())
+                            .toString();
+                    if (DEBUG_LOGS) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+                }
             } else {
                 mNavigationLight = mHasLightNavigationBar
                         && (mDirectReplying && mNavbarColorManagedByIme || !mForceDarkForScrim)
                         && !mQsCustomizing;
-                mLastNavigationBarAppearanceChangedLog = "onNavigationBarAppearanceChanged()"
-                        + " appearance=" + appearance
-                        + " nbModeChanged=" + nbModeChanged
-                        + " navigationBarMode=" + navigationBarMode
-                        + " navbarColorManagedByIme=" + navbarColorManagedByIme
-                        + " mHasLightNavigationBar=" + mHasLightNavigationBar
-                        + " mNavigationLight=" + mNavigationLight
-                        + " last=" + last
-                        + " timestamp=" + new Date();
-                if (DEBUG) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+                if (DEBUG_NAVBAR) {
+                    mLastNavigationBarAppearanceChangedLog = getLogStringBuilder()
+                            .append("onNavigationBarAppearanceChanged()")
+                            .append(" appearance=").append(appearance)
+                            .append(" nbModeChanged=").append(nbModeChanged)
+                            .append(" navigationBarMode=").append(navigationBarMode)
+                            .append(" navbarColorManagedByIme=").append(navbarColorManagedByIme)
+                            .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+                            .append(" mNavigationLight=").append(mNavigationLight)
+                            .append(" last=").append(last)
+                            .append(" timestamp=").append(System.currentTimeMillis())
+                            .toString();
+                    if (DEBUG_LOGS) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+                }
             }
             if (mNavigationLight != last) {
                 updateNavigation();
@@ -319,18 +330,22 @@
             } else {
                 if (mForceLightForScrim != forceLightForScrimLast) reevaluate();
             }
-            mLastSetScrimStateLog = "setScrimState()"
-                    + " scrimState=" + scrimState
-                    + " scrimBehindAlpha=" + scrimBehindAlpha
-                    + " scrimInFrontColor=" + scrimInFrontColor
-                    + " forceForScrim=" + forceForScrim
-                    + " scrimColorIsLight=" + scrimColorIsLight
-                    + " mHasLightNavigationBar=" + mHasLightNavigationBar
-                    + " mBouncerVisible=" + mBouncerVisible
-                    + " mForceDarkForScrim=" + mForceDarkForScrim
-                    + " mForceLightForScrim=" + mForceLightForScrim
-                    + " timestamp=" + new Date();
-            if (DEBUG) Log.d(TAG, mLastSetScrimStateLog);
+            if (DEBUG_NAVBAR) {
+                mLastSetScrimStateLog = getLogStringBuilder()
+                        .append("setScrimState()")
+                        .append(" scrimState=").append(scrimState)
+                        .append(" scrimBehindAlpha=").append(scrimBehindAlpha)
+                        .append(" scrimInFrontColor=").append(scrimInFrontColor)
+                        .append(" forceForScrim=").append(forceForScrim)
+                        .append(" scrimColorIsLight=").append(scrimColorIsLight)
+                        .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+                        .append(" mBouncerVisible=").append(mBouncerVisible)
+                        .append(" mForceDarkForScrim=").append(mForceDarkForScrim)
+                        .append(" mForceLightForScrim=").append(mForceLightForScrim)
+                        .append(" timestamp=").append(System.currentTimeMillis())
+                        .toString();
+                if (DEBUG_LOGS) Log.d(TAG, mLastSetScrimStateLog);
+            }
         } else {
             boolean forceDarkForScrimLast = mForceDarkForScrim;
             // For BOUNCER/BOUNCER_SCRIMMED cases, we assume that alpha is always below threshold.
@@ -344,17 +359,30 @@
             if (mHasLightNavigationBar && (mForceDarkForScrim != forceDarkForScrimLast)) {
                 reevaluate();
             }
-            mLastSetScrimStateLog = "setScrimState()"
-                    + " scrimState=" + scrimState
-                    + " scrimBehindAlpha=" + scrimBehindAlpha
-                    + " scrimInFrontColor=" + scrimInFrontColor
-                    + " mHasLightNavigationBar=" + mHasLightNavigationBar
-                    + " mForceDarkForScrim=" + mForceDarkForScrim
-                    + " timestamp=" + new Date();
-            if (DEBUG) Log.d(TAG, mLastSetScrimStateLog);
+            if (DEBUG_NAVBAR) {
+                mLastSetScrimStateLog = getLogStringBuilder()
+                        .append("setScrimState()")
+                        .append(" scrimState=").append(scrimState)
+                        .append(" scrimBehindAlpha=").append(scrimBehindAlpha)
+                        .append(" scrimInFrontColor=").append(scrimInFrontColor)
+                        .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+                        .append(" mForceDarkForScrim=").append(mForceDarkForScrim)
+                        .append(" timestamp=").append(System.currentTimeMillis())
+                        .toString();
+                if (DEBUG_LOGS) Log.d(TAG, mLastSetScrimStateLog);
+            }
         }
     }
 
+    @NonNull
+    private StringBuilder getLogStringBuilder() {
+        if (mLogStringBuilder == null) {
+            mLogStringBuilder = new StringBuilder();
+        }
+        mLogStringBuilder.setLength(0);
+        return mLogStringBuilder;
+    }
+
     private static boolean isLight(int appearance, int barMode, int flag) {
         final boolean isTransparentBar = (barMode == MODE_TRANSPARENT
                 || barMode == MODE_LIGHTS_OUT_TRANSPARENT);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 5ba02fa..b24a692 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -73,7 +73,6 @@
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.VibrationEffect;
-import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.text.InputFilter;
@@ -113,7 +112,6 @@
 import com.android.app.animation.Interpolators;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.view.RotationPolicy;
@@ -133,15 +131,11 @@
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.AlphaTintDrawableWrapper;
-import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.RoundedCornerProgressDrawable;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -198,9 +192,6 @@
     private ViewGroup mDialogRowsView;
     private ViewGroup mRinger;
 
-    private DeviceConfigProxy mDeviceConfigProxy;
-    private Executor mExecutor;
-
     /**
      * Container for the top part of the dialog, which contains the ringer, the ringer drawer, the
      * volume rows, and the ellipsis button. This does not include the live caption button.
@@ -290,14 +281,12 @@
     private BackgroundBlurDrawable mDialogRowsViewBackground;
     private final InteractionJankMonitor mInteractionJankMonitor;
 
-    private boolean mSeparateNotification;
-
     private int mWindowGravity;
 
     @VisibleForTesting
-    int mVolumeRingerIconDrawableId;
+    final int mVolumeRingerIconDrawableId = R.drawable.ic_speaker_on;
     @VisibleForTesting
-    int mVolumeRingerMuteIconDrawableId;
+    final int mVolumeRingerMuteIconDrawableId = R.drawable.ic_speaker_mute;
 
     private int mOriginalGravity;
     private final DevicePostureController.Callback mDevicePostureControllerCallback;
@@ -315,8 +304,6 @@
             VolumePanelFactory volumePanelFactory,
             ActivityStarter activityStarter,
             InteractionJankMonitor interactionJankMonitor,
-            DeviceConfigProxy deviceConfigProxy,
-            Executor executor,
             CsdWarningDialog.Factory csdWarningDialogFactory,
             DevicePostureController devicePostureController,
             Looper looper,
@@ -374,12 +361,6 @@
         } else {
             mDevicePostureControllerCallback = null;
         }
-
-        mDeviceConfigProxy = deviceConfigProxy;
-        mExecutor = executor;
-        mSeparateNotification = mDeviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
-        updateRingerModeIconSet();
     }
 
     /**
@@ -401,44 +382,6 @@
         return mWindowGravity;
     }
 
-    /**
-     * If ringer and notification are the same stream (T and earlier), use notification-like bell
-     * icon set.
-     * If ringer and notification are separated, then use generic speaker icons.
-     */
-    private void updateRingerModeIconSet() {
-        if (mSeparateNotification) {
-            mVolumeRingerIconDrawableId = R.drawable.ic_speaker_on;
-            mVolumeRingerMuteIconDrawableId = R.drawable.ic_speaker_mute;
-        } else {
-            mVolumeRingerIconDrawableId = R.drawable.ic_volume_ringer;
-            mVolumeRingerMuteIconDrawableId = R.drawable.ic_volume_ringer_mute;
-        }
-
-        if (mRingerDrawerMuteIcon != null) {
-            mRingerDrawerMuteIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
-        }
-        if (mRingerDrawerNormalIcon != null) {
-            mRingerDrawerNormalIcon.setImageResource(mVolumeRingerIconDrawableId);
-        }
-    }
-
-    /**
-     * Change icon for ring stream (not ringer mode icon)
-     */
-    private void updateRingRowIcon() {
-        Optional<VolumeRow> volumeRow = mRows.stream().filter(row -> row.stream == STREAM_RING)
-                .findFirst();
-        if (volumeRow.isPresent()) {
-            VolumeRow volRow = volumeRow.get();
-            volRow.iconRes = mSeparateNotification ? R.drawable.ic_ring_volume
-                    : R.drawable.ic_volume_ringer;
-            volRow.iconMuteRes = mSeparateNotification ? R.drawable.ic_ring_volume_off
-                    : R.drawable.ic_volume_ringer_mute;
-            volRow.setIcon(volRow.iconRes, mContext.getTheme());
-        }
-    }
-
     @Override
     public void onUiModeChanged() {
         mContext.getTheme().applyStyle(mContext.getThemeResId(), true);
@@ -454,9 +397,6 @@
 
         mConfigurationController.addCallback(this);
 
-        mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
-                mExecutor, this::onDeviceConfigChange);
-
         if (mDevicePostureController != null) {
             mDevicePostureController.addCallback(mDevicePostureControllerCallback);
         }
@@ -464,31 +404,15 @@
 
     @Override
     public void destroy() {
+        Log.d(TAG, "destroy() called");
         mController.removeCallback(mControllerCallbackH);
         mHandler.removeCallbacksAndMessages(null);
         mConfigurationController.removeCallback(this);
-        mDeviceConfigProxy.removeOnPropertiesChangedListener(this::onDeviceConfigChange);
         if (mDevicePostureController != null) {
             mDevicePostureController.removeCallback(mDevicePostureControllerCallback);
         }
     }
 
-    /**
-     * Update ringer mode icon based on the config
-     */
-    private void onDeviceConfigChange(DeviceConfig.Properties properties) {
-        Set<String> changeSet = properties.getKeyset();
-        if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) {
-            boolean newVal = properties.getBoolean(
-                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
-            if (newVal != mSeparateNotification) {
-                mSeparateNotification = newVal;
-                updateRingerModeIconSet();
-                updateRingRowIcon();
-            }
-        }
-    }
-
     @Override
     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo internalInsetsInfo) {
         // Set touchable region insets on the root dialog view. This tells WindowManager that
@@ -542,6 +466,7 @@
     }
 
     private void initDialog(int lockTaskModeState) {
+        Log.d(TAG, "initDialog: called!");
         mDialog = new CustomDialog(mContext);
         initDimens();
 
@@ -699,7 +624,12 @@
         mRingerDrawerNormalIcon = mDialog.findViewById(R.id.volume_drawer_normal_icon);
         mRingerDrawerNewSelectionBg = mDialog.findViewById(R.id.volume_drawer_selection_background);
 
-        updateRingerModeIconSet();
+        if (mRingerDrawerMuteIcon != null) {
+            mRingerDrawerMuteIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
+        }
+        if (mRingerDrawerNormalIcon != null) {
+            mRingerDrawerNormalIcon.setImageResource(mVolumeRingerIconDrawableId);
+        }
 
         setupRingerDrawer();
 
@@ -724,13 +654,10 @@
             addRow(AudioManager.STREAM_MUSIC,
                     R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
             if (!AudioSystem.isSingleVolume(mContext)) {
-                if (mSeparateNotification) {
-                    addRow(AudioManager.STREAM_RING, R.drawable.ic_ring_volume,
-                            R.drawable.ic_ring_volume_off, true, false);
-                } else {
-                    addRow(AudioManager.STREAM_RING, R.drawable.ic_volume_ringer,
-                            R.drawable.ic_volume_ringer, true, false);
-                }
+
+                addRow(AudioManager.STREAM_RING, R.drawable.ic_ring_volume,
+                        R.drawable.ic_ring_volume_off, true, false);
+
 
                 addRow(STREAM_ALARM,
                         R.drawable.ic_alarm, R.drawable.ic_volume_alarm_mute, true, false);
@@ -1344,7 +1271,7 @@
     }
 
     protected void tryToRemoveCaptionsTooltip() {
-        if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) {
+        if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null && mDialog != null) {
             ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container);
             container.removeView(mODICaptionsTooltipView);
             mODICaptionsTooltipView = null;
@@ -1551,8 +1478,16 @@
 
         mHandler.removeMessages(H.DISMISS);
         mHandler.removeMessages(H.SHOW);
-        if (mIsAnimatingDismiss) {
-            Log.d(TAG, "dismissH: isAnimatingDismiss");
+
+        boolean showingStateInconsistent = !mShowing && mDialog != null && mDialog.isShowing();
+        // If incorrectly assuming dialog is not showing, continue and make the state consistent.
+        if (showingStateInconsistent) {
+            Log.d(TAG, "dismissH: volume dialog possible in inconsistent state:"
+                    + "mShowing=" + mShowing + ", mDialog==null?" + (mDialog == null));
+        }
+        if (mIsAnimatingDismiss && !showingStateInconsistent) {
+            Log.d(TAG, "dismissH: skipping dismiss because isAnimatingDismiss is true"
+                    + " and showingStateInconsistent is false");
             Trace.endSection();
             return;
         }
@@ -1570,8 +1505,12 @@
                 .setDuration(mDialogHideAnimationDurationMs)
                 .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
                 .withEndAction(() -> mHandler.postDelayed(() -> {
-                    mController.notifyVisible(false);
-                    mDialog.dismiss();
+                    if (mController != null) {
+                        mController.notifyVisible(false);
+                    }
+                    if (mDialog != null) {
+                        mDialog.dismiss();
+                    }
                     tryToRemoveCaptionsTooltip();
                     mIsAnimatingDismiss = false;
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index bb04f82..aa4ee54 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -21,7 +21,6 @@
 import android.os.Looper;
 
 import com.android.internal.jank.InteractionJankMonitor;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
@@ -31,7 +30,6 @@
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.volume.CsdWarningDialog;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.volume.VolumeDialogComponent;
@@ -42,8 +40,6 @@
 import dagger.Module;
 import dagger.Provides;
 
-import java.util.concurrent.Executor;
-
 /** Dagger Module for code in the volume package. */
 @Module
 public interface VolumeModule {
@@ -63,8 +59,6 @@
             VolumePanelFactory volumePanelFactory,
             ActivityStarter activityStarter,
             InteractionJankMonitor interactionJankMonitor,
-            DeviceConfigProxy deviceConfigProxy,
-            @Main Executor executor,
             CsdWarningDialog.Factory csdFactory,
             DevicePostureController devicePostureController,
             DumpManager dumpManager) {
@@ -78,8 +72,6 @@
                 volumePanelFactory,
                 activityStarter,
                 interactionJankMonitor,
-                deviceConfigProxy,
-                executor,
                 csdFactory,
                 devicePostureController,
                 Looper.getMainLooper(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 44c9905..1990c8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -145,50 +145,59 @@
     @Test
     fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
         testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
             assertThat(isUnlocked).isFalse()
 
             assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
             assertThat(isUnlocked).isTrue()
+            assertThat(failedAttemptCount).isEqualTo(0)
         }
 
     @Test
     fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
         testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
             assertThat(isUnlocked).isFalse()
 
             assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
             assertThat(isUnlocked).isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
         }
 
     @Test
     fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
         testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
             assertThat(isUnlocked).isFalse()
 
             assertThat(underTest.authenticate("password".toList())).isTrue()
             assertThat(isUnlocked).isTrue()
+            assertThat(failedAttemptCount).isEqualTo(0)
         }
 
     @Test
     fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
         testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
             assertThat(isUnlocked).isFalse()
 
             assertThat(underTest.authenticate("alohomora".toList())).isFalse()
             assertThat(isUnlocked).isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
         }
 
     @Test
     fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
         testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             underTest.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(
@@ -230,11 +239,13 @@
                 )
                 .isTrue()
             assertThat(isUnlocked).isTrue()
+            assertThat(failedAttemptCount).isEqualTo(0)
         }
 
     @Test
     fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
         testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             underTest.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(
@@ -276,6 +287,7 @@
                 )
                 .isFalse()
             assertThat(isUnlocked).isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
index 213dc87..2d1e8a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
@@ -73,7 +73,7 @@
 
     @Test
     fun fingerprintSuccessDoesNotRequireExplicitConfirmation() {
-        biometricView.onDialogAnimatedIn()
+        biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
         biometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT)
         TestableLooper.get(this).moveTimeForward(1000)
         waitForIdleSync()
@@ -84,7 +84,7 @@
 
     @Test
     fun faceSuccessRequiresExplicitConfirmation() {
-        biometricView.onDialogAnimatedIn()
+        biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
         biometricView.onAuthenticationSucceeded(TYPE_FACE)
         waitForIdleSync()
 
@@ -104,7 +104,7 @@
 
     @Test
     fun ignoresFaceErrors_faceIsNotClass3_notLockoutError() {
-        biometricView.onDialogAnimatedIn()
+        biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
         biometricView.onError(TYPE_FACE, "not a face")
         waitForIdleSync()
 
@@ -121,7 +121,7 @@
     @Test
     fun doNotIgnoresFaceErrors_faceIsClass3_notLockoutError() {
         biometricView.isFaceClass3 = true
-        biometricView.onDialogAnimatedIn()
+        biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
         biometricView.onError(TYPE_FACE, "not a face")
         waitForIdleSync()
 
@@ -138,7 +138,7 @@
     @Test
     fun doNotIgnoresFaceErrors_faceIsClass3_lockoutError() {
         biometricView.isFaceClass3 = true
-        biometricView.onDialogAnimatedIn()
+        biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
         biometricView.onError(
             TYPE_FACE,
             FaceManager.getErrorString(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
index 22ebc7e..8e5d96b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
@@ -120,7 +120,7 @@
 
     @Test
     fun testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
-        biometricView.onDialogAnimatedIn()
+        biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
         biometricView.mNegativeButton.performClick()
         TestableLooper.get(this).moveTimeForward(1000)
         waitForIdleSync()
@@ -212,7 +212,7 @@
     @Test
     fun testIgnoresUselessHelp() {
         biometricView.mAnimationDurationHideDialog = 10_000
-        biometricView.onDialogAnimatedIn()
+        biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
         waitForIdleSync()
 
         assertThat(biometricView.isAuthenticating).isTrue()
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 9d68cf3..d31a86a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -41,11 +41,15 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.FakePromptRepository
 import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
-import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
 import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
@@ -53,29 +57,34 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import org.junit.After
+import org.junit.Before
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.anyLong
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
-class AuthContainerViewTest : SysuiTestCase() {
+open class AuthContainerViewTest : SysuiTestCase() {
 
     @JvmField @Rule
     var mockitoRule = MockitoJUnit.rule()
 
+    private val featureFlags = FakeFeatureFlags()
+
     @Mock
     lateinit var callback: AuthDialogCallback
     @Mock
@@ -91,16 +100,25 @@
     @Mock
     lateinit var interactionJankMonitor: InteractionJankMonitor
 
+    // TODO(b/278622168): remove with flag
+    open val useNewBiometricPrompt = false
+
     private val testScope = TestScope(StandardTestDispatcher())
     private val fakeExecutor = FakeExecutor(FakeSystemClock())
     private val biometricPromptRepository = FakePromptRepository()
     private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
     private val credentialInteractor = FakeCredentialInteractor()
-    private val bpCredentialInteractor = BiometricPromptCredentialInteractor(
+    private val bpCredentialInteractor = PromptCredentialInteractor(
         Dispatchers.Main.immediate,
         biometricPromptRepository,
-        credentialInteractor
+        credentialInteractor,
     )
+    private val promptSelectorInteractor by lazy {
+        PromptSelectorInteractorImpl(
+            biometricPromptRepository,
+            lockPatternUtils,
+        )
+    }
     private val displayStateInteractor = DisplayStateInteractorImpl(
         testScope.backgroundScope,
         mContext,
@@ -115,6 +133,11 @@
 
     private var authContainer: TestAuthContainerView? = null
 
+    @Before
+    fun setup() {
+        featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt)
+    }
+
     @After
     fun tearDown() {
         if (authContainer?.isAttachedToWindow == true) {
@@ -125,7 +148,7 @@
     @Test
     fun testNotifiesAnimatedIn() {
         initializeFingerprintContainer()
-        verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+        verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
     }
 
     @Test
@@ -164,13 +187,13 @@
         container.dismissFromSystemServer()
         waitForIdleSync()
 
-        verify(callback, never()).onDialogAnimatedIn(anyLong())
+        verify(callback, never()).onDialogAnimatedIn(anyLong(), anyBoolean())
 
         container.addToView()
         waitForIdleSync()
 
         // attaching the view resets the state and allows this to happen again
-        verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+        verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
     }
 
     @Test
@@ -185,7 +208,7 @@
 
         // the first time is triggered by initializeFingerprintContainer()
         // the second time was triggered by dismissWithoutCallback()
-        verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+        verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
     }
 
     @Test
@@ -479,6 +502,8 @@
                 this.authenticators = authenticators
             }
         },
+        featureFlags,
+        testScope.backgroundScope,
         fingerprintProps,
         faceProps,
         wakefulnessLifecycle,
@@ -486,8 +511,10 @@
         userManager,
         lockPatternUtils,
         interactionJankMonitor,
-        { bpCredentialInteractor },
         { authBiometricFingerprintViewModel },
+        { promptSelectorInteractor },
+        { bpCredentialInteractor },
+        PromptViewModel(promptSelectorInteractor),
         { credentialViewModel },
         Handler(TestableLooper.get(this).looper),
         fakeExecutor
@@ -497,7 +524,10 @@
         }
     }
 
-    override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages()
+    override fun waitForIdleSync() {
+        testScope.runCurrent()
+        TestableLooper.get(this).processAllMessages()
+    }
 
     private fun AuthContainerView.addToView() {
         ViewUtils.attachView(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt
new file mode 100644
index 0000000..b56d055
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.runner.RunWith
+
+// TODO(b/278622168): remove with flag
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class AuthContainerViewTest2 : AuthContainerViewTest() {
+    override val useNewBiometricPrompt = true
+}
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 a326cc7..b9f92a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -18,7 +18,6 @@
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -54,7 +53,6 @@
 import android.graphics.Point;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
-import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.ComponentInfoInternal;
@@ -91,10 +89,14 @@
 import com.android.settingslib.udfps.UdfpsUtils;
 import com.android.systemui.RoboPilotTest;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
 import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -171,12 +173,16 @@
     @Mock
     private InteractionJankMonitor mInteractionJankMonitor;
     @Mock
-    private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
+    private PromptCredentialInteractor mBiometricPromptCredentialInteractor;
+    @Mock
+    private PromptSelectorInteractor mPromptSelectionInteractor;
     @Mock
     private AuthBiometricFingerprintViewModel mAuthBiometricFingerprintViewModel;
     @Mock
     private CredentialViewModel mCredentialViewModel;
     @Mock
+    private PromptViewModel mPromptViewModel;
+    @Mock
     private UdfpsUtils mUdfpsUtils;
 
     @Captor
@@ -194,12 +200,17 @@
     private Handler mHandler;
     private DelayableExecutor mBackgroundExecutor;
     private TestableAuthController mAuthController;
+    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     @Mock
     private VibratorHelper mVibratorHelper;
 
     @Before
     public void setup() throws RemoteException {
+        // TODO(b/278622168): remove with flag
+        // AuthController simply passes this through to AuthContainerView (does not impact test)
+        mFeatureFlags.set(Flags.BIOMETRIC_BP_STRONG, false);
+
         mContextSpy = spy(mContext);
         mExecution = new FakeExecution();
         mTestableLooper = TestableLooper.get(this);
@@ -952,8 +963,7 @@
                 0 /* userId */,
                 0 /* operationId */,
                 "testPackage",
-                REQUEST_ID,
-                BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE);
+                REQUEST_ID);
     }
 
     private void switchTask(String packageName) {
@@ -993,25 +1003,26 @@
         private PromptInfo mLastBiometricPromptInfo;
 
         TestableAuthController(Context context) {
-            super(context, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
+            super(context, mFeatureFlags, null /* applicationCoroutineScope */,
+                    mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
                     mFingerprintManager, mFaceManager, () -> mUdfpsController,
                     () -> mSideFpsController, mDisplayManager, mWakefulnessLifecycle,
                     mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger,
-                    mLogContextInteractor, () -> mBiometricPromptCredentialInteractor,
-                    () -> mAuthBiometricFingerprintViewModel, () -> mCredentialViewModel,
-                    mInteractionJankMonitor, mHandler, mBackgroundExecutor, mVibratorHelper,
-                    mUdfpsUtils);
+                    mLogContextInteractor, () -> mAuthBiometricFingerprintViewModel,
+                    () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,
+                    () -> mCredentialViewModel, () -> mPromptViewModel,
+                    mInteractionJankMonitor, mHandler,
+                    mBackgroundExecutor, mVibratorHelper, mUdfpsUtils);
         }
 
         @Override
         protected AuthDialog buildDialog(DelayableExecutor bgExecutor, PromptInfo promptInfo,
                 boolean requireConfirmation, int userId, int[] sensorIds,
                 String opPackageName, boolean skipIntro, long operationId, long requestId,
-                @BiometricManager.BiometricMultiSensorMode int multiSensorConfig,
                 WakefulnessLifecycle wakefulnessLifecycle,
                 AuthDialogPanelInteractionDetector panelInteractionDetector,
                 UserManager userManager,
-                LockPatternUtils lockPatternUtils) {
+                LockPatternUtils lockPatternUtils, PromptViewModel viewModel) {
 
             mLastBiometricPromptInfo = promptInfo;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 1379a0e..94244cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -18,10 +18,11 @@
 
 import android.annotation.IdRes
 import android.content.Context
-import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.BiometricManager.Authenticators
 import android.hardware.biometrics.ComponentInfoInternal
 import android.hardware.biometrics.PromptInfo
 import android.hardware.biometrics.SensorProperties
+import android.hardware.biometrics.SensorPropertiesInternal
 import android.hardware.face.FaceSensorProperties
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.fingerprint.FingerprintSensorProperties
@@ -61,9 +62,9 @@
 private fun buildPromptInfo(allowDeviceCredential: Boolean): PromptInfo {
     val promptInfo = PromptInfo()
     promptInfo.title = "Title"
-    var authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+    var authenticators = Authenticators.BIOMETRIC_WEAK
     if (allowDeviceCredential) {
-        authenticators = authenticators or BiometricManager.Authenticators.DEVICE_CREDENTIAL
+        authenticators = authenticators or Authenticators.DEVICE_CREDENTIAL
     } else {
         promptInfo.negativeButtonText = "Negative"
     }
@@ -80,7 +81,8 @@
 
 /** Create [FingerprintSensorPropertiesInternal] for a test. */
 internal fun fingerprintSensorPropertiesInternal(
-    ids: List<Int> = listOf(0)
+    ids: List<Int> = listOf(0),
+    strong: Boolean = true,
 ): List<FingerprintSensorPropertiesInternal> {
     val componentInfo =
         listOf(
@@ -102,7 +104,7 @@
     return ids.map { id ->
         FingerprintSensorPropertiesInternal(
             id,
-            SensorProperties.STRENGTH_STRONG,
+            if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK,
             5 /* maxEnrollmentsPerUser */,
             componentInfo,
             FingerprintSensorProperties.TYPE_REAR,
@@ -113,7 +115,8 @@
 
 /** Create [FaceSensorPropertiesInternal] for a test. */
 internal fun faceSensorPropertiesInternal(
-    ids: List<Int> = listOf(1)
+    ids: List<Int> = listOf(1),
+    strong: Boolean = true,
 ): List<FaceSensorPropertiesInternal> {
     val componentInfo =
         listOf(
@@ -135,7 +138,7 @@
     return ids.map { id ->
         FaceSensorPropertiesInternal(
             id,
-            SensorProperties.STRENGTH_STRONG,
+            if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK,
             2 /* maxEnrollmentsPerUser */,
             componentInfo,
             FaceSensorProperties.TYPE_RGB,
@@ -146,6 +149,24 @@
     }
 }
 
+@Authenticators.Types
+internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): Int {
+    var authenticators = Authenticators.EMPTY_SET
+    mapNotNull { it?.sensorStrength }
+        .forEach { strength ->
+            authenticators =
+                authenticators or
+                    when (strength) {
+                        SensorProperties.STRENGTH_CONVENIENCE ->
+                            Authenticators.BIOMETRIC_CONVENIENCE
+                        SensorProperties.STRENGTH_WEAK -> Authenticators.BIOMETRIC_WEAK
+                        SensorProperties.STRENGTH_STRONG -> Authenticators.BIOMETRIC_STRONG
+                        else -> Authenticators.EMPTY_SET
+                    }
+        }
+    return authenticators
+}
+
 internal fun promptInfo(
     title: String = "title",
     subtitle: String = "sub",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
index 2d5614c..4836af6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
@@ -4,7 +4,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
@@ -60,7 +60,7 @@
 
     @Test
     fun setsAndUnsetsPrompt() = runBlockingTest {
-        val kind = PromptKind.PIN
+        val kind = PromptKind.Pin
         val uid = 8
         val challenge = 90L
         val promptInfo = PromptInfo()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
index dbcbf41..720a35c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
@@ -9,15 +9,17 @@
 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
 import com.android.systemui.biometrics.domain.model.BiometricUserInfo
 import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.coroutines.collectLastValue
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
@@ -36,42 +38,39 @@
 
     @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
 
-    private val dispatcher = UnconfinedTestDispatcher()
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
     private val biometricPromptRepository = FakePromptRepository()
     private val credentialInteractor = FakeCredentialInteractor()
 
-    private lateinit var interactor: BiometricPromptCredentialInteractor
+    private lateinit var interactor: PromptCredentialInteractor
 
     @Before
     fun setup() {
         interactor =
-            BiometricPromptCredentialInteractor(
-                dispatcher,
+            PromptCredentialInteractor(
+                testDispatcher,
                 biometricPromptRepository,
-                credentialInteractor
+                credentialInteractor,
             )
     }
 
     @Test
     fun testIsShowing() =
-        runTest(dispatcher) {
-            var showing = false
-            val job = launch { interactor.isShowing.collect { showing = it } }
+        testScope.runTest {
+            val showing by collectLastValue(interactor.isShowing)
 
             biometricPromptRepository.setIsShowing(false)
             assertThat(showing).isFalse()
 
             biometricPromptRepository.setIsShowing(true)
             assertThat(showing).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun testShowError() =
-        runTest(dispatcher) {
-            var error: CredentialStatus.Fail? = null
-            val job = launch { interactor.verificationError.collect { error = it } }
+        testScope.runTest {
+            val error by collectLastValue(interactor.verificationError)
 
             for (msg in listOf("once", "again")) {
                 interactor.setVerificationError(error(msg))
@@ -80,19 +79,14 @@
 
             interactor.resetVerificationError()
             assertThat(error).isNull()
-
-            job.cancel()
         }
 
     @Test
     fun nullWhenNoPromptInfo() =
-        runTest(dispatcher) {
-            var prompt: BiometricPromptRequest? = null
-            val job = launch { interactor.prompt.collect { prompt = it } }
+        testScope.runTest {
+            val prompt by collectLastValue(interactor.prompt)
 
             assertThat(prompt).isNull()
-
-            job.cancel()
         }
 
     @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN)
@@ -102,12 +96,11 @@
     @Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN)
 
     private fun useCredentialForPrompt(kind: Int) =
-        runTest(dispatcher) {
+        testScope.runTest {
             val isStealth = false
             credentialInteractor.stealthMode = isStealth
 
-            var prompt: BiometricPromptRequest? = null
-            val job = launch { interactor.prompt.collect { prompt = it } }
+            val prompt by collectLastValue(interactor.prompt)
 
             val title = "what a prompt"
             val subtitle = "s"
@@ -124,14 +117,12 @@
                 challenge = OPERATION_ID
             )
 
-            val p = prompt as? BiometricPromptRequest.Credential
-            assertThat(p).isNotNull()
-            assertThat(p!!.title).isEqualTo(title)
-            assertThat(p.subtitle).isEqualTo(subtitle)
-            assertThat(p.description).isEqualTo(description)
-            assertThat(p.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
-            assertThat(p.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
-            assertThat(p)
+            assertThat(prompt?.title).isEqualTo(title)
+            assertThat(prompt?.subtitle).isEqualTo(subtitle)
+            assertThat(prompt?.description).isEqualTo(description)
+            assertThat(prompt?.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+            assertThat(prompt?.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+            assertThat(prompt)
                 .isInstanceOf(
                     when (kind) {
                         Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java
@@ -142,25 +133,25 @@
                         else -> throw Exception("wrong kind")
                     }
                 )
-            if (p is BiometricPromptRequest.Credential.Pattern) {
-                assertThat(p.stealthMode).isEqualTo(isStealth)
+            val pattern = prompt as? BiometricPromptRequest.Credential.Pattern
+            if (pattern != null) {
+                assertThat(pattern.stealthMode).isEqualTo(isStealth)
             }
 
             interactor.resetPrompt()
 
             assertThat(prompt).isNull()
-
-            job.cancel()
         }
 
     @Test
     fun checkCredential() =
-        runTest(dispatcher) {
+        testScope.runTest {
             val hat = ByteArray(4)
             credentialInteractor.verifyCredentialResponse = { _ -> flowOf(verified(hat)) }
 
             val errors = mutableListOf<CredentialStatus.Fail?>()
             val job = launch { interactor.verificationError.toList(errors) }
+            runCurrent()
 
             val checked =
                 interactor.checkCredential(pinRequest(), text = "1234")
@@ -168,6 +159,8 @@
 
             assertThat(checked).isNotNull()
             assertThat(checked!!.hat).isSameInstanceAs(hat)
+
+            runCurrent()
             assertThat(errors.map { it?.error }).containsExactly(null)
 
             job.cancel()
@@ -175,7 +168,7 @@
 
     @Test
     fun checkCredentialWhenBad() =
-        runTest(dispatcher) {
+        testScope.runTest {
             val errorMessage = "bad"
             val remainingAttempts = 12
             credentialInteractor.verifyCredentialResponse = { _ ->
@@ -184,6 +177,7 @@
 
             val errors = mutableListOf<CredentialStatus.Fail?>()
             val job = launch { interactor.verificationError.toList(errors) }
+            runCurrent()
 
             val checked =
                 interactor.checkCredential(pinRequest(), text = "1234")
@@ -192,6 +186,8 @@
             assertThat(checked).isNotNull()
             assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
             assertThat(checked.urgentMessage).isNull()
+
+            runCurrent()
             assertThat(errors.map { it?.error }).containsExactly(null, errorMessage).inOrder()
 
             job.cancel()
@@ -199,7 +195,7 @@
 
     @Test
     fun checkCredentialWhenBadAndUrgentMessage() =
-        runTest(dispatcher) {
+        testScope.runTest {
             val error = "not so bad"
             val urgentMessage = "really bad"
             credentialInteractor.verifyCredentialResponse = { _ ->
@@ -208,6 +204,7 @@
 
             val errors = mutableListOf<CredentialStatus.Fail?>()
             val job = launch { interactor.verificationError.toList(errors) }
+            runCurrent()
 
             val checked =
                 interactor.checkCredential(pinRequest(), text = "1234")
@@ -215,6 +212,8 @@
 
             assertThat(checked).isNotNull()
             assertThat(checked!!.urgentMessage).isEqualTo(urgentMessage)
+
+            runCurrent()
             assertThat(errors.map { it?.error }).containsExactly(null, error).inOrder()
             assertThat(errors.last() as? CredentialStatus.Fail.Error)
                 .isEqualTo(error(error, 10, urgentMessage))
@@ -224,7 +223,7 @@
 
     @Test
     fun checkCredentialWhenBadAndThrottled() =
-        runTest(dispatcher) {
+        testScope.runTest {
             val remainingAttempts = 3
             val error = ":("
             val urgentMessage = ":D"
@@ -239,6 +238,7 @@
             }
             val errors = mutableListOf<CredentialStatus.Fail?>()
             val job = launch { interactor.verificationError.toList(errors) }
+            runCurrent()
 
             val checked =
                 interactor.checkCredential(pinRequest(), text = "1234")
@@ -246,6 +246,8 @@
 
             assertThat(checked).isNotNull()
             assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
+
+            runCurrent()
             assertThat(checked.urgentMessage).isEqualTo(urgentMessage)
             assertThat(errors.map { it?.error })
                 .containsExactly(null, "1", "2", "3", error)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
new file mode 100644
index 0000000..a62ea3b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.hardware.biometrics.BiometricManager.Authenticators
+import android.hardware.biometrics.PromptInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+private const val TITLE = "hey there"
+private const val SUBTITLE = "ok"
+private const val DESCRIPTION = "football"
+private const val NEGATIVE_TEXT = "escape"
+
+private const val USER_ID = 8
+private const val CHALLENGE = 999L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptSelectorInteractorImplTest : SysuiTestCase() {
+
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+    private val testScope = TestScope()
+    private val promptRepository = FakePromptRepository()
+
+    private lateinit var interactor: PromptSelectorInteractor
+
+    @Before
+    fun setup() {
+        interactor = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+    }
+
+    @Test
+    fun useBiometricsAndReset() =
+        testScope.runTest { useBiometricsAndReset(allowCredentialFallback = true) }
+
+    @Test
+    fun useBiometricsAndResetWithoutFallback() =
+        testScope.runTest { useBiometricsAndReset(allowCredentialFallback = false) }
+
+    private fun TestScope.useBiometricsAndReset(allowCredentialFallback: Boolean) {
+        setUserCredentialType(isPassword = true)
+
+        val confirmationRequired = true
+        val info =
+            PromptInfo().apply {
+                title = TITLE
+                subtitle = SUBTITLE
+                description = DESCRIPTION
+                negativeButtonText = NEGATIVE_TEXT
+                isConfirmationRequested = confirmationRequired
+                authenticators =
+                    if (allowCredentialFallback) {
+                        Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL
+                    } else {
+                        Authenticators.BIOMETRIC_STRONG
+                    }
+                isDeviceCredentialAllowed = allowCredentialFallback
+            }
+        val modalities =
+            BiometricModalities(
+                fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+                faceProperties = faceSensorPropertiesInternal().first(),
+            )
+
+        val currentPrompt by collectLastValue(interactor.prompt)
+        val credentialKind by collectLastValue(interactor.credentialKind)
+        val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed)
+        val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequested)
+
+        assertThat(currentPrompt).isNull()
+
+        interactor.useBiometricsForAuthentication(
+            info,
+            confirmationRequired,
+            USER_ID,
+            CHALLENGE,
+            modalities
+        )
+
+        assertThat(currentPrompt).isNotNull()
+        assertThat(currentPrompt?.title).isEqualTo(TITLE)
+        assertThat(currentPrompt?.description).isEqualTo(DESCRIPTION)
+        assertThat(currentPrompt?.subtitle).isEqualTo(SUBTITLE)
+        assertThat(currentPrompt?.negativeButtonText).isEqualTo(NEGATIVE_TEXT)
+
+        if (allowCredentialFallback) {
+            assertThat(credentialKind).isSameInstanceAs(PromptKind.Password)
+            assertThat(isCredentialAllowed).isTrue()
+        } else {
+            assertThat(credentialKind).isEqualTo(PromptKind.Biometric())
+            assertThat(isCredentialAllowed).isFalse()
+        }
+        assertThat(isExplicitConfirmationRequired).isEqualTo(confirmationRequired)
+
+        interactor.resetPrompt()
+        verifyUnset()
+    }
+
+    @Test
+    fun usePinCredentialAndReset() =
+        testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PIN) }
+
+    @Test
+    fun usePattermCredentialAndReset() =
+        testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PATTERN) }
+
+    @Test
+    fun usePasswordCredentialAndReset() =
+        testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PASSWORD) }
+
+    private fun TestScope.useCredentialAndReset(@Utils.CredentialType kind: Int) {
+        setUserCredentialType(
+            isPin = kind == Utils.CREDENTIAL_PIN,
+            isPassword = kind == Utils.CREDENTIAL_PASSWORD,
+        )
+
+        val info =
+            PromptInfo().apply {
+                title = TITLE
+                subtitle = SUBTITLE
+                description = DESCRIPTION
+                negativeButtonText = NEGATIVE_TEXT
+                authenticators = Authenticators.DEVICE_CREDENTIAL
+                isDeviceCredentialAllowed = true
+            }
+
+        val currentPrompt by collectLastValue(interactor.prompt)
+        val credentialKind by collectLastValue(interactor.credentialKind)
+
+        assertThat(currentPrompt).isNull()
+
+        interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE)
+
+        // not using biometrics, should be null with no fallback option
+        assertThat(currentPrompt).isNull()
+        assertThat(credentialKind).isEqualTo(PromptKind.Biometric())
+
+        interactor.resetPrompt()
+        verifyUnset()
+    }
+
+    private fun TestScope.verifyUnset() {
+        val currentPrompt by collectLastValue(interactor.prompt)
+        val credentialKind by collectLastValue(interactor.credentialKind)
+
+        assertThat(currentPrompt).isNull()
+
+        val kind = credentialKind as? PromptKind.Biometric
+        assertThat(kind).isNotNull()
+        assertThat(kind?.activeModalities?.isEmpty).isTrue()
+    }
+
+    private fun setUserCredentialType(isPin: Boolean = false, isPassword: Boolean = false) {
+        whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(any()))
+            .thenReturn(
+                when {
+                    isPin -> DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+                    isPassword -> DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
+                    else -> DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
+                }
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt
new file mode 100644
index 0000000..526b833
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BiometricModalitiesTest : SysuiTestCase() {
+
+    @Test
+    fun isEmpty() {
+        assertThat(BiometricModalities().isEmpty).isTrue()
+    }
+
+    @Test
+    fun fingerprintOnly() {
+        with(
+            BiometricModalities(
+                fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+            )
+        ) {
+            assertThat(isEmpty).isFalse()
+            assertThat(hasFace).isFalse()
+            assertThat(hasFaceOnly).isFalse()
+            assertThat(hasFingerprint).isTrue()
+            assertThat(hasFingerprintOnly).isTrue()
+            assertThat(hasFaceAndFingerprint).isFalse()
+        }
+    }
+
+    @Test
+    fun faceOnly() {
+        with(BiometricModalities(faceProperties = faceSensorPropertiesInternal().first())) {
+            assertThat(isEmpty).isFalse()
+            assertThat(hasFace).isTrue()
+            assertThat(hasFaceOnly).isTrue()
+            assertThat(hasFingerprint).isFalse()
+            assertThat(hasFingerprintOnly).isFalse()
+            assertThat(hasFaceAndFingerprint).isFalse()
+        }
+    }
+
+    @Test
+    fun faceStrength() {
+        with(
+            BiometricModalities(
+                fingerprintProperties = fingerprintSensorPropertiesInternal(strong = false).first(),
+                faceProperties = faceSensorPropertiesInternal(strong = true).first()
+            )
+        ) {
+            assertThat(isFaceStrong).isTrue()
+        }
+
+        with(
+            BiometricModalities(
+                fingerprintProperties = fingerprintSensorPropertiesInternal(strong = false).first(),
+                faceProperties = faceSensorPropertiesInternal(strong = false).first()
+            )
+        ) {
+            assertThat(isFaceStrong).isFalse()
+        }
+    }
+
+    @Test
+    fun faceAndFingerprint() {
+        with(
+            BiometricModalities(
+                fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+                faceProperties = faceSensorPropertiesInternal().first(),
+            )
+        ) {
+            assertThat(isEmpty).isFalse()
+            assertThat(hasFace).isTrue()
+            assertThat(hasFingerprint).isTrue()
+            assertThat(hasFaceOnly).isFalse()
+            assertThat(hasFingerprintOnly).isFalse()
+            assertThat(hasFaceAndFingerprint).isTrue()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
index 4c5e3c1..e352905 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -2,6 +2,7 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
 import com.android.systemui.biometrics.promptInfo
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -21,11 +22,13 @@
         val subtitle = "a"
         val description = "request"
 
+        val fpPros = fingerprintSensorPropertiesInternal().first()
         val request =
             BiometricPromptRequest.Biometric(
                 promptInfo(title = title, subtitle = subtitle, description = description),
                 BiometricUserInfo(USER_ID),
-                BiometricOperationInfo(OPERATION_ID)
+                BiometricOperationInfo(OPERATION_ID),
+                BiometricModalities(fingerprintProperties = fpPros),
             )
 
         assertThat(request.title).isEqualTo(title)
@@ -33,6 +36,8 @@
         assertThat(request.description).isEqualTo(description)
         assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
         assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+        assertThat(request.modalities)
+            .isEqualTo(BiometricModalities(fingerprintProperties = fpPros))
     }
 
     @Test
@@ -51,19 +56,19 @@
                         description = description,
                         credentialTitle = null,
                         credentialSubtitle = null,
-                        credentialDescription = null
+                        credentialDescription = null,
                     ),
                     BiometricUserInfo(USER_ID),
-                    BiometricOperationInfo(OPERATION_ID)
+                    BiometricOperationInfo(OPERATION_ID),
                 ),
                 BiometricPromptRequest.Credential.Password(
                     promptInfo(
                         credentialTitle = title,
                         credentialSubtitle = subtitle,
-                        credentialDescription = description
+                        credentialDescription = description,
                     ),
                     BiometricUserInfo(USER_ID),
-                    BiometricOperationInfo(OPERATION_ID)
+                    BiometricOperationInfo(OPERATION_ID),
                 ),
                 BiometricPromptRequest.Credential.Pattern(
                     promptInfo(
@@ -71,11 +76,11 @@
                         description = description,
                         credentialTitle = title,
                         credentialSubtitle = null,
-                        credentialDescription = null
+                        credentialDescription = null,
                     ),
                     BiometricUserInfo(USER_ID),
                     BiometricOperationInfo(OPERATION_ID),
-                    stealth
+                    stealth,
                 )
             )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
index d73cdfc..3245020 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
@@ -2,12 +2,12 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.model.PromptKind
 import com.android.systemui.biometrics.data.repository.FakePromptRepository
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
 import com.android.systemui.biometrics.domain.interactor.CredentialStatus
 import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
 import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
@@ -40,17 +40,13 @@
         viewModel =
             CredentialViewModel(
                 mContext,
-                BiometricPromptCredentialInteractor(
-                    dispatcher,
-                    promptRepository,
-                    credentialInteractor
-                )
+                PromptCredentialInteractor(dispatcher, promptRepository, credentialInteractor)
             )
     }
 
-    @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.PIN, expectFlags = true)
-    @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.PASSWORD, expectFlags = false)
-    @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.PATTERN, expectFlags = false)
+    @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.Pin, expectFlags = true)
+    @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.Password, expectFlags = false)
+    @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.Pattern, expectFlags = false)
 
     private fun setsInputFlags(type: PromptKind, expectFlags: Boolean) =
         runTestWithKind(type) {
@@ -65,10 +61,10 @@
             job.cancel()
         }
 
-    @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.PIN, expectStealth = false)
+    @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.Pin, expectStealth = false)
     @Test
-    fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.PASSWORD, expectStealth = false)
-    @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.PATTERN, expectStealth = true)
+    fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.Password, expectStealth = false)
+    @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.Pattern, expectStealth = true)
 
     private fun isStealthMode(type: PromptKind, expectStealth: Boolean) =
         runTestWithKind(type, init = { credentialInteractor.stealthMode = true }) {
@@ -119,7 +115,7 @@
 
         val attestations = mutableListOf<ByteArray?>()
         val remainingAttempts = mutableListOf<RemainingAttempts?>()
-        var header: HeaderViewModel? = null
+        var header: CredentialHeaderViewModel? = null
         val job = launch {
             launch { viewModel.validatedAttestation.toList(attestations) }
             launch { viewModel.remainingAttempts.toList(remainingAttempts) }
@@ -147,7 +143,7 @@
 
         val attestations = mutableListOf<ByteArray?>()
         val remainingAttempts = mutableListOf<RemainingAttempts?>()
-        var header: HeaderViewModel? = null
+        var header: CredentialHeaderViewModel? = null
         val job = launch {
             launch { viewModel.validatedAttestation.toList(attestations) }
             launch { viewModel.remainingAttempts.toList(remainingAttempts) }
@@ -169,7 +165,7 @@
     }
 
     private fun runTestWithKind(
-        kind: PromptKind = PromptKind.PIN,
+        kind: PromptKind = PromptKind.Pin,
         init: () -> Unit = {},
         block: suspend TestScope.() -> Unit,
     ) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt
new file mode 100644
index 0000000..689bb00
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptAuthStateTest : SysuiTestCase() {
+
+    @Test
+    fun notAuthenticated() {
+        with(PromptAuthState(isAuthenticated = false)) {
+            assertThat(isNotAuthenticated).isTrue()
+            assertThat(isAuthenticatedAndConfirmed).isFalse()
+            assertThat(isAuthenticatedByFace).isFalse()
+            assertThat(isAuthenticatedByFingerprint).isFalse()
+        }
+    }
+
+    @Test
+    fun authenticatedByUnknown() {
+        with(PromptAuthState(isAuthenticated = true)) {
+            assertThat(isNotAuthenticated).isFalse()
+            assertThat(isAuthenticatedAndConfirmed).isTrue()
+            assertThat(isAuthenticatedByFace).isFalse()
+            assertThat(isAuthenticatedByFingerprint).isFalse()
+        }
+
+        with(PromptAuthState(isAuthenticated = true, needsUserConfirmation = true)) {
+            assertThat(isNotAuthenticated).isFalse()
+            assertThat(isAuthenticatedAndConfirmed).isFalse()
+            assertThat(isAuthenticatedByFace).isFalse()
+            assertThat(isAuthenticatedByFingerprint).isFalse()
+
+            assertThat(asConfirmed().isAuthenticatedAndConfirmed).isTrue()
+        }
+    }
+
+    @Test
+    fun authenticatedWithFace() {
+        with(
+            PromptAuthState(isAuthenticated = true, authenticatedModality = BiometricModality.Face)
+        ) {
+            assertThat(isNotAuthenticated).isFalse()
+            assertThat(isAuthenticatedAndConfirmed).isTrue()
+            assertThat(isAuthenticatedByFace).isTrue()
+            assertThat(isAuthenticatedByFingerprint).isFalse()
+        }
+    }
+
+    @Test
+    fun authenticatedWithFingerprint() {
+        with(
+            PromptAuthState(
+                isAuthenticated = true,
+                authenticatedModality = BiometricModality.Fingerprint,
+            )
+        ) {
+            assertThat(isNotAuthenticated).isFalse()
+            assertThat(isAuthenticatedAndConfirmed).isTrue()
+            assertThat(isAuthenticatedByFace).isFalse()
+            assertThat(isAuthenticatedByFingerprint).isTrue()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
new file mode 100644
index 0000000..3ba6004
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -0,0 +1,639 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.hardware.biometrics.PromptInfo
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.extractAuthenticatorTypes
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+private const val USER_ID = 4
+private const val CHALLENGE = 2L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(Parameterized::class)
+internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCase() {
+
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+    private val testScope = TestScope()
+    private val promptRepository = FakePromptRepository()
+
+    private lateinit var selector: PromptSelectorInteractor
+    private lateinit var viewModel: PromptViewModel
+
+    @Before
+    fun setup() {
+        selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+        selector.resetPrompt()
+
+        viewModel = PromptViewModel(selector)
+    }
+
+    @Test
+    fun `start idle and show authenticating`() =
+        runGenericTest(doNotStart = true) {
+            val expectedSize =
+                if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM
+            val authenticating by collectLastValue(viewModel.isAuthenticating)
+            val authenticated by collectLastValue(viewModel.isAuthenticated)
+            val modalities by collectLastValue(viewModel.modalities)
+            val message by collectLastValue(viewModel.message)
+            val size by collectLastValue(viewModel.size)
+            val legacyState by collectLastValue(viewModel.legacyState)
+
+            assertThat(authenticating).isFalse()
+            assertThat(authenticated?.isNotAuthenticated).isTrue()
+            with(modalities ?: throw Exception("missing modalities")) {
+                assertThat(hasFace).isEqualTo(testCase.face != null)
+                assertThat(hasFingerprint).isEqualTo(testCase.fingerprint != null)
+            }
+            assertThat(message).isEqualTo(PromptMessage.Empty)
+            assertThat(size).isEqualTo(expectedSize)
+            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+
+            val startMessage = "here we go"
+            viewModel.showAuthenticating(startMessage, isRetry = false)
+
+            assertThat(message).isEqualTo(PromptMessage.Help(startMessage))
+            assertThat(authenticating).isTrue()
+            assertThat(authenticated?.isNotAuthenticated).isTrue()
+            assertThat(size).isEqualTo(expectedSize)
+            assertButtonsVisible(negative = expectedSize != PromptSize.SMALL)
+            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING)
+        }
+
+    @Test
+    fun `shows authenticated - no errors`() = runGenericTest {
+        // this case can't happen until fingerprint is started
+        // trigger it now since no error has occurred in this test
+        val forceError = testCase.isCoex && testCase.authenticatedByFingerprint
+
+        if (forceError) {
+            assertThat(viewModel.fingerprintStartMode.first())
+                .isEqualTo(FingerprintStartMode.Pending)
+            viewModel.ensureFingerprintHasStarted(isDelayed = true)
+        }
+
+        showAuthenticated(
+            testCase.authenticatedModality,
+            testCase.expectConfirmation(atLeastOneFailure = forceError),
+        )
+    }
+
+    private suspend fun TestScope.showAuthenticated(
+        authenticatedModality: BiometricModality,
+        expectConfirmation: Boolean,
+    ) {
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+        val fpStartMode by collectLastValue(viewModel.fingerprintStartMode)
+        val size by collectLastValue(viewModel.size)
+        val legacyState by collectLastValue(viewModel.legacyState)
+
+        val authWithSmallPrompt =
+            testCase.shouldStartAsImplicitFlow &&
+                (fpStartMode == FingerprintStartMode.Pending || testCase.isFaceOnly)
+        assertThat(authenticating).isTrue()
+        assertThat(authenticated?.isNotAuthenticated).isTrue()
+        assertThat(size).isEqualTo(if (authWithSmallPrompt) PromptSize.SMALL else PromptSize.MEDIUM)
+        assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING)
+        assertButtonsVisible(negative = !authWithSmallPrompt)
+
+        val delay = 1000L
+        viewModel.showAuthenticated(authenticatedModality, delay)
+
+        assertThat(authenticated?.isAuthenticated).isTrue()
+        assertThat(authenticated?.delay).isEqualTo(delay)
+        assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+        assertThat(size)
+            .isEqualTo(
+                if (authenticatedModality == BiometricModality.Fingerprint || expectConfirmation) {
+                    PromptSize.MEDIUM
+                } else {
+                    PromptSize.SMALL
+                }
+            )
+        assertThat(legacyState)
+            .isEqualTo(
+                if (expectConfirmation) {
+                    AuthBiometricView.STATE_PENDING_CONFIRMATION
+                } else {
+                    AuthBiometricView.STATE_AUTHENTICATED
+                }
+            )
+        assertButtonsVisible(
+            cancel = expectConfirmation,
+            confirm = expectConfirmation,
+        )
+    }
+
+    @Test
+    fun `shows temporary errors`() = runGenericTest {
+        val checkAtEnd = suspend { assertButtonsVisible(negative = true) }
+
+        showTemporaryErrors(restart = false) { checkAtEnd() }
+        showTemporaryErrors(restart = false, helpAfterError = "foo") { checkAtEnd() }
+        showTemporaryErrors(restart = true) { checkAtEnd() }
+    }
+
+    private suspend fun TestScope.showTemporaryErrors(
+        restart: Boolean,
+        helpAfterError: String = "",
+        block: suspend TestScope.() -> Unit = {},
+    ) {
+        val errorMessage = "oh no!"
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+        val message by collectLastValue(viewModel.message)
+        val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+        val size by collectLastValue(viewModel.size)
+        val legacyState by collectLastValue(viewModel.legacyState)
+        val canTryAgainNow by collectLastValue(viewModel.canTryAgainNow)
+
+        val errorJob = launch {
+            viewModel.showTemporaryError(
+                errorMessage,
+                authenticateAfterError = restart,
+                messageAfterError = helpAfterError,
+            )
+        }
+
+        assertThat(size).isEqualTo(PromptSize.MEDIUM)
+        assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
+        assertThat(messageVisible).isTrue()
+        assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_ERROR)
+
+        // temporary error should disappear after a delay
+        errorJob.join()
+        if (helpAfterError.isNotBlank()) {
+            assertThat(message).isEqualTo(PromptMessage.Help(helpAfterError))
+            assertThat(messageVisible).isTrue()
+        } else {
+            assertThat(message).isEqualTo(PromptMessage.Empty)
+            assertThat(messageVisible).isFalse()
+        }
+        assertThat(legacyState)
+            .isEqualTo(
+                if (restart) {
+                    AuthBiometricView.STATE_AUTHENTICATING
+                } else {
+                    AuthBiometricView.STATE_HELP
+                }
+            )
+
+        assertThat(authenticating).isEqualTo(restart)
+        assertThat(authenticated?.isNotAuthenticated).isTrue()
+        assertThat(canTryAgainNow).isFalse()
+
+        block()
+    }
+
+    @Test
+    fun `no errors or temporary help after authenticated`() = runGenericTest {
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+        val message by collectLastValue(viewModel.message)
+        val messageIsShowing by collectLastValue(viewModel.isIndicatorMessageVisible)
+        val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+        viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+        val verifyNoError = {
+            assertThat(authenticating).isFalse()
+            assertThat(authenticated?.isAuthenticated).isTrue()
+            assertThat(message).isEqualTo(PromptMessage.Empty)
+            assertThat(canTryAgain).isFalse()
+        }
+
+        val errorJob = launch { viewModel.showTemporaryError("error") }
+        verifyNoError()
+        errorJob.join()
+        verifyNoError()
+
+        val helpJob = launch { viewModel.showTemporaryHelp("hi") }
+        verifyNoError()
+        helpJob.join()
+        verifyNoError()
+
+        // persistent help is allowed
+        val stickyHelpMessage = "blah"
+        viewModel.showHelp(stickyHelpMessage)
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isTrue()
+        assertThat(message).isEqualTo(PromptMessage.Help(stickyHelpMessage))
+        assertThat(messageIsShowing).isTrue()
+    }
+
+    //    @Test
+    fun `suppress errors`() = runGenericTest {
+        val errorMessage = "woot"
+        val message by collectLastValue(viewModel.message)
+
+        val errorJob = launch { viewModel.showTemporaryError(errorMessage) }
+    }
+
+    @Test
+    fun `authenticated at most once`() = runGenericTest {
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+        viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isTrue()
+
+        viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isTrue()
+    }
+
+    @Test
+    fun `authenticating cannot restart after authenticated`() = runGenericTest {
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+        viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isTrue()
+
+        viewModel.showAuthenticating("again!")
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isTrue()
+    }
+
+    @Test
+    fun `confirm authentication`() = runGenericTest {
+        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+
+        viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+        val message by collectLastValue(viewModel.message)
+        val size by collectLastValue(viewModel.size)
+        val legacyState by collectLastValue(viewModel.legacyState)
+        val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+        assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+        if (expectConfirmation) {
+            assertThat(size).isEqualTo(PromptSize.MEDIUM)
+            assertButtonsVisible(
+                cancel = true,
+                confirm = true,
+            )
+
+            viewModel.confirmAuthenticated()
+            assertThat(message).isEqualTo(PromptMessage.Empty)
+            assertButtonsVisible()
+        }
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isTrue()
+        assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED)
+        assertThat(canTryAgain).isFalse()
+    }
+
+    @Test
+    fun `cannot confirm unless authenticated`() = runGenericTest {
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+        viewModel.confirmAuthenticated()
+        assertThat(authenticating).isTrue()
+        assertThat(authenticated?.isNotAuthenticated).isTrue()
+
+        viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+        // reconfirm should be a no-op
+        viewModel.confirmAuthenticated()
+        viewModel.confirmAuthenticated()
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isNotAuthenticated).isFalse()
+    }
+
+    @Test
+    fun `shows help - before authenticated`() = runGenericTest {
+        val helpMessage = "please help yourself to some cookies"
+        val message by collectLastValue(viewModel.message)
+        val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+        val size by collectLastValue(viewModel.size)
+        val legacyState by collectLastValue(viewModel.legacyState)
+
+        viewModel.showHelp(helpMessage)
+
+        assertThat(size).isEqualTo(PromptSize.MEDIUM)
+        assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_HELP)
+        assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+        assertThat(messageVisible).isTrue()
+
+        assertThat(viewModel.isAuthenticating.first()).isFalse()
+        assertThat(viewModel.isAuthenticated.first().isNotAuthenticated).isTrue()
+    }
+
+    @Test
+    fun `shows help - after authenticated`() = runGenericTest {
+        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+        val helpMessage = "more cookies please"
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+        val message by collectLastValue(viewModel.message)
+        val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+        val size by collectLastValue(viewModel.size)
+        val legacyState by collectLastValue(viewModel.legacyState)
+
+        if (testCase.isCoex && testCase.authenticatedByFingerprint) {
+            viewModel.ensureFingerprintHasStarted(isDelayed = true)
+        }
+        viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+        viewModel.showHelp(helpMessage)
+
+        assertThat(size).isEqualTo(PromptSize.MEDIUM)
+        assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION)
+        assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+        assertThat(messageVisible).isTrue()
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isTrue()
+        assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+        assertButtonsVisible(
+            cancel = expectConfirmation,
+            confirm = expectConfirmation,
+        )
+    }
+
+    @Test
+    fun `retries after failure`() = runGenericTest {
+        val errorMessage = "bad"
+        val helpMessage = "again?"
+        val expectTryAgainButton = testCase.isFaceOnly
+        val authenticating by collectLastValue(viewModel.isAuthenticating)
+        val authenticated by collectLastValue(viewModel.isAuthenticated)
+        val message by collectLastValue(viewModel.message)
+        val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+        val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+        viewModel.showAuthenticating("go")
+        val errorJob = launch {
+            viewModel.showTemporaryError(
+                errorMessage,
+                messageAfterError = helpMessage,
+                authenticateAfterError = false,
+                failedModality = testCase.authenticatedModality
+            )
+        }
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isFalse()
+        assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
+        assertThat(messageVisible).isTrue()
+        assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
+        assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
+
+        errorJob.join()
+
+        assertThat(authenticating).isFalse()
+        assertThat(authenticated?.isAuthenticated).isFalse()
+        assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+        assertThat(messageVisible).isTrue()
+        assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
+        assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
+
+        val helpMessage2 = "foo"
+        viewModel.showAuthenticating(helpMessage2, isRetry = true)
+        assertThat(authenticating).isTrue()
+        assertThat(authenticated?.isAuthenticated).isFalse()
+        assertThat(message).isEqualTo(PromptMessage.Help(helpMessage2))
+        assertThat(messageVisible).isTrue()
+        assertButtonsVisible(negative = true)
+    }
+
+    @Test
+    fun `switch to credential fallback`() = runGenericTest {
+        val size by collectLastValue(viewModel.size)
+
+        // TODO(b/251476085): remove Spaghetti, migrate logic, and update this test
+        viewModel.onSwitchToCredential()
+
+        assertThat(size).isEqualTo(PromptSize.LARGE)
+    }
+
+    /** Asserts that the selected buttons are visible now. */
+    private suspend fun TestScope.assertButtonsVisible(
+        tryAgain: Boolean = false,
+        confirm: Boolean = false,
+        cancel: Boolean = false,
+        negative: Boolean = false,
+        credential: Boolean = false,
+    ) {
+        runCurrent()
+        assertThat(viewModel.isTryAgainButtonVisible.first()).isEqualTo(tryAgain)
+        assertThat(viewModel.isConfirmButtonVisible.first()).isEqualTo(confirm)
+        assertThat(viewModel.isCancelButtonVisible.first()).isEqualTo(cancel)
+        assertThat(viewModel.isNegativeButtonVisible.first()).isEqualTo(negative)
+        assertThat(viewModel.isCredentialButtonVisible.first()).isEqualTo(credential)
+    }
+
+    private fun runGenericTest(
+        doNotStart: Boolean = false,
+        allowCredentialFallback: Boolean = false,
+        block: suspend TestScope.() -> Unit
+    ) {
+        selector.initializePrompt(
+            requireConfirmation = testCase.confirmationRequested,
+            allowCredentialFallback = allowCredentialFallback,
+            fingerprint = testCase.fingerprint,
+            face = testCase.face,
+        )
+
+        // put the view model in the initial authenticating state, unless explicitly skipped
+        val startMode =
+            when {
+                doNotStart -> null
+                testCase.isCoex -> FingerprintStartMode.Delayed
+                else -> FingerprintStartMode.Normal
+            }
+        when (startMode) {
+            FingerprintStartMode.Normal -> {
+                viewModel.ensureFingerprintHasStarted(isDelayed = false)
+                viewModel.showAuthenticating()
+            }
+            FingerprintStartMode.Delayed -> {
+                viewModel.showAuthenticating()
+            }
+            else -> {
+                /* skip */
+            }
+        }
+
+        testScope.runTest { block() }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data(): Collection<TestCase> = singleModalityTestCases + coexTestCases
+
+        private val singleModalityTestCases =
+            listOf(
+                TestCase(
+                    face = faceSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Face,
+                ),
+                TestCase(
+                    fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Fingerprint,
+                ),
+                TestCase(
+                    face = faceSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Face,
+                    confirmationRequested = true,
+                ),
+                TestCase(
+                    fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Fingerprint,
+                    confirmationRequested = true,
+                ),
+            )
+
+        private val coexTestCases =
+            listOf(
+                TestCase(
+                    face = faceSensorPropertiesInternal(strong = true).first(),
+                    fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Face,
+                ),
+                TestCase(
+                    face = faceSensorPropertiesInternal(strong = true).first(),
+                    fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Fingerprint,
+                ),
+                TestCase(
+                    face = faceSensorPropertiesInternal(strong = true).first(),
+                    fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Face,
+                    confirmationRequested = true,
+                ),
+                TestCase(
+                    face = faceSensorPropertiesInternal(strong = true).first(),
+                    fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+                    authenticatedModality = BiometricModality.Fingerprint,
+                    confirmationRequested = true,
+                ),
+            )
+    }
+}
+
+internal data class TestCase(
+    val fingerprint: FingerprintSensorPropertiesInternal? = null,
+    val face: FaceSensorPropertiesInternal? = null,
+    val authenticatedModality: BiometricModality,
+    val confirmationRequested: Boolean = false,
+) {
+    override fun toString(): String {
+        val modality =
+            when {
+                fingerprint != null && face != null -> "coex"
+                fingerprint != null -> "fingerprint only"
+                face != null -> "face only"
+                else -> "?"
+            }
+        return "[$modality, by: $authenticatedModality, confirm: $confirmationRequested]"
+    }
+
+    fun expectConfirmation(atLeastOneFailure: Boolean): Boolean =
+        when {
+            isCoex && authenticatedModality == BiometricModality.Face ->
+                atLeastOneFailure || confirmationRequested
+            isFaceOnly -> confirmationRequested
+            else -> false
+        }
+
+    val authenticatedByFingerprint: Boolean
+        get() = authenticatedModality == BiometricModality.Fingerprint
+
+    val authenticatedByFace: Boolean
+        get() = authenticatedModality == BiometricModality.Face
+
+    val isFaceOnly: Boolean
+        get() = face != null && fingerprint == null
+
+    val isFingerprintOnly: Boolean
+        get() = face == null && fingerprint != null
+
+    val isCoex: Boolean
+        get() = face != null && fingerprint != null
+
+    val shouldStartAsImplicitFlow: Boolean
+        get() = (isFaceOnly || isCoex) && !confirmationRequested
+}
+
+/** Initialize the test by selecting the give [fingerprint] or [face] configuration(s). */
+private fun PromptSelectorInteractor.initializePrompt(
+    fingerprint: FingerprintSensorPropertiesInternal? = null,
+    face: FaceSensorPropertiesInternal? = null,
+    requireConfirmation: Boolean = false,
+    allowCredentialFallback: Boolean = false,
+) {
+    val info =
+        PromptInfo().apply {
+            title = "t"
+            subtitle = "s"
+            authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
+            isDeviceCredentialAllowed = allowCredentialFallback
+            isConfirmationRequested = requireConfirmation
+        }
+    useBiometricsForAuthentication(
+        info,
+        requireConfirmation,
+        USER_ID,
+        CHALLENGE,
+        BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),
+    )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 730f89d..9f5c181 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -27,6 +27,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -75,7 +76,7 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
 
             underTest.clearMessage()
-            assertThat(message).isNull()
+            assertThat(message).isEmpty()
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
@@ -107,7 +108,7 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
 
             underTest.clearMessage()
-            assertThat(message).isNull()
+            assertThat(message).isEmpty()
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
@@ -139,7 +140,7 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             underTest.clearMessage()
-            assertThat(message).isNull()
+            assertThat(message).isEmpty()
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
@@ -201,6 +202,56 @@
             assertThat(message).isEqualTo(customMessage)
         }
 
+    @Test
+    fun throttling() =
+        testScope.runTest {
+            val throttling by collectLastValue(underTest.throttling)
+            val message by collectLastValue(underTest.message)
+            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(throttling).isNull()
+            assertThat(message).isEqualTo("")
+            assertThat(isUnlocked).isFalse()
+            repeat(BouncerInteractor.THROTTLE_EVERY) { times ->
+                // Wrong PIN.
+                underTest.authenticate(listOf(6, 7, 8, 9))
+                if (times < BouncerInteractor.THROTTLE_EVERY - 1) {
+                    assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+                }
+            }
+            assertThat(throttling).isNotNull()
+            assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
+
+            // Correct PIN, but throttled, so doesn't unlock:
+            underTest.authenticate(listOf(1, 2, 3, 4))
+            assertThat(isUnlocked).isFalse()
+            assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
+
+            throttling?.totalDurationSec?.let { seconds ->
+                repeat(seconds) { time ->
+                    advanceTimeBy(1000)
+                    val remainingTime = seconds - time - 1
+                    if (remainingTime > 0) {
+                        assertTryAgainMessage(message, remainingTime)
+                    }
+                }
+            }
+            assertThat(message).isEqualTo("")
+            assertThat(throttling).isNull()
+            assertThat(isUnlocked).isFalse()
+
+            // Correct PIN and no longer throttled so unlocks:
+            underTest.authenticate(listOf(1, 2, 3, 4))
+            assertThat(isUnlocked).isTrue()
+        }
+
+    private fun assertTryAgainMessage(
+        message: String?,
+        time: Int,
+    ) {
+        assertThat(message).isEqualTo("Try again in $time seconds.")
+    }
+
     companion object {
         private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
         private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 954e67d..b942ccb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -19,11 +19,15 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.scene.SceneTestUtils
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -40,13 +44,12 @@
         utils.authenticationInteractor(
             repository = utils.authenticationRepository(),
         )
-    private val underTest =
-        utils.bouncerViewModel(
-            utils.bouncerInteractor(
-                authenticationInteractor = authenticationInteractor,
-                sceneInteractor = utils.sceneInteractor(),
-            )
+    private val bouncerInteractor =
+        utils.bouncerInteractor(
+            authenticationInteractor = authenticationInteractor,
+            sceneInteractor = utils.sceneInteractor(),
         )
+    private val underTest = utils.bouncerViewModel(bouncerInteractor)
 
     @Test
     fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
@@ -89,6 +92,65 @@
             .isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet())
     }
 
+    @Test
+    fun isMessageUpdateAnimationsEnabled() =
+        testScope.runTest {
+            val isMessageUpdateAnimationsEnabled by
+                collectLastValue(underTest.isMessageUpdateAnimationsEnabled)
+            val throttling by collectLastValue(bouncerInteractor.throttling)
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isMessageUpdateAnimationsEnabled).isTrue()
+
+            repeat(BouncerInteractor.THROTTLE_EVERY) {
+                // Wrong PIN.
+                bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+            }
+            assertThat(isMessageUpdateAnimationsEnabled).isFalse()
+
+            throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
+            assertThat(isMessageUpdateAnimationsEnabled).isTrue()
+        }
+
+    @Test
+    fun isInputEnabled() =
+        testScope.runTest {
+            val isInputEnabled by
+                collectLastValue(
+                    underTest.authMethod.flatMapLatest { authViewModel ->
+                        authViewModel?.isInputEnabled ?: emptyFlow()
+                    }
+                )
+            val throttling by collectLastValue(bouncerInteractor.throttling)
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isInputEnabled).isTrue()
+
+            repeat(BouncerInteractor.THROTTLE_EVERY) {
+                // Wrong PIN.
+                bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+            }
+            assertThat(isInputEnabled).isFalse()
+
+            throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
+            assertThat(isInputEnabled).isTrue()
+        }
+
+    @Test
+    fun throttlingDialogMessage() =
+        testScope.runTest {
+            val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+
+            repeat(BouncerInteractor.THROTTLE_EVERY) {
+                // Wrong PIN.
+                assertThat(throttlingDialogMessage).isNull()
+                bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+            }
+            assertThat(throttlingDialogMessage).isNotEmpty()
+
+            underTest.onThrottlingDialogDismissed()
+            assertThat(throttlingDialogMessage).isNull()
+        }
+
     private fun authMethodsToTest(): List<AuthenticationMethodModel> {
         return listOf(
             AuthenticationMethodModel.None,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index e48b638..b7b90de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -26,6 +26,8 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -57,6 +59,7 @@
     private val underTest =
         PasswordBouncerViewModel(
             interactor = bouncerInteractor,
+            isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 6ce29e6..b588ba2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -27,6 +27,8 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -60,6 +62,7 @@
             applicationContext = context,
             applicationScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
+            isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index bb28520..83f9687 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -27,6 +27,8 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
@@ -68,6 +70,7 @@
         PinBouncerViewModel(
             applicationScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
+            isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 548d26f..78a65a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -98,7 +98,8 @@
             )
             testScope.runCurrent()
             verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
         }
 
     @Test
@@ -115,7 +116,8 @@
             )
             testScope.runCurrent()
             verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
         }
 
     @Test
@@ -161,7 +163,8 @@
             keyguardRepository.setDozeAmount(1f)
             testScope.runCurrent()
             verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 3bcefcf..56698e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -41,7 +41,6 @@
 import androidx.media.utils.MediaConstants
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
-import com.android.internal.statusbar.IStatusBarService
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.InstanceIdSequenceFake
 import com.android.systemui.R
@@ -133,7 +132,6 @@
     @Mock lateinit var activityStarter: ActivityStarter
     @Mock lateinit var smartspaceManager: SmartspaceManager
     @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    @Mock lateinit var statusBarService: IStatusBarService
     lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
     @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
     @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
@@ -197,7 +195,6 @@
                 logger = logger,
                 smartspaceManager = smartspaceManager,
                 keyguardUpdateMonitor = keyguardUpdateMonitor,
-                statusBarService = statusBarService,
             )
         verify(tunerService)
             .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
@@ -522,143 +519,12 @@
     }
 
     @Test
-    fun testOnNotificationAdded_emptyTitle_isRequired_notLoaded() {
-        // When the manager has a notification with an empty title, and the app is required
-        // to include a non-empty title
-        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
-        whenever(controller.metadata)
-            .thenReturn(
-                metadataBuilder
-                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
-                    .build()
-            )
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-
-        // Then the media control is not added and we report a notification error
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(statusBarService)
-            .onNotificationError(
-                eq(PACKAGE_NAME),
-                eq(mediaNotification.tag),
-                eq(mediaNotification.id),
-                eq(mediaNotification.uid),
-                eq(mediaNotification.initialPid),
-                eq(MEDIA_TITLE_ERROR_MESSAGE),
-                eq(mediaNotification.user.identifier)
-            )
-        verify(listener, never())
-            .onMediaDataLoaded(
-                eq(KEY),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
-        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
-        verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any())
-    }
-
-    @Test
-    fun testOnNotificationAdded_blankTitle_isRequired_notLoaded() {
-        // When the manager has a notification with a blank title, and the app is required
-        // to include a non-empty title
-        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
-        whenever(controller.metadata)
-            .thenReturn(
-                metadataBuilder
-                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
-                    .build()
-            )
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-
-        // Then the media control is not added and we report a notification error
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(statusBarService)
-            .onNotificationError(
-                eq(PACKAGE_NAME),
-                eq(mediaNotification.tag),
-                eq(mediaNotification.id),
-                eq(mediaNotification.uid),
-                eq(mediaNotification.initialPid),
-                eq(MEDIA_TITLE_ERROR_MESSAGE),
-                eq(mediaNotification.user.identifier)
-            )
-        verify(listener, never())
-            .onMediaDataLoaded(
-                eq(KEY),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
-        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
-        verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any())
-    }
-
-    @Test
-    fun testOnNotificationUpdated_invalidTitle_isRequired_logMediaRemoved() {
-        // When the app is required to provide a non-blank title, and updates a previously valid
-        // title to an empty one
-        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-
-        verify(listener)
-            .onMediaDataLoaded(
-                eq(KEY),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
-
-        reset(listener)
-        whenever(controller.metadata)
-            .thenReturn(
-                metadataBuilder
-                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
-                    .build()
-            )
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-
-        // Then the media control is removed
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(statusBarService)
-            .onNotificationError(
-                eq(PACKAGE_NAME),
-                eq(mediaNotification.tag),
-                eq(mediaNotification.id),
-                eq(mediaNotification.uid),
-                eq(mediaNotification.initialPid),
-                eq(MEDIA_TITLE_ERROR_MESSAGE),
-                eq(mediaNotification.user.identifier)
-            )
-        verify(listener, never())
-            .onMediaDataLoaded(
-                eq(KEY),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
-        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
-    }
-
-    @Test
-    fun testOnNotificationAdded_emptyTitle_notRequired_hasPlaceholder() {
+    fun testOnNotificationAdded_emptyTitle_hasPlaceholder() {
         // When the manager has a notification with an empty title, and the app is not
         // required to include a non-empty title
         val mockPackageManager = mock(PackageManager::class.java)
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
-        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false)
         whenever(controller.metadata)
             .thenReturn(
                 metadataBuilder
@@ -684,13 +550,12 @@
     }
 
     @Test
-    fun testOnNotificationAdded_blankTitle_notRequired_hasPlaceholder() {
+    fun testOnNotificationAdded_blankTitle_hasPlaceholder() {
         // GIVEN that the manager has a notification with a blank title, and the app is not
         // required to include a non-empty title
         val mockPackageManager = mock(PackageManager::class.java)
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
-        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false)
         whenever(controller.metadata)
             .thenReturn(
                 metadataBuilder
@@ -722,7 +587,6 @@
         val mockPackageManager = mock(PackageManager::class.java)
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
-        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
         whenever(controller.metadata)
             .thenReturn(
                 metadataBuilder
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index 2eca78a..e92368d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -19,6 +19,7 @@
 import android.testing.AndroidTestingRunner
 import android.view.LayoutInflater
 import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.TextAnimator
@@ -64,8 +65,8 @@
                 color = 200,
                 strokeWidth = -1F,
                 animate = false,
-                duration = 350L,
-                interpolator = null,
+                duration = 833L,
+                interpolator = Interpolators.EMPHASIZED_DECELERATE,
                 delay = 0L,
                 onAnimationEnd = null
             )
@@ -98,8 +99,8 @@
                 color = 200,
                 strokeWidth = -1F,
                 animate = true,
-                duration = 350L,
-                interpolator = null,
+                duration = 833L,
+                interpolator = Interpolators.EMPHASIZED_DECELERATE,
                 delay = 0L,
                 onAnimationEnd = null
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index f4cd383..1643e17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -28,7 +28,6 @@
 
 import android.content.ComponentName;
 import android.graphics.Rect;
-import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
@@ -443,15 +442,13 @@
         final long operationId = 1;
         final String packageName = "test";
         final long requestId = 10;
-        final int multiSensorConfig = BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
 
         mCommandQueue.showAuthenticationDialog(promptInfo, receiver, sensorIds,
-                credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId,
-                multiSensorConfig);
+                credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId);
         waitForIdleSync();
         verify(mCallbacks).showAuthenticationDialog(eq(promptInfo), eq(receiver), eq(sensorIds),
                 eq(credentialAllowed), eq(requireConfirmation), eq(userId), eq(operationId),
-                eq(packageName), eq(requestId), eq(multiSensorConfig));
+                eq(packageName), eq(requestId));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 551499e..7632d01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -392,23 +392,32 @@
 
     @Test
     public void testSnapchild_targetIsZero() {
-        doNothing().when(mSwipeHelper).superSnapChild(mView, 0, 0);
-        mSwipeHelper.snapChild(mView, 0, 0);
+        doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 0, 0);
+        mSwipeHelper.snapChild(mNotificationRow, 0, 0);
 
-        verify(mCallback, times(1)).onDragCancelled(mView);
-        verify(mSwipeHelper, times(1)).superSnapChild(mView, 0, 0);
+        verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
+        verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 0, 0);
         verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
     }
 
 
     @Test
     public void testSnapchild_targetNotZero() {
+        doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 10, 0);
+        mSwipeHelper.snapChild(mNotificationRow, 10, 0);
+
+        verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
+        verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 10, 0);
+        verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
+    }
+
+    @Test
+    public void testSnapchild_targetNotSwipeable() {
         doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0);
         mSwipeHelper.snapChild(mView, 10, 0);
 
-        verify(mCallback, times(1)).onDragCancelled(mView);
-        verify(mSwipeHelper, times(1)).superSnapChild(mView, 10, 0);
-        verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
+        verify(mCallback).onDragCancelled(mView);
+        verify(mSwipeHelper, never()).superSnapChild(mView, 10, 0);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 45a37cf..8f725be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -35,7 +35,6 @@
 import android.content.res.Configuration;
 import android.media.AudioManager;
 import android.os.SystemClock;
-import android.provider.DeviceConfig;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.Gravity;
@@ -47,7 +46,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
@@ -62,9 +60,6 @@
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.FakeConfigurationController;
-import com.android.systemui.util.DeviceConfigProxyFake;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.After;
 import org.junit.Before;
@@ -88,8 +83,6 @@
     View mDrawerVibrate;
     View mDrawerMute;
     View mDrawerNormal;
-    private DeviceConfigProxyFake mDeviceConfigProxy;
-    private FakeExecutor mExecutor;
     private TestableLooper mTestableLooper;
     private ConfigurationController mConfigurationController;
     private int mOriginalOrientation;
@@ -131,8 +124,6 @@
         getContext().addMockSystemService(KeyguardManager.class, mKeyguard);
 
         mTestableLooper = TestableLooper.get(this);
-        mDeviceConfigProxy = new DeviceConfigProxyFake();
-        mExecutor = new FakeExecutor(new FakeSystemClock());
 
         when(mPostureController.getDevicePosture())
                 .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
@@ -151,8 +142,6 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
-                mDeviceConfigProxy,
-                mExecutor,
                 mCsdWarningDialogFactory,
                 mPostureController,
                 mTestableLooper.getLooper(),
@@ -173,9 +162,6 @@
                 VolumePrefs.SHOW_RINGER_TOAST_COUNT + 1);
 
         Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
-
-        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false);
     }
 
     private State createShellState() {
@@ -351,13 +337,8 @@
      * API does not exist. So we do the next best thing; we check the cached icon id.
      */
     @Test
-    public void notificationVolumeSeparated_theRingerIconChanges() {
-        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false);
-
-        mExecutor.runAllReady(); // for the config change to take effect
-
-        // assert icon is new based on res id
+    public void notificationVolumeSeparated_theRingerIconChangesToSpeakerIcon() {
+        // already separated. assert icon is new based on res id
         assertEquals(mDialog.mVolumeRingerIconDrawableId,
                 R.drawable.ic_speaker_on);
         assertEquals(mDialog.mVolumeRingerMuteIconDrawableId,
@@ -365,17 +346,6 @@
     }
 
     @Test
-    public void notificationVolumeNotSeparated_theRingerIconRemainsTheSame() {
-        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false);
-
-        mExecutor.runAllReady();
-
-        assertEquals(mDialog.mVolumeRingerIconDrawableId, R.drawable.ic_volume_ringer);
-        assertEquals(mDialog.mVolumeRingerMuteIconDrawableId, R.drawable.ic_volume_ringer_mute);
-    }
-
-    @Test
     public void testDialogDismissAnimation_notifyVisibleIsNotCalledBeforeAnimation() {
         mDialog.dismissH(DISMISS_REASON_UNKNOWN);
         // notifyVisible(false) should not be called immediately but only after the dismiss
@@ -408,8 +378,6 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
-                mDeviceConfigProxy,
-                mExecutor,
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
@@ -447,8 +415,6 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
-                mDeviceConfigProxy,
-                mExecutor,
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
@@ -485,8 +451,6 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
-                mDeviceConfigProxy,
-                mExecutor,
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
@@ -525,8 +489,6 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
-                mDeviceConfigProxy,
-                mExecutor,
                 mCsdWarningDialogFactory,
                 mPostureController,
                 mTestableLooper.getLooper(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
index 96658c6..d270700 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
@@ -1,7 +1,7 @@
 package com.android.systemui.biometrics.data.repository
 
 import android.hardware.biometrics.PromptInfo
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
@@ -20,26 +20,32 @@
     private var _challenge = MutableStateFlow<Long?>(null)
     override val challenge = _challenge.asStateFlow()
 
-    private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+    private val _kind = MutableStateFlow<PromptKind>(PromptKind.Biometric())
     override val kind = _kind.asStateFlow()
 
+    private val _isConfirmationRequired = MutableStateFlow(false)
+    override val isConfirmationRequired = _isConfirmationRequired.asStateFlow()
+
     override fun setPrompt(
         promptInfo: PromptInfo,
         userId: Int,
         gatekeeperChallenge: Long?,
-        kind: PromptKind
+        kind: PromptKind,
+        requireConfirmation: Boolean,
     ) {
         _promptInfo.value = promptInfo
         _userId.value = userId
         _challenge.value = gatekeeperChallenge
         _kind.value = kind
+        _isConfirmationRequired.value = requireConfirmation
     }
 
     override fun unsetPrompt() {
         _promptInfo.value = null
         _userId.value = null
         _challenge.value = null
-        _kind.value = PromptKind.ANY_BIOMETRIC
+        _kind.value = PromptKind.Biometric()
+        _isConfirmationRequired.value = false
     }
 
     fun setIsShowing(showing: Boolean) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0da25be..31d60f2 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8297,8 +8297,8 @@
                 r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mDelegationService
                         : ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT,
                 0 /* api_sate */,
-                0 /* api_type */,
-                0 /* api_timestamp */,
+                null /* api_type */,
+                null /* api_timestamp */,
                 mAm.getUidStateLocked(r.appInfo.uid),
                 mAm.getUidProcessCapabilityLocked(r.appInfo.uid),
                 mAm.getUidStateLocked(r.mRecentCallingUid),
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 030d596..8c1fd51 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -292,6 +292,15 @@
     private static final String KEY_CORE_DEFER_UNTIL_ACTIVE = "bcast_core_defer_until_active";
     private static final boolean DEFAULT_CORE_DEFER_UNTIL_ACTIVE = true;
 
+    /**
+     * For {@link BroadcastQueueModernImpl}: How frequently we should check for the pending
+     * cold start validity.
+     */
+    public long PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 30 * 1000;
+    private static final String KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS =
+            "pending_cold_start_check_interval_millis";
+    private static final long DEFAULT_PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 30_000;
+
     // Settings override tracking for this instance
     private String mSettingsKey;
     private SettingsObserver mSettingsObserver;
@@ -441,6 +450,9 @@
                     DEFAULT_MAX_HISTORY_SUMMARY_SIZE);
             CORE_DEFER_UNTIL_ACTIVE = getDeviceConfigBoolean(KEY_CORE_DEFER_UNTIL_ACTIVE,
                     DEFAULT_CORE_DEFER_UNTIL_ACTIVE);
+            PENDING_COLD_START_CHECK_INTERVAL_MILLIS = getDeviceConfigLong(
+                    KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS,
+                    DEFAULT_PENDING_COLD_START_CHECK_INTERVAL_MILLIS);
         }
 
         // TODO: migrate BroadcastRecord to accept a BroadcastConstants
@@ -499,6 +511,8 @@
                     MAX_CONSECUTIVE_NORMAL_DISPATCHES).println();
             pw.print(KEY_CORE_DEFER_UNTIL_ACTIVE,
                     CORE_DEFER_UNTIL_ACTIVE).println();
+            pw.print(KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS,
+                    PENDING_COLD_START_CHECK_INTERVAL_MILLIS).println();
             pw.decreaseIndent();
             pw.println();
         }
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index d6e692c..f180f02 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -248,6 +248,7 @@
     private static final int MSG_DELIVERY_TIMEOUT_HARD = 3;
     private static final int MSG_BG_ACTIVITY_START_TIMEOUT = 4;
     private static final int MSG_CHECK_HEALTH = 5;
+    private static final int MSG_CHECK_PENDING_COLD_START_VALIDITY = 6;
 
     private void enqueueUpdateRunningList() {
         mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST);
@@ -284,6 +285,10 @@
                 checkHealth();
                 return true;
             }
+            case MSG_CHECK_PENDING_COLD_START_VALIDITY: {
+                checkPendingColdStartValidity();
+                return true;
+            }
         }
         return false;
     };
@@ -450,10 +455,14 @@
                 // skip to look for another warm process
                 if (mRunningColdStart == null) {
                     mRunningColdStart = queue;
-                } else {
+                } else if (isPendingColdStartValid()) {
                     // Move to considering next runnable queue
                     queue = nextQueue;
                     continue;
+                } else {
+                    // Pending cold start is not valid, so clear it and move on.
+                    clearInvalidPendingColdStart();
+                    mRunningColdStart = queue;
                 }
             }
 
@@ -486,11 +495,46 @@
             mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER);
         }
 
+        checkPendingColdStartValidity();
         checkAndRemoveWaitingFor();
 
         traceEnd(cookie);
     }
 
+    private boolean isPendingColdStartValid() {
+        if (mRunningColdStart.app.getPid() > 0) {
+            // If the process has already started, check if it wasn't killed.
+            return !mRunningColdStart.app.isKilled();
+        } else {
+            // Otherwise, check if the process start is still pending.
+            return mRunningColdStart.app.isPendingStart();
+        }
+    }
+
+    private void clearInvalidPendingColdStart() {
+        logw("Clearing invalid pending cold start: " + mRunningColdStart);
+        onApplicationCleanupLocked(mRunningColdStart.app);
+    }
+
+    private void checkPendingColdStartValidity() {
+        // There are a few cases where a starting process gets killed but AMS doesn't report
+        // this event. So, once we start waiting for a pending cold start, periodically check
+        // if the pending start is still valid and if not, clear it so that the queue doesn't
+        // keep waiting for the process start forever.
+        synchronized (mService) {
+            // If there is no pending cold start, then nothing to do.
+            if (mRunningColdStart == null) {
+                return;
+            }
+            if (isPendingColdStartValid()) {
+                mLocalHandler.sendEmptyMessageDelayed(MSG_CHECK_PENDING_COLD_START_VALIDITY,
+                        mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS);
+            } else {
+                clearInvalidPendingColdStart();
+            }
+        }
+    }
+
     @Override
     public boolean onApplicationAttachedLocked(@NonNull ProcessRecord app) {
         // Process records can be recycled, so always start by looking up the
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index daa4ba4..9b3f249 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -451,6 +451,10 @@
             @ForegroundServiceApiType int apiType, long timestamp) {
         final long apiDurationBeforeFgsStart = r.mFgsEnterTime - timestamp;
         final long apiDurationAfterFgsEnd = timestamp - r.mFgsExitTime;
+        final int[] apiTypes = new int[1];
+        apiTypes[0] = apiType;
+        final long[] timeStamps = new long[1];
+        timeStamps[0] = timestamp;
         FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED,
                 r.appInfo.uid,
                 r.shortInstanceName,
@@ -475,8 +479,8 @@
                 r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mDelegationService
                         : ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT,
                 apiState,
-                apiType,
-                timestamp,
+                apiTypes,
+                timeStamps,
                 ActivityManager.PROCESS_STATE_UNKNOWN,
                 ActivityManager.PROCESS_CAPABILITY_NONE,
                 ActivityManager.PROCESS_STATE_UNKNOWN,
@@ -500,6 +504,10 @@
                 apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType);
             }
         }
+        final int[] apiTypes = new int[1];
+        apiTypes[0] = apiType;
+        final long[] timeStamps = new long[1];
+        timeStamps[0] = timestamp;
         FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED,
                 uid,
                 null,
@@ -522,8 +530,8 @@
                 0,
                 0,
                 apiState,
-                apiType,
-                timestamp,
+                apiTypes,
+                timeStamps,
                 ActivityManager.PROCESS_STATE_UNKNOWN,
                 ActivityManager.PROCESS_CAPABILITY_NONE,
                 ActivityManager.PROCESS_STATE_UNKNOWN,
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 1e5f187..85a0185 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -837,7 +837,7 @@
      */
     @GuardedBy("mService")
     void enqueueOomAdjTargetLocked(ProcessRecord app) {
-        if (app != null) {
+        if (app != null && app.mState.getMaxAdj() > FOREGROUND_APP_ADJ) {
             mPendingProcessSet.add(app);
         }
     }
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index ab4fb46..202d407 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -349,21 +349,22 @@
      * use caller's BAL permission.
      */
     public static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller(
-            @Nullable ActivityOptions activityOptions, int callingUid) {
+            @Nullable ActivityOptions activityOptions, int callingUid,
+            @Nullable String callingPackage) {
         if (activityOptions == null) {
             // since the ActivityOptions were not created by the app itself, determine the default
             // for the app
-            return getDefaultBackgroundStartPrivileges(callingUid);
+            return getDefaultBackgroundStartPrivileges(callingUid, callingPackage);
         }
         return getBackgroundStartPrivilegesAllowedByCaller(activityOptions.toBundle(),
-                callingUid);
+                callingUid, callingPackage);
     }
 
     private static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller(
-            @Nullable Bundle options, int callingUid) {
+            @Nullable Bundle options, int callingUid, @Nullable String callingPackage) {
         if (options == null || !options.containsKey(
                         ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)) {
-            return getDefaultBackgroundStartPrivileges(callingUid);
+            return getDefaultBackgroundStartPrivileges(callingUid, callingPackage);
         }
         return options.getBoolean(ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)
                 ? BackgroundStartPrivileges.ALLOW_BAL
@@ -382,7 +383,7 @@
                     android.Manifest.permission.LOG_COMPAT_CHANGE
             })
     public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges(
-            int callingUid) {
+            int callingUid, @Nullable String callingPackage) {
         if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
             // We temporarily allow BAL for system processes, while we verify that all valid use
             // cases are opted in explicitly to grant their BAL permission.
@@ -391,7 +392,9 @@
             // as soon as that app is upgraded (or removed) BAL would be blocked. (b/283138430)
             return BackgroundStartPrivileges.ALLOW_BAL;
         }
-        boolean isChangeEnabledForApp = CompatChanges.isChangeEnabled(
+        boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled(
+                DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage,
+                UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled(
                 DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingUid);
         if (isChangeEnabledForApp) {
             return BackgroundStartPrivileges.ALLOW_FGS;
@@ -647,7 +650,7 @@
         // temporarily allow receivers and services to open activities from background if the
         // PendingIntent.send() caller was foreground at the time of sendInner() call
         if (uid != callingUid && controller.mAtmInternal.isUidForeground(callingUid)) {
-            return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid);
+            return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid, null);
         }
         return BackgroundStartPrivileges.NONE;
     }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 355981a..d0b6cdc 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -45,7 +45,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
-import android.app.ActivityThread;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
@@ -167,7 +166,6 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorManager;
-import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.System;
 import android.service.notification.ZenModeConfig;
@@ -187,10 +185,8 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.Toast;
 
-
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
@@ -252,7 +248,6 @@
             AudioSystemAdapter.OnVolRangeInitRequestListener {
 
     private static final String TAG = "AS.AudioService";
-    private static final boolean CONFIG_DEFAULT_VAL = false;
 
     private final AudioSystemAdapter mAudioSystem;
     private final SystemServerAdapter mSystemServer;
@@ -309,7 +304,7 @@
      * indicates whether STREAM_NOTIFICATION is aliased to STREAM_RING
      *     not final due to test method, see {@link #setNotifAliasRingForTest(boolean)}.
      */
-    private boolean mNotifAliasRing;
+    private boolean mNotifAliasRing = false;
 
     /**
      * Test method to temporarily override whether STREAM_NOTIFICATION is aliased to STREAM_RING,
@@ -1057,13 +1052,6 @@
         mUseVolumeGroupAliases = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups);
 
-        mNotifAliasRing = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
-
-        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
-                ActivityThread.currentApplication().getMainExecutor(),
-                this::onDeviceConfigChange);
-
         // Initialize volume
         // Priority 1 - Android Property
         // Priority 2 - Audio Policy Service
@@ -1157,6 +1145,11 @@
                         MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM];
         }
 
+        int minAssistantVolume = SystemProperties.getInt("ro.config.assistant_vol_min", -1);
+        if (minAssistantVolume != -1) {
+            MIN_STREAM_VOLUME[AudioSystem.STREAM_ASSISTANT] = minAssistantVolume;
+        }
+
         // Read following properties to configure max volume (number of steps) and default volume
         //   for STREAM_NOTIFICATION and STREAM_RING:
         //      config_audio_notif_vol_default
@@ -1277,22 +1270,6 @@
     }
 
     /**
-     * Separating notification volume from ring is NOT of aliasing the corresponding streams
-     * @param properties
-     */
-    private void onDeviceConfigChange(DeviceConfig.Properties properties) {
-        Set<String> changeSet = properties.getKeyset();
-        if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) {
-            boolean newNotifAliasRing = !properties.getBoolean(
-                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL);
-            if (mNotifAliasRing != newNotifAliasRing) {
-                mNotifAliasRing = newNotifAliasRing;
-                updateStreamVolumeAlias(true, TAG);
-            }
-        }
-    }
-
-    /**
      * Called by handling of MSG_INIT_STREAMS_VOLUMES
      */
     private void onInitStreamsAndVolumes() {
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index bf5e8ee..1989bc7 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -21,8 +21,6 @@
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR_BASE;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
 
 import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
 import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
@@ -44,7 +42,6 @@
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.IBiometricSensorReceiver;
@@ -68,7 +65,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Collection;
 import java.util.List;
 import java.util.Random;
 import java.util.function.Function;
@@ -134,7 +130,6 @@
 
     // The current state, which can be either idle, called, or started
     private @SessionState int mState = STATE_AUTH_IDLE;
-    private @BiometricMultiSensorMode int mMultiSensorMode;
     private int[] mSensors;
     // TODO(b/197265902): merge into state
     private boolean mCancelled;
@@ -255,7 +250,6 @@
             // SystemUI invokes that path.
             mState = STATE_SHOWING_DEVICE_CREDENTIAL;
             mSensors = new int[0];
-            mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
 
             mStatusBarService.showAuthenticationDialog(
                     mPromptInfo,
@@ -266,8 +260,7 @@
                     mUserId,
                     mOperationId,
                     mOpPackageName,
-                    mRequestId,
-                    mMultiSensorMode);
+                    mRequestId);
         } else if (!mPreAuthInfo.eligibleSensors.isEmpty()) {
             // Some combination of biometric or biometric|credential is requested
             setSensorsToStateWaitingForCookie(false /* isTryAgain */);
@@ -310,8 +303,6 @@
                     for (int i = 0; i < mPreAuthInfo.eligibleSensors.size(); i++) {
                         mSensors[i] = mPreAuthInfo.eligibleSensors.get(i).id;
                     }
-                    mMultiSensorMode = getMultiSensorModeForNewSession(
-                            mPreAuthInfo.eligibleSensors);
 
                     mStatusBarService.showAuthenticationDialog(mPromptInfo,
                             mSysuiReceiver,
@@ -321,8 +312,7 @@
                             mUserId,
                             mOperationId,
                             mOpPackageName,
-                            mRequestId,
-                            mMultiSensorMode);
+                            mRequestId);
                     mState = STATE_AUTH_STARTED;
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception", e);
@@ -438,7 +428,6 @@
                     mPromptInfo.setAuthenticators(authenticators);
 
                     mState = STATE_SHOWING_DEVICE_CREDENTIAL;
-                    mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
                     mSensors = new int[0];
 
                     mStatusBarService.showAuthenticationDialog(
@@ -450,8 +439,7 @@
                             mUserId,
                             mOperationId,
                             mOpPackageName,
-                            mRequestId,
-                            mMultiSensorMode);
+                            mRequestId);
                 } else {
                     mClientReceiver.onError(modality, error, vendorCode);
                     return true;
@@ -545,13 +533,30 @@
         }
     }
 
-    void onDialogAnimatedIn() {
+    void onDialogAnimatedIn(boolean startFingerprintNow) {
         if (mState != STATE_AUTH_STARTED) {
             Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
             return;
         }
 
         mState = STATE_AUTH_STARTED_UI_SHOWING;
+        if (startFingerprintNow) {
+            startAllPreparedFingerprintSensors();
+        } else {
+            Slog.d(TAG, "delaying fingerprint sensor start");
+        }
+    }
+
+    // call once anytime after onDialogAnimatedIn() to indicate it's appropriate to start the
+    // fingerprint sensor (i.e. face auth has failed or is not available)
+    void onStartFingerprint() {
+        if (mState != STATE_AUTH_STARTED
+                && mState != STATE_AUTH_STARTED_UI_SHOWING
+                && mState != STATE_AUTH_PAUSED
+                && mState != STATE_ERROR_PENDING_SYSUI) {
+            Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
+        }
+
         startAllPreparedFingerprintSensors();
     }
 
@@ -919,25 +924,6 @@
         }
     }
 
-    @BiometricMultiSensorMode
-    private static int getMultiSensorModeForNewSession(Collection<BiometricSensor> sensors) {
-        boolean hasFace = false;
-        boolean hasFingerprint = false;
-
-        for (BiometricSensor sensor: sensors) {
-            if (sensor.modality == TYPE_FACE) {
-                hasFace = true;
-            } else if (sensor.modality == TYPE_FINGERPRINT) {
-                hasFingerprint = true;
-            }
-        }
-
-        if (hasFace && hasFingerprint) {
-            return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
-        }
-        return BIOMETRIC_MULTI_SENSOR_DEFAULT;
-    }
-
     @Override
     public String toString() {
         return "State: " + mState
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 4488434..0942d85 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -480,8 +480,13 @@
             }
 
             @Override
-            public void onDialogAnimatedIn() {
-                mHandler.post(() -> handleOnDialogAnimatedIn(requestId));
+            public void onDialogAnimatedIn(boolean startFingerprintNow) {
+                mHandler.post(() -> handleOnDialogAnimatedIn(requestId, startFingerprintNow));
+            }
+
+            @Override
+            public void onStartFingerprintNow() {
+                mHandler.post(() -> handleOnStartFingerprintNow(requestId));
             }
         };
     }
@@ -1237,7 +1242,7 @@
         }
     }
 
-    private void handleOnDialogAnimatedIn(long requestId) {
+    private void handleOnDialogAnimatedIn(long requestId, boolean startFingerprintNow) {
         Slog.d(TAG, "handleOnDialogAnimatedIn");
 
         final AuthSession session = getAuthSessionIfCurrent(requestId);
@@ -1246,7 +1251,19 @@
             return;
         }
 
-        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn(startFingerprintNow);
+    }
+
+    private void handleOnStartFingerprintNow(long requestId) {
+        Slog.d(TAG, "handleOnStartFingerprintNow");
+
+        final AuthSession session = getAuthSessionIfCurrent(requestId);
+        if (session == null) {
+            Slog.w(TAG, "handleOnStartFingerprintNow: AuthSession is not current");
+            return;
+        }
+
+        session.onStartFingerprint();
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/TEST_MAPPING b/services/core/java/com/android/server/biometrics/TEST_MAPPING
index daca00b..9e60ba8 100644
--- a/services/core/java/com/android/server/biometrics/TEST_MAPPING
+++ b/services/core/java/com/android/server/biometrics/TEST_MAPPING
@@ -6,24 +6,5 @@
         {
             "name": "CtsBiometricsHostTestCases"
         }
-    ],
-    "ironwood-postsubmit": [
-     {
-      "name": "BiometricsE2eTests",
-      "options": [
-        {
-            "include-annotation": "android.platform.test.annotations.IwTest"
-        },
-        {
-            "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-            "include-filter": "android.platform.test.scenario.biometrics"
-        },
-        {
-            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
-        }
-      ]
-    }
-   ]
+    ]
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
index a486d16..694dfd2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
@@ -70,9 +70,11 @@
 
                 // Set mStopUserClient to null when StopUserClient fails. Otherwise it's possible
                 // for that the queue will wait indefinitely until the field is cleared.
-                if (clientMonitor instanceof StopUserClient<?> && !success) {
-                    Slog.w(getTag(),
-                            "StopUserClient failed(), is the HAL stuck? Clearing mStopUserClient");
+                if (clientMonitor instanceof StopUserClient<?>) {
+                    if (!success) {
+                        Slog.w(getTag(), "StopUserClient failed(), is the HAL stuck? "
+                                + "Clearing mStopUserClient");
+                    }
                     mStopUserClient = null;
                 }
                 if (mCurrentOperation != null && mCurrentOperation.isFor(mOwner)) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index ffbf4e1..2ad41c2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -89,7 +89,7 @@
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
 
     @NonNull private final Supplier<AidlSession> mLazySession;
-    @Nullable private AidlSession mCurrentSession;
+    @Nullable AidlSession mCurrentSession;
 
     @VisibleForTesting
     public static class HalSessionCallback extends ISessionCallback.Stub {
@@ -486,7 +486,7 @@
     Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
             @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext) {
+            @NonNull BiometricContext biometricContext, AidlSession session) {
         mTag = tag;
         mProvider = provider;
         mContext = context;
@@ -549,6 +549,14 @@
         mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
     }
 
+    Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext) {
+        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+                biometricContext, null);
+    }
+
     @NonNull Supplier<AidlSession> getLazySession() {
         return mLazySession;
     }
@@ -557,7 +565,7 @@
         return mSensorProperties;
     }
 
-    @Nullable AidlSession getSessionForUser(int userId) {
+    @VisibleForTesting @Nullable AidlSession getSessionForUser(int userId) {
         if (mCurrentSession != null && mCurrentSession.getUserId() == userId) {
             return mCurrentSession;
         } else {
@@ -641,6 +649,8 @@
                     BiometricsProtoEnums.MODALITY_FACE,
                     BiometricsProtoEnums.ISSUE_HAL_DEATH,
                     -1 /* sensorId */);
+        } else if (client != null) {
+            client.cancel();
         }
 
         mScheduler.recordCrashState();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
index c0dde72..56b85ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
@@ -90,7 +90,7 @@
     @NonNull private final LockoutCache mLockoutCache;
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
 
-    @Nullable private AidlSession mCurrentSession;
+    @Nullable AidlSession mCurrentSession;
     @NonNull private final Supplier<AidlSession> mLazySession;
 
     @VisibleForTesting
@@ -439,7 +439,7 @@
             @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            @NonNull BiometricContext biometricContext) {
+            @NonNull BiometricContext biometricContext, AidlSession session) {
         mTag = tag;
         mProvider = provider;
         mContext = context;
@@ -501,6 +501,16 @@
                 });
         mAuthenticatorIds = new HashMap<>();
         mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
+        mCurrentSession = session;
+    }
+
+    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext) {
+        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+                gestureAvailabilityDispatcher, biometricContext, null);
     }
 
     @NonNull Supplier<AidlSession> getLazySession() {
@@ -599,6 +609,8 @@
                     BiometricsProtoEnums.MODALITY_FINGERPRINT,
                     BiometricsProtoEnums.ISSUE_HAL_DEATH,
                     -1 /* sensorId */);
+        } else if (client != null) {
+            client.cancel();
         }
 
         mScheduler.recordCrashState();
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 6a01042..42b2682 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.broadcastradio;
 
+import android.Manifest;
+import android.content.pm.PackageManager;
 import android.hardware.broadcastradio.IBroadcastRadio;
 import android.hardware.radio.IAnnouncementListener;
 import android.hardware.radio.ICloseHandle;
@@ -23,6 +25,7 @@
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ITunerCallback;
 import android.hardware.radio.RadioManager;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -112,6 +115,13 @@
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+        if (mService.getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            printWriter.println("Permission Denial: can't dump AIDL BroadcastRadioService from "
+                    + "from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                    + " without permission " + Manifest.permission.DUMP);
+            return;
+        }
         IndentingPrintWriter radioPrintWriter = new IndentingPrintWriter(printWriter);
         radioPrintWriter.printf("BroadcastRadioService\n");
 
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 408fba1..bc72a4b 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -16,12 +16,15 @@
 
 package com.android.server.broadcastradio;
 
+import android.Manifest;
+import android.content.pm.PackageManager;
 import android.hardware.radio.IAnnouncementListener;
 import android.hardware.radio.ICloseHandle;
 import android.hardware.radio.IRadioService;
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ITunerCallback;
 import android.hardware.radio.RadioManager;
+import android.os.Binder;
 import android.os.RemoteException;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
@@ -129,6 +132,13 @@
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (mService.getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump HIDL BroadcastRadioService from "
+                    + "from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                    + " without permission " + Manifest.permission.DUMP);
+            return;
+        }
         IndentingPrintWriter radioPw = new IndentingPrintWriter(pw);
         radioPw.printf("BroadcastRadioService\n");
 
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index e8c65ef..d550650 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -796,6 +796,7 @@
                 pw.print(", isUserSetBrightness=" + events[i].isUserSetBrightness);
                 pw.print(", powerBrightnessFactor=" + events[i].powerBrightnessFactor);
                 pw.print(", isDefaultBrightnessConfig=" + events[i].isDefaultBrightnessConfig);
+                pw.print(", recent lux values=");
                 pw.print(" {");
                 for (int j = 0; j < events[i].luxValues.length; ++j){
                     if (j != 0) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 1674141..41e4671d 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -157,6 +157,7 @@
     private static final int REPORTED_TO_POLICY_SCREEN_TURNING_OFF = 3;
 
     private static final int RINGBUFFER_MAX = 100;
+    private static final int RINGBUFFER_RBC_MAX = 20;
 
     private static final float[] BRIGHTNESS_RANGE_BOUNDARIES = {
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80,
@@ -390,6 +391,10 @@
     // Keeps a record of brightness changes for dumpsys.
     private RingBuffer<BrightnessEvent> mBrightnessEventRingBuffer;
 
+    // Keeps a record of rbc changes for dumpsys.
+    private final RingBuffer<BrightnessEvent> mRbcEventRingBuffer =
+            new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_RBC_MAX);
+
     // Controls and tracks all the wakelocks that are acquired/released by the system. Also acts as
     // a medium of communication between this class and the PowerManagerService.
     private final WakelockController mWakelockController;
@@ -1593,6 +1598,10 @@
                 mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY
                         && mLastBrightnessEvent.getReason().getReason()
                         == BrightnessReason.REASON_TEMPORARY;
+        // Purely for dumpsys;
+        final boolean isRbcEvent =
+                mLastBrightnessEvent.isRbcEnabled() != mTempBrightnessEvent.isRbcEnabled();
+
         if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition)
                 || brightnessAdjustmentFlags != 0) {
             mTempBrightnessEvent.setInitialBrightness(mLastBrightnessEvent.getBrightness());
@@ -1612,6 +1621,10 @@
             if (mBrightnessEventRingBuffer != null) {
                 mBrightnessEventRingBuffer.append(newEvent);
             }
+            if (isRbcEvent) {
+                mRbcEventRingBuffer.append(newEvent);
+            }
+
         }
 
         // Update display white-balance.
@@ -2359,6 +2372,8 @@
             dumpBrightnessEvents(pw);
         }
 
+        dumpRbcEvents(pw);
+
         if (mHbmController != null) {
             mHbmController.dump(pw);
         }
@@ -2431,6 +2446,20 @@
         }
     }
 
+    private void dumpRbcEvents(PrintWriter pw) {
+        int size = mRbcEventRingBuffer.size();
+        if (size < 1) {
+            pw.println("No Reduce Bright Colors Adjustments");
+            return;
+        }
+
+        pw.println("Reduce Bright Colors Adjustments Last " + size + " Events: ");
+        BrightnessEvent[] eventArray = mRbcEventRingBuffer.toArray();
+        for (int i = 0; i < mRbcEventRingBuffer.size(); i++) {
+            pw.println("  " + eventArray[i]);
+        }
+    }
+
 
     private void noteScreenState(int screenState) {
         // Log screen state change with display id
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index cede273..be9df4a 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -63,10 +63,12 @@
 import android.hardware.hdmi.IHdmiVendorCommandListener;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.media.AudioAttributes;
+import android.media.AudioDescriptor;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioDeviceVolumeManager;
 import android.media.AudioManager;
+import android.media.AudioProfile;
 import android.media.VolumeInfo;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
@@ -4727,9 +4729,22 @@
         // reported connection state changes, but even if it did, it won't take effect.
         if (mEarcLocalDevice != null) {
             mEarcLocalDevice.handleEarcStateChange(status);
+        } else if (status == HDMI_EARC_STATUS_ARC_PENDING) {
+            // If the local device is null we notify the Audio Service that eARC connection
+            // is disabled.
+            notifyEarcStatusToAudioService(false, new ArrayList<>());
+            startArcAction(true, null);
         }
     }
 
+    protected void notifyEarcStatusToAudioService(
+            boolean enabled, List<AudioDescriptor> audioDescriptors) {
+        AudioDeviceAttributes attributes = new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "",
+                new ArrayList<AudioProfile>(), audioDescriptors);
+        getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0);
+    }
+
     @ServiceThreadOnly
     void handleEarcCapabilitiesReported(byte[] rawCapabilities, int portId) {
         assertRunOnServiceThread();
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
index 9058c98..873d5fc 100644
--- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
@@ -23,8 +23,6 @@
 
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.media.AudioDescriptor;
-import android.media.AudioDeviceAttributes;
-import android.media.AudioDeviceInfo;
 import android.media.AudioProfile;
 import android.os.Handler;
 import android.util.IndentingPrintWriter;
@@ -88,10 +86,10 @@
 
         mReportCapsHandler.removeCallbacksAndMessages(null);
         if (status == HDMI_EARC_STATUS_IDLE) {
-            notifyEarcStatusToAudioService(false, new ArrayList<>());
+            mService.notifyEarcStatusToAudioService(false, new ArrayList<>());
             mService.startArcAction(false, null);
         } else if (status == HDMI_EARC_STATUS_ARC_PENDING) {
-            notifyEarcStatusToAudioService(false, new ArrayList<>());
+            mService.notifyEarcStatusToAudioService(false, new ArrayList<>());
             mService.startArcAction(true, null);
         } else if (status == HDMI_EARC_STATUS_EARC_PENDING
                 && oldEarcStatus == HDMI_EARC_STATUS_ARC_PENDING) {
@@ -110,19 +108,11 @@
                     && mReportCapsHandler.hasCallbacks(mReportCapsRunnable)) {
                 mReportCapsHandler.removeCallbacksAndMessages(null);
                 List<AudioDescriptor> audioDescriptors = parseCapabilities(rawCapabilities);
-                notifyEarcStatusToAudioService(true, audioDescriptors);
+                mService.notifyEarcStatusToAudioService(true, audioDescriptors);
             }
         }
     }
 
-    private void notifyEarcStatusToAudioService(
-            boolean enabled, List<AudioDescriptor> audioDescriptors) {
-        AudioDeviceAttributes attributes = new AudioDeviceAttributes(
-                AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "",
-                new ArrayList<AudioProfile>(), audioDescriptors);
-        mService.getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0);
-    }
-
     /**
      * Runnable for waiting for a certain amount of time for the audio system to report its
      * capabilities after eARC was connected. If the audio system doesn´t report its capabilities in
@@ -134,7 +124,7 @@
         public void run() {
             synchronized (mLock) {
                 if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED) {
-                    notifyEarcStatusToAudioService(true, new ArrayList<>());
+                    mService.notifyEarcStatusToAudioService(true, new ArrayList<>());
                 }
             }
         }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 20f0697..2e62ef4 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -889,22 +889,31 @@
 
     }
 
-    private void migrateOldDataAfterSystemReady() {
-        // Migrate the FRP credential to the persistent data block
+    @VisibleForTesting
+    void migrateOldDataAfterSystemReady() {
+        // Write the FRP persistent data block if needed.
+        //
+        // The original purpose of this code was to write the FRP block for the first time, when
+        // upgrading from Android 8.1 or earlier which didn't use the FRP block.  This code has
+        // since been repurposed to also fix the "bad" (non-forwards-compatible) FRP block written
+        // by Android 14 Beta 2.  For this reason, the database key used here has been renamed from
+        // "migrated_frp" to "migrated_frp2" to cause migrateFrpCredential() to run again on devices
+        // where it had run before.
         if (LockPatternUtils.frpCredentialEnabled(mContext)
-                && !getBoolean("migrated_frp", false, 0)) {
+                && !getBoolean("migrated_frp2", false, 0)) {
             migrateFrpCredential();
-            setBoolean("migrated_frp", true, 0);
+            setBoolean("migrated_frp2", true, 0);
         }
     }
 
     /**
-     * Migrate the credential for the FRP credential owner user if the following are satisfied:
-     * - the user has a secure credential
-     * - the FRP credential is not set up
+     * Write the FRP persistent data block if the following are satisfied:
+     * - the user who owns the FRP credential has a nonempty credential
+     * - the FRP persistent data block doesn't exist or uses the "bad" format from Android 14 Beta 2
      */
     private void migrateFrpCredential() {
-        if (mStorage.readPersistentDataBlock() != PersistentData.NONE) {
+        PersistentData data = mStorage.readPersistentDataBlock();
+        if (data != PersistentData.NONE && !data.isBadFormatFromAndroid14Beta()) {
             return;
         }
         for (UserInfo userInfo : mUserManager.getUsers()) {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 731ecad..2fa637e 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -606,6 +606,11 @@
             this.payload = payload;
         }
 
+        public boolean isBadFormatFromAndroid14Beta() {
+            return (this.type == TYPE_SP_GATEKEEPER || this.type == TYPE_SP_WEAVER)
+                && SyntheticPasswordManager.PasswordData.isBadFormatFromAndroid14Beta(this.payload);
+        }
+
         public static PersistentData fromBytes(byte[] frpData) {
             if (frpData == null || frpData.length == 0) {
                 return NONE;
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 65e7a00..66f862a 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -370,6 +370,15 @@
             return result;
         }
 
+        /**
+         * Returns true if the given serialized PasswordData begins with the value 2 as a short.
+         * This detects the "bad" (non-forwards-compatible) PasswordData format that was temporarily
+         * used during development of Android 14.  For more details, see fromBytes() below.
+         */
+        public static boolean isBadFormatFromAndroid14Beta(byte[] data) {
+            return data != null && data.length >= 2 && data[0] == 0 && data[1] == 2;
+        }
+
         public static PasswordData fromBytes(byte[] data) {
             PasswordData result = new PasswordData();
             ByteBuffer buffer = ByteBuffer.allocate(data.length);
diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java
index 7a5664f..5288e85 100644
--- a/services/core/java/com/android/server/policy/AppOpsPolicy.java
+++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java
@@ -37,6 +37,7 @@
 import android.os.IBinder;
 import android.os.PackageTagsList;
 import android.os.Process;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.service.voice.VoiceInteractionManagerInternal;
 import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
@@ -68,6 +69,8 @@
     private static final String ACTIVITY_RECOGNITION_TAGS =
             "android:activity_recognition_allow_listed_tags";
     private static final String ACTIVITY_RECOGNITION_TAGS_SEPARATOR = ";";
+    private static final boolean SYSPROP_HOTWORD_DETECTION_SERVICE_REQUIRED =
+            SystemProperties.getBoolean("ro.hotword.detection_service_required", false);
 
     @NonNull
     private final Object mLock = new Object();
@@ -199,10 +202,16 @@
         }
     }
 
-    private static boolean isHotwordDetectionServiceRequired(PackageManager pm) {
+    /**
+     * @hide
+     */
+    public static boolean isHotwordDetectionServiceRequired(PackageManager pm) {
         // The HotwordDetectionService APIs aren't ready yet for Auto or TV.
-        return !(pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
-                || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+        if (pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+                || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+            return false;
+        }
+        return SYSPROP_HOTWORD_DETECTION_SERVICE_REQUIRED;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 363d2fd..044d30b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -53,7 +53,6 @@
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Icon;
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
@@ -949,14 +948,12 @@
     @Override
     public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
             int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
-            int userId, long operationId, String opPackageName, long requestId,
-            @BiometricMultiSensorMode int multiSensorConfig) {
+            int userId, long operationId, String opPackageName, long requestId) {
         enforceBiometricDialog();
         if (mBar != null) {
             try {
                 mBar.showAuthenticationDialog(promptInfo, receiver, sensorIds, credentialAllowed,
-                        requireConfirmation, userId, operationId, opPackageName, requestId,
-                        multiSensorConfig);
+                        requireConfirmation, userId, operationId, opPackageName, requestId);
             } catch (RemoteException ex) {
             }
         }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 9add537..a079875 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -82,6 +82,7 @@
 import android.os.IInterface;
 import android.os.IRemoteCallback;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -2514,6 +2515,7 @@
      * Propagate a wake event to the wallpaper engine(s).
      */
     public void notifyWakingUp(int x, int y, @NonNull Bundle extras) {
+        checkCallerIsSystemOrSystemUi();
         synchronized (mLock) {
             if (mIsLockscreenLiveWallpaperEnabled) {
                 for (WallpaperData data : getActiveWallpapers()) {
@@ -2551,6 +2553,7 @@
      * Propagate a sleep event to the wallpaper engine(s).
      */
     public void notifyGoingToSleep(int x, int y, @NonNull Bundle extras) {
+        checkCallerIsSystemOrSystemUi();
         synchronized (mLock) {
             if (mIsLockscreenLiveWallpaperEnabled) {
                 for (WallpaperData data : getActiveWallpapers()) {
@@ -3684,6 +3687,14 @@
                 mActivityManager.getPackageImportance(callingPackage) == IMPORTANCE_FOREGROUND);
     }
 
+    /** Check that the caller is either system_server or systemui */
+    private void checkCallerIsSystemOrSystemUi() {
+        if (Binder.getCallingUid() != Process.myUid() && mContext.checkCallingPermission(
+                android.Manifest.permission.STATUS_BAR_SERVICE) != PERMISSION_GRANTED) {
+            throw new SecurityException("Access denied: only system processes can call this");
+        }
+    }
+
     /**
      * Certain user types do not support wallpapers (e.g. managed profiles). The check is
      * implemented through through the OP_WRITE_WALLPAPER AppOp.
diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
index 5c929a9..bd07622 100644
--- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
@@ -30,6 +30,7 @@
 import android.graphics.Rect;
 import android.graphics.RenderNode;
 import android.hardware.HardwareBuffer;
+import android.os.SystemClock;
 import android.os.Trace;
 import android.util.Pair;
 import android.util.Slog;
@@ -213,6 +214,7 @@
             // Failed to acquire image. Has been logged.
             return null;
         }
+        builder.setCaptureTime(SystemClock.elapsedRealtimeNanos());
         builder.setSnapshot(screenshotBuffer.getHardwareBuffer());
         builder.setColorSpace(screenshotBuffer.getColorSpace());
         return builder.build();
@@ -432,6 +434,7 @@
         // color above
         return new TaskSnapshot(
                 System.currentTimeMillis() /* id */,
+                SystemClock.elapsedRealtimeNanos() /* captureTime */,
                 topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(),
                 hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation,
                 mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight),
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d84c013..3db0315 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3570,6 +3570,14 @@
 
                 // Tell window manager to prepare for this one to be removed.
                 setVisibility(false);
+                // Propagate the last IME visibility in the same task, so the IME can show
+                // automatically if the next activity has a focused editable view.
+                if (mLastImeShown && mTransitionController.isShellTransitionsEnabled()) {
+                    final ActivityRecord nextRunning = task.topRunningActivity();
+                    if (nextRunning != null) {
+                        nextRunning.mLastImeShown = true;
+                    }
+                }
 
                 if (getTaskFragment().getPausingActivity() == null) {
                     ProtoLog.v(WM_DEBUG_STATES, "Finish needs to pause: %s", this);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 1360a95..750ed98 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5342,6 +5342,12 @@
         return null;
     }
 
+    /**
+     * Returns the {@link WindowProcessController} for the app process for the given uid and pid.
+     *
+     * If no such {@link WindowProcessController} is found, it does not belong to an app, or the
+     * pid does not match the uid {@code null} is returned.
+     */
     WindowProcessController getProcessController(int pid, int uid) {
         final WindowProcessController proc = mProcessMap.getProcess(pid);
         if (proc == null) return null;
@@ -5351,6 +5357,27 @@
         return null;
     }
 
+    /**
+     * Returns the package name if (and only if) the package name can be uniquely determined.
+     * Otherwise returns {@code null}.
+     *
+     * The provided pid must match the provided uid, otherwise this also returns null.
+     */
+    @Nullable String getPackageNameIfUnique(int uid, int pid) {
+        final WindowProcessController proc = mProcessMap.getProcess(pid);
+        if (proc == null || proc.mUid != uid) {
+            Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") has no WPC");
+            return null;
+        }
+        List<String> realCallingPackages = proc.getPackageList();
+        if (realCallingPackages.size() != 1) {
+            Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") is ambiguous: "
+                    + realCallingPackages);
+            return null;
+        }
+        return realCallingPackages.get(0);
+    }
+
     /** A uid is considered to be foreground if it has a visible non-toast window. */
     @HotPath(caller = HotPath.START_SERVICE)
     boolean hasActiveVisibleWindow(int uid) {
diff --git a/services/core/java/com/android/server/wm/AppSnapshotLoader.java b/services/core/java/com/android/server/wm/AppSnapshotLoader.java
index 88c4752..ed65a2b 100644
--- a/services/core/java/com/android/server/wm/AppSnapshotLoader.java
+++ b/services/core/java/com/android/server/wm/AppSnapshotLoader.java
@@ -28,6 +28,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
+import android.os.SystemClock;
 import android.util.Slog;
 import android.window.TaskSnapshot;
 
@@ -195,8 +196,9 @@
                 taskSize = new Point(proto.taskWidth, proto.taskHeight);
             }
 
-            return new TaskSnapshot(proto.id, topActivityComponent, buffer,
-                    hwBitmap.getColorSpace(), proto.orientation, proto.rotation, taskSize,
+            return new TaskSnapshot(proto.id, SystemClock.elapsedRealtimeNanos(),
+                    topActivityComponent, buffer, hwBitmap.getColorSpace(),
+                    proto.orientation, proto.rotation, taskSize,
                     new Rect(proto.insetLeft, proto.insetTop, proto.insetRight, proto.insetBottom),
                     new Rect(proto.letterboxInsetLeft, proto.letterboxInsetTop,
                             proto.letterboxInsetRight, proto.letterboxInsetBottom),
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index d22c38e..123a74d 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -28,11 +28,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.SystemProperties;
 import android.util.AtomicFile;
 import android.util.DisplayMetrics;
 import android.util.Slog;
 import android.util.Xml;
 
+import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
@@ -56,6 +58,7 @@
     public static final int FLAG_HIDE_DISPLAY_SIZE = 0x01;
     public static final int FLAG_HIDE_COMPILE_SDK = 0x02;
     public static final int FLAG_HIDE_DEPRECATED_SDK = 0x04;
+    public static final int FLAG_HIDE_DEPRECATED_ABI = 0x08;
 
     private final HashMap<String, Integer> mPackageFlags = new HashMap<>();
 
@@ -68,6 +71,7 @@
     private UnsupportedDisplaySizeDialog mUnsupportedDisplaySizeDialog;
     private UnsupportedCompileSdkDialog mUnsupportedCompileSdkDialog;
     private DeprecatedTargetSdkVersionDialog mDeprecatedTargetSdkVersionDialog;
+    private DeprecatedAbiDialog mDeprecatedAbiDialog;
 
     /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
     private HashSet<ComponentName> mAlwaysShowUnsupportedCompileSdkWarningActivities =
@@ -166,6 +170,31 @@
     }
 
     /**
+     * Shows the "deprecated abi" warning, if necessary. This can only happen is the device
+     * supports both 64-bit and 32-bit ABIs, and the app only contains 32-bit libraries. The app
+     * cannot be installed if the device only supports 64-bit ABI while the app contains only 32-bit
+     * libraries.
+     *
+     * @param r activity record for which the warning may be displayed
+     */
+    public void showDeprecatedAbiDialogIfNeeded(ActivityRecord r) {
+        final boolean disableDeprecatedAbiDialog = SystemProperties.getBoolean(
+                "debug.wm.disable_deprecated_abi_dialog", false);
+        if (disableDeprecatedAbiDialog) {
+            return;
+        }
+        final String appPrimaryAbi = r.info.applicationInfo.primaryCpuAbi;
+        final String appSecondaryAbi = r.info.applicationInfo.secondaryCpuAbi;
+        final boolean appContainsOnly32bitLibraries =
+                (appPrimaryAbi != null && appSecondaryAbi == null && !appPrimaryAbi.contains("64"));
+        final boolean is64BitDevice =
+                ArrayUtils.find(Build.SUPPORTED_ABIS, abi -> abi.contains("64")) != null;
+        if (is64BitDevice && appContainsOnly32bitLibraries) {
+            mUiHandler.showDeprecatedAbiDialog(r);
+        }
+    }
+
+    /**
      * Called when an activity is being started.
      *
      * @param r record for the activity being started
@@ -174,6 +203,7 @@
         showUnsupportedCompileSdkDialogIfNeeded(r);
         showUnsupportedDisplaySizeDialogIfNeeded(r);
         showDeprecatedTargetDialogIfNeeded(r);
+        showDeprecatedAbiDialogIfNeeded(r);
     }
 
     /**
@@ -299,6 +329,27 @@
     }
 
     /**
+     * Shows the "deprecated abi" warning for the given application.
+     * <p>
+     * <strong>Note:</strong> Must be called on the UI thread.
+     *
+     * @param ar record for the activity that triggered the warning
+     */
+    @UiThread
+    private void showDeprecatedAbiDialogUiThread(ActivityRecord ar) {
+        if (mDeprecatedAbiDialog != null) {
+            mDeprecatedAbiDialog.dismiss();
+            mDeprecatedAbiDialog = null;
+        }
+        if (ar != null && !hasPackageFlag(
+                ar.packageName, FLAG_HIDE_DEPRECATED_ABI)) {
+            mDeprecatedAbiDialog = new DeprecatedAbiDialog(
+                    AppWarnings.this, mUiContext, ar.info.applicationInfo);
+            mDeprecatedAbiDialog.show();
+        }
+    }
+
+    /**
      * Dismisses all warnings for the given package.
      * <p>
      * <strong>Note:</strong> Must be called on the UI thread.
@@ -328,6 +379,13 @@
             mDeprecatedTargetSdkVersionDialog.dismiss();
             mDeprecatedTargetSdkVersionDialog = null;
         }
+
+        // Hides the "deprecated abi" dialog if necessary.
+        if (mDeprecatedAbiDialog != null && (name == null || name.equals(
+                mDeprecatedAbiDialog.mPackageName))) {
+            mDeprecatedAbiDialog.dismiss();
+            mDeprecatedAbiDialog = null;
+        }
     }
 
     /**
@@ -381,6 +439,7 @@
         private static final int MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG = 3;
         private static final int MSG_HIDE_DIALOGS_FOR_PACKAGE = 4;
         private static final int MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG = 5;
+        private static final int MSG_SHOW_DEPRECATED_ABI_DIALOG = 6;
 
         public UiHandler(Looper looper) {
             super(looper, null, true);
@@ -408,6 +467,10 @@
                     final ActivityRecord ar = (ActivityRecord) msg.obj;
                     showDeprecatedTargetSdkDialogUiThread(ar);
                 } break;
+                case MSG_SHOW_DEPRECATED_ABI_DIALOG: {
+                    final ActivityRecord ar = (ActivityRecord) msg.obj;
+                    showDeprecatedAbiDialogUiThread(ar);
+                } break;
             }
         }
 
@@ -431,6 +494,11 @@
             obtainMessage(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG, r).sendToTarget();
         }
 
+        public void showDeprecatedAbiDialog(ActivityRecord r) {
+            removeMessages(MSG_SHOW_DEPRECATED_ABI_DIALOG);
+            obtainMessage(MSG_SHOW_DEPRECATED_ABI_DIALOG, r).sendToTarget();
+        }
+
         public void hideDialogsForPackage(String name) {
             obtainMessage(MSG_HIDE_DIALOGS_FOR_PACKAGE, name).sendToTarget();
         }
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index dc49e8c..b216578 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -180,7 +180,8 @@
             Intent intent,
             ActivityOptions checkedOptions) {
         return checkBackgroundActivityStart(callingUid, callingPid, callingPackage,
-                realCallingUid, realCallingPid, callerApp, originatingPendingIntent,
+                realCallingUid, realCallingPid,
+                callerApp, originatingPendingIntent,
                 backgroundStartPrivileges, intent, checkedOptions) == BAL_BLOCK;
     }
 
@@ -288,11 +289,13 @@
             }
         }
 
+        String realCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+
         // Legacy behavior allows to use caller foreground state to bypass BAL restriction.
         // The options here are the options passed by the sender and not those on the intent.
         final BackgroundStartPrivileges balAllowedByPiSender =
                 PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
-                        checkedOptions, realCallingUid);
+                        checkedOptions, realCallingUid, realCallingPackage);
 
         final boolean logVerdictChangeByPiDefaultChange = checkedOptions == null
                 || checkedOptions.getPendingIntentBackgroundActivityStartMode()
@@ -460,8 +463,11 @@
         // If we are here, it means all exemptions not based on PI sender failed, so we'll block
         // unless resultIfPiSenderAllowsBal is an allow and the PI sender allows BAL
 
-        String realCallingPackage = callingUid == realCallingUid ? callingPackage :
-                mService.mContext.getPackageManager().getNameForUid(realCallingUid);
+        if (realCallingPackage == null) {
+            realCallingPackage = (callingUid == realCallingUid ? callingPackage :
+                    mService.mContext.getPackageManager().getNameForUid(realCallingUid))
+                    + "[debugOnly]";
+        }
 
         String stateDumpLog = " [callingPackage: " + callingPackage
                 + "; callingUid: " + callingUid
diff --git a/services/core/java/com/android/server/wm/DeprecatedAbiDialog.java b/services/core/java/com/android/server/wm/DeprecatedAbiDialog.java
new file mode 100644
index 0000000..e96208d
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DeprecatedAbiDialog.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageItemInfo;
+import android.content.pm.PackageManager;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+
+class DeprecatedAbiDialog extends AppWarnings.BaseDialog {
+    DeprecatedAbiDialog(final AppWarnings manager, Context context,
+            ApplicationInfo appInfo) {
+        super(manager, appInfo.packageName);
+
+        final PackageManager pm = context.getPackageManager();
+        final CharSequence label = appInfo.loadSafeLabel(pm,
+                PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX,
+                PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE
+                        | PackageItemInfo.SAFE_LABEL_FLAG_TRIM);
+        final CharSequence message = context.getString(R.string.deprecated_abi_message);
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context)
+                .setPositiveButton(R.string.ok, (dialog, which) ->
+                    manager.setPackageFlag(
+                            mPackageName, AppWarnings.FLAG_HIDE_DEPRECATED_ABI, true))
+                .setMessage(message)
+                .setTitle(label);
+
+        // Ensure the content view is prepared.
+        mDialog = builder.create();
+        mDialog.create();
+
+        final Window window = mDialog.getWindow();
+        window.setType(WindowManager.LayoutParams.TYPE_PHONE);
+
+        // DO NOT MODIFY. Used by CTS to verify the dialog is displayed.
+        window.getAttributes().setTitle("DeprecatedAbiDialog");
+    }
+}
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 7e20b3b..c747c09 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -180,6 +180,18 @@
     }
 
     /**
+     * Returns the elapsed real time (in nanoseconds) at which a snapshot for the given task was
+     * last taken, or -1 if no such snapshot exists for that task.
+     */
+    long getSnapshotCaptureTime(int taskId) {
+        final TaskSnapshot snapshot = mCache.getSnapshot(taskId);
+        if (snapshot != null) {
+            return snapshot.getCaptureTime();
+        }
+        return -1;
+    }
+
+    /**
      * @see WindowManagerInternal#clearSnapshotCache
      */
     public void clearSnapshotCache() {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c763cfa..663db86 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -382,11 +382,7 @@
             // Add FLAG_ABOVE_TRANSIENT_LAUNCH to the tree of transient-hide tasks,
             // so ChangeInfo#hasChanged() can return true to report the transition info.
             for (int i = mChanges.size() - 1; i >= 0; --i) {
-                final WindowContainer<?> wc = mChanges.keyAt(i);
-                if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) continue;
-                if (isInTransientHide(wc)) {
-                    mChanges.valueAt(i).mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH;
-                }
+                updateTransientFlags(mChanges.valueAt(i));
             }
         }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as "
@@ -581,7 +577,9 @@
         for (WindowContainer<?> curr = getAnimatableParent(wc);
                 curr != null && !mChanges.containsKey(curr);
                 curr = getAnimatableParent(curr)) {
-            mChanges.put(curr, new ChangeInfo(curr));
+            final ChangeInfo info = new ChangeInfo(curr);
+            updateTransientFlags(info);
+            mChanges.put(curr, info);
             if (isReadyGroup(curr)) {
                 mReadyTracker.addGroup(curr);
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for"
@@ -600,6 +598,7 @@
         ChangeInfo info = mChanges.get(wc);
         if (info == null) {
             info = new ChangeInfo(wc);
+            updateTransientFlags(info);
             mChanges.put(wc, info);
         }
         mParticipants.add(wc);
@@ -615,6 +614,14 @@
         }
     }
 
+    private void updateTransientFlags(@NonNull ChangeInfo info) {
+        final WindowContainer<?> wc = info.mContainer;
+        // Only look at tasks, taskfragments, or activities
+        if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) return;
+        if (!isInTransientHide(wc)) return;
+        info.mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH;
+    }
+
     private void recordDisplay(DisplayContent dc) {
         if (dc == null || mTargetDisplays.contains(dc)) return;
         mTargetDisplays.add(dc);
@@ -1079,12 +1086,23 @@
                     if (commitVisibility) {
                         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                                 "  Commit activity becoming invisible: %s", ar);
+                        final SnapshotController snapController = mController.mSnapshotController;
                         if (mTransientLaunches != null && !task.isVisibleRequested()) {
+                            final long startTimeNs = mLogger.mSendTimeNs;
+                            final long lastSnapshotTimeNs = snapController.mTaskSnapshotController
+                                    .getSnapshotCaptureTime(task.mTaskId);
                             // If transition is transient, then snapshots are taken at end of
-                            // transition.
-                            mController.mSnapshotController.mTaskSnapshotController
-                                    .recordSnapshot(task, false /* allowSnapshotHome */);
-                            mController.mSnapshotController.mActivitySnapshotController
+                            // transition only if a snapshot was not already captured by request
+                            // during the transition
+                            if (lastSnapshotTimeNs < startTimeNs) {
+                                snapController.mTaskSnapshotController
+                                        .recordSnapshot(task, false /* allowSnapshotHome */);
+                            } else {
+                                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+                                        "  Skipping post-transition snapshot for task %d",
+                                        task.mTaskId);
+                            }
+                            snapController.mActivitySnapshotController
                                     .notifyAppVisibilityChanged(ar, false /* visible */);
                         }
                         ar.commitVisibility(false /* visible */, false /* performLayout */,
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 322c11a..e33c6f0 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9254,7 +9254,6 @@
 
     boolean shouldRestoreImeVisibility(IBinder imeTargetWindowToken) {
         final Task imeTargetWindowTask;
-        boolean hadRequestedShowIme = false;
         synchronized (mGlobalLock) {
             final WindowState imeTargetWindow = mWindowMap.get(imeTargetWindowToken);
             if (imeTargetWindow == null) {
@@ -9264,14 +9263,15 @@
             if (imeTargetWindowTask == null) {
                 return false;
             }
-            if (imeTargetWindow.mActivityRecord != null) {
-                hadRequestedShowIme = imeTargetWindow.mActivityRecord.mLastImeShown;
+            if (imeTargetWindow.mActivityRecord != null
+                    && imeTargetWindow.mActivityRecord.mLastImeShown) {
+                return true;
             }
         }
         final TaskSnapshot snapshot = getTaskSnapshot(imeTargetWindowTask.mTaskId,
                 imeTargetWindowTask.mUserId, false /* isLowResolution */,
                 false /* restoreFromDisk */);
-        return snapshot != null && snapshot.hasImeSurface() || hadRequestedShowIme;
+        return snapshot != null && snapshot.hasImeSurface();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index dbd9e4b..3672820 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -721,6 +721,12 @@
         }
     }
 
+    List<String> getPackageList() {
+        synchronized (mPkgList) {
+            return new ArrayList<>(mPkgList);
+        }
+    }
+
     void addActivityIfNeeded(ActivityRecord r) {
         // even if we already track this activity, note down that it has been launched
         setLastActivityLaunchTime(r);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index ad5f0d7..6365764 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -269,7 +269,9 @@
                     deliverRes = res;
                     break;
             }
+            res.setPendingStart(true);
             mHandlerThread.getThreadHandler().post(() -> {
+                res.setPendingStart(false);
                 synchronized (mAms) {
                     switch (behavior) {
                         case SUCCESS:
@@ -281,6 +283,10 @@
                             mActiveProcesses.remove(deliverRes);
                             mQueue.onApplicationTimeoutLocked(deliverRes);
                             break;
+                        case KILLED_WITHOUT_NOTIFY:
+                            mActiveProcesses.remove(res);
+                            res.setKilled(true);
+                            break;
                         default:
                             throw new UnsupportedOperationException();
                     }
@@ -310,6 +316,7 @@
         mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
         mConstants.TIMEOUT = 100;
         mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
+        mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
 
         mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
         doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
@@ -381,6 +388,8 @@
         FAIL_TIMEOUT_PREDECESSOR,
         /** Process fails by immediately returning null */
         FAIL_NULL,
+        /** Process is killed without reporting to BroadcastQueue */
+        KILLED_WITHOUT_NOTIFY,
     }
 
     private enum ProcessBehavior {
@@ -522,6 +531,11 @@
         return info;
     }
 
+    static BroadcastFilter withPriority(BroadcastFilter filter, int priority) {
+        filter.setPriority(priority);
+        return filter;
+    }
+
     static ResolveInfo makeManifestReceiver(String packageName, String name) {
         return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
     }
@@ -1261,6 +1275,46 @@
                 new ComponentName(PACKAGE_GREEN, CLASS_GREEN));
     }
 
+    /**
+     * Verify that when BroadcastQueue doesn't get notified when a process gets killed, it
+     * doesn't get stuck.
+     */
+    @Test
+    public void testKillWithoutNotify() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+        mNextProcessStartBehavior.set(ProcessStartBehavior.KILLED_WITHOUT_NOTIFY);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
+                withPriority(makeRegisteredReceiver(receiverBlueApp), 5),
+                withPriority(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), 0))));
+
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE))));
+
+        waitForIdle();
+        final ProcessRecord receiverGreenApp = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+                getUidForPackage(PACKAGE_YELLOW));
+        final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+                getUidForPackage(PACKAGE_ORANGE));
+
+        if (mImpl == Impl.MODERN) {
+            // Modern queue does not retry sending a broadcast once any broadcast delivery fails.
+            assertNull(receiverGreenApp);
+        } else {
+            verifyScheduleReceiver(times(1), receiverGreenApp, airplane);
+        }
+        verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
+        verifyScheduleReceiver(times(1), receiverYellowApp, airplane);
+        verifyScheduleReceiver(times(1), receiverOrangeApp, timezone);
+    }
+
     @Test
     public void testCold_Success() throws Exception {
         doCold(ProcessStartBehavior.SUCCESS);
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 154aa7d4..4268eb9 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -231,7 +231,14 @@
     public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes()
             throws Exception {
         setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
-        testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
+    }
+
+    @Test
+    public void testMultiAuth_singleSensor_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+            throws Exception {
+        setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
     }
 
     @Test
@@ -239,10 +246,18 @@
             throws Exception {
         setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
         setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
-        testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
     }
 
-    public void testMultiAuth_fingerprintSensorStartsAfterUINotifies()
+    @Test
+    public void testMultiAuth_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+            throws Exception {
+        setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+        setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
+    }
+
+    public void testMultiAuth_fingerprintSensorStartsAfterUINotifies(boolean startFingerprintNow)
             throws Exception {
         final long operationId = 123;
         final int userId = 10;
@@ -282,13 +297,21 @@
         // fingerprint sensor does not start even if all cookies are received
         assertEquals(STATE_AUTH_STARTED, session.getState());
         verify(mStatusBarService).showAuthenticationDialog(any(), any(), any(),
-                anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong(), anyInt());
+                anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong());
 
         // Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started.
-        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn(startFingerprintNow);
         assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
-        assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+        assertEquals(startFingerprintNow ? BiometricSensor.STATE_AUTHENTICATING
+                        : BiometricSensor.STATE_COOKIE_RETURNED,
                 session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+
+        // start fingerprint sensor if it was delayed
+        if (!startFingerprintNow) {
+            session.onStartFingerprint();
+            assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+                    session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+        }
     }
 
     @Test
@@ -316,14 +339,14 @@
         verify(impl, never()).startPreparedClient(anyInt());
 
         // First invocation should start the client monitor.
-        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn(true /* startFingerprintNow */);
         assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
         verify(impl).startPreparedClient(anyInt());
 
         // Subsequent invocations should not start the client monitor again.
-        session.onDialogAnimatedIn();
-        session.onDialogAnimatedIn();
-        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn(true /* startFingerprintNow */);
+        session.onDialogAnimatedIn(false /* startFingerprintNow */);
+        session.onDialogAnimatedIn(true /* startFingerprintNow */);
         assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
         verify(impl, times(1)).startPreparedClient(anyInt());
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 520e1c8..67be376 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -18,7 +18,7 @@
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
 
 import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
@@ -311,8 +311,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -397,8 +396,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -516,7 +514,7 @@
         assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
 
         // startPreparedClient invoked
-        mBiometricService.mAuthSession.onDialogAnimatedIn();
+        mBiometricService.mAuthSession.onDialogAnimatedIn(true /* startFingerprintNow */);
         verify(mBiometricService.mSensors.get(0).impl)
                 .startPreparedClient(cookieCaptor.getValue());
 
@@ -530,8 +528,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
 
         // Hardware authenticated
         final byte[] HAT = generateRandomHAT();
@@ -587,8 +584,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -752,8 +748,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 anyString(),
-                anyLong() /* requestId */,
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                anyLong() /* requestId */);
     }
 
     @Test
@@ -854,8 +849,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -935,8 +929,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -1432,8 +1425,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(requestId),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(requestId));
 
         // Requesting strong and credential, when credential is setup
         resetReceivers();
@@ -1456,8 +1448,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(requestId),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(requestId));
 
         // Un-downgrading the authenticator allows successful strong auth
         for (BiometricSensor sensor : mBiometricService.mSensors) {
@@ -1482,8 +1473,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(requestId),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(requestId));
     }
 
     @Test(expected = IllegalStateException.class)
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
index 25bd9bc..be9f52e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
@@ -17,12 +17,14 @@
 package com.android.server.biometrics.sensors.face.aidl;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -41,6 +43,7 @@
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -82,6 +85,10 @@
     private AuthSessionCoordinator mAuthSessionCoordinator;
     @Mock
     FaceProvider mFaceProvider;
+    @Mock
+    BaseClientMonitor mClientMonitor;
+    @Mock
+    AidlSession mCurrentSession;
 
     private final TestLooper mLooper = new TestLooper();
     private final LockoutCache mLockoutCache = new LockoutCache();
@@ -161,6 +168,39 @@
         assertNull(sensor.getSessionForUser(USER_ID));
     }
 
+    @Test
+    public void onBinderDied_cancelNonInterruptableClient() {
+        mLooper.dispatchAll();
+
+        when(mCurrentSession.getUserId()).thenReturn(USER_ID);
+        when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID);
+        when(mClientMonitor.isInterruptable()).thenReturn(false);
+
+        final SensorProps sensorProps = new SensorProps();
+        sensorProps.commonProps = new CommonProps();
+        sensorProps.commonProps.sensorId = 1;
+        final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+                sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
+                sensorProps.commonProps.maxEnrollmentsPerUser, null,
+                sensorProps.sensorType, sensorProps.supportsDetectInteraction,
+                sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
+        final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null,
+                internalProp, mLockoutResetDispatcher, mBiometricContext, mCurrentSession);
+        mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
+        sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
+                USER_ID, mHalCallback);
+
+        mScheduler.scheduleClientMonitor(mClientMonitor);
+
+        assertNotNull(mScheduler.getCurrentClient());
+
+        sensor.onBinderDied();
+
+        verify(mClientMonitor).cancel();
+        assertNull(sensor.getSessionForUser(USER_ID));
+        assertNull(mScheduler.getCurrentClient());
+    }
+
     private void verifyNotLocked() {
         assertEquals(LockoutTracker.LOCKOUT_NONE, mLockoutCache.getLockoutModeForUser(USER_ID));
         verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID));
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
index 0c13466..15d7601 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
@@ -17,17 +17,23 @@
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.face.SensorProps;
 import android.hardware.biometrics.fingerprint.ISession;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Handler;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -37,11 +43,13 @@
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -76,6 +84,14 @@
     private BiometricContext mBiometricContext;
     @Mock
     private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    FingerprintProvider mFingerprintProvider;
+    @Mock
+    GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
+    @Mock
+    private AidlSession mCurrentSession;
+    @Mock
+    private BaseClientMonitor mClientMonitor;
 
     private final TestLooper mLooper = new TestLooper();
     private final LockoutCache mLockoutCache = new LockoutCache();
@@ -130,6 +146,40 @@
         verifyNotLocked();
     }
 
+    @Test
+    public void onBinderDied_cancelNonInterruptableClient() {
+        mLooper.dispatchAll();
+
+        when(mCurrentSession.getUserId()).thenReturn(USER_ID);
+        when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID);
+        when(mClientMonitor.isInterruptable()).thenReturn(false);
+
+        final SensorProps sensorProps = new SensorProps();
+        sensorProps.commonProps = new CommonProps();
+        sensorProps.commonProps.sensorId = 1;
+        final FingerprintSensorPropertiesInternal internalProp = new
+                FingerprintSensorPropertiesInternal(
+                        sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
+                        sensorProps.commonProps.maxEnrollmentsPerUser, null,
+                        sensorProps.sensorType, false /* resetLockoutRequiresHardwareAuthToken */);
+        final Sensor sensor = new Sensor("SensorTest", mFingerprintProvider, mContext,
+                null /* handler */, internalProp, mLockoutResetDispatcher,
+                mGestureAvailabilityDispatcher, mBiometricContext, mCurrentSession);
+        mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
+        sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
+                USER_ID, mHalCallback);
+
+        mScheduler.scheduleClientMonitor(mClientMonitor);
+
+        assertNotNull(mScheduler.getCurrentClient());
+
+        sensor.onBinderDied();
+
+        verify(mClientMonitor).cancel();
+        assertNull(sensor.getSessionForUser(USER_ID));
+        assertNull(mScheduler.getCurrentClient());
+    }
+
     private void verifyNotLocked() {
         assertEquals(LockoutTracker.LOCKOUT_NONE, mLockoutCache.getLockoutModeForUser(USER_ID));
         verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID));
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 0e6b412..39930bc 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -92,6 +92,7 @@
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private HdmiPortInfo[] mHdmiPortInfo;
     private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
+    private static final int PORT_ID_EARC_SUPPORTED = 3;
 
     @Before
     public void setUp() throws Exception {
@@ -148,7 +149,7 @@
                         .setEarcSupported(false)
                         .build();
         mHdmiPortInfo[2] =
-                new HdmiPortInfo.Builder(3, HdmiPortInfo.PORT_INPUT, 0x2000)
+                new HdmiPortInfo.Builder(PORT_ID_EARC_SUPPORTED, HdmiPortInfo.PORT_INPUT, 0x2000)
                         .setCecSupported(true)
                         .setMhlSupported(false)
                         .setArcSupported(true)
@@ -1129,6 +1130,23 @@
     }
 
     @Test
+    public void disableEarc_noEarcLocalDevice_enableArc() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        mHdmiControlServiceSpy.addEarcLocalDevice(
+                new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy));
+        mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNull();
+
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+        mHdmiControlServiceSpy.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING,
+                PORT_ID_EARC_SUPPORTED);
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyEarcStatusToAudioService(eq(false), eq(new ArrayList<>()));
+        verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(true), any());
+    }
+
+    @Test
     public void disableCec_doNotClearEarcLocalDevice() {
         mHdmiControlServiceSpy.clearEarcLocalDevice();
         mHdmiControlServiceSpy.addEarcLocalDevice(
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
index 2b49b8a..a242cde 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
@@ -23,6 +23,8 @@
 import static com.android.internal.widget.LockPatternUtils.USER_FRP;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.app.PropertyInvalidatedCache;
 import android.app.admin.DevicePolicyManager;
@@ -38,8 +40,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.nio.ByteBuffer;
 
-/** Test setting a lockscreen credential and then verify it under USER_FRP */
+/** Tests that involve the Factory Reset Protection (FRP) credential. */
 @SmallTest
 @Presubmit
 @RunWith(AndroidJUnit4.class)
@@ -148,4 +151,68 @@
                 mService.verifyCredential(newPin("1234"), USER_FRP, 0 /* flags */)
                         .getResponseCode());
     }
+
+    // The FRP block that gets written by the current version of Android must still be accepted by
+    // old versions of Android.  This test tries to detect non-forward-compatible changes in
+    // PasswordData#toBytes(), which would break that.
+    @Test
+    public void testFrpBlock_isForwardsCompatible() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+        PersistentData data = mStorage.readPersistentDataBlock();
+        ByteBuffer buffer = ByteBuffer.wrap(data.payload);
+
+        final int credentialType = buffer.getInt();
+        assertEquals(CREDENTIAL_TYPE_PIN, credentialType);
+
+        final byte scryptLogN = buffer.get();
+        assertTrue(scryptLogN >= 0);
+
+        final byte scryptLogR = buffer.get();
+        assertTrue(scryptLogR >= 0);
+
+        final byte scryptLogP = buffer.get();
+        assertTrue(scryptLogP >= 0);
+
+        final int saltLength = buffer.getInt();
+        assertTrue(saltLength > 0);
+        final byte[] salt = new byte[saltLength];
+        buffer.get(salt);
+
+        final int passwordHandleLength = buffer.getInt();
+        assertTrue(passwordHandleLength > 0);
+        final byte[] passwordHandle = new byte[passwordHandleLength];
+        buffer.get(passwordHandle);
+    }
+
+    @Test
+    public void testFrpBlock_inBadAndroid14FormatIsAutomaticallyFixed() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+
+        // Write a "bad" FRP block with PasswordData beginning with the bytes [0, 2].
+        byte[] badPasswordData = new byte[] {
+                0, 2, /* version 2 */
+                0, 3, /* CREDENTIAL_TYPE_PIN */
+                11, /* scryptLogN */
+                22, /* scryptLogR */
+                33, /* scryptLogP */
+                0, 0, 0, 5, /* salt.length */
+                1, 2, -1, -2, 55, /* salt */
+                0, 0, 0, 6, /* passwordHandle.length */
+                2, 3, -2, -3, 44, 1, /* passwordHandle */
+                0, 0, 0, 6, /* pinLength */
+        };
+        mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_GATEKEEPER, PRIMARY_USER_ID, 0,
+                badPasswordData);
+
+        // Execute the code that should fix the FRP block.
+        assertFalse(mStorage.getBoolean("migrated_frp2", false, 0));
+        mService.migrateOldDataAfterSystemReady();
+        assertTrue(mStorage.getBoolean("migrated_frp2", false, 0));
+
+        // Verify that the FRP block has been fixed.
+        PersistentData data = mStorage.readPersistentDataBlock();
+        assertEquals(PersistentData.TYPE_SP_GATEKEEPER, data.type);
+        ByteBuffer buffer = ByteBuffer.wrap(data.payload);
+        assertEquals(CREDENTIAL_TYPE_PIN, buffer.getInt());
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 067feae..ce0347d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -638,6 +638,7 @@
                 2, 3, -2, -3, 44, 1, /* passwordHandle */
                 0, 0, 0, 6, /* pinLength */
         };
+        assertFalse(PasswordData.isBadFormatFromAndroid14Beta(serialized));
         PasswordData deserialized = PasswordData.fromBytes(serialized);
 
         assertEquals(11, deserialized.scryptLogN);
@@ -690,6 +691,7 @@
                 2, 3, -2, -3, 44, 1, /* passwordHandle */
                 0, 0, 0, 6, /* pinLength */
         };
+        assertTrue(PasswordData.isBadFormatFromAndroid14Beta(serialized));
         PasswordData deserialized = PasswordData.fromBytes(serialized);
 
         assertEquals(11, deserialized.scryptLogN);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index 27677e1..0e627b2 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -88,7 +88,6 @@
     private static final Multimap<Class<?>, String> KNOWN_BAD =
             ImmutableMultimap.<Class<?>, String>builder()
                     .put(Notification.Builder.class, "setPublicVersion") // b/276294099
-                    .putAll(RemoteViews.class, "addView", "addStableView") // b/277740082
                     .put(RemoteViews.class, "setIcon") // b/281018094
                     .put(Notification.WearableExtender.class, "addAction") // TODO: b/281044385
                     .put(Person.Builder.class, "setUri") // TODO: b/281044385
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 2671e77..2b589bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -944,7 +944,7 @@
                 anyInt(), anyInt()));
         doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when(
                 () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
-                anyObject(), anyInt()));
+                anyObject(), anyInt(), anyObject()));
         runAndVerifyBackgroundActivityStartsSubtest(
                 "allowed_notAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 10f4158..0ccb0d0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -1286,7 +1286,7 @@
             doReturn(bufferSize.x).when(buffer).getWidth();
             doReturn(bufferSize.y).when(buffer).getHeight();
         }
-        return new TaskSnapshot(1, new ComponentName("", ""), buffer,
+        return new TaskSnapshot(1, 0 /* captureTime */, new ComponentName("", ""), buffer,
                 ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
                 Surface.ROTATION_0, taskSize, new Rect() /* contentInsets */,
                 new Rect() /* letterboxInsets*/, false /* isLowResolution */,
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
index b69874a..84c0696 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -218,7 +218,7 @@
             Canvas c = buffer.lockCanvas();
             c.drawColor(Color.RED);
             buffer.unlockCanvasAndPost(c);
-            return new TaskSnapshot(MOCK_SNAPSHOT_ID, mTopActivityComponent,
+            return new TaskSnapshot(MOCK_SNAPSHOT_ID, 0 /* captureTime */, mTopActivityComponent,
                     HardwareBuffer.createFromGraphicBuffer(buffer),
                     ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
                     mRotation, taskSize, TEST_CONTENT_INSETS, TEST_LETTERBOX_INSETS,
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index 3a65104..7598952 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -84,6 +84,7 @@
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.infra.AndroidFuture;
 import com.android.server.LocalServices;
+import com.android.server.policy.AppOpsPolicy;
 import com.android.server.voiceinteraction.VoiceInteractionManagerServiceImpl.DetectorRemoteExceptionListener;
 
 import java.io.Closeable;
@@ -742,18 +743,24 @@
     void enforcePermissionsForDataDelivery() {
         Binder.withCleanCallingIdentity(() -> {
             synchronized (mLock) {
-                int result = PermissionChecker.checkPermissionForPreflight(
-                        mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid,
-                        mVoiceInteractorIdentity.packageName);
-                if (result != PermissionChecker.PERMISSION_GRANTED) {
-                    throw new SecurityException(
-                        "Failed to obtain permission RECORD_AUDIO for identity "
-                        + mVoiceInteractorIdentity);
+                if (AppOpsPolicy.isHotwordDetectionServiceRequired(mContext.getPackageManager())) {
+                    int result = PermissionChecker.checkPermissionForPreflight(
+                            mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid,
+                            mVoiceInteractorIdentity.packageName);
+                    if (result != PermissionChecker.PERMISSION_GRANTED) {
+                        throw new SecurityException(
+                                "Failed to obtain permission RECORD_AUDIO for identity "
+                                        + mVoiceInteractorIdentity);
+                    }
+                    int hotwordOp = AppOpsManager.strOpToOp(
+                            AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
+                    mAppOpsManager.noteOpNoThrow(hotwordOp,
+                            mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+                            mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE);
+                } else {
+                    enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
+                            RECORD_AUDIO, HOTWORD_DETECTION_OP_MESSAGE);
                 }
-                int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
-                mAppOpsManager.noteOpNoThrow(hotwordOp,
-                        mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
-                        mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE);
                 enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
                         CAPTURE_AUDIO_HOTWORD, HOTWORD_DETECTION_OP_MESSAGE);
             }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/TEST_MAPPING b/tests/FlickerTests/src/com/android/server/wm/flicker/TEST_MAPPING
deleted file mode 100644
index 945de33..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/TEST_MAPPING
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "ironwood-postsubmit": [
-    {
-      "name": "FlickerTests",
-      "options": [
-        {
-          "include-annotation": "android.platform.test.annotations.IwTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
-    }
-  ]
-}
\ No newline at end of file