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:
@@ -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>
|
||||
@@ -162,6 +162,11 @@ public class AuthBiometricFaceView extends AuthBiometricView {
|
||||
resetErrorView(mContext, mIndicatorView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsSmallDialog() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user