Merge "bt: Fix displaying late bond device name" into udc-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/provider/Settings.java b/core/java/android/provider/Settings.java
index 5c79f69..d695c0c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -15310,18 +15310,6 @@
         public static final String ANGLE_EGL_FEATURES = "angle_egl_features";
 
         /**
-         * Comma-separated list of package names that ANGLE may have issues with
-         * @hide
-         */
-        public static final String ANGLE_DEFERLIST = "angle_deferlist";
-
-        /**
-         * Integer mode of the logic for applying `angle_deferlist`
-         * @hide
-         */
-        public static final String ANGLE_DEFERLIST_MODE = "angle_deferlist_mode";
-
-        /**
          * Show the "ANGLE In Use" dialog box to the user when ANGLE is the OpenGL driver.
          * The value is a boolean (1 or 0).
          * @hide
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 02cd037..99a4f6b 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
+import android.graphics.HardwareRenderer;
 import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -550,6 +551,11 @@
         ThreadedRenderer.trimMemory(level);
     }
 
+    /** @hide */
+    public void trimCaches(@HardwareRenderer.CacheTrimLevel int level) {
+        ThreadedRenderer.trimCaches(level);
+    }
+
     public void dumpGfxInfo(FileDescriptor fd, String[] args) {
         FileOutputStream fout = new FileOutputStream(fd);
         PrintWriter pw = new FastPrintWriter(fout);
diff --git a/core/java/com/android/internal/config/appcloning/AppCloningDeviceConfigHelper.java b/core/java/com/android/internal/config/appcloning/AppCloningDeviceConfigHelper.java
deleted file mode 100644
index ddd3d2c..0000000
--- a/core/java/com/android/internal/config/appcloning/AppCloningDeviceConfigHelper.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.config.appcloning;
-
-import android.content.Context;
-import android.provider.DeviceConfig;
-
-import com.android.internal.annotations.GuardedBy;
-
-/**
- * Helper class that holds the flags related to the app_cloning namespace in {@link DeviceConfig}.
- *
- * @hide
- */
-public class AppCloningDeviceConfigHelper {
-
-    @GuardedBy("sLock")
-    private static AppCloningDeviceConfigHelper sInstance;
-
-    private static final Object sLock = new Object();
-
-    private DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangeListener;
-
-    /**
-     * This flag is defined inside {@link DeviceConfig#NAMESPACE_APP_CLONING}. Please check
-     * {@link #mEnableAppCloningBuildingBlocks} for details.
-     */
-    public static final String ENABLE_APP_CLONING_BUILDING_BLOCKS =
-            "enable_app_cloning_building_blocks";
-
-    /**
-     * Checks whether the support for app-cloning building blocks (like contacts
-     * sharing/intent redirection), which are available starting from the U release, is turned on.
-     * The default value is true to ensure the features are always enabled going forward.
-     *
-     * TODO:(b/253449368) Add information about the app-cloning config and mention that the devices
-     * that do not support app-cloning should use the app-cloning config to disable all app-cloning
-     * features.
-     */
-    private volatile boolean mEnableAppCloningBuildingBlocks = true;
-
-    private AppCloningDeviceConfigHelper() {}
-
-    /**
-     * @hide
-     */
-    public static AppCloningDeviceConfigHelper getInstance(Context context) {
-        synchronized (sLock) {
-            if (sInstance == null) {
-                sInstance = new AppCloningDeviceConfigHelper();
-                sInstance.init(context);
-            }
-            return sInstance;
-        }
-    }
-
-    private void init(Context context) {
-        initializeDeviceConfigChangeListener();
-        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_CLONING,
-                context.getMainExecutor(),
-                mDeviceConfigChangeListener);
-    }
-
-    private void initializeDeviceConfigChangeListener() {
-        mDeviceConfigChangeListener = properties -> {
-            if (!DeviceConfig.NAMESPACE_APP_CLONING.equals(properties.getNamespace())) {
-                return;
-            }
-            for (String name : properties.getKeyset()) {
-                if (name == null) {
-                    return;
-                }
-                if (ENABLE_APP_CLONING_BUILDING_BLOCKS.equals(name)) {
-                    updateEnableAppCloningBuildingBlocks();
-                }
-            }
-        };
-    }
-
-    private void updateEnableAppCloningBuildingBlocks() {
-        mEnableAppCloningBuildingBlocks = DeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_APP_CLONING, ENABLE_APP_CLONING_BUILDING_BLOCKS, true);
-    }
-
-    /**
-     * Fetch the feature flag to check whether the support for the app-cloning building blocks
-     * (like contacts sharing/intent redirection) is enabled on the device.
-     * @hide
-     */
-    public boolean getEnableAppCloningBuildingBlocks() {
-        return mEnableAppCloningBuildingBlocks;
-    }
-}
diff --git a/core/java/com/android/internal/config/appcloning/OWNERS b/core/java/com/android/internal/config/appcloning/OWNERS
deleted file mode 100644
index 0645a8c5..0000000
--- a/core/java/com/android/internal/config/appcloning/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bug component: 1207885
-jigarthakkar@google.com
-saumyap@google.com
\ No newline at end of file
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/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto
index 128de8b..052e2f2 100644
--- a/core/proto/android/providers/settings/global.proto
+++ b/core/proto/android/providers/settings/global.proto
@@ -471,10 +471,6 @@
         optional SettingProto updatable_driver_prerelease_opt_in_apps = 18;
 
         optional SettingProto angle_egl_features = 19;
-        // ANGLE - List of Apps that ANGLE may have issues with
-        optional SettingProto angle_deferlist = 20;
-        // ANGLE - Integer mode of the logic for applying `angle_deferlist`
-        optional SettingProto angle_deferlist_mode = 21;
     }
     optional Gpu gpu = 59;
 
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index 9ed3d9c..9cde187 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -144,6 +144,32 @@
     public @interface DumpFlags {
     }
 
+
+    /**
+     * Trims all Skia caches.
+     * @hide
+     */
+    public static final int CACHE_TRIM_ALL = 0;
+    /**
+     * Trims Skia font caches.
+     * @hide
+     */
+    public static final int CACHE_TRIM_FONT = 1;
+    /**
+     * Trims Skia resource caches.
+     * @hide
+     */
+    public static final int CACHE_TRIM_RESOURCES = 2;
+
+    /** @hide */
+    @IntDef(prefix = {"CACHE_TRIM_"}, value = {
+            CACHE_TRIM_ALL,
+            CACHE_TRIM_FONT,
+            CACHE_TRIM_RESOURCES
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CacheTrimLevel {}
+
     /**
      * Name of the file that holds the shaders cache.
      */
@@ -1131,6 +1157,20 @@
         nTrimMemory(level);
     }
 
+    /**
+     * Invoke this when all font caches should be flushed. This can cause jank on next render
+     * commands so use it only after expensive font allocation operations which would
+     * allocate large amount of temporary memory.
+     *
+     * @param level Hint about which caches to trim. See {@link #CACHE_TRIM_ALL},
+     *              {@link #CACHE_TRIM_FONT}, {@link #CACHE_TRIM_RESOURCES}
+     *
+     * @hide
+     */
+    public static void trimCaches(@CacheTrimLevel int level) {
+        nTrimCaches(level);
+    }
+
     /** @hide */
     public static void overrideProperty(@NonNull String name, @NonNull String value) {
         if (name == null || value == null) {
@@ -1497,6 +1537,8 @@
 
     private static native void nTrimMemory(int level);
 
+    private static native void nTrimCaches(int level);
+
     private static native void nOverrideProperty(String name, String value);
 
     private static native void nFence(long nativeProxy);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index a48be5e..91c7cc0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -1346,7 +1346,7 @@
 
     // Recreates & shows the education views. Call when a theme/config change happens.
     private void updateUserEdu() {
-        if (isStackEduVisible()) {
+        if (isStackEduVisible() && !mStackEduView.isHiding()) {
             removeView(mStackEduView);
             mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
             addView(mStackEduView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 627273f..d0598cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -37,8 +37,7 @@
     context: Context,
     positioner: BubblePositioner,
     controller: BubbleController
-)
-    : LinearLayout(context) {
+) : LinearLayout(context) {
 
     private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
         else BubbleDebugConfig.TAG_BUBBLES
@@ -53,7 +52,8 @@
     private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) }
     private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) }
 
-    private var isHiding = false
+    var isHiding = false
+        private set
 
     init {
         LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this)
diff --git a/libs/hwui/MemoryPolicy.h b/libs/hwui/MemoryPolicy.h
index 139cdde..347daf34 100644
--- a/libs/hwui/MemoryPolicy.h
+++ b/libs/hwui/MemoryPolicy.h
@@ -31,6 +31,12 @@
     RUNNING_MODERATE = 5,
 };
 
+enum class CacheTrimLevel {
+    ALL_CACHES = 0,
+    FONT_CACHE = 1,
+    RESOURCE_CACHE = 2,
+};
+
 struct MemoryPolicy {
     // The initial scale factor applied to the display resolution. The default is 1, but
     // lower values may be used to start with a smaller initial cache size. The cache will
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index 6a7411f..d04de37 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -362,6 +362,10 @@
     RenderProxy::trimMemory(level);
 }
 
+static void android_view_ThreadedRenderer_trimCaches(JNIEnv* env, jobject clazz, jint level) {
+    RenderProxy::trimCaches(level);
+}
+
 static void android_view_ThreadedRenderer_overrideProperty(JNIEnv* env, jobject clazz,
         jstring name, jstring value) {
     const char* nameCharArray = env->GetStringUTFChars(name, NULL);
@@ -1018,6 +1022,7 @@
          (void*)android_view_ThreadedRenderer_notifyCallbackPending},
         {"nNotifyExpensiveFrame", "(J)V",
          (void*)android_view_ThreadedRenderer_notifyExpensiveFrame},
+        {"nTrimCaches", "(I)V", (void*)android_view_ThreadedRenderer_trimCaches},
 };
 
 static JavaVM* mJvm = nullptr;
diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp
index c00a270..babce88 100644
--- a/libs/hwui/renderthread/CacheManager.cpp
+++ b/libs/hwui/renderthread/CacheManager.cpp
@@ -139,6 +139,25 @@
     }
 }
 
+void CacheManager::trimCaches(CacheTrimLevel mode) {
+    switch (mode) {
+        case CacheTrimLevel::FONT_CACHE:
+            SkGraphics::PurgeFontCache();
+            break;
+        case CacheTrimLevel::RESOURCE_CACHE:
+            SkGraphics::PurgeResourceCache();
+            break;
+        case CacheTrimLevel::ALL_CACHES:
+            SkGraphics::PurgeAllCaches();
+            if (mGrContext) {
+                mGrContext->purgeUnlockedResources(false);
+            }
+            break;
+        default:
+            break;
+    }
+}
+
 void CacheManager::trimStaleResources() {
     if (!mGrContext) {
         return;
diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h
index d21ac9b..5e43ac2 100644
--- a/libs/hwui/renderthread/CacheManager.h
+++ b/libs/hwui/renderthread/CacheManager.h
@@ -48,6 +48,7 @@
     void configureContext(GrContextOptions* context, const void* identity, ssize_t size);
 #endif
     void trimMemory(TrimLevel mode);
+    void trimCaches(CacheTrimLevel mode);
     void trimStaleResources();
     void dumpMemoryUsage(String8& log, const RenderState* renderState = nullptr);
     void getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage);
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 31b4b20..224c878 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -231,6 +231,15 @@
     }
 }
 
+void RenderProxy::trimCaches(int level) {
+    // Avoid creating a RenderThread to do a trimMemory.
+    if (RenderThread::hasInstance()) {
+        RenderThread& thread = RenderThread::getInstance();
+        const auto trimLevel = static_cast<CacheTrimLevel>(level);
+        thread.queue().post([&thread, trimLevel]() { thread.trimCaches(trimLevel); });
+    }
+}
+
 void RenderProxy::purgeCaches() {
     if (RenderThread::hasInstance()) {
         RenderThread& thread = RenderThread::getInstance();
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 82072a6..47c1b0c 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -105,6 +105,7 @@
 
     void destroyHardwareResources();
     static void trimMemory(int level);
+    static void trimCaches(int level);
     static void purgeCaches();
     static void overrideProperty(const char* name, const char* value);
 
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 9ba67a2..eb28c08 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -521,6 +521,11 @@
     cacheManager().trimMemory(level);
 }
 
+void RenderThread::trimCaches(CacheTrimLevel level) {
+    ATRACE_CALL();
+    cacheManager().trimCaches(level);
+}
+
 } /* namespace renderthread */
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index c77cd41..79e57de 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -174,6 +174,7 @@
     }
 
     void trimMemory(TrimLevel level);
+    void trimCaches(CacheTrimLevel level);
 
     /**
      * isCurrent provides a way to query, if the caller is running on
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1fd84c7..a83bfda 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -785,12 +785,6 @@
                 Settings.Global.ANGLE_EGL_FEATURES,
                 GlobalSettingsProto.Gpu.ANGLE_EGL_FEATURES);
         dumpSetting(s, p,
-                Settings.Global.ANGLE_DEFERLIST,
-                GlobalSettingsProto.Gpu.ANGLE_DEFERLIST);
-        dumpSetting(s, p,
-                Settings.Global.ANGLE_DEFERLIST_MODE,
-                GlobalSettingsProto.Gpu.ANGLE_DEFERLIST_MODE);
-        dumpSetting(s, p,
                 Settings.Global.SHOW_ANGLE_IN_USE_DIALOG_BOX,
                 GlobalSettingsProto.Gpu.SHOW_ANGLE_IN_USE_DIALOG);
         dumpSetting(s, p,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index ef4b814..873b434 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -518,8 +518,6 @@
                     Settings.Global.ANGLE_GL_DRIVER_SELECTION_PKGS,
                     Settings.Global.ANGLE_GL_DRIVER_SELECTION_VALUES,
                     Settings.Global.ANGLE_EGL_FEATURES,
-                    Settings.Global.ANGLE_DEFERLIST,
-                    Settings.Global.ANGLE_DEFERLIST_MODE,
                     Settings.Global.UPDATABLE_DRIVER_ALL_APPS,
                     Settings.Global.UPDATABLE_DRIVER_PRODUCTION_OPT_IN_APPS,
                     Settings.Global.UPDATABLE_DRIVER_PRERELEASE_OPT_IN_APPS,
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index d1ee18a..25269dc 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -85,7 +85,7 @@
 
 @Composable
 private fun filledButtonColors(): ButtonColors {
-    val colors = LocalAndroidColorScheme.current
+    val colors = LocalAndroidColorScheme.current.deprecated
     return ButtonDefaults.buttonColors(
         containerColor = colors.colorAccentPrimary,
         contentColor = colors.textColorOnAccent,
@@ -94,7 +94,7 @@
 
 @Composable
 private fun outlineButtonColors(): ButtonColors {
-    val colors = LocalAndroidColorScheme.current
+    val colors = LocalAndroidColorScheme.current.deprecated
     return ButtonDefaults.outlinedButtonColors(
         contentColor = colors.textColorPrimary,
     )
@@ -102,7 +102,7 @@
 
 @Composable
 private fun outlineButtonBorder(): BorderStroke {
-    val colors = LocalAndroidColorScheme.current
+    val colors = LocalAndroidColorScheme.current.deprecated
     return BorderStroke(
         width = 1.dp,
         color = colors.colorAccentPrimaryVariant,
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
index 4f1657f..1d6f813 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
@@ -37,33 +37,83 @@
  * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future,
  * most of the colors in this class will be removed in favor of their M3 counterpart.
  */
-class AndroidColorScheme internal constructor(context: Context) {
-    val colorPrimary = getColor(context, R.attr.colorPrimary)
-    val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
-    val colorAccent = getColor(context, R.attr.colorAccent)
-    val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary)
-    val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary)
-    val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary)
-    val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant)
-    val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant)
-    val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant)
-    val colorSurface = getColor(context, R.attr.colorSurface)
-    val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight)
-    val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant)
-    val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader)
-    val colorError = getColor(context, R.attr.colorError)
-    val colorBackground = getColor(context, R.attr.colorBackground)
-    val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating)
-    val panelColorBackground = getColor(context, R.attr.panelColorBackground)
-    val textColorPrimary = getColor(context, R.attr.textColorPrimary)
-    val textColorSecondary = getColor(context, R.attr.textColorSecondary)
-    val textColorTertiary = getColor(context, R.attr.textColorTertiary)
-    val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse)
-    val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse)
-    val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse)
-    val textColorOnAccent = getColor(context, R.attr.textColorOnAccent)
-    val colorForeground = getColor(context, R.attr.colorForeground)
-    val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
+class AndroidColorScheme(context: Context) {
+    val onSecondaryFixedVariant = getColor(context, R.attr.materialColorOnSecondaryFixedVariant)
+    val onTertiaryFixedVariant = getColor(context, R.attr.materialColorOnTertiaryFixedVariant)
+    val surfaceContainerLowest = getColor(context, R.attr.materialColorSurfaceContainerLowest)
+    val onPrimaryFixedVariant = getColor(context, R.attr.materialColorOnPrimaryFixedVariant)
+    val onSecondaryContainer = getColor(context, R.attr.materialColorOnSecondaryContainer)
+    val onTertiaryContainer = getColor(context, R.attr.materialColorOnTertiaryContainer)
+    val surfaceContainerLow = getColor(context, R.attr.materialColorSurfaceContainerLow)
+    val onPrimaryContainer = getColor(context, R.attr.materialColorOnPrimaryContainer)
+    val secondaryFixedDim = getColor(context, R.attr.materialColorSecondaryFixedDim)
+    val onErrorContainer = getColor(context, R.attr.materialColorOnErrorContainer)
+    val onSecondaryFixed = getColor(context, R.attr.materialColorOnSecondaryFixed)
+    val onSurfaceInverse = getColor(context, R.attr.materialColorOnSurfaceInverse)
+    val tertiaryFixedDim = getColor(context, R.attr.materialColorTertiaryFixedDim)
+    val onTertiaryFixed = getColor(context, R.attr.materialColorOnTertiaryFixed)
+    val primaryFixedDim = getColor(context, R.attr.materialColorPrimaryFixedDim)
+    val secondaryContainer = getColor(context, R.attr.materialColorSecondaryContainer)
+    val errorContainer = getColor(context, R.attr.materialColorErrorContainer)
+    val onPrimaryFixed = getColor(context, R.attr.materialColorOnPrimaryFixed)
+    val primaryInverse = getColor(context, R.attr.materialColorPrimaryInverse)
+    val secondaryFixed = getColor(context, R.attr.materialColorSecondaryFixed)
+    val surfaceInverse = getColor(context, R.attr.materialColorSurfaceInverse)
+    val surfaceVariant = getColor(context, R.attr.materialColorSurfaceVariant)
+    val tertiaryContainer = getColor(context, R.attr.materialColorTertiaryContainer)
+    val tertiaryFixed = getColor(context, R.attr.materialColorTertiaryFixed)
+    val primaryContainer = getColor(context, R.attr.materialColorPrimaryContainer)
+    val onBackground = getColor(context, R.attr.materialColorOnBackground)
+    val primaryFixed = getColor(context, R.attr.materialColorPrimaryFixed)
+    val onSecondary = getColor(context, R.attr.materialColorOnSecondary)
+    val onTertiary = getColor(context, R.attr.materialColorOnTertiary)
+    val surfaceDim = getColor(context, R.attr.materialColorSurfaceDim)
+    val surfaceBright = getColor(context, R.attr.materialColorSurfaceBright)
+    val onError = getColor(context, R.attr.materialColorOnError)
+    val surface = getColor(context, R.attr.materialColorSurface)
+    val surfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh)
+    val surfaceContainerHighest = getColor(context, R.attr.materialColorSurfaceContainerHighest)
+    val onSurfaceVariant = getColor(context, R.attr.materialColorOnSurfaceVariant)
+    val outline = getColor(context, R.attr.materialColorOutline)
+    val outlineVariant = getColor(context, R.attr.materialColorOutlineVariant)
+    val onPrimary = getColor(context, R.attr.materialColorOnPrimary)
+    val onSurface = getColor(context, R.attr.materialColorOnSurface)
+    val surfaceContainer = getColor(context, R.attr.materialColorSurfaceContainer)
+    val primary = getColor(context, R.attr.materialColorPrimary)
+    val secondary = getColor(context, R.attr.materialColorSecondary)
+    val tertiary = getColor(context, R.attr.materialColorTertiary)
+
+    @Deprecated("Use the new android tokens: go/sysui-colors")
+    val deprecated = DeprecatedValues(context)
+
+    class DeprecatedValues(context: Context) {
+        val colorPrimary = getColor(context, R.attr.colorPrimary)
+        val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
+        val colorAccent = getColor(context, R.attr.colorAccent)
+        val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary)
+        val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary)
+        val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary)
+        val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant)
+        val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant)
+        val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant)
+        val colorSurface = getColor(context, R.attr.colorSurface)
+        val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight)
+        val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant)
+        val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader)
+        val colorError = getColor(context, R.attr.colorError)
+        val colorBackground = getColor(context, R.attr.colorBackground)
+        val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating)
+        val panelColorBackground = getColor(context, R.attr.panelColorBackground)
+        val textColorPrimary = getColor(context, R.attr.textColorPrimary)
+        val textColorSecondary = getColor(context, R.attr.textColorSecondary)
+        val textColorTertiary = getColor(context, R.attr.textColorTertiary)
+        val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse)
+        val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse)
+        val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse)
+        val textColorOnAccent = getColor(context, R.attr.textColorOnAccent)
+        val colorForeground = getColor(context, R.attr.colorForeground)
+        val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
+    }
 
     companion object {
         fun getColor(context: Context, attr: Int): Color {
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt
index 9bc6856..fe34017 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/SystemUIThemeTest.kt
@@ -40,7 +40,9 @@
     @Test
     fun testAndroidColorsAreAvailableInsideTheme() {
         composeRule.setContent {
-            PlatformTheme { Text("foo", color = LocalAndroidColorScheme.current.colorAccent) }
+            PlatformTheme {
+                Text("foo", color = LocalAndroidColorScheme.current.deprecated.colorAccent)
+            }
         }
 
         composeRule.onNodeWithText("foo").assertIsDisplayed()
@@ -50,7 +52,7 @@
     fun testAccessingAndroidColorsWithoutThemeThrows() {
         assertThrows(IllegalStateException::class.java) {
             composeRule.setContent {
-                Text("foo", color = LocalAndroidColorScheme.current.colorAccent)
+                Text("foo", color = LocalAndroidColorScheme.current.deprecated.colorAccent)
             }
         }
     }
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
index 24064b1..5413f90 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
@@ -17,10 +17,12 @@
 package com.android.systemui.scene.ui.composable
 
 import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneContainerNames
 import dagger.Module
 import dagger.multibindings.Multibinds
+import javax.inject.Named
 
 @Module
 interface SceneModule {
-    @Multibinds fun scenes(): Set<Scene>
+    @Multibinds @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) fun scenes(): Set<Scene>
 }
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
index ee53ece..954bad56 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt
@@ -17,22 +17,32 @@
 package com.android.systemui.scene.ui.composable
 
 import com.android.systemui.bouncer.ui.composable.BouncerScene
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.ui.composable.LockscreenScene
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
 import com.android.systemui.qs.ui.composable.QuickSettingsScene
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneContainerNames
 import com.android.systemui.shade.ui.composable.ShadeScene
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
 import dagger.Module
 import dagger.Provides
+import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
 
 @Module
 object SceneModule {
     @Provides
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
     fun scenes(
-        bouncer: BouncerScene,
-        gone: GoneScene,
-        lockScreen: LockscreenScene,
-        qs: QuickSettingsScene,
-        shade: ShadeScene,
+        @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) bouncer: BouncerScene,
+        @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) gone: GoneScene,
+        @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) lockScreen: LockscreenScene,
+        @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) qs: QuickSettingsScene,
+        @Named(SceneContainerNames.SYSTEM_UI_DEFAULT) shade: ShadeScene,
     ): Set<Scene> {
         return setOf(
             bouncer,
@@ -42,4 +52,71 @@
             shade,
         )
     }
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun bouncerScene(
+        viewModelFactory: BouncerViewModel.Factory,
+    ): BouncerScene {
+        return BouncerScene(
+            viewModel =
+                viewModelFactory.create(
+                    containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+                ),
+        )
+    }
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun goneScene(): GoneScene {
+        return GoneScene()
+    }
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun lockscreenScene(
+        @Application applicationScope: CoroutineScope,
+        viewModelFactory: LockscreenSceneViewModel.Factory,
+    ): LockscreenScene {
+        return LockscreenScene(
+            applicationScope = applicationScope,
+            viewModel =
+                viewModelFactory.create(
+                    containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+                ),
+        )
+    }
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun quickSettingsScene(
+        viewModelFactory: QuickSettingsSceneViewModel.Factory,
+    ): QuickSettingsScene {
+        return QuickSettingsScene(
+            viewModel =
+                viewModelFactory.create(
+                    containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+                ),
+        )
+    }
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun shadeScene(
+        @Application applicationScope: CoroutineScope,
+        viewModelFactory: ShadeSceneViewModel.Factory,
+    ): ShadeScene {
+        return ShadeScene(
+            applicationScope = applicationScope,
+            viewModel =
+                viewModelFactory.create(
+                    containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+                ),
+        )
+    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index f48bab9..3c74ef5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -40,22 +40,17 @@
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.scene.ui.composable.ComposableScene
-import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
 /** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
-@SysUISingleton
-class BouncerScene
-@Inject
-constructor(
-    private val viewModelFactory: BouncerViewModel.Factory,
+class BouncerScene(
+    private val viewModel: BouncerViewModel,
 ) : ComposableScene {
     override val key = SceneKey.Bouncer
 
@@ -73,7 +68,7 @@
     override fun Content(
         containerName: String,
         modifier: Modifier,
-    ) = BouncerScene(viewModelFactory.create(containerName), modifier)
+    ) = BouncerScene(viewModel, modifier)
 }
 
 @Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 7c07a8b..1065267 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.unit.dp
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
 import com.android.systemui.scene.shared.model.Direction
@@ -39,7 +38,6 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.scene.ui.composable.ComposableScene
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -47,29 +45,22 @@
 import kotlinx.coroutines.flow.stateIn
 
 /** The lock screen scene shows when the device is locked. */
-@SysUISingleton
-class LockscreenScene
-@Inject
-constructor(
+class LockscreenScene(
     @Application private val applicationScope: CoroutineScope,
-    private val viewModelFactory: LockscreenSceneViewModel.Factory,
+    private val viewModel: LockscreenSceneViewModel,
 ) : ComposableScene {
     override val key = SceneKey.Lockscreen
 
-    private var unsafeViewModel: LockscreenSceneViewModel? = null
-
     override fun destinationScenes(
         containerName: String,
     ): StateFlow<Map<UserAction, SceneModel>> =
-        getOrCreateViewModelSingleton(containerName).let { viewModel ->
-            viewModel.upDestinationSceneKey
-                .map { pageKey -> destinationScenes(up = pageKey) }
-                .stateIn(
-                    scope = applicationScope,
-                    started = SharingStarted.Eagerly,
-                    initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value)
-                )
-        }
+        viewModel.upDestinationSceneKey
+            .map { pageKey -> destinationScenes(up = pageKey) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value)
+            )
 
     @Composable
     override fun Content(
@@ -77,7 +68,7 @@
         modifier: Modifier,
     ) {
         LockscreenScene(
-            viewModel = getOrCreateViewModelSingleton(containerName),
+            viewModel = viewModel,
             modifier = modifier,
         )
     }
@@ -90,13 +81,6 @@
             UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade)
         )
     }
-
-    private fun getOrCreateViewModelSingleton(
-        containerName: String,
-    ): LockscreenSceneViewModel {
-        return unsafeViewModel
-            ?: viewModelFactory.create(containerName).also { unsafeViewModel = it }
-    }
 }
 
 @Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index a74e56b..f88fc21 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -91,7 +91,7 @@
 
     // Make sure to use the Android colors and not the default Material3 colors to have the exact
     // same colors as the View implementation.
-    val androidColors = LocalAndroidColorScheme.current
+    val androidColors = LocalAndroidColorScheme.current.deprecated
     Surface(
         color = androidColors.colorBackground,
         contentColor = androidColors.textColorPrimary,
@@ -170,7 +170,7 @@
             stringResource(headerTextResource),
             Modifier.padding(start = 16.dp),
             style = MaterialTheme.typography.labelLarge,
-            color = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
+            color = LocalAndroidColorScheme.current.deprecated.colorAccentPrimaryVariant,
         )
 
         Spacer(Modifier.height(10.dp))
@@ -180,7 +180,7 @@
         if (index > 0) {
             item {
                 Divider(
-                    color = LocalAndroidColorScheme.current.colorBackground,
+                    color = LocalAndroidColorScheme.current.deprecated.colorBackground,
                     thickness = 2.dp,
                 )
             }
@@ -204,7 +204,7 @@
     withTopCornerRadius: Boolean,
     withBottomCornerRadius: Boolean,
 ) {
-    val androidColors = LocalAndroidColorScheme.current
+    val androidColors = LocalAndroidColorScheme.current.deprecated
     val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
     val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
     val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
index 0484ff4..1e6f4a2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -76,8 +76,8 @@
             Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp),
             colors =
                 ButtonDefaults.buttonColors(
-                    containerColor = androidColors.colorAccentPrimary,
-                    contentColor = androidColors.textColorOnAccent,
+                    containerColor = androidColors.deprecated.colorAccentPrimary,
+                    contentColor = androidColors.deprecated.textColorOnAccent,
                 )
         ) {
             Text(stringResource(R.string.got_it))
@@ -90,8 +90,8 @@
     val androidColors = LocalAndroidColorScheme.current
     Surface(
         shape = RoundedCornerShape(28.dp),
-        color = androidColors.colorSurface,
-        contentColor = androidColors.textColorPrimary,
+        color = androidColors.deprecated.colorSurface,
+        contentColor = androidColors.deprecated.textColorPrimary,
     ) {
         Row(
             Modifier.padding(vertical = 20.dp, horizontal = 16.dp),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index 349f5c3..75bf281 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -122,7 +122,7 @@
     }
 
     val backgroundColor = colorAttr(R.attr.underSurfaceColor)
-    val contentColor = LocalAndroidColorScheme.current.textColorPrimary
+    val contentColor = LocalAndroidColorScheme.current.deprecated.textColorPrimary
     val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius)
     val backgroundModifier =
         remember(
@@ -287,7 +287,7 @@
                     number.toString(),
                     modifier = Modifier.align(Alignment.Center),
                     style = MaterialTheme.typography.bodyLarge,
-                    color = LocalAndroidColorScheme.current.textColorPrimary,
+                    color = LocalAndroidColorScheme.current.deprecated.textColorPrimary,
                     // TODO(b/242040009): This should only use a standard text style instead and
                     // should not override the text size.
                     fontSize = 18.sp,
@@ -305,7 +305,7 @@
 @Composable
 private fun NewChangesDot(modifier: Modifier = Modifier) {
     val contentDescription = stringResource(R.string.fgs_dot_content_description)
-    val color = LocalAndroidColorScheme.current.colorAccentTertiary
+    val color = LocalAndroidColorScheme.current.deprecated.colorAccentTertiary
 
     Canvas(modifier.size(12.dp).semantics { this.contentDescription = contentDescription }) {
         drawCircle(color)
@@ -324,8 +324,9 @@
     Expandable(
         shape = CircleShape,
         color = colorAttr(R.attr.underSurfaceColor),
-        contentColor = LocalAndroidColorScheme.current.textColorSecondary,
-        borderStroke = BorderStroke(1.dp, LocalAndroidColorScheme.current.colorBackground),
+        contentColor = LocalAndroidColorScheme.current.deprecated.textColorSecondary,
+        borderStroke =
+            BorderStroke(1.dp, LocalAndroidColorScheme.current.deprecated.colorBackground),
         modifier = modifier.padding(horizontal = 4.dp),
         onClick = onClick,
     ) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 58db37e..30b80ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -27,24 +27,19 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.scene.ui.composable.ComposableScene
-import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
 /** The Quick Settings (AKA "QS") scene shows the quick setting tiles. */
-@SysUISingleton
-class QuickSettingsScene
-@Inject
-constructor(
-    private val viewModelFactory: QuickSettingsSceneViewModel.Factory,
+class QuickSettingsScene(
+    private val viewModel: QuickSettingsSceneViewModel,
 ) : ComposableScene {
     override val key = SceneKey.QuickSettings
 
@@ -64,7 +59,7 @@
         modifier: Modifier,
     ) {
         QuickSettingsScene(
-            viewModel = viewModelFactory.create(containerName),
+            viewModel = viewModel,
             modifier = modifier,
         )
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index b387463..0a4da1d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -23,12 +23,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.scene.shared.model.UserAction
-import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -37,8 +35,7 @@
  * "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any
  * content from the scene framework.
  */
-@SysUISingleton
-class GoneScene @Inject constructor() : ComposableScene {
+class GoneScene : ComposableScene {
     override val key = SceneKey.Gone
 
     override fun destinationScenes(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index e4513d0..20e1751 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -27,7 +27,6 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
@@ -35,7 +34,6 @@
 import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.scene.ui.composable.ComposableScene
 import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -43,29 +41,22 @@
 import kotlinx.coroutines.flow.stateIn
 
 /** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */
-@SysUISingleton
-class ShadeScene
-@Inject
-constructor(
+class ShadeScene(
     @Application private val applicationScope: CoroutineScope,
-    private val viewModelFactory: ShadeSceneViewModel.Factory,
+    private val viewModel: ShadeSceneViewModel,
 ) : ComposableScene {
     override val key = SceneKey.Shade
 
-    private var unsafeViewModel: ShadeSceneViewModel? = null
-
     override fun destinationScenes(
         containerName: String,
     ): StateFlow<Map<UserAction, SceneModel>> =
-        getOrCreateViewModelSingleton(containerName).let { viewModel ->
-            viewModel.upDestinationSceneKey
-                .map { sceneKey -> destinationScenes(up = sceneKey) }
-                .stateIn(
-                    scope = applicationScope,
-                    started = SharingStarted.Eagerly,
-                    initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value),
-                )
-        }
+        viewModel.upDestinationSceneKey
+            .map { sceneKey -> destinationScenes(up = sceneKey) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value),
+            )
 
     @Composable
     override fun Content(
@@ -73,7 +64,7 @@
         modifier: Modifier,
     ) {
         ShadeScene(
-            viewModel = getOrCreateViewModelSingleton(containerName),
+            viewModel = viewModel,
             modifier = modifier,
         )
     }
@@ -86,13 +77,6 @@
             UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.QuickSettings),
         )
     }
-
-    private fun getOrCreateViewModelSingleton(
-        containerName: String,
-    ): ShadeSceneViewModel {
-        return unsafeViewModel
-            ?: viewModelFactory.create(containerName).also { unsafeViewModel = it }
-    }
 }
 
 @Composable
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/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 271fab1..a62dead 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1730,15 +1730,15 @@
     <!-- Keyboard backlight indicator-->
     <dimen name="backlight_indicator_root_corner_radius">48dp</dimen>
     <dimen name="backlight_indicator_root_vertical_padding">8dp</dimen>
-    <dimen name="backlight_indicator_root_horizontal_padding">4dp</dimen>
+    <dimen name="backlight_indicator_root_horizontal_padding">6dp</dimen>
     <dimen name="backlight_indicator_icon_width">22dp</dimen>
     <dimen name="backlight_indicator_icon_height">11dp</dimen>
-    <dimen name="backlight_indicator_icon_left_margin">2dp</dimen>
+    <dimen name="backlight_indicator_icon_padding">10dp</dimen>
     <dimen name="backlight_indicator_step_width">52dp</dimen>
     <dimen name="backlight_indicator_step_height">40dp</dimen>
-    <dimen name="backlight_indicator_step_horizontal_margin">4dp</dimen>
+    <dimen name="backlight_indicator_step_horizontal_margin">2dp</dimen>
     <dimen name="backlight_indicator_step_small_radius">4dp</dimen>
-    <dimen name="backlight_indicator_step_large_radius">48dp</dimen>
+    <dimen name="backlight_indicator_step_large_radius">28dp</dimen>
 
     <!-- Broadcast dialog -->
     <dimen name="broadcast_dialog_title_img_margin_top">18dp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index eaeaabe..e5c9461 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -203,5 +203,8 @@
 
     <item type="id" name="log_access_dialog_allow_button" />
     <item type="id" name="log_access_dialog_deny_button" />
+
+    <!-- keyboard backlight indicator-->
+    <item type="id" name="backlight_icon" />
 </resources>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 003f9b0..67fdb4c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -353,7 +353,7 @@
     <!-- Message shown when a biometric is authenticated, waiting for the user to confirm authentication [CHAR LIMIT=40]-->
     <string name="biometric_dialog_tap_confirm">Tap Confirm to complete</string>
     <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
-    <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face. Press the unlock icon to continue.</string>
+    <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face.</string>
     <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
     <string name="biometric_dialog_tap_confirm_with_face_alt_1">Unlocked by face. Press to continue.</string>
     <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
diff --git a/packages/SystemUI/src/com/android/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/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index c8485dc..e118fdf 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -217,6 +217,7 @@
             )
 
     /** Whether to use a new data source for intents to run on keyguard dismissal. */
+    // TODO(b/275069969): Tracking bug.
     @JvmField
     val REFACTOR_KEYGUARD_DISMISS_INTENT = unreleasedFlag(231, "refactor_keyguard_dismiss_intent")
 
@@ -247,6 +248,11 @@
     @JvmField
     val MIGRATE_INDICATION_AREA = unreleasedFlag(236, "migrate_indication_area")
 
+    /** Whether to listen for fingerprint authentication over keyguard occluding activities. */
+    // TODO(b/283260512): Tracking bug.
+    @JvmField
+    val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -673,6 +679,10 @@
     val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
             unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
 
+    // TODO:(b/283203305): Tracking bug
+    @JvmField
+    val TRIM_FONT_CACHES_AT_UNLOCK = releasedFlag(2402, "trim_font_caches_on_unlock")
+
     // 2700 - unfold transitions
     // TODO(b/265764985): Tracking Bug
     @Keep
@@ -717,4 +727,12 @@
     @JvmField
     val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
             unreleasedFlag(2805, "split_shade_subpixel_optimization", teamfood = true)
+
+    // TODO(b/278761837): Tracking Bug
+    @JvmField
+    val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
+
+    // TODO(b/283084712): Tracking Bug
+    @JvmField
+    val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index d3678b5..7078341 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -22,9 +22,12 @@
 import android.app.Dialog
 import android.content.Context
 import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.OvalShape
 import android.graphics.drawable.shapes.RoundRectShape
 import android.os.Bundle
 import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
 import android.view.Window
 import android.view.WindowManager
 import android.widget.FrameLayout
@@ -32,9 +35,10 @@
 import android.widget.LinearLayout
 import android.widget.LinearLayout.LayoutParams
 import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
+import androidx.annotation.IdRes
+import androidx.core.view.setPadding
 import com.android.settingslib.Utils
 import com.android.systemui.R
-import com.android.systemui.util.children
 
 class KeyboardBacklightDialog(
     context: Context,
@@ -51,7 +55,7 @@
     private data class BacklightIconProperties(
         val width: Int,
         val height: Int,
-        val leftMargin: Int,
+        val padding: Int,
     )
 
     private data class StepViewProperties(
@@ -71,6 +75,7 @@
     private lateinit var rootProperties: RootProperties
     private lateinit var iconProperties: BacklightIconProperties
     private lateinit var stepProperties: StepViewProperties
+
     @ColorInt
     var filledRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
     @ColorInt
@@ -78,7 +83,16 @@
         getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
     @ColorInt
     var backgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
-    @ColorInt var iconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+    @ColorInt
+    var defaultIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+    @ColorInt
+    var defaultIconBackgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
+    @ColorInt
+    var dimmedIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+    @ColorInt
+    var dimmedIconBackgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim)
 
     init {
         currentLevel = initialCurrentLevel
@@ -111,8 +125,7 @@
                 BacklightIconProperties(
                     width = getDimensionPixelSize(R.dimen.backlight_indicator_icon_width),
                     height = getDimensionPixelSize(R.dimen.backlight_indicator_icon_height),
-                    leftMargin =
-                        getDimensionPixelSize(R.dimen.backlight_indicator_icon_left_margin),
+                    padding = getDimensionPixelSize(R.dimen.backlight_indicator_icon_padding),
                 )
             stepProperties =
                 StepViewProperties(
@@ -139,23 +152,34 @@
         if (maxLevel != max || forceRefresh) {
             maxLevel = max
             rootView.removeAllViews()
+            rootView.addView(buildIconTile())
             buildStepViews().forEach { rootView.addView(it) }
         }
         currentLevel = current
-        updateLevel()
+        updateIconTile()
+        updateStepColors()
     }
 
-    private fun updateLevel() {
-        rootView.children.forEachIndexed(
-            action = { index, v ->
-                val drawable = v.background as ShapeDrawable
-                if (index <= currentLevel) {
-                    updateColor(drawable, filledRectangleColor)
-                } else {
-                    updateColor(drawable, emptyRectangleColor)
-                }
-            }
-        )
+    private fun updateIconTile() {
+        val iconTile = rootView.findViewById(BACKLIGHT_ICON_ID) as ImageView
+        val backgroundDrawable = iconTile.background as ShapeDrawable
+        if (currentLevel == 0) {
+            iconTile.setColorFilter(dimmedIconColor)
+            updateColor(backgroundDrawable, dimmedIconBackgroundColor)
+        } else {
+            iconTile.setColorFilter(defaultIconColor)
+            updateColor(backgroundDrawable, defaultIconBackgroundColor)
+        }
+    }
+
+    private fun updateStepColors() {
+        (1 until rootView.childCount).forEach { index ->
+            val drawable = rootView.getChildAt(index).background as ShapeDrawable
+            updateColor(
+                drawable,
+                if (index <= currentLevel) filledRectangleColor else emptyRectangleColor,
+            )
+        }
     }
 
     private fun updateColor(drawable: ShapeDrawable, @ColorInt color: Int) {
@@ -192,9 +216,33 @@
     }
 
     private fun buildStepViews(): List<FrameLayout> {
-        val stepViews = (0..maxLevel).map { i -> createStepViewAt(i) }
-        stepViews[0].addView(createBacklightIconView())
-        return stepViews
+        return (1..maxLevel).map { i -> createStepViewAt(i) }
+    }
+
+    private fun buildIconTile(): View {
+        val diameter = stepProperties.height
+        val circleDrawable =
+            ShapeDrawable(OvalShape()).apply {
+                intrinsicHeight = diameter
+                intrinsicWidth = diameter
+            }
+
+        return ImageView(context).apply {
+            setImageResource(R.drawable.ic_keyboard_backlight)
+            id = BACKLIGHT_ICON_ID
+            setColorFilter(defaultIconColor)
+            setPadding(iconProperties.padding)
+            layoutParams =
+                MarginLayoutParams(diameter, diameter).apply {
+                    setMargins(
+                        /* left= */ stepProperties.horizontalMargin,
+                        /* top= */ 0,
+                        /* right= */ stepProperties.horizontalMargin,
+                        /* bottom= */ 0
+                    )
+                }
+            background = circleDrawable
+        }
     }
 
     private fun createStepViewAt(i: Int): FrameLayout {
@@ -221,18 +269,6 @@
         }
     }
 
-    private fun createBacklightIconView(): ImageView {
-        return ImageView(context).apply {
-            setImageResource(R.drawable.ic_keyboard_backlight)
-            setColorFilter(iconColor)
-            layoutParams =
-                FrameLayout.LayoutParams(iconProperties.width, iconProperties.height).apply {
-                    gravity = Gravity.CENTER
-                    leftMargin = iconProperties.leftMargin
-                }
-        }
-    }
-
     private fun setWindowPosition() {
         window?.apply {
             setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
@@ -262,30 +298,29 @@
     private fun radiiForIndex(i: Int, last: Int): FloatArray {
         val smallRadius = stepProperties.smallRadius
         val largeRadius = stepProperties.largeRadius
-        return when (i) {
-            0 -> // left radii bigger
-            floatArrayOf(
-                    largeRadius,
-                    largeRadius,
-                    smallRadius,
-                    smallRadius,
-                    smallRadius,
-                    smallRadius,
-                    largeRadius,
-                    largeRadius
-                )
-            last -> // right radii bigger
-            floatArrayOf(
-                    smallRadius,
-                    smallRadius,
-                    largeRadius,
-                    largeRadius,
-                    largeRadius,
-                    largeRadius,
-                    smallRadius,
-                    smallRadius
-                )
-            else -> FloatArray(8) { smallRadius } // all radii equal
+        val radii = FloatArray(8) { smallRadius }
+        if (i == 1) {
+            radii.setLeftCorners(largeRadius)
         }
+        // note "first" and "last" might be the same tile
+        if (i == last) {
+            radii.setRightCorners(largeRadius)
+        }
+        return radii
+    }
+
+    private fun FloatArray.setLeftCorners(radius: Float) {
+        LEFT_CORNERS_INDICES.forEach { this[it] = radius }
+    }
+    private fun FloatArray.setRightCorners(radius: Float) {
+        RIGHT_CORNERS_INDICES.forEach { this[it] = radius }
+    }
+
+    private companion object {
+        @IdRes val BACKLIGHT_ICON_ID = R.id.backlight_icon
+
+        // indices used to define corners radii in ShapeDrawable
+        val LEFT_CORNERS_INDICES: IntArray = intArrayOf(0, 1, 6, 7)
+        val RIGHT_CORNERS_INDICES: IntArray = intArrayOf(2, 3, 4, 5)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index 8386a05b..d8affa4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.WorkerThread
 import android.content.ComponentCallbacks2
+import android.graphics.HardwareRenderer
 import android.os.Trace
 import android.util.Log
 import com.android.systemui.CoreStartable
@@ -27,12 +28,13 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.utils.GlobalWindowManager
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
@@ -50,6 +52,7 @@
 @Inject
 constructor(
     private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val globalWindowManager: GlobalWindowManager,
     @Application private val applicationScope: CoroutineScope,
     @Background private val bgDispatcher: CoroutineDispatcher,
@@ -58,7 +61,10 @@
 
     override fun start() {
         Log.d(LOG_TAG, "Resource trimmer registered.")
-        if (!featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) {
+        if (
+            !(featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK) ||
+                featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK))
+        ) {
             return
         }
 
@@ -78,6 +84,30 @@
                 .distinctUntilChanged()
                 .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
         }
+
+        applicationScope.launch(bgDispatcher) {
+            // We drop 1 to avoid triggering on initial collect().
+            keyguardTransitionInteractor.anyStateToGoneTransition.collect { transition ->
+                if (transition.transitionState == TransitionState.FINISHED) {
+                    onKeyguardGone()
+                }
+            }
+        }
+    }
+
+    @WorkerThread
+    private fun onKeyguardGone() {
+        if (!featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
+            return
+        }
+
+        if (DEBUG) {
+            Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
+        }
+        // We want to clear temporary caches we've created while rendering and animating
+        // lockscreen elements, especially clocks.
+        globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+        globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
     }
 
     @WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 6d7455f..752471d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,12 +16,16 @@
 
 package com.android.systemui.scene
 
+import com.android.systemui.scene.data.model.SceneContainerConfigModule
 import com.android.systemui.scene.ui.composable.SceneModule
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModelModule
 import dagger.Module
 
 @Module(
     includes =
         [
+            SceneContainerConfigModule::class,
+            SceneContainerViewModelModule::class,
             SceneModule::class,
         ],
 )
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt
new file mode 100644
index 0000000..0af8094
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.scene.data.model
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.shared.model.SceneContainerNames
+import com.android.systemui.scene.shared.model.SceneKey
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+object SceneContainerConfigModule {
+
+    @Provides
+    fun containerConfigs(): Map<String, SceneContainerConfig> {
+        return mapOf(
+            SceneContainerNames.SYSTEM_UI_DEFAULT to
+                SceneContainerConfig(
+                    name = SceneContainerNames.SYSTEM_UI_DEFAULT,
+                    // Note that this list is in z-order. The first one is the bottom-most and the
+                    // last
+                    // one is top-most.
+                    sceneKeys =
+                        listOf(
+                            SceneKey.Gone,
+                            SceneKey.Lockscreen,
+                            SceneKey.Bouncer,
+                            SceneKey.Shade,
+                            SceneKey.QuickSettings,
+                        ),
+                    initialSceneKey = SceneKey.Lockscreen,
+                ),
+        )
+    }
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun provideDefaultSceneContainerConfig(
+        configs: Map<String, SceneContainerConfig>,
+    ): SceneContainerConfig {
+        return checkNotNull(configs[SceneContainerNames.SYSTEM_UI_DEFAULT]) {
+            "No SceneContainerConfig named \"${SceneContainerNames.SYSTEM_UI_DEFAULT}\"."
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerNames.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerNames.kt
new file mode 100644
index 0000000..64f5087
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerNames.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.scene.shared.model
+
+object SceneContainerNames {
+    const val SYSTEM_UI_DEFAULT = "system_ui"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index a4daafc..8c1ad9b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,17 +19,12 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
 import kotlinx.coroutines.flow.StateFlow
 
 /** Models UI state for a single scene container. */
-class SceneContainerViewModel
-@AssistedInject
-constructor(
+class SceneContainerViewModel(
     private val interactor: SceneInteractor,
-    @Assisted val containerName: String,
+    val containerName: String,
 ) {
     /**
      * Keys of all scenes in the container.
@@ -54,11 +49,4 @@
     fun setSceneTransitionProgress(progress: Float) {
         interactor.setSceneTransitionProgress(containerName, progress)
     }
-
-    @AssistedFactory
-    interface Factory {
-        fun create(
-            containerName: String,
-        ): SceneContainerViewModel
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelModule.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelModule.kt
new file mode 100644
index 0000000..100f427
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneContainerNames
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+object SceneContainerViewModelModule {
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun defaultSceneContainerViewModel(
+        interactor: SceneInteractor,
+    ): SceneContainerViewModel {
+        return SceneContainerViewModel(
+            interactor = interactor,
+            containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index fb4feb8..a532195 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -33,7 +33,6 @@
 import android.content.Context;
 import android.graphics.drawable.Icon;
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
@@ -317,7 +316,7 @@
                 IBiometricSysuiReceiver receiver,
                 int[] sensorIds, boolean credentialAllowed,
                 boolean requireConfirmation, int userId, long operationId, String opPackageName,
-                long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
+                long requestId) {
         }
 
         /** @see IStatusBar#onBiometricAuthenticated(int) */
@@ -956,8 +955,7 @@
     @Override
     public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
             int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
-            int userId, long operationId, String opPackageName, long requestId,
-            @BiometricMultiSensorMode int multiSensorConfig) {
+            int userId, long operationId, String opPackageName, long requestId) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = promptInfo;
@@ -969,7 +967,6 @@
             args.arg6 = opPackageName;
             args.argl1 = operationId;
             args.argl2 = requestId;
-            args.argi2 = multiSensorConfig;
             mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args)
                     .sendToTarget();
         }
@@ -1573,8 +1570,7 @@
                                 someArgs.argi1 /* userId */,
                                 someArgs.argl1 /* operationId */,
                                 (String) someArgs.arg6 /* opPackageName */,
-                                someArgs.argl2 /* requestId */,
-                                someArgs.argi2 /* multiSensorConfig */);
+                                someArgs.argl2 /* requestId */);
                     }
                     someArgs.recycle();
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index df68e7e..0414a14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -23,7 +23,6 @@
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
 
-import android.content.ComponentCallbacks2;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.hardware.biometrics.BiometricSourceType;
@@ -36,7 +35,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
-import android.view.WindowManagerGlobal;
 import android.window.BackEvent;
 import android.window.OnBackAnimationCallback;
 import android.window.OnBackInvokedDispatcher;
@@ -985,8 +983,6 @@
         mShadeViewController.resetViewGroupFade();
         mCentralSurfaces.finishKeyguardFadingAway();
         mBiometricUnlockController.finishKeyguardFadingAway();
-        WindowManagerGlobal.getInstance().trimMemory(
-                ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
     }
 
     private void wakeAndUnlockDejank() {
diff --git a/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt
index 038fddc..4111850 100644
--- a/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.utils
 
+import android.graphics.HardwareRenderer.CacheTrimLevel
 import android.view.WindowManagerGlobal
 import javax.inject.Inject
 
@@ -13,4 +14,9 @@
     fun trimMemory(level: Int) {
         WindowManagerGlobal.getInstance().trimMemory(level)
     }
+
+    /** Sends a trim caches command to [WindowManagerGlobal]. */
+    fun trimCaches(@CacheTrimLevel level: Int) {
+        WindowManagerGlobal.getInstance().trimCaches(level)
+    }
 }
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/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 367d206..548d26f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.keyguard
 
 import android.content.ComponentCallbacks2
+import android.graphics.HardwareRenderer
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -9,7 +10,11 @@
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.shared.model.WakeSleepReason
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.keyguard.shared.model.WakefulnessState
@@ -25,6 +30,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
 
@@ -37,6 +43,7 @@
     private val testScope = TestScope(testDispatcher)
     private val keyguardRepository = FakeKeyguardRepository()
     private val featureFlags = FakeFeatureFlags()
+    private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
 
     @Mock private lateinit var globalWindowManager: GlobalWindowManager
     private lateinit var resourceTrimmer: ResourceTrimmer
@@ -45,13 +52,15 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         featureFlags.set(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK, true)
+        featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, true)
         featureFlags.set(Flags.FACE_AUTH_REFACTOR, false)
         keyguardRepository.setWakefulnessModel(
             WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER)
         )
         keyguardRepository.setDozeAmount(0f)
+        keyguardRepository.setKeyguardGoingAway(false)
 
-        val interactor =
+        val keyguardInteractor =
             KeyguardInteractor(
                 keyguardRepository,
                 FakeCommandQueue(),
@@ -60,7 +69,8 @@
             )
         resourceTrimmer =
             ResourceTrimmer(
-                interactor,
+                keyguardInteractor,
+                KeyguardTransitionInteractor(keyguardTransitionRepository),
                 globalWindowManager,
                 testScope.backgroundScope,
                 testDispatcher,
@@ -191,4 +201,26 @@
             verifyZeroInteractions(globalWindowManager)
         }
     }
+
+    @Test
+    fun keyguardTransitionsToGone_trimsFontCache() =
+        testScope.runTest {
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+            )
+            verify(globalWindowManager, times(1))
+                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+            verifyNoMoreInteractions(globalWindowManager)
+        }
+
+    @Test
+    fun keyguardTransitionsToGone_flagDisabled_doesNotTrimFontCache() =
+        testScope.runTest {
+            featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+            )
+            verifyNoMoreInteractions(globalWindowManager)
+        }
 }
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/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
index 4aeb4a4..cd6de87 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
@@ -349,7 +349,8 @@
     private int isFieldDetectionServiceEnabled(PrintWriter pw) {
         final int userId = getNextIntArgRequired();
         String name = mService.getFieldDetectionServiceName(userId);
-        boolean enabled = !TextUtils.isEmpty(name);
+        boolean pccFlagEnabled = mService.isPccClassificationFlagEnabled();
+        boolean enabled = (!TextUtils.isEmpty(name)) && pccFlagEnabled;
         pw.println(enabled);
         return 0;
     }
diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java
index 5156c54..fb527c1 100644
--- a/services/core/java/com/android/server/DockObserver.java
+++ b/services/core/java/com/android/server/DockObserver.java
@@ -37,6 +37,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.ExtconUEventObserver.ExtconInfo;
 
 import java.io.FileDescriptor;
@@ -195,6 +196,8 @@
     @Override
     public void onStart() {
         publishBinderService(TAG, new BinderService());
+        // Logs dock state after setDockStateFromProviderLocked sets mReportedDockState
+        FrameworkStatsLog.write(FrameworkStatsLog.DOCK_STATE_CHANGED, mReportedDockState);
     }
 
     @Override
@@ -256,7 +259,6 @@
                     + mReportedDockState);
             final int previousDockState = mPreviousDockState;
             mPreviousDockState = mReportedDockState;
-
             // Skip the dock intent if not yet provisioned.
             final ContentResolver cr = getContext().getContentResolver();
             if (!mDeviceProvisionedObserver.isDeviceProvisioned()) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index f4c9d05..d7a5ee9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -420,6 +420,20 @@
             mBtHelper.stopBluetoothSco(eventSource);
         }
 
+        // In BT classic for communication, the device changes from a2dp to sco device, but for
+        // LE Audio it stays the same and we must trigger the proper stream volume alignment, if
+        // LE Audio communication device is activated after the audio system has already switched to
+        // MODE_IN_CALL mode.
+        if (isBluetoothLeAudioRequested()) {
+            final int streamType = mAudioService.getBluetoothContextualVolumeStream();
+            final int leAudioVolIndex = getVssVolumeForDevice(streamType, device.getInternalType());
+            final int leAudioMaxVolIndex = getMaxVssVolumeForStream(streamType);
+            if (AudioService.DEBUG_COMM_RTE) {
+                Log.v(TAG, "setCommunicationRouteForClient restoring LE Audio device volume lvl.");
+            }
+            postSetLeAudioVolumeIndex(leAudioVolIndex, leAudioMaxVolIndex, streamType);
+        }
+
         updateCommunicationRoute(eventSource);
     }
 
@@ -633,6 +647,16 @@
     }
 
     /**
+     * Helper method on top of isDeviceRequestedForCommunication() indicating if
+     * Bluetooth LE Audio communication device is currently requested or not.
+     * @return true if Bluetooth LE Audio device is requested, false otherwise.
+     */
+    /*package*/ boolean isBluetoothLeAudioRequested() {
+        return isDeviceRequestedForCommunication(AudioDeviceInfo.TYPE_BLE_HEADSET)
+                || isDeviceRequestedForCommunication(AudioDeviceInfo.TYPE_BLE_SPEAKER);
+    }
+
+    /**
      * Indicates if preferred route selection for communication is Bluetooth SCO.
      * @return true if Bluetooth SCO is preferred , false otherwise.
      */
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/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index 3e31bd1..a339756 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -104,7 +104,6 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
-import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.BackgroundThread;
@@ -257,8 +256,6 @@
 
     private final SyncLogger mLogger;
 
-    private final AppCloningDeviceConfigHelper mAppCloningDeviceConfigHelper;
-
     private boolean isJobIdInUseLockedH(int jobId, List<JobInfo> pendingJobs) {
         for (int i = 0, size = pendingJobs.size(); i < size; i++) {
             JobInfo job = pendingJobs.get(i);
@@ -684,7 +681,6 @@
         }, mSyncHandler);
 
         mConstants = new SyncManagerConstants(context);
-        mAppCloningDeviceConfigHelper = AppCloningDeviceConfigHelper.getInstance(context);
 
         IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
         context.registerReceiver(mConnectivityIntentReceiver, intentFilter);
@@ -892,8 +888,7 @@
      * @return true/false if contact sharing is enabled/disabled
      */
     protected boolean isContactSharingAllowedForCloneProfile() {
-        return mContext.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks)
-                && mAppCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks();
+        return mContext.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks);
     }
 
     /**
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 8b8c5f6..65e7a00 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -152,9 +152,6 @@
     // The security strength of the synthetic password, in bytes
     private static final int SYNTHETIC_PASSWORD_SECURITY_STRENGTH = 256 / 8;
 
-    public static final short PASSWORD_DATA_V1 = 1;
-    public static final short PASSWORD_DATA_V2 = 2;
-
     private static final int PASSWORD_SCRYPT_LOG_N = 11;
     private static final int PASSWORD_SCRYPT_LOG_R = 3;
     private static final int PASSWORD_SCRYPT_LOG_P = 1;
@@ -379,21 +376,18 @@
             buffer.put(data, 0, data.length);
             buffer.flip();
 
-          /*
-           * Originally this file did not contain a version number. However, its first field was
-           * 'credentialType' as an 'int'. Since 'credentialType' could only be in the range
-           * [-1, 4] and this file uses big endian byte order, the first two bytes were redundant,
-           * and when interpreted as a 'short' could only contain -1 or 0. Therefore, we've now
-           * reclaimed these two bytes for a 'short' version number and shrunk 'credentialType'
-           * to a 'short'.
-           */
-            short version = buffer.getShort();
-            if (version == ((short) 0) || version == (short) -1) {
-                version = PASSWORD_DATA_V1;
-            } else if (version != PASSWORD_DATA_V2) {
-                throw new IllegalArgumentException("Unknown PasswordData version: " + version);
-            }
-            result.credentialType = buffer.getShort();
+            /*
+             * The serialized PasswordData is supposed to begin with credentialType as an int.
+             * However, all credentialType values fit in a short and the byte order is big endian,
+             * so the first two bytes don't convey any non-redundant information.  For this reason,
+             * temporarily during development of Android 14, the first two bytes were "stolen" from
+             * credentialType to use for a data format version number.
+             *
+             * However, this change was reverted as it was a non-forwards-compatible change.  (See
+             * toBytes() for why this data format needs to be forwards-compatible.)  Therefore,
+             * recover from this misstep by ignoring the first two bytes.
+             */
+            result.credentialType = (short) buffer.getInt();
             result.scryptLogN = buffer.get();
             result.scryptLogR = buffer.get();
             result.scryptLogP = buffer.get();
@@ -407,7 +401,7 @@
             } else {
                 result.passwordHandle = null;
             }
-            if (version == PASSWORD_DATA_V2) {
+            if (buffer.remaining() >= Integer.BYTES) {
                 result.pinLength = buffer.getInt();
             } else {
                 result.pinLength = PIN_LENGTH_UNAVAILABLE;
@@ -415,16 +409,25 @@
             return result;
         }
 
+        /**
+         * Serializes this PasswordData into a byte array.
+         * <p>
+         * Careful: all changes to the format of the serialized PasswordData must be forwards
+         * compatible.  I.e., older versions of Android must still accept the latest PasswordData.
+         * This is because a serialized PasswordData is stored in the Factory Reset Protection (FRP)
+         * persistent data block.  It's possible that a device has FRP set up on a newer version of
+         * Android, is factory reset, and then is set up with an older version of Android.
+         */
         public byte[] toBytes() {
 
-            ByteBuffer buffer = ByteBuffer.allocate(2 * Short.BYTES + 3 * Byte.BYTES
+            ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES
                     + Integer.BYTES + salt.length + Integer.BYTES +
                     (passwordHandle != null ? passwordHandle.length : 0) + Integer.BYTES);
+            // credentialType must fit in a short.  For an explanation, see fromBytes().
             if (credentialType < Short.MIN_VALUE || credentialType > Short.MAX_VALUE) {
                 throw new IllegalArgumentException("Unknown credential type: " + credentialType);
             }
-            buffer.putShort(PASSWORD_DATA_V2);
-            buffer.putShort((short) credentialType);
+            buffer.putInt(credentialType);
             buffer.put(scryptLogN);
             buffer.put(scryptLogR);
             buffer.put(scryptLogP);
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index a97126c..5324acd 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -72,6 +72,7 @@
 import android.view.ContentRecordingSession;
 
 import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
@@ -111,7 +112,11 @@
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     static final long MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT = 266201607L; // buganizer id
 
-    private final Object mLock = new Object(); // Protects the list of media projections
+    // Protects access to state at service level & IMediaProjection level.
+    // Invocation order while holding locks must follow below to avoid deadlock:
+    // WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService
+    // See mediaprojection.md
+    private final Object mLock = new Object();
     private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
     private final CallbackDelegate mCallbackDelegate;
 
@@ -127,7 +132,9 @@
     private final MediaRouterCallback mMediaRouterCallback;
     private MediaRouter.RouteInfo mMediaRouteInfo;
 
+    @GuardedBy("mLock")
     private IBinder mProjectionToken;
+    @GuardedBy("mLock")
     private MediaProjection mProjectionGrant;
 
     public MediaProjectionManagerService(Context context) {
@@ -314,9 +321,11 @@
      */
     @VisibleForTesting
     boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) {
+        // NEVER lock while calling into WindowManagerService, since WindowManagerService is
+        // ALWAYS locked when it invokes MediaProjectionManagerService.
+        final boolean setSessionSucceeded = mWmInternal.setContentRecordingSession(incomingSession);
         synchronized (mLock) {
-            if (!mWmInternal.setContentRecordingSession(
-                    incomingSession)) {
+            if (!setSessionSucceeded) {
                 // Unable to start mirroring, so tear down this projection.
                 if (mProjectionGrant != null) {
                     mProjectionGrant.stop();
@@ -359,13 +368,20 @@
      */
     @VisibleForTesting
     void requestConsentForInvalidProjection() {
+        Intent reviewConsentIntent;
+        int uid;
         synchronized (mLock) {
-            Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
-            // Trigger the permission dialog again in SysUI
-            // Do not handle the result; SysUI will update us when the user has consented.
-            mContext.startActivityAsUser(buildReviewGrantedConsentIntent(),
-                    UserHandle.getUserHandleForUid(mProjectionGrant.uid));
+            reviewConsentIntent = buildReviewGrantedConsentIntentLocked();
+            uid = mProjectionGrant.uid;
         }
+        // NEVER lock while calling into a method that eventually acquires the WindowManagerService
+        // lock, since WindowManagerService is ALWAYS locked when it invokes
+        // MediaProjectionManagerService.
+        Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
+        // Trigger the permission dialog again in SysUI
+        // Do not handle the result; SysUI will update us when the user has consented.
+        mContext.startActivityAsUser(reviewConsentIntent,
+                UserHandle.getUserHandleForUid(uid));
     }
 
     /**
@@ -375,7 +391,7 @@
      * <p>Consent dialog result handled in
      * {@link BinderService#setUserReviewGrantedConsentResult(int)}.
      */
-    private Intent buildReviewGrantedConsentIntent() {
+    private Intent buildReviewGrantedConsentIntentLocked() {
         final String permissionDialogString = mContext.getResources().getString(
                 R.string.config_mediaProjectionPermissionDialogComponent);
         final ComponentName mediaProjectionPermissionDialogComponent =
@@ -388,7 +404,8 @@
     }
 
     /**
-     * Handles result of dialog shown from {@link BinderService#buildReviewGrantedConsentIntent()}.
+     * Handles result of dialog shown from
+     * {@link BinderService#buildReviewGrantedConsentIntentLocked()}.
      *
      * <p>Tears down session if user did not consent, or starts mirroring if user did consent.
      */
@@ -490,23 +507,26 @@
     MediaProjection getProjectionInternal(int uid, String packageName) {
         final long callingToken = Binder.clearCallingIdentity();
         try {
-            // Supposedly the package has re-used the user's consent; confirm the provided details
-            // against the current projection token before re-using the current projection.
-            if (mProjectionGrant == null || mProjectionGrant.mSession == null
-                    || !mProjectionGrant.mSession.isWaitingForConsent()) {
-                Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
-                        + "instance");
-                return null;
-            }
+            synchronized (mLock) {
+                // Supposedly the package has re-used the user's consent; confirm the provided
+                // details against the current projection token before re-using the current
+                // projection.
+                if (mProjectionGrant == null || mProjectionGrant.mSession == null
+                        || !mProjectionGrant.mSession.isWaitingForConsent()) {
+                    Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+                            + "instance");
+                    return null;
+                }
                 // The package matches, go ahead and re-use the token for this request.
-            if (mProjectionGrant.uid == uid
-                    && Objects.equals(mProjectionGrant.packageName, packageName)) {
-                Slog.v(TAG, "Reusing token: getProjection can reuse the current projection");
-                return mProjectionGrant;
-            } else {
-                Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
-                        + "instance due to package details mismatching");
-                return null;
+                if (mProjectionGrant.uid == uid
+                        && Objects.equals(mProjectionGrant.packageName, packageName)) {
+                    Slog.v(TAG, "Reusing token: getProjection can reuse the current projection");
+                    return mProjectionGrant;
+                } else {
+                    Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+                            + "instance due to package details mismatching");
+                    return null;
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(callingToken);
@@ -626,8 +646,10 @@
             }
             final long token = Binder.clearCallingIdentity();
             try {
-                if (mProjectionGrant != null) {
-                    mProjectionGrant.stop();
+                synchronized (mLock) {
+                    if (mProjectionGrant != null) {
+                        mProjectionGrant.stop();
+                    }
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -641,13 +663,17 @@
                 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
                         + "on captured content resize");
             }
-            if (!isCurrentProjection(mProjectionGrant)) {
-                return;
+            synchronized (mLock) {
+                if (!isCurrentProjection(mProjectionGrant)) {
+                    return;
+                }
             }
             final long token = Binder.clearCallingIdentity();
             try {
-                if (mProjectionGrant != null && mCallbackDelegate != null) {
-                    mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
+                synchronized (mLock) {
+                    if (mProjectionGrant != null && mCallbackDelegate != null) {
+                        mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
+                    }
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -661,13 +687,17 @@
                 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
                         + "on captured content visibility changed");
             }
-            if (!isCurrentProjection(mProjectionGrant)) {
-                return;
+            synchronized (mLock) {
+                if (!isCurrentProjection(mProjectionGrant)) {
+                    return;
+                }
             }
             final long token = Binder.clearCallingIdentity();
             try {
-                if (mProjectionGrant != null && mCallbackDelegate != null) {
-                    mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible);
+                synchronized (mLock) {
+                    if (mProjectionGrant != null && mCallbackDelegate != null) {
+                        mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible);
+                    }
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -712,9 +742,11 @@
                 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session "
                         + "details.");
             }
-            if (!isCurrentProjection(projection)) {
-                throw new SecurityException("Unable to set ContentRecordingSession on "
-                        + "non-current MediaProjection");
+            synchronized (mLock) {
+                if (!isCurrentProjection(projection)) {
+                    throw new SecurityException("Unable to set ContentRecordingSession on "
+                            + "non-current MediaProjection");
+                }
             }
             final long origId = Binder.clearCallingIdentity();
             try {
@@ -732,10 +764,12 @@
                 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given"
                         + "projection is valid.");
             }
-            if (!isCurrentProjection(projection)) {
-                Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
-                        + "isn't current");
-                return;
+            synchronized (mLock) {
+                if (!isCurrentProjection(projection)) {
+                    Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
+                            + "isn't current");
+                    return;
+                }
             }
 
             // Remove calling app identity before performing any privileged operations.
diff --git a/services/core/java/com/android/server/media/projection/mediaprojection.md b/services/core/java/com/android/server/media/projection/mediaprojection.md
new file mode 100644
index 0000000..bccdf34
--- /dev/null
+++ b/services/core/java/com/android/server/media/projection/mediaprojection.md
@@ -0,0 +1,30 @@
+# MediaProjection
+
+## Locking model
+`MediaProjectionManagerService` needs to have consistent lock ordering with its interactions with
+`WindowManagerService` to prevent deadlock.
+
+### TLDR
+`MediaProjectionManagerService` must lock when updating its own fields.
+
+Calls must follow the below invocation order while holding locks:
+
+`WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService`
+
+### Justification
+
+`MediaProjectionManagerService` calls into `WindowManagerService` in the below cases. While handling
+each invocation, `WindowManagerService` acquires its own lock:
+* setting a `ContentRecordingSession`
+  * starting a new `MediaProjection` recording session through
+`MediaProjection#createVirtualDisplay`
+  * indicating the user has granted consent to reuse the consent token
+
+`WindowManagerService` calls into `MediaProjectionManagerService`, always while holding
+`WindowManagerGlobalLock`:
+* `ContentRecorder` handling various events such as resizing recorded content
+
+
+Since `WindowManagerService -> MediaProjectionManagerService` is guaranteed to always hold the
+`WindowManagerService` lock, we must ensure that `MediaProjectionManagerService ->
+WindowManagerService` is NEVER holding the `MediaProjectionManagerService` lock.
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
index b5c0417..e149b04 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
@@ -37,7 +37,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 
-import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper;
 import com.android.server.LocalServices;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
@@ -62,8 +61,6 @@
     private final Context mContext;
     private final UserManagerInternal mUserManagerInternal;
 
-    private AppCloningDeviceConfigHelper mAppCloningDeviceConfigHelper;
-
     public CrossProfileIntentResolverEngine(UserManagerService userManager,
             DomainVerificationManagerInternal domainVerificationManager,
             DefaultAppProvider defaultAppProvider, Context context) {
@@ -253,12 +250,7 @@
          * We would return NoFilteringResolver only if it is allowed(feature flag is set).
          */
         if (shouldUseNoFilteringResolver(sourceUserId, targetUserId)) {
-            if (mAppCloningDeviceConfigHelper == null) {
-                //lazy initialization of helper till required, to improve performance.
-                mAppCloningDeviceConfigHelper = AppCloningDeviceConfigHelper.getInstance(mContext);
-            }
-            if (NoFilteringResolver.isIntentRedirectionAllowed(mContext,
-                    mAppCloningDeviceConfigHelper, resolveForStart, flags)) {
+            if (NoFilteringResolver.isIntentRedirectionAllowed(mContext, resolveForStart, flags)) {
                 return new NoFilteringResolver(computer.getComponentResolver(),
                         mUserManager);
             } else {
diff --git a/services/core/java/com/android/server/pm/NoFilteringResolver.java b/services/core/java/com/android/server/pm/NoFilteringResolver.java
index b87256d..817e1f6 100644
--- a/services/core/java/com/android/server/pm/NoFilteringResolver.java
+++ b/services/core/java/com/android/server/pm/NoFilteringResolver.java
@@ -24,7 +24,6 @@
 import android.os.Binder;
 
 import com.android.internal.R;
-import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.resolution.ComponentResolverApi;
 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
@@ -57,10 +56,9 @@
      * (PackageManager.MATCH_CLONE_PROFILE) bit set.
      * @return true if resolver would be used for cross profile resolution.
      */
-    public static boolean isIntentRedirectionAllowed(Context context,
-            AppCloningDeviceConfigHelper appCloningDeviceConfigHelper, boolean resolveForStart,
+    public static boolean isIntentRedirectionAllowed(Context context, boolean resolveForStart,
             long flags) {
-        return isAppCloningBuildingBlocksEnabled(context, appCloningDeviceConfigHelper)
+        return isAppCloningBuildingBlocksEnabled(context)
                     && (resolveForStart || (((flags & PackageManager.MATCH_CLONE_PROFILE) != 0)
                     && hasPermission(context, Manifest.permission.QUERY_CLONED_APPS)));
     }
@@ -142,14 +140,12 @@
     }
 
     /**
-     * Checks if the AppCloningBuildingBlocks flag is enabled.
+     * Checks if the AppCloningBuildingBlocks config is enabled.
      */
-    private static boolean isAppCloningBuildingBlocksEnabled(Context context,
-            AppCloningDeviceConfigHelper appCloningDeviceConfigHelper) {
+    private static boolean isAppCloningBuildingBlocksEnabled(Context context) {
         final long token = Binder.clearCallingIdentity();
         try {
-            return context.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks)
-                    && appCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks();
+            return context.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
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 322c11a..e33c6f0 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9254,7 +9254,6 @@
 
     boolean shouldRestoreImeVisibility(IBinder imeTargetWindowToken) {
         final Task imeTargetWindowTask;
-        boolean hadRequestedShowIme = false;
         synchronized (mGlobalLock) {
             final WindowState imeTargetWindow = mWindowMap.get(imeTargetWindowToken);
             if (imeTargetWindow == null) {
@@ -9264,14 +9263,15 @@
             if (imeTargetWindowTask == null) {
                 return false;
             }
-            if (imeTargetWindow.mActivityRecord != null) {
-                hadRequestedShowIme = imeTargetWindow.mActivityRecord.mLastImeShown;
+            if (imeTargetWindow.mActivityRecord != null
+                    && imeTargetWindow.mActivityRecord.mLastImeShown) {
+                return true;
             }
         }
         final TaskSnapshot snapshot = getTaskSnapshot(imeTargetWindowTask.mTaskId,
                 imeTargetWindowTask.mUserId, false /* isLowResolution */,
                 false /* restoreFromDisk */);
-        return snapshot != null && snapshot.hasImeSurface() || hadRequestedShowIme;
+        return snapshot != null && snapshot.hasImeSurface();
     }
 
     @Override
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 154aa7d4..4268eb9 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -231,7 +231,14 @@
     public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes()
             throws Exception {
         setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
-        testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
+    }
+
+    @Test
+    public void testMultiAuth_singleSensor_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+            throws Exception {
+        setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
     }
 
     @Test
@@ -239,10 +246,18 @@
             throws Exception {
         setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
         setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
-        testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
     }
 
-    public void testMultiAuth_fingerprintSensorStartsAfterUINotifies()
+    @Test
+    public void testMultiAuth_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+            throws Exception {
+        setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+        setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
+        testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
+    }
+
+    public void testMultiAuth_fingerprintSensorStartsAfterUINotifies(boolean startFingerprintNow)
             throws Exception {
         final long operationId = 123;
         final int userId = 10;
@@ -282,13 +297,21 @@
         // fingerprint sensor does not start even if all cookies are received
         assertEquals(STATE_AUTH_STARTED, session.getState());
         verify(mStatusBarService).showAuthenticationDialog(any(), any(), any(),
-                anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong(), anyInt());
+                anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong());
 
         // Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started.
-        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn(startFingerprintNow);
         assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
-        assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+        assertEquals(startFingerprintNow ? BiometricSensor.STATE_AUTHENTICATING
+                        : BiometricSensor.STATE_COOKIE_RETURNED,
                 session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+
+        // start fingerprint sensor if it was delayed
+        if (!startFingerprintNow) {
+            session.onStartFingerprint();
+            assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+                    session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+        }
     }
 
     @Test
@@ -316,14 +339,14 @@
         verify(impl, never()).startPreparedClient(anyInt());
 
         // First invocation should start the client monitor.
-        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn(true /* startFingerprintNow */);
         assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
         verify(impl).startPreparedClient(anyInt());
 
         // Subsequent invocations should not start the client monitor again.
-        session.onDialogAnimatedIn();
-        session.onDialogAnimatedIn();
-        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn(true /* startFingerprintNow */);
+        session.onDialogAnimatedIn(false /* startFingerprintNow */);
+        session.onDialogAnimatedIn(true /* startFingerprintNow */);
         assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
         verify(impl, times(1)).startPreparedClient(anyInt());
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 520e1c8..67be376 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -18,7 +18,7 @@
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
 
 import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
@@ -311,8 +311,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -397,8 +396,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -516,7 +514,7 @@
         assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
 
         // startPreparedClient invoked
-        mBiometricService.mAuthSession.onDialogAnimatedIn();
+        mBiometricService.mAuthSession.onDialogAnimatedIn(true /* startFingerprintNow */);
         verify(mBiometricService.mSensors.get(0).impl)
                 .startPreparedClient(cookieCaptor.getValue());
 
@@ -530,8 +528,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
 
         // Hardware authenticated
         final byte[] HAT = generateRandomHAT();
@@ -587,8 +584,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -752,8 +748,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 anyString(),
-                anyLong() /* requestId */,
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                anyLong() /* requestId */);
     }
 
     @Test
@@ -854,8 +849,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -935,8 +929,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(TEST_REQUEST_ID),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(TEST_REQUEST_ID));
     }
 
     @Test
@@ -1432,8 +1425,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(requestId),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(requestId));
 
         // Requesting strong and credential, when credential is setup
         resetReceivers();
@@ -1456,8 +1448,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(requestId),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(requestId));
 
         // Un-downgrading the authenticator allows successful strong auth
         for (BiometricSensor sensor : mBiometricService.mSensors) {
@@ -1482,8 +1473,7 @@
                 anyInt() /* userId */,
                 anyLong() /* operationId */,
                 eq(TEST_PACKAGE_NAME),
-                eq(requestId),
-                eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+                eq(requestId));
     }
 
     @Test(expected = IllegalStateException.class)
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index bfb6b0f1..067feae 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -23,6 +23,7 @@
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
 import static com.android.internal.widget.LockPatternUtils.PIN_LENGTH_UNAVAILABLE;
 
 import static org.junit.Assert.assertEquals;
@@ -625,11 +626,9 @@
     }
 
     @Test
-    public void testPasswordDataV2VersionCredentialTypePin_deserialize() {
-        // Test that we can deserialize existing PasswordData and don't inadvertently change the
-        // wire format.
+    public void testDeserializePasswordData_forPinWithLengthAvailable() {
         byte[] serialized = new byte[] {
-                0, 2, 0, 2, /* CREDENTIAL_TYPE_PASSWORD_OR_PIN */
+                0, 0, 0, 3, /* CREDENTIAL_TYPE_PIN */
                 11, /* scryptLogN */
                 22, /* scryptLogR */
                 33, /* scryptLogP */
@@ -637,25 +636,23 @@
                 1, 2, -1, -2, 55, /* salt */
                 0, 0, 0, 6, /* passwordHandle.length */
                 2, 3, -2, -3, 44, 1, /* passwordHandle */
-                0, 0, 0, 5, /* pinLength */
+                0, 0, 0, 6, /* pinLength */
         };
         PasswordData deserialized = PasswordData.fromBytes(serialized);
 
         assertEquals(11, deserialized.scryptLogN);
         assertEquals(22, deserialized.scryptLogR);
         assertEquals(33, deserialized.scryptLogP);
-        assertEquals(5, deserialized.pinLength);
-        assertEquals(CREDENTIAL_TYPE_PASSWORD_OR_PIN, deserialized.credentialType);
+        assertEquals(CREDENTIAL_TYPE_PIN, deserialized.credentialType);
         assertArrayEquals(PAYLOAD, deserialized.salt);
         assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+        assertEquals(6, deserialized.pinLength);
     }
 
     @Test
-    public void testPasswordDataV2VersionNegativePinLengthNoCredential_deserialize() {
-        // Test that we can deserialize existing PasswordData and don't inadvertently change the
-        // wire format.
+    public void testDeserializePasswordData_forPinWithLengthExplicitlyUnavailable() {
         byte[] serialized = new byte[] {
-                0, 2, -1, -1, /* CREDENTIAL_TYPE_NONE */
+                0, 0, 0, 3, /* CREDENTIAL_TYPE_PIN */
                 11, /* scryptLogN */
                 22, /* scryptLogR */
                 33, /* scryptLogP */
@@ -663,23 +660,52 @@
                 1, 2, -1, -2, 55, /* salt */
                 0, 0, 0, 6, /* passwordHandle.length */
                 2, 3, -2, -3, 44, 1, /* passwordHandle */
-                -1, -1, -1, -2, /* pinLength */
+                -1, -1, -1, -1, /* pinLength */
         };
         PasswordData deserialized = PasswordData.fromBytes(serialized);
 
         assertEquals(11, deserialized.scryptLogN);
         assertEquals(22, deserialized.scryptLogR);
         assertEquals(33, deserialized.scryptLogP);
-        assertEquals(-2, deserialized.pinLength);
-        assertEquals(CREDENTIAL_TYPE_NONE, deserialized.credentialType);
+        assertEquals(CREDENTIAL_TYPE_PIN, deserialized.credentialType);
         assertArrayEquals(PAYLOAD, deserialized.salt);
         assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+        assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
     }
 
     @Test
-    public void testPasswordDataV1VersionNoCredential_deserialize() {
-        // Test that we can deserialize existing PasswordData and don't inadvertently change the
-        // wire format.
+    public void testDeserializePasswordData_forPinWithVersionNumber() {
+        // Test deserializing a PasswordData that has a version number in the first two bytes.
+        // Files like this were created by some Android 14 beta versions.  This version number was a
+        // mistake and should be ignored by the deserializer.
+        byte[] serialized = new byte[] {
+                0, 2, /* version 2 */
+                0, 3, /* CREDENTIAL_TYPE_PIN */
+                11, /* scryptLogN */
+                22, /* scryptLogR */
+                33, /* scryptLogP */
+                0, 0, 0, 5, /* salt.length */
+                1, 2, -1, -2, 55, /* salt */
+                0, 0, 0, 6, /* passwordHandle.length */
+                2, 3, -2, -3, 44, 1, /* passwordHandle */
+                0, 0, 0, 6, /* pinLength */
+        };
+        PasswordData deserialized = PasswordData.fromBytes(serialized);
+
+        assertEquals(11, deserialized.scryptLogN);
+        assertEquals(22, deserialized.scryptLogR);
+        assertEquals(33, deserialized.scryptLogP);
+        assertEquals(CREDENTIAL_TYPE_PIN, deserialized.credentialType);
+        assertArrayEquals(PAYLOAD, deserialized.salt);
+        assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+        assertEquals(6, deserialized.pinLength);
+    }
+
+    @Test
+    public void testDeserializePasswordData_forNoneCred() {
+        // Test that a PasswordData that uses CREDENTIAL_TYPE_NONE and lacks the PIN length field
+        // can be deserialized.  Files like this were created by Android 13 and earlier.  Android 14
+        // and later no longer create PasswordData for CREDENTIAL_TYPE_NONE.
         byte[] serialized = new byte[] {
                 -1, -1, -1, -1, /* CREDENTIAL_TYPE_NONE */
                 11, /* scryptLogN */
@@ -695,16 +721,17 @@
         assertEquals(11, deserialized.scryptLogN);
         assertEquals(22, deserialized.scryptLogR);
         assertEquals(33, deserialized.scryptLogP);
-        assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
         assertEquals(CREDENTIAL_TYPE_NONE, deserialized.credentialType);
         assertArrayEquals(PAYLOAD, deserialized.salt);
         assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+        assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
     }
 
     @Test
-    public void testPasswordDataV1VersionCredentialTypePin_deserialize() {
-        // Test that we can deserialize existing PasswordData and don't inadvertently change the
-        // wire format.
+    public void testDeserializePasswordData_forPasswordOrPin() {
+        // Test that a PasswordData that uses CREDENTIAL_TYPE_PASSWORD_OR_PIN and lacks the PIN
+        // length field can be deserialized.  Files like this were created by Android 10 and
+        // earlier.  Android 11 eliminated CREDENTIAL_TYPE_PASSWORD_OR_PIN.
         byte[] serialized = new byte[] {
                 0, 0, 0, 2, /* CREDENTIAL_TYPE_PASSWORD_OR_PIN */
                 11, /* scryptLogN */
@@ -720,10 +747,10 @@
         assertEquals(11, deserialized.scryptLogN);
         assertEquals(22, deserialized.scryptLogR);
         assertEquals(33, deserialized.scryptLogP);
-        assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
         assertEquals(CREDENTIAL_TYPE_PASSWORD_OR_PIN, deserialized.credentialType);
         assertArrayEquals(PAYLOAD, deserialized.salt);
         assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+        assertEquals(PIN_LENGTH_UNAVAILABLE, deserialized.pinLength);
     }
 
     @Test
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);
             }