12/n: Add LockPatternView for setDeviceCredentialAllowed(true)

Includes lock icon, title, subtitle, description, lock pattern view.
Corner radius and padding animates nicely from !=0 --> 0.

Support for password/pin will come in a subsequent CL.
Unit tests for AuthCredentialView will be added when
password/pin are implemented.

Support for persisting across configuration changes
and landscape view will also be added in a subsequent
change.

Test: BiometricPromptDemo with the following:
      1) Confirm pattern, callback received
      2) Rejected, error string shown
      3) Lockout (5 attempts), countdown string shown,
         pattern view disabled until countdown is over
      4) Cancel pattern auth, callback received
Test: atest BiometricServiceTest
Test: atest com.android.systemui.biometrics

Change-Id: Idc01e33be0074a6c8a43f60b172a4391bfbe5e8a
diff --git a/packages/SystemUI/res/drawable/auth_dialog_lock.xml b/packages/SystemUI/res/drawable/auth_dialog_lock.xml
new file mode 100644
index 0000000..8146c16
--- /dev/null
+++ b/packages/SystemUI/res/drawable/auth_dialog_lock.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="32dp"
+    android:height="32dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="?android:attr/colorAccent"
+        android:pathData="M12,15m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
+    <path
+        android:fillColor="?android:attr/colorAccent"
+        android:pathData="M18,8h-1.5V5.5C16.5,3.01 14.49,1 12,1S7.5,3.01 7.5,5.5V8H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10C20,8.9 19.1,8 18,8zM9.5,5.5C9.5,4.12 10.62,3 12,3c1.38,0 2.5,1.12 2.5,2.5V8h-5V5.5zM18,20H6V10h1.5h9H18V20z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/auth_container_view.xml b/packages/SystemUI/res/layout/auth_container_view.xml
index 23199aa..3db01a4 100644
--- a/packages/SystemUI/res/layout/auth_container_view.xml
+++ b/packages/SystemUI/res/layout/auth_container_view.xml
@@ -34,7 +34,7 @@
         android:elevation="@dimen/biometric_dialog_elevation"/>
 
     <ScrollView
-        android:id="@+id/scrollview"
+        android:id="@+id/biometric_scrollview"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal|bottom"
diff --git a/packages/SystemUI/res/layout/auth_credential_view.xml b/packages/SystemUI/res/layout/auth_credential_view.xml
new file mode 100644
index 0000000..d30dd68
--- /dev/null
+++ b/packages/SystemUI/res/layout/auth_credential_view.xml
@@ -0,0 +1,98 @@
+<!--
+  ~ 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.
+  -->
+
+<com.android.systemui.biometrics.AuthCredentialView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contents"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:gravity="center_horizontal"
+    android:elevation="@dimen/biometric_dialog_elevation">
+
+    <Space
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+
+    <ImageView
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:background="@drawable/auth_dialog_lock"/>
+
+    <TextView
+        android:id="@+id/title"
+        android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="24dp"
+        android:layout_marginTop="12dp"
+        android:textSize="20sp"
+        android:gravity="center"
+        android:textColor="?android:attr/textColorPrimary"/>
+
+    <TextView
+        android:id="@+id/subtitle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="24dp"
+        android:layout_marginTop="8dp"
+        android:textSize="16sp"
+        android:gravity="center"
+        android:textColor="?android:attr/textColorPrimary"/>
+
+    <TextView
+        android:id="@+id/description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="24dp"
+        android:layout_marginTop="8dp"
+        android:gravity="center"
+        android:textSize="16sp"
+        android:textColor="?android:attr/textColorPrimary"/>
+
+    <Space
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="3"/>
+
+    <TextView
+        android:id="@+id/error"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="24dp"
+        android:textSize="16sp"
+        android:gravity="center"
+        android:textColor="?android:attr/colorError"/>
+
+    <com.android.internal.widget.LockPatternView
+        android:id="@+id/lockPattern"
+        android:layout_marginBottom="20dp"
+        android:layout_marginLeft="40dp"
+        android:layout_marginRight="40dp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        style="@style/LockPatternStyleBiometricPrompt"/>
+
+    <Space
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+
+</com.android.systemui.biometrics.AuthCredentialView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1079206..d722d61 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1009,10 +1009,15 @@
     <!-- Biometric Dialog values -->
     <dimen name="biometric_dialog_biometric_icon_size">64dp</dimen>
     <dimen name="biometric_dialog_corner_size">4dp</dimen>
+    <!-- Y translation when showing/dismissing the dialog-->
     <dimen name="biometric_dialog_animation_translation_offset">350dp</dimen>
     <dimen name="biometric_dialog_border_padding">4dp</dimen>
     <dimen name="biometric_dialog_elevation">1dp</dimen>
     <dimen name="biometric_dialog_icon_padding">16dp</dimen>
+    <!-- Y translation for biometric contents when transitioning to device credential UI -->
+    <dimen name="biometric_dialog_medium_to_large_translation_offset">100dp</dimen>
+    <!-- Y translation for credential contents when animating in -->
+    <dimen name="biometric_dialog_credential_translation_offset">60dp</dimen>
 
     <!-- Wireless Charging Animation values -->
     <dimen name="wireless_charging_dots_radius_start">0dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 85d2e1a..8335c11 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -317,6 +317,14 @@
     <string name="biometric_dialog_use_pattern">Use pattern</string>
     <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pass) [CHAR LIMIT=30] -->
     <string name="biometric_dialog_use_password">Use password</string>
+    <!-- Error string shown when the user enters an incorrect PIN [CHAR LIMIT=40]-->
+    <string name="biometric_dialog_wrong_pin">Wrong PIN</string>
+    <!-- Error string shown when the user enters an incorrect pattern [CHAR LIMIT=40]-->
+    <string name="biometric_dialog_wrong_pattern">Wrong pattern</string>
+    <!-- Error string shown when the user enters an incorrect password [CHAR LIMIT=40]-->
+    <string name="biometric_dialog_wrong_password">Wrong password</string>
+    <!-- Error string shown when the user enters too many incorrect attempts [CHAR LIMIT=120]-->
+    <string name="biometric_dialog_credential_too_many_attempts">Too many incorrect attempts.\nTry again in <xliff:g id="number">%d</xliff:g> seconds.</string>
 
     <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
     <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 6374191..96fbcbb 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -314,6 +314,12 @@
         <item name="*android:errorColor">?android:attr/colorError</item>
     </style>
 
+    <style name="LockPatternStyleBiometricPrompt">
+        <item name="*android:regularColor">?android:attr/colorForeground</item>
+        <item name="*android:successColor">?android:attr/colorForeground</item>
+        <item name="*android:errorColor">?android:attr/colorError</item>
+    </style>
+
     <style name="qs_theme" parent="@*android:style/Theme.DeviceDefault.QuickSettings">
         <item name="lightIconTheme">@style/QSIconTheme</item>
         <item name="darkIconTheme">@style/QSIconTheme</item>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 4c4a745..18d2747 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -147,8 +147,12 @@
             return BiometricPrompt.HIDE_DIALOG_DELAY;
         }
 
-        public int getAnimationDuration() {
-            return AuthDialog.ANIMATE_DURATION_MS;
+        public int getMediumToLargeAnimationDurationMs() {
+            return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
+        }
+
+        public int getAnimateCredentialStartDelayMs() {
+            return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS;
         }
     }
 
@@ -159,7 +163,7 @@
     private final int mTextColorHint;
 
     private AuthPanelController mPanelController;
-    private Bundle mBundle;
+    private Bundle mBiometricPromptBundle;
     private boolean mRequireConfirmation;
     private int mUserId;
     @AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN;
@@ -265,7 +269,7 @@
     }
 
     public void setBiometricPromptBundle(Bundle bundle) {
-        mBundle = bundle;
+        mBiometricPromptBundle = bundle;
     }
 
     public void setCallback(Callback callback) {
@@ -300,7 +304,7 @@
 
             final int newHeight = mIconView.getHeight() + 2 * (int) iconPadding;
             mPanelController.updateForContentDimensions(mMediumWidth, newHeight,
-                    false /* animate */);
+                    0 /* animateDurationMs */);
 
             mSize = newSize;
         } else if (mSize == AuthDialog.SIZE_SMALL && newSize == AuthDialog.SIZE_MEDIUM) {
@@ -318,7 +322,6 @@
 
             // Animate the text
             final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1);
-            opacityAnimator.setDuration(mInjector.getAnimationDuration());
             opacityAnimator.addUpdateListener((animation) -> {
                 final float opacity = (float) animation.getAnimatedValue();
                 mTitleView.setAlpha(opacity);
@@ -336,7 +339,7 @@
 
             // Choreograph together
             final AnimatorSet as = new AnimatorSet();
-            as.setDuration(mInjector.getAnimationDuration());
+            as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
             as.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
@@ -367,41 +370,51 @@
             as.start();
             // Animate the panel
             mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
-                    true /* animate */);
+                    AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
         } else if (newSize == AuthDialog.SIZE_MEDIUM) {
             mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
-                    false /* animate */);
+                    0 /* animateDurationMs */);
             mSize = newSize;
         } else if (newSize == AuthDialog.SIZE_LARGE) {
-            final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
-            opacityAnimator.setDuration(mInjector.getAnimationDuration());
-            opacityAnimator.addUpdateListener((animation) -> {
-                final float opacity = (float) animation.getAnimatedValue();
-                mTitleView.setAlpha(opacity);
-                mSubtitleView.setAlpha(opacity);
-                mDescriptionView.setAlpha(opacity);
-                mIconView.setAlpha(opacity);
-                mIndicatorView.setAlpha(opacity);
-                mNegativeButton.setAlpha(opacity);
+            final float translationY = getResources().getDimension(
+                    R.dimen.biometric_dialog_medium_to_large_translation_offset);
+            final AuthBiometricView biometricView = this;
+
+            // Translate at full duration
+            final ValueAnimator translationAnimator = ValueAnimator.ofFloat(
+                    biometricView.getY(), biometricView.getY() - translationY);
+            translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs());
+            translationAnimator.addUpdateListener((animation) -> {
+                final float translation = (float) animation.getAnimatedValue();
+                biometricView.setTranslationY(translation);
             });
-            opacityAnimator.addListener(new AnimatorListenerAdapter() {
+            translationAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
                     super.onAnimationEnd(animation);
-                    AuthBiometricView view = AuthBiometricView.this;
-                    if (view.getParent() != null) {
-                        ((ViewGroup) view.getParent()).removeView(view);
+                    if (biometricView.getParent() != null) {
+                        ((ViewGroup) biometricView.getParent()).removeView(biometricView);
                     }
                     mSize = newSize;
                 }
             });
 
+            // Opacity to 0 in half duration
+            final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
+            opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2);
+            opacityAnimator.addUpdateListener((animation) -> {
+                final float opacity = (float) animation.getAnimatedValue();
+                biometricView.setAlpha(opacity);
+            });
+
             mPanelController.setUseFullScreen(true);
             mPanelController.updateForContentDimensions(
                     mPanelController.getContainerWidth(),
                     mPanelController.getContainerHeight(),
-                    true /* animate */);
-            opacityAnimator.start();
+                    mInjector.getMediumToLargeAnimationDurationMs());
+            AnimatorSet as = new AnimatorSet();
+            as.play(translationAnimator).with(opacityAnimator);
+            as.start();
         } else {
             Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
         }
@@ -572,7 +585,10 @@
             } else {
                 if (isDeviceCredentialAllowed()) {
                     updateSize(AuthDialog.SIZE_LARGE);
-                    mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
+                    mHandler.postDelayed(() -> {
+                        mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
+                    }, mInjector.getAnimateCredentialStartDelayMs());
+
                 } else {
                     mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
                 }
@@ -603,7 +619,7 @@
      */
     @VisibleForTesting
     void onAttachedToWindowInternal() {
-        setText(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE));
+        setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE));
 
         final String negativeText;
         if (isDeviceCredentialAllowed()) {
@@ -628,12 +644,14 @@
             }
 
         } else {
-            negativeText = mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT);
+            negativeText = mBiometricPromptBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT);
         }
         setText(mNegativeButton, negativeText);
 
-        setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE));
-        setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
+        setTextOrHide(mSubtitleView,
+                mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE));
+        setTextOrHide(mDescriptionView,
+                mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
 
         if (mSavedState == null) {
             updateState(STATE_AUTHENTICATING_ANIMATING_IN);
@@ -730,6 +748,6 @@
     }
 
     private boolean isDeviceCredentialAllowed() {
-        return mBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
+        return mBiometricPromptBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 6781fd2..db23e62 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -37,6 +37,7 @@
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
@@ -78,12 +79,14 @@
     private final AuthPanelController mPanelController;
     private final Interpolator mLinearOutSlowIn;
     @VisibleForTesting final BiometricCallback mBiometricCallback;
+    private final CredentialCallback mCredentialCallback;
 
-    private final ViewGroup mContainerView;
+    @VisibleForTesting final FrameLayout mFrameLayout;
     private final AuthBiometricView mBiometricView;
+    @VisibleForTesting AuthCredentialView mCredentialView;
 
     private final ImageView mBackgroundView;
-    private final ScrollView mScrollView;
+    private final ScrollView mBiometricScrollView;
     private final View mPanelView;
 
     private final float mTranslationY;
@@ -156,7 +159,7 @@
         public void onAction(int action) {
             switch (action) {
                 case AuthBiometricView.Callback.ACTION_AUTHENTICATED:
-                    animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED);
+                    animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
                     break;
                 case AuthBiometricView.Callback.ACTION_USER_CANCELED:
                     animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
@@ -171,7 +174,8 @@
                     animateAway(AuthDialogCallback.DISMISSED_ERROR);
                     break;
                 case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
-                    Log.v(TAG, "ACTION_USE_DEVICE_CREDENTIAL");
+                    mConfig.mCallback.onDeviceCredentialPressed();
+                    showCredentialView();
                     break;
                 default:
                     Log.e(TAG, "Unhandled action: " + action);
@@ -179,6 +183,13 @@
         }
     }
 
+    final class CredentialCallback implements AuthCredentialView.Callback {
+        @Override
+        public void onCredentialMatched() {
+            animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
+        }
+    }
+
     @VisibleForTesting
     AuthContainerView(Config config) {
         super(config.mContext);
@@ -191,12 +202,13 @@
                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
         mBiometricCallback = new BiometricCallback();
+        mCredentialCallback = new CredentialCallback();
 
         final LayoutInflater factory = LayoutInflater.from(mContext);
-        mContainerView = (ViewGroup) factory.inflate(
+        mFrameLayout = (FrameLayout) factory.inflate(
                 R.layout.auth_container_view, this, false /* attachToRoot */);
 
-        mPanelView = mContainerView.findViewById(R.id.panel);
+        mPanelView = mFrameLayout.findViewById(R.id.panel);
         mPanelController = new AuthPanelController(mContext, mPanelView);
 
         // TODO: Update with new controllers if multi-modal authentication can occur simultaneously
@@ -210,11 +222,11 @@
             Log.e(TAG, "Unsupported modality mask: " + config.mModalityMask);
             mBiometricView = null;
             mBackgroundView = null;
-            mScrollView = null;
+            mBiometricScrollView = null;
             return;
         }
 
-        mBackgroundView = mContainerView.findViewById(R.id.background);
+        mBackgroundView = mFrameLayout.findViewById(R.id.background);
 
         UserManager userManager = mContext.getSystemService(UserManager.class);
         DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
@@ -234,9 +246,9 @@
         mBiometricView.setBackgroundView(mBackgroundView);
         mBiometricView.setUserId(mConfig.mUserId);
 
-        mScrollView = mContainerView.findViewById(R.id.scrollview);
-        mScrollView.addView(mBiometricView);
-        addView(mContainerView);
+        mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
+        mBiometricScrollView.addView(mBiometricView);
+        addView(mFrameLayout);
 
         setOnKeyListener((v, keyCode, event) -> {
             if (keyCode != KeyEvent.KEYCODE_BACK) {
@@ -252,6 +264,16 @@
         requestFocus();
     }
 
+    private void showCredentialView() {
+        final LayoutInflater factory = LayoutInflater.from(mContext);
+        mCredentialView = (AuthCredentialView) factory.inflate(
+                R.layout.auth_credential_view, null, false);
+        mCredentialView.setUser(mConfig.mUserId);
+        mCredentialView.setCallback(mCredentialCallback);
+        mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
+        mFrameLayout.addView(mCredentialView);
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -270,7 +292,7 @@
             // The background panel and content are different views since we need to be able to
             // animate them separately in other places.
             mPanelView.setY(mTranslationY);
-            mScrollView.setY(mTranslationY);
+            mBiometricScrollView.setY(mTranslationY);
 
             setAlpha(0f);
             postOnAnimation(() -> {
@@ -281,7 +303,7 @@
                         .withLayer()
                         .withEndAction(this::onDialogAnimatedIn)
                         .start();
-                mScrollView.animate()
+                mBiometricScrollView.animate()
                         .translationY(0)
                         .setDuration(ANIMATION_DURATION_SHOW_MS)
                         .setInterpolator(mLinearOutSlowIn)
@@ -396,12 +418,20 @@
                     .withLayer()
                     .withEndAction(endActionRunnable)
                     .start();
-            mScrollView.animate()
+            mBiometricScrollView.animate()
                     .translationY(mTranslationY)
                     .setDuration(ANIMATION_DURATION_AWAY_MS)
                     .setInterpolator(mLinearOutSlowIn)
                     .withLayer()
                     .start();
+            if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
+                mCredentialView.animate()
+                        .translationY(mTranslationY)
+                        .setDuration(ANIMATION_DURATION_AWAY_MS)
+                        .setInterpolator(mLinearOutSlowIn)
+                        .withLayer()
+                        .start();
+            }
             animate()
                     .alpha(0f)
                     .setDuration(ANIMATION_DURATION_AWAY_MS)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index d10a3fe..eb87834 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -105,6 +105,15 @@
     }
 
     @Override
+    public void onDeviceCredentialPressed() {
+        try {
+            mReceiver.onDeviceCredentialPressed();
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException when handling credential button", e);
+        }
+    }
+
+    @Override
     public void onDismissed(@DismissedReason int reason) {
         switch (reason) {
             case AuthDialogCallback.DISMISSED_USER_CANCELED:
@@ -116,11 +125,12 @@
                 break;
 
             case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE:
-                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
                 break;
 
-            case AuthDialogCallback.DISMISSED_AUTHENTICATED:
-                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
+            case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED:
+                sendResultAndCleanUp(
+                        BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
                 break;
 
             case AuthDialogCallback.DISMISSED_ERROR:
@@ -131,6 +141,10 @@
                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
                 break;
 
+            case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED:
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED);
+                break;
+
             default:
                 Log.e(TAG, "Unhandled reason: " + reason);
                 break;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
new file mode 100644
index 0000000..dabe84b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import android.content.Context;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.widget.LockPatternChecker;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockPatternView;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Shows Pin, Pattern, or Password for
+ * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)}
+ */
+public class AuthCredentialView extends LinearLayout {
+
+    private static final int ERROR_DURATION_MS = 3000;
+
+    private final AccessibilityManager mAccessibilityManager;
+    private final LockPatternUtils mLockPatternUtils;
+    private final Handler mHandler;
+
+    private LockPatternView mLockPatternView;
+    private int mUserId;
+    private AsyncTask<?, ?, ?> mPendingLockCheck;
+    private Callback mCallback;
+    private ErrorTimer mErrorTimer;
+    private Bundle mBiometricPromptBundle;
+
+    private TextView mTitleView;
+    private TextView mSubtitleView;
+    private TextView mDescriptionView;
+    private TextView mErrorView;
+
+    interface Callback {
+        void onCredentialMatched();
+    }
+
+    private static class ErrorTimer extends CountDownTimer {
+        private final TextView mErrorView;
+        private final Context mContext;
+
+        /**
+         * @param millisInFuture    The number of millis in the future from the call
+         *                          to {@link #start()} until the countdown is done and {@link
+         *                          #onFinish()}
+         *                          is called.
+         * @param countDownInterval The interval along the way to receive
+         *                          {@link #onTick(long)} callbacks.
+         */
+        public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
+                TextView errorView) {
+            super(millisInFuture, countDownInterval);
+            mErrorView = errorView;
+            mContext = context;
+        }
+
+        @Override
+        public void onTick(long millisUntilFinished) {
+            final int secondsCountdown = (int) (millisUntilFinished / 1000);
+            mErrorView.setText(mContext.getString(
+                    R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
+        }
+
+        @Override
+        public void onFinish() {
+            mErrorView.setText("");
+        }
+    }
+
+    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
+
+        @Override
+        public void onPatternStart() {
+
+        }
+
+        @Override
+        public void onPatternCleared() {
+
+        }
+
+        @Override
+        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
+
+        }
+
+        @Override
+        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
+            if (mPendingLockCheck != null) {
+                mPendingLockCheck.cancel(false);
+            }
+
+            mLockPatternView.setEnabled(false);
+
+            if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
+                // Pattern size is less than the minimum, do not count it as a failed attempt.
+                onPatternChecked(false /* matched */, 0 /* timeoutMs */);
+                return;
+            }
+
+            mPendingLockCheck = LockPatternChecker.checkPattern(
+                    mLockPatternUtils,
+                    pattern,
+                    mUserId,
+                    this::onPatternChecked);
+        }
+
+        private void onPatternChecked(boolean matched, int timeoutMs) {
+            mLockPatternView.setEnabled(true);
+
+            if (matched) {
+                mClearErrorRunnable.run();
+                mCallback.onCredentialMatched();
+            } else {
+                if (timeoutMs > 0) {
+                    mHandler.removeCallbacks(mClearErrorRunnable);
+                    mLockPatternView.setEnabled(false);
+                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline(mUserId, timeoutMs);
+                    mErrorTimer = new ErrorTimer(mContext,
+                            deadline - SystemClock.elapsedRealtime(),
+                            LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
+                            mErrorView) {
+                        @Override
+                        public void onFinish() {
+                            mClearErrorRunnable.run();
+                            mLockPatternView.setEnabled(true);
+                        }
+                    };
+                    mErrorTimer.start();
+                } else {
+                    showError(getResources().getString(R.string.biometric_dialog_wrong_pattern));
+                }
+            }
+        }
+    }
+
+    private final Runnable mClearErrorRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mErrorView.setText("");
+        }
+    };
+
+    public AuthCredentialView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mHandler = new Handler(Looper.getMainLooper());
+        mLockPatternUtils = new LockPatternUtils(mContext);
+        mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
+    }
+
+    private void showError(String error) {
+        mHandler.removeCallbacks(mClearErrorRunnable);
+        mErrorView.setText(error);
+        mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
+    }
+
+    private void setTextOrHide(TextView view, String string) {
+        if (TextUtils.isEmpty(string)) {
+            view.setVisibility(View.GONE);
+        } else {
+            view.setText(string);
+        }
+
+        Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
+    }
+
+    private void setText(TextView view, String string) {
+        view.setText(string);
+    }
+
+    void setUser(int user) {
+        mUserId = user;
+    }
+
+    void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    void setBiometricPromptBundle(Bundle bundle) {
+        mBiometricPromptBundle = bundle;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE));
+        setTextOrHide(mSubtitleView,
+                mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE));
+        setTextOrHide(mDescriptionView,
+                mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
+
+        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();
+        });
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mErrorTimer != null) {
+            mErrorTimer.cancel();
+        }
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mTitleView = findViewById(R.id.title);
+        mSubtitleView = findViewById(R.id.subtitle);
+        mDescriptionView = findViewById(R.id.description);
+        mErrorView = findViewById(R.id.error);
+        mLockPatternView = findViewById(R.id.lockPattern);
+        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
+        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(mUserId));
+        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index edb2953..29b84bb 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -40,17 +40,38 @@
     String KEY_BIOMETRIC_DIALOG_SIZE = "size";
 
     int SIZE_UNKNOWN = 0;
+    /**
+     * Minimal UI, showing only biometric icon.
+     */
     int SIZE_SMALL = 1;
+    /**
+     * Normal-sized biometric UI, showing title, icon, buttons, etc.
+     */
     int SIZE_MEDIUM = 2;
+    /**
+     * Full-screen credential UI.
+     */
     int SIZE_LARGE = 3;
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({SIZE_UNKNOWN, SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE})
     @interface DialogSize {}
 
     /**
-     * Animation duration, e.g. small to medium dialog, icon translation, etc.
+     * Animation duration, from small to medium dialog, including back panel, icon translation, etc
      */
-    int ANIMATE_DURATION_MS = 150;
+    int ANIMATE_SMALL_TO_MEDIUM_DURATION_MS = 150;
+    /**
+     * Animation duration from medium to large dialog, including biometric fade out, back panel, etc
+     */
+    int ANIMATE_MEDIUM_TO_LARGE_DURATION_MS = 450;
+    /**
+     * Delay before notifying {@link AuthCredentialView} to start animating in.
+     */
+    int ANIMATE_CREDENTIAL_START_DELAY_MS = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS * 2 / 3;
+    /**
+     * Animation duration when sliding in credential UI
+     */
+    int ANIMATE_CREDENTIAL_INITIAL_DURATION_MS = 150;
 
     /**
      * Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index 70752f5..12bb122 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
@@ -27,17 +27,18 @@
     int DISMISSED_USER_CANCELED = 1;
     int DISMISSED_BUTTON_NEGATIVE = 2;
     int DISMISSED_BUTTON_POSITIVE = 3;
-
-    int DISMISSED_AUTHENTICATED = 4;
+    int DISMISSED_BIOMETRIC_AUTHENTICATED = 4;
     int DISMISSED_ERROR = 5;
     int DISMISSED_BY_SYSTEM_SERVER = 6;
+    int DISMISSED_CREDENTIAL_AUTHENTICATED = 7;
 
     @IntDef({DISMISSED_USER_CANCELED,
             DISMISSED_BUTTON_NEGATIVE,
             DISMISSED_BUTTON_POSITIVE,
-            DISMISSED_AUTHENTICATED,
+            DISMISSED_BIOMETRIC_AUTHENTICATED,
             DISMISSED_ERROR,
-            DISMISSED_BY_SYSTEM_SERVER})
+            DISMISSED_BY_SYSTEM_SERVER,
+            DISMISSED_CREDENTIAL_AUTHENTICATED})
     @interface DismissedReason {}
 
     /**
@@ -50,4 +51,9 @@
      * Invoked when the "try again" button is clicked
      */
     void onTryAgainPressed();
+
+    /**
+     * Invoked when the "use password" button is clicked
+     */
+    void onDeviceCredentialPressed();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index 60c9ca4..27040f0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -32,12 +32,10 @@
 public class AuthPanelController extends ViewOutlineProvider {
 
     private static final String TAG = "BiometricPrompt/AuthPanelController";
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     private final Context mContext;
     private final View mPanelView;
-    private final float mCornerRadius;
-    private final int mBiometricMargin;
 
     private boolean mUseFullScreen;
 
@@ -47,19 +45,24 @@
     private int mContentWidth;
     private int mContentHeight;
 
+    private float mCornerRadius;
+    private int mMargin;
+
     @Override
     public void getOutline(View view, Outline outline) {
         final int left = (mContainerWidth - mContentWidth) / 2;
         final int right = mContainerWidth - left;
 
-        final int margin = mUseFullScreen ? 0 : mBiometricMargin;
-        final float cornerRadius = mUseFullScreen ? 0 : mCornerRadius;
-
+        // If the content fits within the container, shrink the height to wrap the content.
+        // Otherwise, set the outline to be the display size minus the margin - the content within
+        // is scrollable.
         final int top = mContentHeight < mContainerHeight
-                ? mContainerHeight - mContentHeight - margin
-                : margin;
-        final int bottom = mContainerHeight - margin;
-        outline.setRoundRect(left, top, right, bottom, cornerRadius);
+                ? mContainerHeight - mContentHeight - mMargin
+                : mMargin;
+
+        // TODO(b/139954942) Likely don't need to "+1" after we resolve the navbar styling.
+        final int bottom = mContainerHeight - mMargin + 1;
+        outline.setRoundRect(left, top, right, bottom, mCornerRadius);
     }
 
     public void setContainerDimensions(int containerWidth, int containerHeight) {
@@ -74,11 +77,12 @@
         mUseFullScreen = fullScreen;
     }
 
-    public void updateForContentDimensions(int contentWidth, int contentHeight, boolean animate) {
+    public void updateForContentDimensions(int contentWidth, int contentHeight,
+            int animateDurationMs) {
         if (DEBUG) {
             Log.v(TAG, "Content Width: " + contentWidth
                     + " Height: " + contentHeight
-                    + " Animate: " + animate);
+                    + " Animate: " + animateDurationMs);
         }
 
         if (mContainerWidth == 0 || mContainerHeight == 0) {
@@ -86,7 +90,24 @@
             return;
         }
 
-        if (animate) {
+        if (animateDurationMs > 0) {
+            // Animate margin
+            final int margin = mUseFullScreen ? 0 : (int) mContext.getResources()
+                    .getDimension(R.dimen.biometric_dialog_border_padding);
+            ValueAnimator marginAnimator = ValueAnimator.ofInt(mMargin, margin);
+            marginAnimator.addUpdateListener((animation) -> {
+                mMargin = (int) animation.getAnimatedValue();
+            });
+
+            // Animate corners
+            final float cornerRadius = mUseFullScreen ? 0 : mContext.getResources()
+                    .getDimension(R.dimen.biometric_dialog_corner_size);
+            ValueAnimator cornerAnimator = ValueAnimator.ofFloat(mCornerRadius, cornerRadius);
+            cornerAnimator.addUpdateListener((animation) -> {
+                mCornerRadius = (float) animation.getAnimatedValue();
+            });
+
+            // Animate height
             ValueAnimator heightAnimator = ValueAnimator.ofInt(mContentHeight, contentHeight);
             heightAnimator.addUpdateListener((animation) -> {
                 mContentHeight = (int) animation.getAnimatedValue();
@@ -94,14 +115,16 @@
             });
             heightAnimator.start();
 
+            // Animate width
             ValueAnimator widthAnimator = ValueAnimator.ofInt(mContentWidth, contentWidth);
             widthAnimator.addUpdateListener((animation) -> {
                 mContentWidth = (int) animation.getAnimatedValue();
             });
 
+            // Play together
             AnimatorSet as = new AnimatorSet();
-            as.setDuration(AuthDialog.ANIMATE_DURATION_MS);
-            as.play(heightAnimator).with(widthAnimator);
+            as.setDuration(animateDurationMs);
+            as.playTogether(cornerAnimator, heightAnimator, widthAnimator, marginAnimator);
             as.start();
         } else {
             mContentWidth = contentWidth;
@@ -123,7 +146,7 @@
         mPanelView = panelView;
         mCornerRadius = context.getResources()
                 .getDimension(R.dimen.biometric_dialog_corner_size);
-        mBiometricMargin = (int) context.getResources()
+        mMargin = (int) context.getResources()
                 .getDimension(R.dimen.biometric_dialog_border_padding);
         mPanelView.setOutlineProvider(this);
         mPanelView.setClipToOutline(true);
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 fa3006f..d110940 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
@@ -364,7 +364,12 @@
         }
 
         @Override
-        public int getAnimationDuration() {
+        public int getMediumToLargeAnimationDurationMs() {
+            return 0;
+        }
+
+        @Override
+        public int getAnimateCredentialStartDelayMs() {
             return 0;
         }
     }
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 d4fc3f8..838a931 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 
@@ -54,7 +55,7 @@
     public void testActionAuthenticated_sendsDismissedAuthenticated() {
         mAuthContainer.mBiometricCallback.onAction(
                 AuthBiometricView.Callback.ACTION_AUTHENTICATED);
-        verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_AUTHENTICATED));
+        verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED));
     }
 
     @Test
@@ -85,6 +86,17 @@
         verify(mCallback).onDismissed(AuthDialogCallback.DISMISSED_ERROR);
     }
 
+    @Test
+    public void testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
+        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());
+    }
+
     private class TestableAuthContainer extends AuthContainerView {
         TestableAuthContainer(AuthContainerView.Config config) {
             super(config);
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 eb7be4f..8ee96bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -117,14 +117,15 @@
     public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception {
         showDialog(BiometricPrompt.TYPE_FACE);
         mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
-        verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
+        verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
     }
 
     @Test
     public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception {
         showDialog(BiometricPrompt.TYPE_FACE);
-        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_AUTHENTICATED);
-        verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
+        mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
+        verify(mReceiver).onDialogDismissed(
+                BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
     }
 
     @Test
@@ -135,12 +136,20 @@
     }
 
     @Test
-    public void testSendsReasonDismissedBySystemServer_whenDismissedByServer() throws Exception {
+    public void testSendsReasonServerRequested_whenDismissedByServer() throws Exception {
         showDialog(BiometricPrompt.TYPE_FACE);
         mBiometricDialogImpl.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);
+        verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED);
+    }
+
     // Statusbar tests
 
     @Test