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);