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
This commit is contained in:
Kevin Chyn
2019-09-05 18:17:32 -07:00
parent ded4f36e72
commit 889de4c680
7 changed files with 277 additions and 24 deletions

View File

@@ -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>

View File

@@ -162,6 +162,11 @@ public class AuthBiometricFaceView extends AuthBiometricView {
resetErrorView(mContext, mIndicatorView);
}
@Override
protected boolean supportsSmallDialog() {
return true;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();

View File

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

View File

@@ -92,6 +92,7 @@ public abstract class AuthBiometricView extends LinearLayout {
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 abstract class AuthBiometricView extends LinearLayout {
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 @@ public abstract class AuthBiometricView extends LinearLayout {
*/
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 @@ public abstract class AuthBiometricView extends LinearLayout {
@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 @@ public abstract class AuthBiometricView extends LinearLayout {
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 @@ public abstract class AuthBiometricView extends LinearLayout {
// 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 @@ public abstract class AuthBiometricView extends LinearLayout {
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);
}
}
}
}

View File

@@ -21,9 +21,9 @@ import android.annotation.NonNull;
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 @@ public class AuthContainerView extends LinearLayout
return this;
}
public AuthContainerView build(int modalityMask) { // TODO
public AuthContainerView build(int modalityMask) {
mConfig.mModalityMask = modalityMask;
return new AuthContainerView(mConfig);
}
}
@@ -158,6 +159,9 @@ public class AuthContainerView extends LinearLayout
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 @@ public class AuthContainerView extends LinearLayout
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 @@ public class AuthContainerView extends LinearLayout
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 @@ public class AuthContainerView extends LinearLayout
@Override
public void onError(String error) {
mBiometricView.onError(error);
}
@Override
@@ -340,7 +355,7 @@ public class AuthContainerView extends LinearLayout
final Runnable endActionRunnable = () -> {
setVisibility(View.INVISIBLE);
mWindowManager.removeView(this);
removeWindowIfAttached();
if (sendReason) {
mConfig.mCallback.onDismissed(reason);
}
@@ -369,6 +384,14 @@ public class AuthContainerView extends LinearLayout
});
}
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");

View File

@@ -163,6 +163,17 @@ public class AuthBiometricViewTest extends SysuiTestCase {
assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState);
}
@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 @@ public class AuthBiometricViewTest extends SysuiTestCase {
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 class AuthBiometricViewTest extends SysuiTestCase {
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 @@ public class AuthBiometricViewTest extends SysuiTestCase {
protected void handleResetAfterHelp() {
}
@Override
protected boolean supportsSmallDialog() {
return false;
}
}
}

View File

@@ -78,6 +78,13 @@ public class AuthContainerViewTest extends SysuiTestCase {
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);