14/n: Animate to device credential UI when lockout occurs

Test: manual test with BiometricPromptDemo
Test: atest com.android.systemui.biometrics
Test: atest BiometricServiceTest

Change-Id: I83547c28f1b164dc772b01861430f0d479e47a75
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 18d2747..e08707d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -584,11 +584,7 @@
                 mCallback.onAction(Callback.ACTION_USER_CANCELED);
             } else {
                 if (isDeviceCredentialAllowed()) {
-                    updateSize(AuthDialog.SIZE_LARGE);
-                    mHandler.postDelayed(() -> {
-                        mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
-                    }, mInjector.getAnimateCredentialStartDelayMs());
-
+                    startTransitionToCredentialUI();
                 } else {
                     mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
                 }
@@ -607,6 +603,16 @@
         });
     }
 
+    /**
+     * Kicks off the animation process and invokes the callback.
+     */
+    void startTransitionToCredentialUI() {
+        updateSize(AuthDialog.SIZE_LARGE);
+        mHandler.postDelayed(() -> {
+            mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
+        }, mInjector.getAnimateCredentialStartDelayMs());
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index a0ce67d..1a3189b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -82,7 +82,7 @@
     private final CredentialCallback mCredentialCallback;
 
     @VisibleForTesting final FrameLayout mFrameLayout;
-    private final AuthBiometricView mBiometricView;
+    @VisibleForTesting AuthBiometricView mBiometricView;
     @VisibleForTesting AuthCredentialView mCredentialView;
 
     private final ImageView mBackgroundView;
@@ -275,6 +275,12 @@
         requestFocus();
     }
 
+    @Override
+    public boolean isAllowDeviceCredentials() {
+        return mConfig.mBiometricPromptBundle
+                .getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
+    }
+
     private void addBiometricView() {
         mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
         mBiometricView.setPanelController(mPanelController);
@@ -429,6 +435,11 @@
         return mConfig.mOpPackageName;
     }
 
+    @Override
+    public void animateToCredentialUI() {
+        mBiometricView.startTransitionToCredentialUI();
+    }
+
     @VisibleForTesting
     void animateAway(int reason) {
         animateAway(true /* sendReason */, reason);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 1e8c26d..74e170d 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.BiometricConstants;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.os.Bundle;
@@ -242,9 +243,16 @@
     }
 
     @Override
-    public void onBiometricError(String error) {
-        if (DEBUG) Log.d(TAG, "onBiometricError: " + error);
-        mCurrentDialog.onError(error);
+    public void onBiometricError(int errorCode, String error) {
+        if (DEBUG) Log.d(TAG, "onBiometricError: " + errorCode + ", " + error);
+
+        final boolean isLockout = errorCode == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT
+                || errorCode == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
+        if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) {
+            mCurrentDialog.animateToCredentialUI();
+        } else {
+            mCurrentDialog.onError(error);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index 461f237..ca95f9d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -124,4 +124,14 @@
      * Get the client's package name
      */
     String getOpPackageName();
+
+    /**
+     * Animate to credential UI. Typically called after biometric is locked out.
+     */
+    void animateToCredentialUI();
+
+    /**
+     * @return true if device credential is allowed.
+     */
+    boolean isAllowDeviceCredentials();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 134d4b8..39b65c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -274,7 +274,7 @@
                 int type, boolean requireConfirmation, int userId, String opPackageName) { }
         default void onBiometricAuthenticated(boolean authenticated, String failureReason) { }
         default void onBiometricHelp(String message) { }
-        default void onBiometricError(String error) { }
+        default void onBiometricError(int errorCode, String error) { }
         default void hideBiometricDialog() { }
 
         /**
@@ -773,9 +773,9 @@
     }
 
     @Override
-    public void onBiometricError(String error) {
+    public void onBiometricError(int errorCode, String error) {
         synchronized (mLock) {
-            mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget();
+            mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, errorCode, 0, error).sendToTarget();
         }
     }
 
@@ -1060,7 +1060,7 @@
                     break;
                 case MSG_BIOMETRIC_ERROR:
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).onBiometricError((String) msg.obj);
+                        mCallbacks.get(i).onBiometricError(msg.arg1, (String) msg.obj);
                     }
                     break;
                 case MSG_BIOMETRIC_HIDE:
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 838a931..aeceba7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
@@ -18,8 +18,10 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
@@ -48,6 +50,7 @@
         AuthContainerView.Config config = new AuthContainerView.Config();
         config.mContext = mContext;
         config.mCallback = mCallback;
+        config.mModalityMask |= BiometricAuthenticator.TYPE_FINGERPRINT;
         mAuthContainer = new TestableAuthContainer(config);
     }
 
@@ -97,6 +100,13 @@
         assertEquals(mAuthContainer.mFrameLayout, mAuthContainer.mCredentialView.getParent());
     }
 
+    @Test
+    public void testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() {
+        mAuthContainer.mBiometricView = mock(AuthBiometricView.class);
+        mAuthContainer.animateToCredentialUI();
+        verify(mAuthContainer.mBiometricView).startTransitionToCredentialUI();
+    }
+
     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 360183d..b8f735e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -22,8 +22,10 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -33,6 +35,7 @@
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.os.Bundle;
@@ -193,8 +196,9 @@
     @Test
     public void testOnErrorInvoked_whenSystemRequested() throws Exception {
         showDialog(BiometricPrompt.TYPE_FACE);
+        final int error = 1;
         final String errMessage = "error message";
-        mBiometricDialogImpl.onBiometricError(errMessage);
+        mBiometricDialogImpl.onBiometricError(error, errMessage);
 
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
         verify(mDialog1).onError(captor.capture());
@@ -203,6 +207,59 @@
     }
 
     @Test
+    public void testErrorLockout_whenCredentialAllowed_AnimatesToCredentialUI() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT;
+        final String errorString = "lockout";
+
+        when(mDialog1.isAllowDeviceCredentials()).thenReturn(true);
+
+        mBiometricDialogImpl.onBiometricError(error, errorString);
+        verify(mDialog1, never()).onError(anyString());
+        verify(mDialog1).animateToCredentialUI();
+    }
+
+    @Test
+    public void testErrorLockoutPermanent_whenCredentialAllowed_AnimatesToCredentialUI()
+            throws Exception {
+        showDialog(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);
+        verify(mDialog1, never()).onError(anyString());
+        verify(mDialog1).animateToCredentialUI();
+    }
+
+    @Test
+    public void testErrorLockout_whenCredentialNotAllowed_sendsOnError() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT;
+        final String errorString = "lockout";
+
+        when(mDialog1.isAllowDeviceCredentials()).thenReturn(false);
+
+        mBiometricDialogImpl.onBiometricError(error, errorString);
+        verify(mDialog1).onError(eq(errorString));
+        verify(mDialog1, never()).animateToCredentialUI();
+    }
+
+    @Test
+    public void testErrorLockoutPermanent_whenCredentialNotAllowed_sendsOnError() throws Exception {
+        showDialog(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);
+        verify(mDialog1).onError(eq(errorString));
+        verify(mDialog1, never()).animateToCredentialUI();
+    }
+
+    @Test
     public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception {
         showDialog(BiometricPrompt.TYPE_FACE);
         mBiometricDialogImpl.hideBiometricDialog();
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 b252a0d..fcd0e23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -394,10 +394,11 @@
 
     @Test
     public void testOnBiometricError() {
+        final int errorCode = 1;
         String errorMessage = "test_error_message";
-        mCommandQueue.onBiometricError(errorMessage);
+        mCommandQueue.onBiometricError(errorCode, errorMessage);
         waitForIdleSync();
-        verify(mCallbacks).onBiometricError(eq(errorMessage));
+        verify(mCallbacks).onBiometricError(eq(errorCode), eq(errorMessage));
     }
 
     @Test