6/n: Add fingerprint support to the refactored UI

Bug: 123378871

Test: atest com.android.systemui.biometrics
Test: manual test of fingerprint auth

Change-Id: Iac308557d5715c2450a2486d84a5a8292e4d3e42
diff --git a/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
new file mode 100644
index 0000000..ce53e27
--- /dev/null
+++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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.AuthBiometricFingerprintView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contents"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <include layout="@layout/auth_biometric_contents"/>
+
+</com.android.systemui.biometrics.AuthBiometricFingerprintView>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
index a68b61e..52a13cc 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
@@ -163,6 +163,11 @@
     }
 
     @Override
+    protected boolean supportsSmallDialog() {
+        return true;
+    }
+
+    @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mIconController = new IconController(mContext, mIconView, mIndicatorView);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.java
new file mode 100644
index 0000000..176e9e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.java
@@ -0,0 +1,147 @@
+/*
+ * 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.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.systemui.R;
+
+public class AuthBiometricFingerprintView extends AuthBiometricView {
+
+    private static final String TAG = "BiometricPrompt/AuthBiometricFingerprintView";
+
+    public AuthBiometricFingerprintView(Context context) {
+        this(context, null);
+    }
+
+    public AuthBiometricFingerprintView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected int getDelayAfterAuthenticatedDurationMs() {
+        return 0;
+    }
+
+    @Override
+    protected int getStateForAfterError() {
+        return STATE_AUTHENTICATING;
+    }
+
+    @Override
+    protected void handleResetAfterError() {
+        showTouchSensorString();
+    }
+
+    @Override
+    protected void handleResetAfterHelp() {
+        showTouchSensorString();
+    }
+
+    @Override
+    protected boolean supportsSmallDialog() {
+        return false;
+    }
+
+    @Override
+    public void updateState(@BiometricState int newState) {
+        updateIcon(mState, newState);
+
+        // Do this last since the state variable gets updated.
+        super.updateState(newState);
+    }
+
+    @Override
+    void onAttachedToWindowInternal() {
+        super.onAttachedToWindowInternal();
+        showTouchSensorString();
+    }
+
+    private void showTouchSensorString() {
+        mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor);
+        mIndicatorView.setTextColor(R.color.biometric_dialog_gray);
+    }
+
+    private void updateIcon(int lastState, int newState) {
+        final Drawable icon = getAnimationForTransition(lastState, newState);
+        if (icon == null) {
+            Log.e(TAG, "Animation not found, " + lastState + " -> " + newState);
+            return;
+        }
+
+        final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
+                ? (AnimatedVectorDrawable) icon
+                : null;
+
+        mIconView.setImageDrawable(icon);
+
+        if (animation != null && shouldAnimateForTransition(lastState, newState)) {
+            animation.forceAnimationOnUI();
+            animation.start();
+        }
+    }
+
+    private boolean shouldAnimateForTransition(int oldState, int newState) {
+        switch (newState) {
+            case STATE_HELP:
+            case STATE_ERROR:
+                return true;
+            case STATE_AUTHENTICATING_ANIMATING_IN:
+            case STATE_AUTHENTICATING:
+                if (oldState == STATE_ERROR || oldState == STATE_HELP) {
+                    return true;
+                } else {
+                    return false;
+                }
+            case STATE_AUTHENTICATED:
+                return false;
+            default:
+                return false;
+        }
+    }
+
+    private Drawable getAnimationForTransition(int oldState, int newState) {
+        int iconRes;
+
+        switch (newState) {
+            case STATE_HELP:
+            case STATE_ERROR:
+                iconRes = R.drawable.fingerprint_dialog_fp_to_error;
+                break;
+            case STATE_AUTHENTICATING_ANIMATING_IN:
+            case STATE_AUTHENTICATING:
+                if (oldState == STATE_ERROR || oldState == STATE_HELP) {
+                    iconRes = R.drawable.fingerprint_dialog_error_to_fp;
+                } else {
+                    iconRes = R.drawable.fingerprint_dialog_fp_to_error;
+                }
+                break;
+            case STATE_AUTHENTICATED:
+                iconRes = R.drawable.fingerprint_dialog_fp_to_error;
+                break;
+            default:
+                return null;
+        }
+
+        return mContext.getDrawable(iconRes);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index df14a17..12902d5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -92,6 +92,7 @@
         int ACTION_USER_CANCELED = 2;
         int ACTION_BUTTON_NEGATIVE = 3;
         int ACTION_BUTTON_TRY_AGAIN = 4;
+        int ACTION_ERROR = 5;
 
         /**
          * When an action has occurred. The caller will only invoke this when the callback should
@@ -136,6 +137,10 @@
         public ImageView getIconView() {
             return mBiometricView.findViewById(R.id.biometric_icon);
         }
+
+        public int getDelayAfterError() {
+            return BiometricPrompt.HIDE_DIALOG_DELAY;
+        }
     }
 
     private final Injector mInjector;
@@ -186,6 +191,11 @@
      */
     protected abstract void handleResetAfterHelp();
 
+    /**
+     * @return true if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}
+     */
+    protected abstract boolean supportsSmallDialog();
+
     private final Runnable mResetErrorRunnable = () -> {
         updateState(getStateForAfterError());
         handleResetAfterError();
@@ -250,7 +260,7 @@
 
     @VisibleForTesting
     void updateSize(@AuthDialog.DialogSize int newSize) {
-        Log.v(TAG, "Current: " + mSize + " New: " + newSize);
+        Log.v(TAG, "Current size: " + mSize + " New size: " + newSize);
         if (newSize == AuthDialog.SIZE_SMALL) {
             mTitleView.setVisibility(View.GONE);
             mSubtitleView.setVisibility(View.GONE);
@@ -406,8 +416,18 @@
         updateState(STATE_ERROR);
     }
 
+    public void onError(String error) {
+        showTemporaryMessage(error, mResetErrorRunnable);
+        updateState(STATE_ERROR);
+
+        mHandler.postDelayed(() -> {
+            mCallback.onAction(Callback.ACTION_ERROR);
+        }, mInjector.getDelayAfterError());
+    }
+
     public void onHelp(String help) {
         if (mSize != AuthDialog.SIZE_MEDIUM) {
+            Log.w(TAG, "Help received in size: " + mSize);
             return;
         }
         showTemporaryMessage(help, mResetHelpRunnable);
@@ -527,15 +547,6 @@
             // Restore positive button state
             mTryAgainButton.setVisibility(
                     mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
-
-            // Restore indicator text state
-            final String indicatorText =
-                    mSavedState.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING);
-            if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING)) {
-                onHelp(indicatorText);
-            } else if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)) {
-                onAuthenticationFailed(indicatorText);
-            }
         }
     }
 
@@ -600,9 +611,20 @@
         if (mIconOriginalY == 0) {
             mIconOriginalY = mIconView.getY();
             if (mSavedState == null) {
-                updateSize(mRequireConfirmation ? AuthDialog.SIZE_MEDIUM : AuthDialog.SIZE_SMALL);
+                updateSize(!mRequireConfirmation && supportsSmallDialog() ? AuthDialog.SIZE_SMALL
+                        : AuthDialog.SIZE_MEDIUM);
             } else {
                 updateSize(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE));
+
+                // Restore indicator text state only after size has been restored
+                final String indicatorText =
+                        mSavedState.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING);
+                if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING)) {
+                    onHelp(indicatorText);
+                } else if (mSavedState.getBoolean(
+                        AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)) {
+                    onAuthenticationFailed(indicatorText);
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index e198060..2f40218 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -21,9 +21,9 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.PixelFormat;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.os.Binder;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -136,7 +136,8 @@
             return this;
         }
 
-        public AuthContainerView build(int modalityMask) { // TODO
+        public AuthContainerView build(int modalityMask) {
+            mConfig.mModalityMask = modalityMask;
             return new AuthContainerView(mConfig);
         }
     }
@@ -158,6 +159,9 @@
                 case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN:
                     mConfig.mCallback.onTryAgainPressed();
                     break;
+                case AuthBiometricView.Callback.ACTION_ERROR:
+                    animateAway(AuthDialogCallback.DISMISSED_ERROR);
+                    break;
                 default:
                     Log.e(TAG, "Unhandled action: " + action);
             }
@@ -181,15 +185,26 @@
         mContainerView = (ViewGroup) factory.inflate(
                 R.layout.auth_container_view, this, false /* attachToRoot */);
 
-        // TODO: Depends on modality
-        mBiometricView = (AuthBiometricFaceView)
-                factory.inflate(R.layout.auth_biometric_face_view, null, false);
-
-        mBackgroundView = mContainerView.findViewById(R.id.background);
-
         mPanelView = mContainerView.findViewById(R.id.panel);
         mPanelController = new AuthPanelController(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;
+            mScrollView = null;
+            return;
+        }
+
+        mBackgroundView = mContainerView.findViewById(R.id.background);
+
         mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
         mBiometricView.setPanelController(mPanelController);
         mBiometricView.setBiometricPromptBundle(config.mBiometricPromptBundle);
@@ -281,13 +296,13 @@
         if (animate) {
             animateAway(false /* sendReason */, 0 /* reason */);
         } else {
-            mWindowManager.removeView(this);
+            removeWindowIfAttached();
         }
     }
 
     @Override
     public void dismissFromSystemServer() {
-        mWindowManager.removeView(this);
+        removeWindowIfAttached();
     }
 
     @Override
@@ -307,7 +322,7 @@
 
     @Override
     public void onError(String error) {
-
+        mBiometricView.onError(error);
     }
 
     @Override
@@ -340,7 +355,7 @@
 
         final Runnable endActionRunnable = () -> {
             setVisibility(View.INVISIBLE);
-            mWindowManager.removeView(this);
+            removeWindowIfAttached();
             if (sendReason) {
                 mConfig.mCallback.onDismissed(reason);
             }
@@ -369,6 +384,14 @@
         });
     }
 
+    private void removeWindowIfAttached() {
+        if (mContainerState == STATE_GONE) {
+            return;
+        }
+        mContainerState = STATE_GONE;
+        mWindowManager.removeView(this);
+    }
+
     private void onDialogAnimatedIn() {
         if (mContainerState == STATE_PENDING_DISMISS) {
             Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
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 fc18707..7a09137 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
@@ -164,6 +164,17 @@
     }
 
     @Test
+    public void testError_sendsActionError() {
+        initDialog(mContext, mCallback, new MockInjector());
+        final String testError = "testError";
+        mBiometricView.onError(testError);
+        waitForIdleSync();
+
+        verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_ERROR);
+        assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
+    }
+
+    @Test
     public void testBackgroundClicked_sendsActionUserCanceled() {
         initDialog(mContext, mCallback, new MockInjector());
 
@@ -255,7 +266,9 @@
         assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
         assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
         assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
-        assertEquals(failureMessage, mBiometricView.mIndicatorView.getText());
+
+        // TODO: Test restored text. Currently cannot test this, since it gets restored only after
+        // dialog size is known.
     }
 
     private Bundle buildBiometricPromptBundle() {
@@ -320,6 +333,11 @@
         public ImageView getIconView() {
             return mIconView;
         }
+
+        @Override
+        public int getDelayAfterError() {
+            return 0; // Keep this at 0 for tests to invoke callback immediately.
+        }
     }
 
     private class TestableBiometricView extends AuthBiometricView {
@@ -347,5 +365,10 @@
         protected void handleResetAfterHelp() {
 
         }
+
+        @Override
+        protected boolean supportsSmallDialog() {
+            return false;
+        }
     }
 }
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 25e27ef..d4fc3f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
@@ -78,6 +78,13 @@
         verify(mCallback).onTryAgainPressed();
     }
 
+    @Test
+    public void testActionError_sendsDismissedError() {
+        mAuthContainer.mBiometricCallback.onAction(
+                AuthBiometricView.Callback.ACTION_ERROR);
+        verify(mCallback).onDismissed(AuthDialogCallback.DISMISSED_ERROR);
+    }
+
     private class TestableAuthContainer extends AuthContainerView {
         TestableAuthContainer(AuthContainerView.Config config) {
             super(config);