Merge "Add setter for textIdEntry in ViewNode" into udc-qpr-dev
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/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/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/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/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..d16b497 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
@@ -492,6 +492,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);
             }
         }
     }
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/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/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 16c8648..e8b9f2a 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/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/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..3753d10
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+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";
+
+    @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);
+
+        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);
+        }
+
+        final AuthDialog.LayoutParams params = onMeasureInternal(width, height);
+        setMeasuredDimension(params.mMediumWidth, params.mMediumHeight);
+    }
+
+    @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/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/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 54da680..5d3f5f2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -333,15 +333,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 +364,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 +375,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 +407,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 +420,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 +432,7 @@
 
         @Override // Binder interface
         public void onFinishedWakingUp() {
+            trace("onFinishedWakingUp");
             Trace.beginSection("KeyguardService.mBinder#onFinishedWakingUp");
             checkPermission();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.FINISHED_WAKING_UP);
@@ -417,6 +441,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 +476,7 @@
 
         @Override // Binder interface
         public void onScreenTurnedOn() {
+            trace("onScreenTurnedOn");
             Trace.beginSection("KeyguardService.mBinder#onScreenTurnedOn");
             checkPermission();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_ON);
@@ -460,12 +486,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 +502,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 +518,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 +549,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/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 7627c0c..efbec29 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -288,15 +288,55 @@
     }
 
     /**
+     * Like [updateNoteTaskAsUser] but automatically apply to the current user and all its work
+     * profiles.
+     *
+     * @see updateNoteTaskAsUser
+     * @see UserTracker.userHandle
+     * @see UserTracker.userProfiles
+     */
+    fun updateNoteTaskForCurrentUserAndManagedProfiles() {
+        updateNoteTaskAsUser(userTracker.userHandle)
+        for (profile in userTracker.userProfiles) {
+            if (userManager.isManagedProfile(profile.id)) {
+                updateNoteTaskAsUser(profile.userHandle)
+            }
+        }
+    }
+
+    /**
      * Updates all [NoteTaskController] related information, including but not exclusively the
      * widget shortcut created by the [user] - by default it will use the current user.
      *
+     * If the user is not current user, the update will be dispatched to run in that user's process.
+     *
      * Keep in mind the shortcut API has a
      * [rate limiting](https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#rate-limiting)
      * and may not be updated in real-time. To reduce the chance of stale shortcuts, we run the
      * function during System UI initialization.
      */
     fun updateNoteTaskAsUser(user: UserHandle) {
+        if (!userManager.isUserUnlocked(user)) {
+            debugLog { "updateNoteTaskAsUser call but user locked: user=$user" }
+            return
+        }
+
+        if (user == userTracker.userHandle) {
+            updateNoteTaskAsUserInternal(user)
+        } else {
+            // TODO(b/278729185): Replace fire and forget service with a bounded service.
+            val intent = NoteTaskControllerUpdateService.createIntent(context)
+            context.startServiceAsUser(intent, user)
+        }
+    }
+
+    @InternalNoteTaskApi
+    fun updateNoteTaskAsUserInternal(user: UserHandle) {
+        if (!userManager.isUserUnlocked(user)) {
+            debugLog { "updateNoteTaskAsUserInternal call but user locked: user=$user" }
+            return
+        }
+
         val packageName = roleManager.getDefaultRoleHolderAsUser(ROLE_NOTES, user)
         val hasNotesRoleHolder = isEnabled && !packageName.isNullOrEmpty()
 
@@ -314,18 +354,8 @@
     /** @see OnRoleHoldersChangedListener */
     fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
         if (roleName != ROLE_NOTES) return
-        if (!userManager.isUserUnlocked(user)) {
-            debugLog { "onRoleHoldersChanged call but user locked: role=$roleName, user=$user" }
-            return
-        }
 
-        if (user == userTracker.userHandle) {
-            updateNoteTaskAsUser(user)
-        } else {
-            // TODO(b/278729185): Replace fire and forget service with a bounded service.
-            val intent = NoteTaskControllerUpdateService.createIntent(context)
-            context.startServiceAsUser(intent, user)
-        }
+        updateNoteTaskAsUser(user)
     }
 
     private val SecureSettings.preferredUser: UserHandle
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
index 26b35cc..3e352af 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
@@ -44,7 +44,7 @@
     override fun onCreate() {
         super.onCreate()
         // TODO(b/278729185): Replace fire and forget service with a bounded service.
-        controller.updateNoteTaskAsUser(user)
+        controller.updateNoteTaskAsUserInternal(user)
         stopSelf()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index 221ff65..fe1034a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -15,7 +15,10 @@
  */
 package com.android.systemui.notetask
 
+import android.app.role.OnRoleHoldersChangedListener
 import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.UserInfo
 import android.os.UserHandle
 import android.view.KeyEvent
 import android.view.KeyEvent.KEYCODE_N
@@ -54,6 +57,7 @@
         initializeHandleSystemKey()
         initializeOnRoleHoldersChanged()
         initializeOnUserUnlocked()
+        initializeUserTracker()
     }
 
     /**
@@ -79,7 +83,7 @@
     private fun initializeOnRoleHoldersChanged() {
         roleManager.addOnRoleHoldersChangedListenerAsUser(
             backgroundExecutor,
-            controller::onRoleHoldersChanged,
+            callbacks,
             UserHandle.ALL,
         )
     }
@@ -93,18 +97,41 @@
      */
     private fun initializeOnUserUnlocked() {
         if (keyguardUpdateMonitor.isUserUnlocked(userTracker.userId)) {
-            controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
-        } else {
-            keyguardUpdateMonitor.registerCallback(onUserUnlockedCallback)
+            controller.updateNoteTaskForCurrentUserAndManagedProfiles()
         }
+        keyguardUpdateMonitor.registerCallback(callbacks)
     }
 
-    // KeyguardUpdateMonitor.registerCallback uses a weak reference, so we need a hard reference.
-    private val onUserUnlockedCallback =
-        object : KeyguardUpdateMonitorCallback() {
+    private fun initializeUserTracker() {
+        userTracker.addCallback(callbacks, backgroundExecutor)
+    }
+
+    // Some callbacks use a weak reference, so we play safe and keep a hard reference to them all.
+    private val callbacks =
+        object :
+            KeyguardUpdateMonitorCallback(),
+            CommandQueue.Callbacks,
+            UserTracker.Callback,
+            OnRoleHoldersChangedListener {
+
+            override fun handleSystemKey(key: KeyEvent) {
+                key.toNoteTaskEntryPointOrNull()?.let(controller::showNoteTask)
+            }
+
+            override fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
+                controller.onRoleHoldersChanged(roleName, user)
+            }
+
             override fun onUserUnlocked() {
-                controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
-                keyguardUpdateMonitor.removeCallback(this)
+                controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+            }
+
+            override fun onUserChanged(newUser: Int, userContext: Context) {
+                controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+            }
+
+            override fun onProfilesChanged(profiles: List<UserInfo>) {
+                controller.updateNoteTaskForCurrentUserAndManagedProfiles()
             }
         }
 }
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/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/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 8b070ef..079ef37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -63,6 +63,7 @@
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.settings.SecureSettings
 import com.android.wm.shell.bubbles.Bubble
 import com.android.wm.shell.bubbles.Bubbles
@@ -649,7 +650,7 @@
     }
 
     @Test
-    fun onRoleHoldersChanged_notesRole_sameUser_shouldUpdateShortcuts() {
+    fun onRoleHoldersChanged_notesRole_shouldUpdateShortcuts() {
         val user = userTracker.userHandle
         val controller = spy(createNoteTaskController())
         doNothing().whenever(controller).updateNoteTaskAsUser(any())
@@ -658,22 +659,41 @@
 
         verify(controller).updateNoteTaskAsUser(user)
     }
-
-    @Test
-    fun onRoleHoldersChanged_notesRole_differentUser_shouldUpdateShortcutsInUserProcess() {
-        // FakeUserTracker will default to UserHandle.SYSTEM.
-        val user = UserHandle.CURRENT
-
-        createNoteTaskController(isEnabled = true).onRoleHoldersChanged(ROLE_NOTES, user)
-
-        verify(context).startServiceAsUser(any(), eq(user))
-    }
     // endregion
 
     // region updateNoteTaskAsUser
     @Test
-    fun updateNoteTaskAsUser_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
-        createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle)
+    fun updateNoteTaskAsUser_sameUser_shouldUpdateShortcuts() {
+        val user = userTracker.userHandle
+        val controller = spy(createNoteTaskController())
+        doNothing().whenever(controller).updateNoteTaskAsUserInternal(any())
+
+        controller.updateNoteTaskAsUser(user)
+
+        verify(controller).updateNoteTaskAsUserInternal(user)
+        verify(context, never()).startServiceAsUser(any(), any())
+    }
+
+    @Test
+    fun updateNoteTaskAsUser_differentUser_shouldUpdateShortcutsInUserProcess() {
+        // FakeUserTracker will default to UserHandle.SYSTEM.
+        val user = UserHandle.CURRENT
+        val controller = spy(createNoteTaskController(isEnabled = true))
+        doNothing().whenever(controller).updateNoteTaskAsUserInternal(any())
+
+        controller.updateNoteTaskAsUser(user)
+
+        verify(controller, never()).updateNoteTaskAsUserInternal(any())
+        val intent = withArgCaptor { verify(context).startServiceAsUser(capture(), eq(user)) }
+        assertThat(intent).hasComponentClass(NoteTaskControllerUpdateService::class.java)
+    }
+    // endregion
+
+    // region internalUpdateNoteTaskAsUser
+    @Test
+    fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
+        createNoteTaskController(isEnabled = true)
+            .updateNoteTaskAsUserInternal(userTracker.userHandle)
 
         val actualComponent = argumentCaptor<ComponentName>()
         verify(context.packageManager)
@@ -702,11 +722,12 @@
     }
 
     @Test
-    fun updateNoteTaskAsUser_noNotesRole_shouldDisableShortcuts() {
+    fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() {
         whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle))
             .thenReturn(emptyList())
 
-        createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle)
+        createNoteTaskController(isEnabled = true)
+            .updateNoteTaskAsUserInternal(userTracker.userHandle)
 
         val argument = argumentCaptor<ComponentName>()
         verify(context.packageManager)
@@ -723,8 +744,9 @@
     }
 
     @Test
-    fun updateNoteTaskAsUser_flagDisabled_shouldDisableShortcuts() {
-        createNoteTaskController(isEnabled = false).updateNoteTaskAsUser(userTracker.userHandle)
+    fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() {
+        createNoteTaskController(isEnabled = false)
+            .updateNoteTaskAsUserInternal(userTracker.userHandle)
 
         val argument = argumentCaptor<ComponentName>()
         verify(context.packageManager)
@@ -741,6 +763,20 @@
     }
     // endregion
 
+    // startregion updateNoteTaskForAllUsers
+    @Test
+    fun updateNoteTaskForAllUsers_shouldRunUpdateForCurrentUserAndProfiles() {
+        userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
+        val controller = spy(createNoteTaskController())
+        doNothing().whenever(controller).updateNoteTaskAsUser(any())
+
+        controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+
+        verify(controller).updateNoteTaskAsUser(mainUserInfo.userHandle)
+        verify(controller).updateNoteTaskAsUser(workUserInfo.userHandle)
+    }
+    // endregion
+
     // region getUserForHandlingNotesTaking
     @Test
     fun getUserForHandlingNotesTaking_cope_quickAffordance_shouldReturnWorkProfileUser() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 4e85b6c..95bb3e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -32,9 +32,12 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Before
@@ -71,19 +74,19 @@
     }
 
     private fun createUnderTest(
-        isEnabled: Boolean,
-        bubbles: Bubbles?,
+            isEnabled: Boolean,
+            bubbles: Bubbles?,
     ): NoteTaskInitializer =
-        NoteTaskInitializer(
-            controller = controller,
-            commandQueue = commandQueue,
-            optionalBubbles = Optional.ofNullable(bubbles),
-            isEnabled = isEnabled,
-            roleManager = roleManager,
-            userTracker = userTracker,
-            keyguardUpdateMonitor = keyguardMonitor,
-            backgroundExecutor = executor,
-        )
+            NoteTaskInitializer(
+                    controller = controller,
+                    commandQueue = commandQueue,
+                    optionalBubbles = Optional.ofNullable(bubbles),
+                    isEnabled = isEnabled,
+                    roleManager = roleManager,
+                    userTracker = userTracker,
+                    keyguardUpdateMonitor = keyguardMonitor,
+                    backgroundExecutor = executor,
+            )
 
     @Test
     fun initialize_withUserUnlocked() {
@@ -93,8 +96,8 @@
 
         verify(commandQueue).addCallback(any())
         verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
-        verify(controller).setNoteTaskShortcutEnabled(any(), any())
-        verify(keyguardMonitor, never()).registerCallback(any())
+        verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles()
+        verify(keyguardMonitor).registerCallback(any())
     }
 
     @Test
@@ -107,6 +110,7 @@
         verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
         verify(controller, never()).setNoteTaskShortcutEnabled(any(), any())
         verify(keyguardMonitor).registerCallback(any())
+        assertThat(userTracker.callbacks).isNotEmpty()
     }
 
     @Test
@@ -116,12 +120,12 @@
         underTest.initialize()
 
         verifyZeroInteractions(
-            commandQueue,
-            bubbles,
-            controller,
-            roleManager,
-            userManager,
-            keyguardMonitor,
+                commandQueue,
+                bubbles,
+                controller,
+                roleManager,
+                userManager,
+                keyguardMonitor,
         )
     }
 
@@ -132,12 +136,12 @@
         underTest.initialize()
 
         verifyZeroInteractions(
-            commandQueue,
-            bubbles,
-            controller,
-            roleManager,
-            userManager,
-            keyguardMonitor,
+                commandQueue,
+                bubbles,
+                controller,
+                roleManager,
+                userManager,
+                keyguardMonitor,
         )
     }
 
@@ -146,7 +150,7 @@
         val expectedKeyEvent = KeyEvent(ACTION_DOWN, KEYCODE_STYLUS_BUTTON_TAIL)
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
-        val callback = captureArgument { verify(commandQueue).addCallback(capture()) }
+        val callback = withArgCaptor { verify(commandQueue).addCallback(capture()) }
 
         callback.handleSystemKey(expectedKeyEvent)
 
@@ -154,31 +158,49 @@
     }
 
     @Test
-    fun initialize_userUnlocked() {
+    fun initialize_userUnlocked_shouldUpdateNoteTask() {
         whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false)
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
-        val callback = captureArgument { verify(keyguardMonitor).registerCallback(capture()) }
+        val callback = withArgCaptor { verify(keyguardMonitor).registerCallback(capture()) }
         whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true)
 
         callback.onUserUnlocked()
-        verify(controller).setNoteTaskShortcutEnabled(any(), any())
+
+        verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles()
     }
 
     @Test
-    fun initialize_onRoleHoldersChanged() {
+    fun initialize_onRoleHoldersChanged_shouldRunOnRoleHoldersChanged() {
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
-        val callback = captureArgument {
+        val callback = withArgCaptor {
             verify(roleManager)
-                .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL))
+                    .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL))
         }
 
         callback.onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
 
         verify(controller).onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
     }
-}
 
-private inline fun <reified T : Any> captureArgument(block: ArgumentCaptor<T>.() -> Unit) =
-    argumentCaptor<T>().apply(block).value
+    @Test
+    fun initialize_onProfilesChanged_shouldUpdateNoteTask() {
+        val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+        underTest.initialize()
+
+        userTracker.callbacks.first().onProfilesChanged(emptyList())
+
+        verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles()
+    }
+
+    @Test
+    fun initialize_onUserChanged_shouldUpdateNoteTask() {
+        val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+        underTest.initialize()
+
+        userTracker.callbacks.first().onUserChanged(0, mock())
+
+        verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles()
+    }
+}
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/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/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/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/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/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/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/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 7230213..9c636ea 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9255,7 +9255,6 @@
 
     boolean shouldRestoreImeVisibility(IBinder imeTargetWindowToken) {
         final Task imeTargetWindowTask;
-        boolean hadRequestedShowIme = false;
         synchronized (mGlobalLock) {
             final WindowState imeTargetWindow = mWindowMap.get(imeTargetWindowToken);
             if (imeTargetWindow == null) {
@@ -9265,14 +9264,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/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/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);
             }