15/n: Allow Auth UI to start in credential UI

If the user is locked out of biometrics, and
BiometricPrompt#setDeviceCredentialAllowed(true), the user should be
shown the credential UI.

This change gives BiometricService the ability to request SystemUI
to show AuthCredentialView without first showing AuthBiometricView.

Bug: 140127687

Test: atest BiometricServiceTest
Test: atest com.android.systemui.biometrics

Change-Id: Ic26986ba044b7992641676c3d3b99fc1395a45b7
diff --git a/core/java/android/hardware/biometrics/Authenticator.java b/core/java/android/hardware/biometrics/Authenticator.java
new file mode 100644
index 0000000..6d7e748
--- /dev/null
+++ b/core/java/android/hardware/biometrics/Authenticator.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.biometrics;
+
+/**
+ * Type of authenticators defined on a granularity that the BiometricManager / BiometricPrompt
+ * supports.
+ * @hide
+ */
+public class Authenticator {
+
+    /**
+     * Device credential, e.g. Pin/Pattern/Password.
+     */
+    public static final int TYPE_CREDENTIAL = 1 << 0;
+    /**
+     * Encompasses all biometrics on the device, e.g. Fingerprint/Iris/Face.
+     */
+    public static final int TYPE_BIOMETRIC = 1 << 1;
+
+}
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 342743d..cf86e251 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -69,19 +69,21 @@
     /**
      * @hide
      */
-    public static final String KEY_POSITIVE_TEXT = "positive_text";
-    /**
-     * @hide
-     */
     public static final String KEY_NEGATIVE_TEXT = "negative_text";
     /**
      * @hide
      */
     public static final String KEY_REQUIRE_CONFIRMATION = "require_confirmation";
     /**
+     * This is deprecated. Internally we should use {@link #KEY_AUTHENTICATORS_ALLOWED}
      * @hide
      */
     public static final String KEY_ALLOW_DEVICE_CREDENTIAL = "allow_device_credential";
+    /**
+     * If this key is set, we will ignore {@link #KEY_ALLOW_DEVICE_CREDENTIAL}
+     * @hide
+     */
+    public static final String KEY_AUTHENTICATORS_ALLOWED = "authenticators_allowed";
 
     /**
      * Error/help message will show for this amount of time.
@@ -203,30 +205,6 @@
         }
 
         /**
-         * Optional: Set the text for the positive button. If not set, the positive button
-         * will not show.
-         * @param text
-         * @return
-         * @hide
-         */
-        @NonNull public Builder setPositiveButton(@NonNull CharSequence text,
-                @NonNull @CallbackExecutor Executor executor,
-                @NonNull DialogInterface.OnClickListener listener) {
-            if (TextUtils.isEmpty(text)) {
-                throw new IllegalArgumentException("Text must be set and non-empty");
-            }
-            if (executor == null) {
-                throw new IllegalArgumentException("Executor must not be null");
-            }
-            if (listener == null) {
-                throw new IllegalArgumentException("Listener must not be null");
-            }
-            mBundle.putCharSequence(KEY_POSITIVE_TEXT, text);
-            mPositiveButtonInfo = new ButtonInfo(executor, listener);
-            return this;
-        }
-
-        /**
          * Required: Set the text for the negative button. This would typically be used as a
          * "Cancel" button, but may be also used to show an alternative method for authentication,
          * such as screen that asks for a backup password.
@@ -306,15 +284,19 @@
             final CharSequence title = mBundle.getCharSequence(KEY_TITLE);
             final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT);
             final boolean useDefaultTitle = mBundle.getBoolean(KEY_USE_DEFAULT_TITLE);
-            final boolean enableFallback = mBundle.getBoolean(KEY_ALLOW_DEVICE_CREDENTIAL);
+            final boolean allowCredential = mBundle.getBoolean(KEY_ALLOW_DEVICE_CREDENTIAL);
+            final Object authenticatorsAllowed = mBundle.get(KEY_AUTHENTICATORS_ALLOWED);
 
             if (TextUtils.isEmpty(title) && !useDefaultTitle) {
                 throw new IllegalArgumentException("Title must be set and non-empty");
-            } else if (TextUtils.isEmpty(negative) && !enableFallback) {
+            } else if (TextUtils.isEmpty(negative) && !allowCredential) {
                 throw new IllegalArgumentException("Negative text must be set and non-empty");
-            } else if (!TextUtils.isEmpty(negative) && enableFallback) {
+            } else if (!TextUtils.isEmpty(negative) && allowCredential) {
                 throw new IllegalArgumentException("Can't have both negative button behavior"
                         + " and device credential enabled");
+            } else if (authenticatorsAllowed != null && allowCredential) {
+                throw new IllegalArgumentException("setAuthenticatorsAllowed and"
+                        + " setDeviceCredentialAllowed should not be used simultaneously");
             }
             return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo);
         }
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 122c080..c8ba52a 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -151,17 +151,17 @@
 
     void showShutdownUi(boolean isReboot, String reason);
 
-    // Used to show the dialog when BiometricService starts authentication
-    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type,
-            boolean requireConfirmation, int userId, String opPackageName);
-    // Used to hide the dialog when a biometric is authenticated
+    // Used to show the authentication dialog (Biometrics, Device Credential)
+    void showAuthenticationDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int biometricModality, boolean requireConfirmation, int userId, String opPackageName);
+    // Used to notify the authentication dialog that a biometric has been authenticated or rejected
     void onBiometricAuthenticated(boolean authenticated, String failureReason);
     // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
     void onBiometricHelp(String message);
     // Used to set a message - the dialog will dismiss after a certain amount of time
     void onBiometricError(int errorCode, String error);
-    // Used to hide the biometric dialog when the AuthenticationClient is stopped
-    void hideBiometricDialog();
+    // Used to hide the authentication dialog, e.g. when the application cancels authentication
+    void hideAuthenticationDialog();
 
     /**
      * Notifies System UI that the display is ready to show system decorations.
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 270ef4d..a845b58 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -99,15 +99,15 @@
     void showPinningEnterExitToast(boolean entering);
     void showPinningEscapeToast();
 
-    // Used to show the dialog when BiometricService starts authentication
-    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type,
-            boolean requireConfirmation, int userId, String opPackageName);
-    // Used to hide the dialog when a biometric is authenticated
+    // Used to show the authentication dialog (Biometrics, Device Credential)
+    void showAuthenticationDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int biometricModality, boolean requireConfirmation, int userId, String opPackageName);
+    // Used to notify the authentication dialog that a biometric has been authenticated or rejected
     void onBiometricAuthenticated(boolean authenticated, String failureReason);
     // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
     void onBiometricHelp(String message);
     // Used to set a message - the dialog will dismiss after a certain amount of time
     void onBiometricError(int errorCode, String error);
-    // Used to hide the biometric dialog when the AuthenticationClient is stopped
-    void hideBiometricDialog();
+    // Used to hide the authentication dialog, e.g. when the application cancels authentication
+    void hideAuthenticationDialog();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index e08707d..ecc1f69 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -754,6 +754,6 @@
     }
 
     private boolean isDeviceCredentialAllowed() {
-        return mBiometricPromptBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
+        return Utils.isDeviceCredentialAllowed(mBiometricPromptBundle);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 1a3189b..7c6cb08 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -24,6 +24,7 @@
 import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.hardware.biometrics.Authenticator;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricPrompt;
 import android.os.Binder;
@@ -74,6 +75,7 @@
     @interface ContainerState {}
 
     final Config mConfig;
+    private final Injector mInjector;
     private final IBinder mWindowToken = new Binder();
     private final WindowManager mWindowManager;
     private final AuthPanelController mPanelController;
@@ -82,11 +84,11 @@
     private final CredentialCallback mCredentialCallback;
 
     @VisibleForTesting final FrameLayout mFrameLayout;
-    @VisibleForTesting AuthBiometricView mBiometricView;
-    @VisibleForTesting AuthCredentialView mCredentialView;
+    @VisibleForTesting @Nullable AuthBiometricView mBiometricView;
+    @VisibleForTesting @Nullable AuthCredentialView mCredentialView;
 
     private final ImageView mBackgroundView;
-    private final ScrollView mBiometricScrollView;
+    @VisibleForTesting final ScrollView mBiometricScrollView;
     private final View mPanelView;
 
     private final float mTranslationY;
@@ -107,25 +109,11 @@
         String mOpPackageName;
         int mModalityMask;
         boolean mSkipIntro;
-        @Builder.InitialView int mInitialView;
     }
 
     public static class Builder {
         Config mConfig;
 
-        /**
-         * Start the prompt with biometric UI. May flow to credential view if
-         * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)} is set to true.
-         */
-        public static final int INITIAL_VIEW_BIOMETRIC = 1;
-        /**
-         * Start the prompt with credential UI
-         */
-        public static final int INITIAL_VIEW_CREDENTIAL = 2;
-        @Retention(RetentionPolicy.SOURCE)
-        @IntDef({INITIAL_VIEW_BIOMETRIC, INITIAL_VIEW_CREDENTIAL})
-        @interface InitialView {}
-
         public Builder(Context context) {
             mConfig = new Config();
             mConfig.mContext = context;
@@ -161,14 +149,32 @@
             return this;
         }
 
-        public Builder setInitialView(@InitialView int initialView) {
-            mConfig.mInitialView = initialView;
-            return this;
-        }
-
         public AuthContainerView build(int modalityMask) {
             mConfig.mModalityMask = modalityMask;
-            return new AuthContainerView(mConfig);
+            return new AuthContainerView(mConfig, new Injector());
+        }
+    }
+
+    public static class Injector {
+        ScrollView getBiometricScrollView(FrameLayout parent) {
+            return parent.findViewById(R.id.biometric_scrollview);
+        }
+
+        FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) {
+            return (FrameLayout) factory.inflate(
+                    R.layout.auth_container_view, root, false /* attachToRoot */);
+        }
+
+        AuthPanelController getPanelController(Context context, View panelView) {
+            return new AuthPanelController(context, panelView);
+        }
+
+        ImageView getBackgroundView(FrameLayout parent) {
+            return parent.findViewById(R.id.background);
+        }
+
+        View getPanelView(FrameLayout parent) {
+            return parent.findViewById(R.id.panel);
         }
     }
 
@@ -194,7 +200,7 @@
                     break;
                 case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
                     mConfig.mCallback.onDeviceCredentialPressed();
-                    addCredentialView(false /* animatePanel */);
+                    addCredentialView(false /* animatePanel */, true /* animateContents */);
                     break;
                 default:
                     Log.e(TAG, "Unhandled action: " + action);
@@ -210,10 +216,12 @@
     }
 
     @VisibleForTesting
-    AuthContainerView(Config config) {
+    AuthContainerView(Config config, Injector injector) {
         super(config.mContext);
 
         mConfig = config;
+        mInjector = injector;
+
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
 
@@ -224,29 +232,30 @@
         mCredentialCallback = new CredentialCallback();
 
         final LayoutInflater factory = LayoutInflater.from(mContext);
-        mFrameLayout = (FrameLayout) factory.inflate(
-                R.layout.auth_container_view, this, false /* attachToRoot */);
+        mFrameLayout = mInjector.inflateContainerView(factory, this);
 
-        mPanelView = mFrameLayout.findViewById(R.id.panel);
-        mPanelController = new AuthPanelController(mContext, mPanelView);
+        mPanelView = mInjector.getPanelView(mFrameLayout);
+        mPanelController = mInjector.getPanelController(mContext, mPanelView);
 
-        // TODO: Update with new controllers if multi-modal authentication can occur simultaneously
-        if (config.mModalityMask == BiometricAuthenticator.TYPE_FINGERPRINT) {
-            mBiometricView = (AuthBiometricFingerprintView)
-                    factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false);
-        } else if (config.mModalityMask == BiometricAuthenticator.TYPE_FACE) {
-            mBiometricView = (AuthBiometricFaceView)
-                    factory.inflate(R.layout.auth_biometric_face_view, null, false);
-        } else {
-            Log.e(TAG, "Unsupported modality mask: " + config.mModalityMask);
-            mBiometricView = null;
-            mBackgroundView = null;
-            mBiometricScrollView = null;
-            return;
+        // Inflate biometric view only if necessary.
+        if (Utils.isBiometricAllowed(mConfig.mBiometricPromptBundle)) {
+            if (config.mModalityMask == BiometricAuthenticator.TYPE_FINGERPRINT) {
+                mBiometricView = (AuthBiometricFingerprintView)
+                        factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false);
+            } else if (config.mModalityMask == BiometricAuthenticator.TYPE_FACE) {
+                mBiometricView = (AuthBiometricFaceView)
+                        factory.inflate(R.layout.auth_biometric_face_view, null, false);
+            } else {
+                Log.e(TAG, "Unsupported biometric modality: " + config.mModalityMask);
+                mBiometricView = null;
+                mBackgroundView = null;
+                mBiometricScrollView = null;
+                return;
+            }
         }
 
-        mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
-        mBackgroundView = mFrameLayout.findViewById(R.id.background);
+        mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout);
+        mBackgroundView = mInjector.getBackgroundView(mFrameLayout);
 
         UserManager userManager = mContext.getSystemService(UserManager.class);
         DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
@@ -277,8 +286,7 @@
 
     @Override
     public boolean isAllowDeviceCredentials() {
-        return mConfig.mBiometricPromptBundle
-                .getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
+        return Utils.isDeviceCredentialAllowed(mConfig.mBiometricPromptBundle);
     }
 
     private void addBiometricView() {
@@ -297,7 +305,7 @@
      * it should own the panel expansion.
      * @param animatePanel if the credential view needs to own the panel expansion animation
      */
-    private void addCredentialView(boolean animatePanel) {
+    private void addCredentialView(boolean animatePanel, boolean animateContents) {
         final LayoutInflater factory = LayoutInflater.from(mContext);
         mCredentialView = (AuthCredentialView) factory.inflate(
                 R.layout.auth_credential_view, null, false);
@@ -305,6 +313,7 @@
         mCredentialView.setCallback(mCredentialCallback);
         mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
         mCredentialView.setPanelController(mPanelController, animatePanel);
+        mCredentialView.setShouldAnimateContents(animateContents);
         mFrameLayout.addView(mCredentialView);
     }
 
@@ -317,23 +326,22 @@
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
+        onAttachedToWindowInternal();
+    }
+
+    @VisibleForTesting
+    void onAttachedToWindowInternal() {
         mWakefulnessLifecycle.addObserver(this);
 
-        Log.v(TAG, "Initial view: " + mConfig.mInitialView);
-
-        switch (mConfig.mInitialView) {
-            case Builder.INITIAL_VIEW_BIOMETRIC:
-                addBiometricView();
-                break;
-            case Builder.INITIAL_VIEW_CREDENTIAL:
-                addCredentialView(true /* animatePanel */);
-                break;
-            default:
-                Log.e(TAG, "Initial view not supported: " + mConfig.mInitialView);
-                break;
+        if (Utils.isBiometricAllowed(mConfig.mBiometricPromptBundle)) {
+            addBiometricView();
+        } else if (Utils.isDeviceCredentialAllowed(mConfig.mBiometricPromptBundle)) {
+            addCredentialView(true /* animatePanel */, false /* animateContents */);
+        } else {
+            throw new IllegalStateException("Unknown configuration: "
+                    + Utils.getAuthenticators(mConfig.mBiometricPromptBundle));
         }
 
-
         if (mConfig.mSkipIntro) {
             mContainerState = STATE_SHOWING;
         } else {
@@ -358,6 +366,15 @@
                         .setInterpolator(mLinearOutSlowIn)
                         .withLayer()
                         .start();
+                if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
+                    mCredentialView.setY(mTranslationY);
+                    mCredentialView.animate()
+                            .translationY(0)
+                            .setDuration(ANIMATION_DURATION_SHOW_MS)
+                            .setInterpolator(mLinearOutSlowIn)
+                            .withLayer()
+                            .start();
+                }
                 animate()
                         .alpha(1f)
                         .setDuration(ANIMATION_DURATION_SHOW_MS)
@@ -381,7 +398,9 @@
 
     @Override
     public void show(WindowManager wm, @Nullable Bundle savedState) {
-        mBiometricView.restoreState(savedState);
+        if (mBiometricView != null) {
+            mBiometricView.restoreState(savedState);
+        }
         wm.addView(this, getLayoutParams(mWindowToken));
     }
 
@@ -427,7 +446,10 @@
         outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING,
                 mBiometricView != null && mCredentialView == null);
         outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null);
-        mBiometricView.onSaveState(outState);
+
+        if (mBiometricView != null) {
+            mBiometricView.onSaveState(outState);
+        }
     }
 
     @Override
@@ -525,7 +547,9 @@
             return;
         }
         mContainerState = STATE_SHOWING;
-        mBiometricView.onDialogAnimatedIn();
+        if (mBiometricView != null) {
+            mBiometricView.onDialogAnimatedIn();
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 74e170d..4c2afb0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.hardware.biometrics.Authenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
@@ -200,16 +201,19 @@
     }
 
     @Override
-    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
-            int type, boolean requireConfirmation, int userId, String opPackageName) {
+    public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int biometricModality, boolean requireConfirmation, int userId, String opPackageName) {
+        final int authenticators = Utils.getAuthenticators(bundle);
+
         if (DEBUG) {
-            Log.d(TAG, "showBiometricDialog, type: " + type
+            Log.d(TAG, "showAuthenticationDialog, authenticators: " + authenticators
+                    + ", biometricModality: " + biometricModality
                     + ", requireConfirmation: " + requireConfirmation);
         }
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = bundle;
         args.arg2 = receiver;
-        args.argi1 = type;
+        args.argi1 = biometricModality;
         args.arg3 = requireConfirmation;
         args.argi2 = userId;
         args.arg4 = opPackageName;
@@ -219,8 +223,8 @@
             Log.w(TAG, "mCurrentDialog: " + mCurrentDialog);
             skipAnimation = true;
         }
-        showDialog(args, skipAnimation, null /* savedState */,
-                AuthContainerView.Builder.INITIAL_VIEW_BIOMETRIC);
+
+        showDialog(args, skipAnimation, null /* savedState */);
     }
 
     @Override
@@ -256,14 +260,13 @@
     }
 
     @Override
-    public void hideBiometricDialog() {
-        if (DEBUG) Log.d(TAG, "hideBiometricDialog");
+    public void hideAuthenticationDialog() {
+        if (DEBUG) Log.d(TAG, "hideAuthenticationDialog");
 
         mCurrentDialog.dismissFromSystemServer();
     }
 
-    private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState,
-            @AuthContainerView.Builder.InitialView int initialView) {
+    private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
         mCurrentDialogArgs = args;
         final int type = args.argi1;
         final Bundle biometricPromptBundle = (Bundle) args.arg1;
@@ -278,8 +281,7 @@
                 userId,
                 type,
                 opPackageName,
-                skipAnimation,
-                initialView);
+                skipAnimation);
 
         if (newDialog == null) {
             Log.e(TAG, "Unsupported type: " + type);
@@ -287,12 +289,11 @@
         }
 
         if (DEBUG) {
-            Log.d(TAG, "showDialog, "
+            Log.d(TAG, "showDialog: " + args
                     + " savedState: " + savedState
                     + " mCurrentDialog: " + mCurrentDialog
                     + " newDialog: " + newDialog
-                    + " type: " + type
-                    + " initialView: " + initialView);
+                    + " type: " + type);
         }
 
         if (mCurrentDialog != null) {
@@ -334,21 +335,20 @@
                     != AuthContainerView.STATE_ANIMATING_OUT) {
                 final boolean credentialShowing =
                         savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING);
+                if (credentialShowing) {
+                    // TODO: Clean this up
+                    Bundle bundle = (Bundle) mCurrentDialogArgs.arg1;
+                    bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED,
+                            Authenticator.TYPE_CREDENTIAL);
+                }
 
-                // We can assume if credential is showing, then biometric doesn't need to be shown,
-                // since credential is always after biometric.
-                final int initialView = credentialShowing
-                        ? AuthContainerView.Builder.INITIAL_VIEW_CREDENTIAL
-                        : AuthContainerView.Builder.INITIAL_VIEW_BIOMETRIC;
-
-                showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState, initialView);
+                showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
             }
         }
     }
 
     protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation,
-            int userId, int type, String opPackageName, boolean skipIntro,
-            @AuthContainerView.Builder.InitialView int initialView) {
+            int userId, int type, String opPackageName, boolean skipIntro) {
         return new AuthContainerView.Builder(mContext)
                 .setCallback(this)
                 .setBiometricPromptBundle(biometricPromptBundle)
@@ -356,7 +356,6 @@
                 .setUserId(userId)
                 .setOpPackageName(opPackageName)
                 .setSkipIntro(skipIntro)
-                .setInitialView(initialView)
                 .build(type);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
index 8eac8f5..1ba88c6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
@@ -59,6 +59,7 @@
     private Bundle mBiometricPromptBundle;
     private AuthPanelController mPanelController;
     private boolean mShouldAnimatePanel;
+    private boolean mShouldAnimateContents;
 
     private TextView mTitleView;
     private TextView mSubtitleView;
@@ -220,6 +221,10 @@
         mShouldAnimatePanel = animatePanel;
     }
 
+    void setShouldAnimateContents(boolean animateContents) {
+        mShouldAnimateContents = animateContents;
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
@@ -230,18 +235,21 @@
         setTextOrHide(mDescriptionView,
                 mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
 
-        setTranslationY(getResources()
-                .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
-        setAlpha(0);
+        // Only animate this if we're transitioning from a biometric view.
+        if (mShouldAnimateContents) {
+            setTranslationY(getResources()
+                    .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
+            setAlpha(0);
 
-        postOnAnimation(() -> {
-           animate().translationY(0)
-                   .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
-                   .alpha(1.f)
-                   .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
-                   .withLayer()
-                   .start();
-        });
+            postOnAnimation(() -> {
+                animate().translationY(0)
+                        .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
+                        .alpha(1.f)
+                        .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+                        .withLayer()
+                        .start();
+            });
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java
index e00cf6a..485e667 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java
@@ -19,6 +19,9 @@
 import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
 
 import android.content.Context;
+import android.hardware.biometrics.Authenticator;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Bundle;
 import android.util.DisplayMetrics;
 import android.view.View;
 import android.view.ViewGroup;
@@ -46,4 +49,18 @@
         view.sendAccessibilityEventUnchecked(event);
         view.notifySubtreeAccessibilityStateChanged(view, view, CONTENT_CHANGE_TYPE_SUBTREE);
     }
+
+    static boolean isDeviceCredentialAllowed(Bundle biometricPromptBundle) {
+        final int authenticators = getAuthenticators(biometricPromptBundle);
+        return (authenticators & Authenticator.TYPE_CREDENTIAL) != 0;
+    }
+
+    static boolean isBiometricAllowed(Bundle biometricPromptBundle) {
+        final int authenticators = getAuthenticators(biometricPromptBundle);
+        return (authenticators & Authenticator.TYPE_BIOMETRIC) != 0;
+    }
+
+    static int getAuthenticators(Bundle biometricPromptBundle) {
+        return biometricPromptBundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 39b65c4..36e04fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -270,12 +270,13 @@
 
         default void onRotationProposal(int rotation, boolean isValid) { }
 
-        default void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
-                int type, boolean requireConfirmation, int userId, String opPackageName) { }
+        default void showAuthenticationDialog(Bundle bundle,
+                IBiometricServiceReceiverInternal receiver, int biometricModality,
+                boolean requireConfirmation, int userId, String opPackageName) { }
         default void onBiometricAuthenticated(boolean authenticated, String failureReason) { }
         default void onBiometricHelp(String message) { }
         default void onBiometricError(int errorCode, String error) { }
-        default void hideBiometricDialog() { }
+        default void hideAuthenticationDialog() { }
 
         /**
          * @see IStatusBar#onDisplayReady(int)
@@ -740,13 +741,13 @@
     }
 
     @Override
-    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
-            int type, boolean requireConfirmation, int userId, String opPackageName) {
+    public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int biometricModality, boolean requireConfirmation, int userId, String opPackageName) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = bundle;
             args.arg2 = receiver;
-            args.argi1 = type;
+            args.argi1 = biometricModality;
             args.arg3 = requireConfirmation;
             args.argi2 = userId;
             args.arg4 = opPackageName;
@@ -780,7 +781,7 @@
     }
 
     @Override
-    public void hideBiometricDialog() {
+    public void hideAuthenticationDialog() {
         synchronized (mLock) {
             mHandler.obtainMessage(MSG_BIOMETRIC_HIDE).sendToTarget();
         }
@@ -1032,10 +1033,10 @@
                     mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED);
                     SomeArgs someArgs = (SomeArgs) msg.obj;
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).showBiometricDialog(
+                        mCallbacks.get(i).showAuthenticationDialog(
                                 (Bundle) someArgs.arg1,
                                 (IBiometricServiceReceiverInternal) someArgs.arg2,
-                                someArgs.argi1 /* type */,
+                                someArgs.argi1 /* biometricModality */,
                                 (boolean) someArgs.arg3 /* requireConfirmation */,
                                 someArgs.argi2 /* userId */,
                                 (String) someArgs.arg4 /* opPackageName */);
@@ -1065,7 +1066,7 @@
                     break;
                 case MSG_BIOMETRIC_HIDE:
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).hideBiometricDialog();
+                        mCallbacks.get(i).hideAuthenticationDialog();
                     }
                     break;
                 case MSG_SHOW_CHARGING_ANIMATION:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
index d110940..2c85424 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
+import android.hardware.biometrics.Authenticator;
 import android.hardware.biometrics.BiometricPrompt;
 import android.os.Bundle;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -291,11 +292,13 @@
     private Bundle buildBiometricPromptBundle(boolean allowDeviceCredential) {
         Bundle bundle = new Bundle();
         bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title");
+        int authenticators = Authenticator.TYPE_BIOMETRIC;
         if (allowDeviceCredential) {
-            bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true);
+            authenticators |= Authenticator.TYPE_CREDENTIAL;
         } else {
             bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative");
         }
+        bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
         return bundle;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
index aeceba7..8550390 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
@@ -17,14 +17,29 @@
 package com.android.systemui.biometrics;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
+import android.hardware.biometrics.Authenticator;
 import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Bundle;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ScrollView;
 
 import com.android.systemui.SysuiTestCase;
 
@@ -46,16 +61,12 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-
-        AuthContainerView.Config config = new AuthContainerView.Config();
-        config.mContext = mContext;
-        config.mCallback = mCallback;
-        config.mModalityMask |= BiometricAuthenticator.TYPE_FINGERPRINT;
-        mAuthContainer = new TestableAuthContainer(config);
     }
 
     @Test
     public void testActionAuthenticated_sendsDismissedAuthenticated() {
+        initializeContainer(Authenticator.TYPE_BIOMETRIC);
+
         mAuthContainer.mBiometricCallback.onAction(
                 AuthBiometricView.Callback.ACTION_AUTHENTICATED);
         verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED));
@@ -63,6 +74,8 @@
 
     @Test
     public void testActionUserCanceled_sendsDismissedUserCanceled() {
+        initializeContainer(Authenticator.TYPE_BIOMETRIC);
+
         mAuthContainer.mBiometricCallback.onAction(
                 AuthBiometricView.Callback.ACTION_USER_CANCELED);
         verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_USER_CANCELED));
@@ -70,6 +83,8 @@
 
     @Test
     public void testActionButtonNegative_sendsDismissedButtonNegative() {
+        initializeContainer(Authenticator.TYPE_BIOMETRIC);
+
         mAuthContainer.mBiometricCallback.onAction(
                 AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE);
         verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE));
@@ -77,6 +92,8 @@
 
     @Test
     public void testActionTryAgain_sendsTryAgain() {
+        initializeContainer(Authenticator.TYPE_BIOMETRIC);
+
         mAuthContainer.mBiometricCallback.onAction(
                 AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
         verify(mCallback).onTryAgainPressed();
@@ -84,6 +101,8 @@
 
     @Test
     public void testActionError_sendsDismissedError() {
+        initializeContainer(Authenticator.TYPE_BIOMETRIC);
+
         mAuthContainer.mBiometricCallback.onAction(
                 AuthBiometricView.Callback.ACTION_ERROR);
         verify(mCallback).onDismissed(AuthDialogCallback.DISMISSED_ERROR);
@@ -91,25 +110,68 @@
 
     @Test
     public void testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
+        initializeContainer(
+                Authenticator.TYPE_BIOMETRIC | Authenticator.TYPE_CREDENTIAL);
+
         mAuthContainer.mBiometricCallback.onAction(
                 AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL);
         verify(mCallback).onDeviceCredentialPressed();
 
         // Credential view is attached to the frame layout
         waitForIdleSync();
-        assertEquals(mAuthContainer.mFrameLayout, mAuthContainer.mCredentialView.getParent());
+        assertNotNull(mAuthContainer.mCredentialView);
+        verify(mAuthContainer.mFrameLayout).addView(eq(mAuthContainer.mCredentialView));
     }
 
     @Test
     public void testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() {
+        initializeContainer(
+                Authenticator.TYPE_BIOMETRIC | Authenticator.TYPE_CREDENTIAL);
+
         mAuthContainer.mBiometricView = mock(AuthBiometricView.class);
         mAuthContainer.animateToCredentialUI();
         verify(mAuthContainer.mBiometricView).startTransitionToCredentialUI();
     }
 
+    @Test
+    public void testShowBiometricUI() {
+        initializeContainer(Authenticator.TYPE_BIOMETRIC);
+
+        assertNotEquals(null, mAuthContainer.mBiometricView);
+
+        mAuthContainer.onAttachedToWindowInternal();
+        verify(mAuthContainer.mBiometricScrollView).addView(mAuthContainer.mBiometricView);
+        // Credential view is not added
+        verify(mAuthContainer.mFrameLayout, never()).addView(any());
+    }
+
+    @Test
+    public void testShowCredentialUI_doesNotInflateBiometricUI() {
+        initializeContainer(Authenticator.TYPE_CREDENTIAL);
+
+        mAuthContainer.onAttachedToWindowInternal();
+
+        assertNull(null, mAuthContainer.mBiometricView);
+        assertNotNull(mAuthContainer.mCredentialView);
+        verify(mAuthContainer.mFrameLayout).addView(mAuthContainer.mCredentialView);
+    }
+
+    private void initializeContainer(int authenticators) {
+        AuthContainerView.Config config = new AuthContainerView.Config();
+        config.mContext = mContext;
+        config.mCallback = mCallback;
+        config.mModalityMask |= BiometricAuthenticator.TYPE_FINGERPRINT;
+
+        Bundle bundle = new Bundle();
+        bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
+        config.mBiometricPromptBundle = bundle;
+
+        mAuthContainer = new TestableAuthContainer(config);
+    }
+
     private class TestableAuthContainer extends AuthContainerView {
         TestableAuthContainer(AuthContainerView.Config config) {
-            super(config);
+            super(config, new MockInjector());
         }
 
         @Override
@@ -117,4 +179,31 @@
             mConfig.mCallback.onDismissed(reason);
         }
     }
+
+    private final class MockInjector extends AuthContainerView.Injector {
+        @Override
+        public ScrollView getBiometricScrollView(FrameLayout parent) {
+            return mock(ScrollView.class);
+        }
+
+        @Override
+        public FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) {
+            return mock(FrameLayout.class);
+        }
+
+        @Override
+        public AuthPanelController getPanelController(Context context, View view) {
+            return mock(AuthPanelController.class);
+        }
+
+        @Override
+        public ImageView getBackgroundView(FrameLayout parent) {
+            return mock(ImageView.class);
+        }
+
+        @Override
+        public View getPanelView(FrameLayout parent) {
+            return mock(View.class);
+        }
+    }
 }
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 b8f735e..dcdb5c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -35,6 +36,7 @@
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.hardware.biometrics.Authenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
@@ -72,7 +74,7 @@
     @Mock
     private AuthDialog mDialog2;
 
-    private TestableBiometricDialogImpl mBiometricDialogImpl;
+    private TestableAuthController mAuthController;
 
 
     @Before
@@ -93,63 +95,66 @@
         when(mDialog1.getOpPackageName()).thenReturn("Dialog1");
         when(mDialog2.getOpPackageName()).thenReturn("Dialog2");
 
-        mBiometricDialogImpl = new TestableBiometricDialogImpl(new MockInjector());
-        mBiometricDialogImpl.mContext = context;
-        mBiometricDialogImpl.mComponents = mContext.getComponents();
+        when(mDialog1.isAllowDeviceCredentials()).thenReturn(false);
+        when(mDialog2.isAllowDeviceCredentials()).thenReturn(false);
 
-        mBiometricDialogImpl.start();
+        mAuthController = new TestableAuthController(new MockInjector());
+        mAuthController.mContext = context;
+        mAuthController.mComponents = mContext.getComponents();
+
+        mAuthController.start();
     }
 
     // Callback tests
 
     @Test
     public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED);
         verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
     }
 
     @Test
     public void testSendsReasonNegative_whenDismissedByButtonNegative() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
         verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
     }
 
     @Test
     public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
         verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
     }
 
     @Test
     public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
         verify(mReceiver).onDialogDismissed(
                 BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
     }
 
     @Test
     public void testSendsReasonError_whenDismissedByError() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_ERROR);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onDismissed(AuthDialogCallback.DISMISSED_ERROR);
         verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR);
     }
 
     @Test
     public void testSendsReasonServerRequested_whenDismissedByServer() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER);
         verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
     }
 
     @Test
     public void testSendsReasonCredentialConfirmed_whenDeviceCredentialAuthenticated()
             throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
         verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED);
     }
 
@@ -158,22 +163,22 @@
     @Test
     public void testShowInvoked_whenSystemRequested()
             throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         verify(mDialog1).show(any(), any());
     }
 
     @Test
-    public void testOnAuthenticationSucceededInvoked_whenSystemRequested() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onBiometricAuthenticated(true, null /* failureReason */);
+    public void testOnAuthenticationSucceededInvoked_whenSystemRequested() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.onBiometricAuthenticated(true, null /* failureReason */);
         verify(mDialog1).onAuthenticationSucceeded();
     }
 
     @Test
-    public void testOnAuthenticationFailedInvoked_whenSystemRequested() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testOnAuthenticationFailedInvoked_whenSystemRequested() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         final String failureReason = "failure reason";
-        mBiometricDialogImpl.onBiometricAuthenticated(false, failureReason);
+        mAuthController.onBiometricAuthenticated(false, failureReason);
 
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
         verify(mDialog1).onAuthenticationFailed(captor.capture());
@@ -182,10 +187,10 @@
     }
 
     @Test
-    public void testOnHelpInvoked_whenSystemRequested() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testOnHelpInvoked_whenSystemRequested() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         final String helpMessage = "help";
-        mBiometricDialogImpl.onBiometricHelp(helpMessage);
+        mAuthController.onBiometricHelp(helpMessage);
 
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
         verify(mDialog1).onHelp(captor.capture());
@@ -194,11 +199,11 @@
     }
 
     @Test
-    public void testOnErrorInvoked_whenSystemRequested() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testOnErrorInvoked_whenSystemRequested() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         final int error = 1;
         final String errMessage = "error message";
-        mBiometricDialogImpl.onBiometricError(error, errMessage);
+        mAuthController.onBiometricError(error, errMessage);
 
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
         verify(mDialog1).onError(captor.capture());
@@ -207,83 +212,82 @@
     }
 
     @Test
-    public void testErrorLockout_whenCredentialAllowed_AnimatesToCredentialUI() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testErrorLockout_whenCredentialAllowed_AnimatesToCredentialUI() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT;
         final String errorString = "lockout";
 
         when(mDialog1.isAllowDeviceCredentials()).thenReturn(true);
 
-        mBiometricDialogImpl.onBiometricError(error, errorString);
+        mAuthController.onBiometricError(error, errorString);
         verify(mDialog1, never()).onError(anyString());
         verify(mDialog1).animateToCredentialUI();
     }
 
     @Test
-    public void testErrorLockoutPermanent_whenCredentialAllowed_AnimatesToCredentialUI()
-            throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testErrorLockoutPermanent_whenCredentialAllowed_AnimatesToCredentialUI() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
         final String errorString = "lockout_permanent";
 
         when(mDialog1.isAllowDeviceCredentials()).thenReturn(true);
 
-        mBiometricDialogImpl.onBiometricError(error, errorString);
+        mAuthController.onBiometricError(error, errorString);
         verify(mDialog1, never()).onError(anyString());
         verify(mDialog1).animateToCredentialUI();
     }
 
     @Test
-    public void testErrorLockout_whenCredentialNotAllowed_sendsOnError() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testErrorLockout_whenCredentialNotAllowed_sendsOnError() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT;
         final String errorString = "lockout";
 
         when(mDialog1.isAllowDeviceCredentials()).thenReturn(false);
 
-        mBiometricDialogImpl.onBiometricError(error, errorString);
+        mAuthController.onBiometricError(error, errorString);
         verify(mDialog1).onError(eq(errorString));
         verify(mDialog1, never()).animateToCredentialUI();
     }
 
     @Test
-    public void testErrorLockoutPermanent_whenCredentialNotAllowed_sendsOnError() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testErrorLockoutPermanent_whenCredentialNotAllowed_sendsOnError() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
         final String errorString = "lockout_permanent";
 
         when(mDialog1.isAllowDeviceCredentials()).thenReturn(false);
 
-        mBiometricDialogImpl.onBiometricError(error, errorString);
+        mAuthController.onBiometricError(error, errorString);
         verify(mDialog1).onError(eq(errorString));
         verify(mDialog1, never()).animateToCredentialUI();
     }
 
     @Test
-    public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.hideBiometricDialog();
+    public void testDismissWithoutCallbackInvoked_whenSystemRequested() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.hideAuthenticationDialog();
         verify(mDialog1).dismissFromSystemServer();
     }
 
     @Test
-    public void testClientNotified_whenDismissedBySystemServer() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.hideBiometricDialog();
+    public void testClientNotified_whenDismissedBySystemServer() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
+        mAuthController.hideAuthenticationDialog();
         verify(mDialog1).dismissFromSystemServer();
 
-        assertNotNull(mBiometricDialogImpl.mCurrentDialog);
-        assertNotNull(mBiometricDialogImpl.mReceiver);
+        assertNotNull(mAuthController.mCurrentDialog);
+        assertNotNull(mAuthController.mReceiver);
     }
 
     // Corner case tests
 
     @Test
-    public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         verify(mDialog1).show(any(), any());
 
-        showDialog(BiometricPrompt.TYPE_FACE);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
 
         // First dialog should be dismissed without animation
         verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */);
@@ -293,11 +297,20 @@
     }
 
     @Test
-    public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+    public void testConfigurationPersists_whenOnConfigurationChanged() {
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
         verify(mDialog1).show(any(), any());
 
-        mBiometricDialogImpl.onConfigurationChanged(new Configuration());
+        // Return that the UI is in "showing" state
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            Bundle savedState = (Bundle) args[0];
+            savedState.putInt(
+                    AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING);
+            return null; // onSaveState returns void
+        }).when(mDialog1).onSaveState(any());
+
+        mAuthController.onConfigurationChanged(new Configuration());
 
         ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
         verify(mDialog1).onSaveState(captor.capture());
@@ -314,37 +327,63 @@
     }
 
     @Test
+    public void testConfigurationPersists_whenBiometricFallbackToCredential() {
+        showDialog(Authenticator.TYPE_CREDENTIAL | Authenticator.TYPE_BIOMETRIC,
+                BiometricPrompt.TYPE_FACE);
+        verify(mDialog1).show(any(), any());
+
+        // Pretend that the UI is now showing device credential UI.
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            Bundle savedState = (Bundle) args[0];
+            savedState.putInt(
+                    AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING);
+            savedState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, true);
+            return null; // onSaveState returns void
+        }).when(mDialog1).onSaveState(any());
+
+        mAuthController.onConfigurationChanged(new Configuration());
+
+        // Check that the new dialog was initialized to the credential UI.
+        ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mDialog2).show(any(), captor.capture());
+        assertEquals(Authenticator.TYPE_CREDENTIAL,
+                mAuthController.mLastBiometricPromptBundle
+                        .getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED));
+    }
+
+    @Test
     public void testClientNotified_whenTaskStackChangesDuringAuthentication() throws Exception {
-        showDialog(BiometricPrompt.TYPE_FACE);
+        showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE);
 
         List<ActivityManager.RunningTaskInfo> tasks = new ArrayList<>();
         ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class);
         taskInfo.topActivity = mock(ComponentName.class);
         when(taskInfo.topActivity.getPackageName()).thenReturn("other_package");
         tasks.add(taskInfo);
-        when(mBiometricDialogImpl.mActivityTaskManager.getTasks(anyInt())).thenReturn(tasks);
+        when(mAuthController.mActivityTaskManager.getTasks(anyInt())).thenReturn(tasks);
 
-        mBiometricDialogImpl.mTaskStackListener.onTaskStackChanged();
+        mAuthController.mTaskStackListener.onTaskStackChanged();
         waitForIdleSync();
 
-        assertNull(mBiometricDialogImpl.mCurrentDialog);
-        assertNull(mBiometricDialogImpl.mReceiver);
+        assertNull(mAuthController.mCurrentDialog);
+        assertNull(mAuthController.mReceiver);
         verify(mDialog1).dismissWithoutCallback(true /* animate */);
         verify(mReceiver).onDialogDismissed(eq(BiometricPrompt.DISMISSED_REASON_USER_CANCEL));
     }
 
     // Helpers
 
-    private void showDialog(int type) {
-        mBiometricDialogImpl.showBiometricDialog(createTestDialogBundle(),
+    private void showDialog(int authenticators, int biometricModality) {
+        mAuthController.showAuthenticationDialog(createTestDialogBundle(authenticators),
                 mReceiver /* receiver */,
-                type,
+                biometricModality,
                 true /* requireConfirmation */,
                 0 /* userId */,
                 "testPackage");
     }
 
-    private Bundle createTestDialogBundle() {
+    private Bundle createTestDialogBundle(int authenticators) {
         Bundle bundle = new Bundle();
 
         bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title");
@@ -356,20 +395,26 @@
         // by user settings, and should be tested in BiometricService.
         bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true);
 
+        bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
+
         return bundle;
     }
 
-    private final class TestableBiometricDialogImpl extends AuthController {
+    private final class TestableAuthController extends AuthController {
         private int mBuildCount = 0;
+        private Bundle mLastBiometricPromptBundle;
 
-        public TestableBiometricDialogImpl(Injector injector) {
+        public TestableAuthController(Injector injector) {
             super(injector);
         }
 
         @Override
         protected AuthDialog buildDialog(Bundle biometricPromptBundle,
                 boolean requireConfirmation, int userId, int type, String opPackageName,
-                boolean skipIntro, @AuthContainerView.Builder.InitialView int initialView) {
+                boolean skipIntro) {
+
+            mLastBiometricPromptBundle = biometricPromptBundle;
+
             AuthDialog dialog;
             if (mBuildCount == 0) {
                 dialog = mDialog1;
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 fcd0e23..1bd01e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -367,12 +367,13 @@
     }
 
     @Test
-    public void testShowBiometricDialog() {
+    public void testShowAuthenticationDialog() {
         Bundle bundle = new Bundle();
         String packageName = "test";
-        mCommandQueue.showBiometricDialog(bundle, null /* receiver */, 1, true, 3, packageName);
+        mCommandQueue.showAuthenticationDialog(bundle, null /* receiver */, 1, true, 3,
+                packageName);
         waitForIdleSync();
-        verify(mCallbacks).showBiometricDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3),
+        verify(mCallbacks).showAuthenticationDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3),
                 eq(packageName));
     }
 
@@ -402,9 +403,9 @@
     }
 
     @Test
-    public void testHideBiometricDialog() {
-        mCommandQueue.hideBiometricDialog();
+    public void testHideAuthenticationDialog() {
+        mCommandQueue.hideAuthenticationDialog();
         waitForIdleSync();
-        verify(mCallbacks).hideBiometricDialog();
+        verify(mCallbacks).hideAuthenticationDialog();
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 08af2a0..1003bf7 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -31,6 +31,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.ContentObserver;
+import android.hardware.biometrics.Authenticator;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricPrompt;
@@ -211,7 +212,8 @@
         }
 
         boolean isAllowDeviceCredential() {
-            return mBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, false);
+            final int authenticators = mBundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED);
+            return (authenticators & Authenticator.TYPE_CREDENTIAL) != 0;
         }
     }
 
@@ -237,7 +239,7 @@
 
     // Get and cache the available authenticator (manager) classes. Used since aidl doesn't support
     // polymorphism :/
-    final ArrayList<Authenticator> mAuthenticators = new ArrayList<>();
+    final ArrayList<AuthenticatorWrapper> mAuthenticators = new ArrayList<>();
 
     // The current authentication session, null if idle/done. We need to track both the current
     // and pending sessions since errors may be sent to either.
@@ -346,11 +348,11 @@
         }
     };
 
-    private final class Authenticator {
+    private final class AuthenticatorWrapper {
         final int mType;
         final BiometricAuthenticator mAuthenticator;
 
-        Authenticator(int type, BiometricAuthenticator authenticator) {
+        AuthenticatorWrapper(int type, BiometricAuthenticator authenticator) {
             mType = type;
             mAuthenticator = authenticator;
         }
@@ -607,6 +609,12 @@
                 return;
             }
 
+            if (bundle.get(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED) != null) {
+                checkInternalPermission();
+            }
+
+            combineAuthenticatorBundles(bundle);
+
             // Check the usage of this in system server. Need to remove this check if it becomes
             // a public API.
             final boolean useDefaultTitle =
@@ -840,8 +848,8 @@
         // Cache the authenticators
         for (int featureId : FEATURE_ID) {
             if (hasFeature(featureId)) {
-                Authenticator authenticator =
-                        new Authenticator(featureId, getAuthenticator(featureId));
+                AuthenticatorWrapper authenticator =
+                        new AuthenticatorWrapper(featureId, getAuthenticator(featureId));
                 mAuthenticators.add(authenticator);
             }
         }
@@ -879,7 +887,7 @@
 
         int modality = TYPE_NONE;
         int firstHwAvailable = TYPE_NONE;
-        for (Authenticator authenticatorWrapper : mAuthenticators) {
+        for (AuthenticatorWrapper authenticatorWrapper : mAuthenticators) {
             modality = authenticatorWrapper.getType();
             BiometricAuthenticator authenticator = authenticatorWrapper.getAuthenticator();
             if (authenticator.isHardwareDetected()) {
@@ -1145,7 +1153,7 @@
                     } else {
                         mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI;
                         if (error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) {
-                            mStatusBarService.hideBiometricDialog();
+                            mStatusBarService.hideAuthenticationDialog();
                         } else {
                             mStatusBarService.onBiometricError(error, message);
                         }
@@ -1155,7 +1163,7 @@
                     // the client and and clean up. The only error we should get here is
                     // ERROR_CANCELED due to another client kicking us out.
                     mCurrentAuthSession.mClientReceiver.onError(error, message);
-                    mStatusBarService.hideBiometricDialog();
+                    mStatusBarService.hideAuthenticationDialog();
                     mCurrentAuthSession = null;
                 } else if (mCurrentAuthSession.mState == STATE_SHOWING_DEVICE_CREDENTIAL) {
                     Slog.d(TAG, "Biometric canceled, ignoring from state: "
@@ -1167,8 +1175,32 @@
             } else if (mPendingAuthSession != null
                     && mPendingAuthSession.containsCookie(cookie)) {
                 if (mPendingAuthSession.mState == STATE_AUTH_CALLED) {
-                    mPendingAuthSession.mClientReceiver.onError(error, message);
-                    mPendingAuthSession = null;
+                    // If any error is received while preparing the auth session (lockout, etc),
+                    // and if device credential is allowed, just show the credential UI.
+                    if (mPendingAuthSession.isAllowDeviceCredential()) {
+                        int authenticators = mPendingAuthSession.mBundle
+                                .getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, 0);
+                        // Disallow biometric and notify SystemUI to show the authentication prompt.
+                        authenticators &= ~Authenticator.TYPE_BIOMETRIC;
+                        mPendingAuthSession.mBundle.putInt(
+                                BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED,
+                                authenticators);
+
+                        mCurrentAuthSession = mPendingAuthSession;
+                        mCurrentAuthSession.mState = STATE_SHOWING_DEVICE_CREDENTIAL;
+                        mPendingAuthSession = null;
+
+                        mStatusBarService.showAuthenticationDialog(
+                                mCurrentAuthSession.mBundle,
+                                mInternalReceiver,
+                                0 /* biometricModality */,
+                                false /* requireConfirmation */,
+                                mCurrentAuthSession.mUserId,
+                                mCurrentAuthSession.mOpPackageName);
+                    } else {
+                        mPendingAuthSession.mClientReceiver.onError(error, message);
+                        mPendingAuthSession = null;
+                    }
                 } else {
                     Slog.e(TAG, "Impossible pending session error state: "
                             + mPendingAuthSession.mState);
@@ -1286,8 +1318,20 @@
         mCurrentAuthSession.mState = STATE_SHOWING_DEVICE_CREDENTIAL;
     }
 
+    /**
+     * Invoked when each service has notified that its client is ready to be started. When
+     * all biometrics are ready, this invokes the SystemUI dialog through StatusBar.
+     */
     private void handleOnReadyForAuthentication(int cookie, boolean requireConfirmation,
             int userId) {
+        if (mPendingAuthSession == null) {
+            // Only should happen if a biometric was locked out when authenticate() was invoked.
+            // In that case, if device credentials are allowed, the UI is already showing. If not
+            // allowed, the error has already been returned to the caller.
+            Slog.w(TAG, "Pending auth session null");
+            return;
+        }
+
         Iterator it = mPendingAuthSession.mModalitiesWaiting.entrySet().iterator();
         while (it.hasNext()) {
             Map.Entry<Integer, Integer> pair = (Map.Entry) it.next();
@@ -1329,7 +1373,7 @@
                 }
 
                 if (!continuing) {
-                    mStatusBarService.showBiometricDialog(mCurrentAuthSession.mBundle,
+                    mStatusBarService.showAuthenticationDialog(mCurrentAuthSession.mBundle,
                             mInternalReceiver, modality, requireConfirmation, userId,
                             mCurrentAuthSession.mOpPackageName);
                 }
@@ -1452,7 +1496,7 @@
                 );
 
                 mCurrentAuthSession = null;
-                mStatusBarService.hideBiometricDialog();
+                mStatusBarService.hideAuthenticationDialog();
             } catch (RemoteException e) {
                 Slog.e(TAG, "Remote exception", e);
             }
@@ -1492,4 +1536,38 @@
             Slog.e(TAG, "Unable to cancel authentication");
         }
     }
+
+
+    /**
+     * Combine {@link BiometricPrompt#KEY_ALLOW_DEVICE_CREDENTIAL} with
+     * {@link BiometricPrompt#KEY_AUTHENTICATORS_ALLOWED}, as the former is not flexible
+     * enough.
+     */
+    @VisibleForTesting
+    static void combineAuthenticatorBundles(Bundle bundle) {
+        boolean biometricEnabled = true; // enabled by default
+        boolean credentialEnabled = false; // disabled by default
+        if (bundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, false)) {
+            credentialEnabled = true;
+        }
+        if (bundle.get(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED) != null) {
+            final int authenticatorFlags =
+                    bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED);
+            biometricEnabled = (authenticatorFlags & Authenticator.TYPE_BIOMETRIC) != 0;
+            // Using both KEY_ALLOW_DEVICE_CREDENTIAL and KEY_AUTHENTICATORS_ALLOWED together
+            // is not supported. Default to overwriting.
+            credentialEnabled = (authenticatorFlags & Authenticator.TYPE_CREDENTIAL) != 0;
+        }
+
+        bundle.remove(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
+
+        int authenticators = 0;
+        if (biometricEnabled) {
+            authenticators |= Authenticator.TYPE_BIOMETRIC;
+        }
+        if (credentialEnabled) {
+            authenticators |= Authenticator.TYPE_CREDENTIAL;
+        }
+        bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
+    }
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 644228d..3439d38 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -609,13 +609,13 @@
     }
 
     @Override
-    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
-            int type, boolean requireConfirmation, int userId, String opPackageName) {
+    public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int biometricModality, boolean requireConfirmation, int userId, String opPackageName) {
         enforceBiometricDialog();
         if (mBar != null) {
             try {
-                mBar.showBiometricDialog(bundle, receiver, type, requireConfirmation, userId,
-                        opPackageName);
+                mBar.showAuthenticationDialog(bundle, receiver, biometricModality,
+                        requireConfirmation, userId, opPackageName);
             } catch (RemoteException ex) {
             }
         }
@@ -655,11 +655,11 @@
     }
 
     @Override
-    public void hideBiometricDialog() {
+    public void hideAuthenticationDialog() {
         enforceBiometricDialog();
         if (mBar != null) {
             try {
-                mBar.hideBiometricDialog();
+                mBar.hideAuthenticationDialog();
             } catch (RemoteException ex) {
             }
         }
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 e474ea9..555b927 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -39,6 +39,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.hardware.biometrics.Authenticator;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricPrompt;
@@ -319,7 +320,7 @@
                 .startPreparedClient(cookieCaptor.getValue());
 
         // StatusBar showBiometricDialog invoked
-        verify(mBiometricService.mStatusBarService).showBiometricDialog(
+        verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
                 eq(mBiometricService.mCurrentAuthSession.mBundle),
                 any(IBiometricServiceReceiverInternal.class),
                 eq(BiometricAuthenticator.TYPE_FINGERPRINT),
@@ -439,7 +440,7 @@
         verify(mReceiver2, never()).onError(anyInt(), any(String.class));
 
         // SystemUI dialog closed
-        verify(mBiometricService.mStatusBarService).hideBiometricDialog();
+        verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
 
         // After SystemUI notifies that the animation has completed
         mBiometricService.mInternalReceiver
@@ -488,7 +489,7 @@
         resetStatusBar();
         startPendingAuthSession(mBiometricService);
         waitForIdle();
-        verify(mBiometricService.mStatusBarService, never()).showBiometricDialog(
+        verify(mBiometricService.mStatusBarService, never()).showAuthenticationDialog(
                 any(Bundle.class),
                 any(IBiometricServiceReceiverInternal.class),
                 anyInt(),
@@ -518,7 +519,7 @@
                 eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
                 eq(ERROR_CANCELED));
         // Dialog is hidden immediately
-        verify(mBiometricService.mStatusBarService).hideBiometricDialog();
+        verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
         // Auth session is over
         assertNull(mBiometricService.mCurrentAuthSession);
     }
@@ -545,7 +546,7 @@
         verify(mBiometricService.mStatusBarService).onBiometricError(
                 eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
                 eq(ERROR_UNABLE_TO_PROCESS));
-        verify(mBiometricService.mStatusBarService, never()).hideBiometricDialog();
+        verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
         verify(mReceiver1, never()).onError(anyInt(), anyString());
 
         // SystemUI animation completed, client is notified, auth session is over
@@ -559,6 +560,103 @@
     }
 
     @Test
+    public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception {
+        setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+        invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
+                false /* requireConfirmation */, true /* allowDeviceCredential */);
+        waitForIdle();
+
+        mBiometricService.mInternalReceiver.onError(
+                getCookieForPendingSession(mBiometricService.mPendingAuthSession),
+                BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
+                ERROR_LOCKOUT);
+        waitForIdle();
+
+        // Pending auth session becomes current auth session, since device credential should
+        // be shown now.
+        assertNull(mBiometricService.mPendingAuthSession);
+        assertNotNull(mBiometricService.mCurrentAuthSession);
+        assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL,
+                mBiometricService.mCurrentAuthSession.mState);
+        assertEquals(Authenticator.TYPE_CREDENTIAL,
+                mBiometricService.mCurrentAuthSession.mBundle.getInt(
+                        BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED));
+        verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
+                eq(mBiometricService.mCurrentAuthSession.mBundle),
+                any(IBiometricServiceReceiverInternal.class),
+                eq(0 /* biometricModality */),
+                anyBoolean() /* requireConfirmation */,
+                anyInt() /* userId */,
+                eq(TEST_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed()
+            throws Exception {
+        setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+        invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
+                false /* requireConfirmation */, false /* allowDeviceCredential */);
+        waitForIdle();
+
+        mBiometricService.mInternalReceiver.onError(
+                getCookieForPendingSession(mBiometricService.mPendingAuthSession),
+                BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
+                ERROR_LOCKOUT);
+        waitForIdle();
+
+        // Error is sent to client
+        assertNull(mBiometricService.mPendingAuthSession);
+        assertNull(mBiometricService.mCurrentAuthSession);
+    }
+
+    @Test
+    public void testCombineAuthenticatorBundle_keyAllowDeviceCredentialAlwaysRemoved() {
+        Bundle bundle;
+        int authenticators;
+
+        // In:
+        // KEY_ALLOW_DEVICE_CREDENTIAL = true
+        // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL
+        // Out:
+        // KEY_ALLOW_DEVICE_CREDENTIAL = null
+        // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL
+        bundle = new Bundle();
+        bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true);
+        authenticators = Authenticator.TYPE_CREDENTIAL | Authenticator.TYPE_BIOMETRIC;
+        bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
+        BiometricService.combineAuthenticatorBundles(bundle);
+        assertNull(bundle.get(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL));
+        assertEquals(authenticators, bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED));
+
+        // In:
+        // KEY_ALLOW_DEVICE_CREDENTIAL = true
+        // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC
+        // Out:
+        // KEY_ALLOW_DEVICE_CREDENTIAL = null
+        // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL
+        bundle = new Bundle();
+        bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true);
+        authenticators = Authenticator.TYPE_BIOMETRIC;
+        bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
+        BiometricService.combineAuthenticatorBundles(bundle);
+        assertNull(bundle.get(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL));
+        assertEquals(authenticators, bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED));
+
+        // In:
+        // KEY_ALLOW_DEVICE_CREDENTIAL = null
+        // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL
+        // Out:
+        // KEY_ALLOW_DEVICE_CREDENTIAL = null
+        // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL
+        bundle = new Bundle();
+        authenticators = Authenticator.TYPE_BIOMETRIC | Authenticator.TYPE_CREDENTIAL;
+        bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
+        BiometricService.combineAuthenticatorBundles(bundle);
+        assertNull(bundle.get(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL));
+        assertEquals(authenticators, bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED));
+    }
+
+    @Test
     public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI()
             throws Exception {
         setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
@@ -827,6 +925,11 @@
         return session.mModalitiesMatched.values().iterator().next();
     }
 
+    private static int getCookieForPendingSession(BiometricService.AuthSession session) {
+        assertEquals(session.mModalitiesWaiting.values().size(), 1);
+        return session.mModalitiesWaiting.values().iterator().next();
+    }
+
     private static void waitForIdle() {
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }