Merge "Fallback to fingerprint on all error types." into sc-dev

This commit is contained in:
Joe Bolinger
2021-06-09 00:20:47 +00:00
committed by Android (Google) Code Review
5 changed files with 94 additions and 63 deletions

View File

@@ -130,19 +130,29 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
@Override @Override
public void onAuthenticationFailed( public void onAuthenticationFailed(
@Modality int modality, @Nullable String failureReason) { @Modality int modality, @Nullable String failureReason) {
if (modality == TYPE_FACE && mActiveSensorType == TYPE_FACE) { super.onAuthenticationFailed(modality, checkErrorForFallback(failureReason));
// switching from face -> fingerprint mode, suppress soft error messages }
failureReason = mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead);
@Override
public void onError(int modality, String error) {
super.onError(modality, checkErrorForFallback(error));
}
private String checkErrorForFallback(String message) {
if (mActiveSensorType == TYPE_FACE) {
Log.d(TAG, "Falling back to fingerprint: " + message);
// switching from face -> fingerprint mode, suppress root error messages
mCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR);
return mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead);
} }
super.onAuthenticationFailed(modality, failureReason); return message;
} }
@Override @Override
@BiometricState @BiometricState
protected int getStateForAfterError() { protected int getStateForAfterError() {
if (mActiveSensorType == TYPE_FACE) { if (mActiveSensorType == TYPE_FACE) {
mHandler.post(() -> mCallback.onAction(
Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
return STATE_AUTHENTICATING; return STATE_AUTHENTICATING;
} }

View File

@@ -22,6 +22,8 @@ import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
@@ -124,16 +126,50 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
} }
@Test @Test
public void testModeUpdated_whenSwitchToFingerprint() { public void testModeUpdated_onSoftError_whenSwitchToFingerprint() {
mFaceToFpView.onDialogAnimatedIn(); mFaceToFpView.onDialogAnimatedIn();
mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face"); mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face");
waitForIdleSync(); waitForIdleSync();
verify(mIndicatorView).setText( verify(mIndicatorView).setText(
eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead))); eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
verify(mCallback).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING, mFaceToFpView.mState); assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING, mFaceToFpView.mState);
} }
@Test
public void testModeUpdated_onHardError_whenSwitchToFingerprint() {
mFaceToFpView.onDialogAnimatedIn();
mFaceToFpView.onError(TYPE_FACE, "oh no!");
waitForIdleSync();
verify(mIndicatorView).setText(
eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
verify(mCallback).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING, mFaceToFpView.mState);
}
@Test
public void testFingerprintOnlyStartsOnFirstError() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
mFaceToFpView.onDialogAnimatedIn();
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR);
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING);
reset(mCallback);
mFaceToFpView.onError(TYPE_FACE, "oh no!");
mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face");
verify(mCallback, never()).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
}
public class TestableView extends AuthBiometricFaceToFingerprintView { public class TestableView extends AuthBiometricFaceToFingerprintView {
public TestableView(Context context) { public TestableView(Context context) {
super(context, null, new MockInjector()); super(context, null, new MockInjector());

View File

@@ -355,10 +355,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
} }
private void cancelAllFingerprintSensors() {
cancelAllSensors(sensor -> sensor.modality == TYPE_FINGERPRINT);
}
private void cancelAllSensors() { private void cancelAllSensors() {
cancelAllSensors(sensor -> true); cancelAllSensors(sensor -> true);
} }
@@ -387,6 +383,9 @@ public final class AuthSession implements IBinder.DeathRecipient {
*/ */
boolean onErrorReceived(int sensorId, int cookie, @BiometricConstants.Errors int error, boolean onErrorReceived(int sensorId, int cookie, @BiometricConstants.Errors int error,
int vendorCode) throws RemoteException { int vendorCode) throws RemoteException {
if (DEBUG) {
Slog.v(TAG, "onErrorReceived sensor: " + sensorId + " error: " + error);
}
if (!containsCookie(cookie)) { if (!containsCookie(cookie)) {
Slog.e(TAG, "Unknown/expired cookie: " + cookie); Slog.e(TAG, "Unknown/expired cookie: " + cookie);
@@ -454,12 +453,14 @@ public final class AuthSession implements IBinder.DeathRecipient {
// a round trip to SystemUI. // a round trip to SystemUI.
mClientReceiver.onError(modality, error, vendorCode); mClientReceiver.onError(modality, error, vendorCode);
return true; return true;
} else if (shouldErrorTriggerMultiSensorTransition()) {
// wait for the UI to signal when modality should switch
mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING;
Slog.d(TAG, "onErrorReceived: waiting for modality switch callback");
} else { } else {
mState = STATE_ERROR_PENDING_SYSUI; mState = STATE_ERROR_PENDING_SYSUI;
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT
&& mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING) {
// wait for the UI to signal when modality should switch
Slog.d(TAG, "onErrorReceived: waiting for modality switch callback");
mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING;
}
mStatusBarService.onBiometricError(modality, error, vendorCode); mStatusBarService.onBiometricError(modality, error, vendorCode);
} }
break; break;
@@ -627,7 +628,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
void onDeviceCredentialPressed() { void onDeviceCredentialPressed() {
// Cancel authentication. Skip the token/package check since we are cancelling // Cancel authentication. Skip the token/package check since we are cancelling
// from system server. The interface is permission protected so this is fine. // from system server. The interface is permission protected so this is fine.
cancelBiometricOnly(); cancelAllSensors();
mState = STATE_SHOWING_DEVICE_CREDENTIAL; mState = STATE_SHOWING_DEVICE_CREDENTIAL;
} }
@@ -733,12 +734,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
mClientReceiver.onAuthenticationSucceeded( mClientReceiver.onAuthenticationSucceeded(
Utils.getAuthenticationTypeForResult(reason)); Utils.getAuthenticationTypeForResult(reason));
cancelBiometricOnly();
break; break;
case BiometricPrompt.DISMISSED_REASON_NEGATIVE: case BiometricPrompt.DISMISSED_REASON_NEGATIVE:
mClientReceiver.onDialogDismissed(reason); mClientReceiver.onDialogDismissed(reason);
cancelBiometricOnly();
break; break;
case BiometricPrompt.DISMISSED_REASON_USER_CANCEL: case BiometricPrompt.DISMISSED_REASON_USER_CANCEL:
@@ -747,7 +746,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED, BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED,
0 /* vendorCode */ 0 /* vendorCode */
); );
cancelBiometricOnly();
break; break;
case BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED: case BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED:
@@ -765,6 +763,9 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
} catch (RemoteException e) { } catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e); Slog.e(TAG, "Remote exception", e);
} finally {
// ensure everything is cleaned up when dismissed
cancelAllSensors();
} }
} }
@@ -780,8 +781,8 @@ public final class AuthSession implements IBinder.DeathRecipient {
|| mState == STATE_AUTH_STARTED || mState == STATE_AUTH_STARTED
|| mState == STATE_AUTH_STARTED_UI_SHOWING; || mState == STATE_AUTH_STARTED_UI_SHOWING;
cancelAllSensors();
if (authStarted && !force) { if (authStarted && !force) {
cancelAllSensors();
// Wait for ERROR_CANCELED to be returned from the sensors // Wait for ERROR_CANCELED to be returned from the sensors
return false; return false;
} else { } else {
@@ -804,22 +805,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
return false; return false;
} }
/**
* Cancels biometric authentication only. AuthSession may either be going into
* {@link #STATE_SHOWING_DEVICE_CREDENTIAL} or dismissed.
*/
private void cancelBiometricOnly() {
if (mState == STATE_AUTH_STARTED || mState == STATE_AUTH_STARTED_UI_SHOWING) {
cancelAllSensors();
} else if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
cancelAllFingerprintSensors();
} else {
if (DEBUG) {
Slog.v(TAG, "nothing to cancel - wrong state: " + mState);
}
}
}
boolean isCrypto() { boolean isCrypto() {
return mOperationId != 0; return mOperationId != 0;
} }
@@ -896,13 +881,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
} }
private boolean shouldErrorTriggerMultiSensorTransition() {
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
return mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING;
}
return false;
}
@BiometricMultiSensorMode @BiometricMultiSensorMode
private static int getMultiSensorModeForNewSession(Collection<BiometricSensor> sensors) { private static int getMultiSensorModeForNewSession(Collection<BiometricSensor> sensors) {
boolean hasFace = false; boolean hasFace = false;

View File

@@ -18,6 +18,7 @@ package com.android.server.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE;
import static com.android.server.biometrics.BiometricServiceStateProto.*; import static com.android.server.biometrics.BiometricServiceStateProto.*;
@@ -38,7 +39,6 @@ import android.annotation.NonNull;
import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustManager; import android.app.trust.ITrustManager;
import android.content.Context; import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.BiometricManager.Authenticators;
import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.ComponentInfoInternal;
import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricAuthenticator;
@@ -67,6 +67,7 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.function.Consumer;
@Presubmit @Presubmit
@SmallTest @SmallTest
@@ -245,9 +246,6 @@ public class AuthSessionTest {
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, assertEquals(BiometricSensor.STATE_COOKIE_RETURNED,
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState()); session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
session.onErrorReceived(fingerprintSensorId,
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getCookie(),
BiometricConstants.BIOMETRIC_ERROR_VENDOR, 0 /* vendorCode */);
session.onStartFingerprint(); session.onStartFingerprint();
} }
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
@@ -258,6 +256,21 @@ public class AuthSessionTest {
@Test @Test
public void testCancelAuthentication_whenStateAuthCalled_invokesCancel() public void testCancelAuthentication_whenStateAuthCalled_invokesCancel()
throws RemoteException { throws RemoteException {
testInvokesCancel(session -> session.onCancelAuthSession(false /* force */));
}
@Test
public void testCancelAuthentication_whenStateAuthForcedCalled_invokesCancel()
throws RemoteException {
testInvokesCancel(session -> session.onCancelAuthSession(true /* force */));
}
@Test
public void testCancelAuthentication_whenDialogDismissed() throws RemoteException {
testInvokesCancel(session -> session.onDialogDismissed(DISMISSED_REASON_NEGATIVE, null));
}
private void testInvokesCancel(Consumer<AuthSession> sessionConsumer) throws RemoteException {
final IBiometricAuthenticator faceAuthenticator = mock(IBiometricAuthenticator.class); final IBiometricAuthenticator faceAuthenticator = mock(IBiometricAuthenticator.class);
setupFace(0 /* id */, false /* confirmationAlwaysRequired */, faceAuthenticator); setupFace(0 /* id */, false /* confirmationAlwaysRequired */, faceAuthenticator);
@@ -269,7 +282,8 @@ public class AuthSessionTest {
session.goToInitialState(); session.goToInitialState();
assertEquals(STATE_AUTH_CALLED, session.getState()); assertEquals(STATE_AUTH_CALLED, session.getState());
session.onCancelAuthSession(false /* force */);
sessionConsumer.accept(session);
verify(faceAuthenticator).cancelAuthenticationFromService(eq(mToken), eq(TEST_PACKAGE)); verify(faceAuthenticator).cancelAuthenticationFromService(eq(mToken), eq(TEST_PACKAGE));
} }

View File

@@ -1036,7 +1036,7 @@ public class BiometricServiceTest {
} }
@Test @Test
public void testDismissedReasonNegative_whilePaused_doesntInvokeHalCancel() throws Exception { public void testDismissedReasonNegative_whilePaused_invokeHalCancel() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
@@ -1050,14 +1050,12 @@ public class BiometricServiceTest {
BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
verify(mBiometricService.mSensors.get(0).impl, verify(mBiometricService.mSensors.get(0).impl)
never()).cancelAuthenticationFromService( .cancelAuthenticationFromService(any(), any());
any(),
any());
} }
@Test @Test
public void testDismissedReasonUserCancel_whilePaused_doesntInvokeHalCancel() throws public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws
Exception { Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
@@ -1072,10 +1070,8 @@ public class BiometricServiceTest {
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
verify(mBiometricService.mSensors.get(0).impl, verify(mBiometricService.mSensors.get(0).impl)
never()).cancelAuthenticationFromService( .cancelAuthenticationFromService(any(), any());
any(),
any());
} }
@Test @Test
@@ -1091,11 +1087,8 @@ public class BiometricServiceTest {
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
// doesn't send cancel to HAL verify(mBiometricService.mSensors.get(0).impl)
verify(mBiometricService.mSensors.get(0).impl, .cancelAuthenticationFromService(any(), any());
never()).cancelAuthenticationFromService(
any(),
any());
verify(mReceiver1).onError( verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FACE), eq(BiometricAuthenticator.TYPE_FACE),
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),