Merge "Update error haptics for coex" into sc-dev

This commit is contained in:
Kevin Chyn
2021-08-04 02:13:03 +00:00
committed by Android (Google) Code Review
8 changed files with 192 additions and 44 deletions

View File

@@ -82,7 +82,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
private long mStartTimeMs;
protected boolean mAuthAttempted;
private boolean mAuthAttempted;
// TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update
// the state. We should think of a way to improve this in the future.
@@ -98,6 +98,12 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
*/
protected abstract void handleLifecycleAfterAuth(boolean authenticated);
/**
* @return true if a user was detected (i.e. face was found, fingerprint sensor was touched.
* etc)
*/
public abstract boolean wasUserDetected();
public AuthenticationClient(@NonNull Context context, @NonNull LazyDaemon<T> lazyDaemon,
@NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener,
int targetUserId, long operationId, boolean restricted, @NonNull String owner,
@@ -381,9 +387,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
}
@Override
public void onError(int errorCode, int vendorCode) {
public void onError(@BiometricConstants.Errors int errorCode, int vendorCode) {
super.onError(errorCode, vendorCode);
mState = STATE_STOPPED;
CoexCoordinator.getInstance().onAuthenticationError(this, errorCode, this::vibrateError);
}
/**
@@ -445,4 +453,8 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
public boolean interruptsPrecedingClients() {
return true;
}
public boolean wasAuthAttempted() {
return mAuthAttempted;
}
}

View File

@@ -22,6 +22,7 @@ import static com.android.server.biometrics.sensors.BiometricScheduler.sensorTyp
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.biometrics.BiometricConstants;
import android.os.Handler;
import android.os.Looper;
import android.util.Slog;
@@ -54,7 +55,7 @@ public class CoexCoordinator {
/**
* Callback interface notifying the owner of "results" from the CoexCoordinator's business
* logic.
* logic for accept and reject.
*/
interface Callback {
/**
@@ -80,6 +81,17 @@ public class CoexCoordinator {
void sendAuthenticationCanceled();
}
/**
* Callback interface notifying the owner of "results" from the CoexCoordinator's business
* logic for errors.
*/
interface ErrorCallback {
/**
* Requests the owner to initiate a vibration for this event.
*/
void sendHapticFeedback();
}
private static CoexCoordinator sInstance;
@VisibleForTesting
@@ -203,6 +215,9 @@ public class CoexCoordinator {
mClientMap.remove(sensorType);
}
/**
* Notify the coordinator that authentication succeeded (accepted)
*/
public void onAuthenticationSucceeded(long currentTimeMillis,
@NonNull AuthenticationClient<?> client,
@NonNull Callback callback) {
@@ -273,6 +288,9 @@ public class CoexCoordinator {
}
}
/**
* Notify the coordinator that a rejection has occurred.
*/
public void onAuthenticationRejected(long currentTimeMillis,
@NonNull AuthenticationClient<?> client,
@LockoutTracker.LockoutMode int lockoutMode,
@@ -357,6 +375,54 @@ public class CoexCoordinator {
}
}
/**
* Notify the coordinator that an error has occurred.
*/
public void onAuthenticationError(@NonNull AuthenticationClient<?> client,
@BiometricConstants.Errors int error, @NonNull ErrorCallback callback) {
// Figure out non-coex state
final boolean shouldUsuallyVibrate;
if (isCurrentFaceAuth(client)) {
final boolean notDetectedOnKeyguard = client.isKeyguard() && !client.wasUserDetected();
final boolean authAttempted = client.wasAuthAttempted();
switch (error) {
case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT:
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT:
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
shouldUsuallyVibrate = authAttempted && !notDetectedOnKeyguard;
break;
default:
shouldUsuallyVibrate = false;
break;
}
} else {
shouldUsuallyVibrate = false;
}
// Figure out coex state
final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
final boolean hapticSuppressedByCoex;
if (keyguardAdvancedLogic) {
if (isSingleAuthOnly(client)) {
hapticSuppressedByCoex = false;
} else {
hapticSuppressedByCoex = isCurrentFaceAuth(client)
&& !client.isKeyguardBypassEnabled();
}
} else {
hapticSuppressedByCoex = false;
}
// Combine and send feedback if appropriate
Slog.d(TAG, "shouldUsuallyVibrate: " + shouldUsuallyVibrate
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
if (shouldUsuallyVibrate && !hapticSuppressedByCoex) {
callback.sendHapticFeedback();
}
}
@Nullable
private SuccessfulAuth popSuccessfulFaceAuthIfExists(long currentTimeMillis) {
for (SuccessfulAuth auth : mSuccessfulAuths) {

View File

@@ -127,7 +127,8 @@ class FaceAuthenticationClient extends AuthenticationClient<ISession> implements
}
}
private boolean wasUserDetected() {
@Override
public boolean wasUserDetected() {
// Do not provide haptic feedback if the user was not detected, and an error (usually
// ERROR_TIMEOUT) is received.
return mLastAcquire != FaceManager.FACE_ACQUIRED_NOT_DETECTED
@@ -160,7 +161,7 @@ class FaceAuthenticationClient extends AuthenticationClient<ISession> implements
}
@Override
public void onError(int error, int vendorCode) {
public void onError(@BiometricConstants.Errors int error, int vendorCode) {
mUsageStats.addEvent(new UsageStats.AuthenticationEvent(
getStartTimeMs(),
System.currentTimeMillis() - getStartTimeMs() /* latency */,
@@ -169,25 +170,8 @@ class FaceAuthenticationClient extends AuthenticationClient<ISession> implements
vendorCode,
getTargetUserId()));
switch (error) {
case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT:
if (!wasUserDetected() && !isBiometricPrompt()) {
// No vibration if user was not detected on keyguard
break;
}
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT:
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
if (mAuthAttempted) {
// Only vibrate if auth was attempted. If the user was already locked out prior
// to starting authentication, do not vibrate.
vibrateError();
}
break;
case BiometricConstants.BIOMETRIC_ERROR_RE_ENROLL:
BiometricNotificationUtils.showReEnrollmentNotification(getContext());
break;
default:
break;
if (error == BiometricConstants.BIOMETRIC_ERROR_RE_ENROLL) {
BiometricNotificationUtils.showReEnrollmentNotification(getContext());
}
super.onError(error, vendorCode);

View File

@@ -115,7 +115,8 @@ class FaceAuthenticationClient extends AuthenticationClient<IBiometricsFace> {
}
}
private boolean wasUserDetected() {
@Override
public boolean wasUserDetected() {
// Do not provide haptic feedback if the user was not detected, and an error (usually
// ERROR_TIMEOUT) is received.
return mLastAcquire != FaceManager.FACE_ACQUIRED_NOT_DETECTED
@@ -147,7 +148,7 @@ class FaceAuthenticationClient extends AuthenticationClient<IBiometricsFace> {
}
@Override
public void onError(int error, int vendorCode) {
public void onError(@BiometricConstants.Errors int error, int vendorCode) {
mUsageStats.addEvent(new UsageStats.AuthenticationEvent(
getStartTimeMs(),
System.currentTimeMillis() - getStartTimeMs() /* latency */,
@@ -156,24 +157,6 @@ class FaceAuthenticationClient extends AuthenticationClient<IBiometricsFace> {
vendorCode,
getTargetUserId()));
switch (error) {
case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT:
if (!wasUserDetected() && !isBiometricPrompt()) {
// No vibration if user was not detected on keyguard
break;
}
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT:
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
if (mAuthAttempted) {
// Only vibrate if auth was attempted. If the user was already locked out prior
// to starting authentication, do not vibrate.
vibrateError();
}
break;
default:
break;
}
super.onError(error, vendorCode);
}

View File

@@ -105,6 +105,12 @@ class FingerprintAuthenticationClient extends AuthenticationClient<ISession> imp
}
}
@Override
public boolean wasUserDetected() {
// TODO: Update if it needs to be used for fingerprint, i.e. success/reject, error_timeout
return false;
}
@Override
public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
boolean authenticated, ArrayList<Byte> token) {

View File

@@ -152,6 +152,12 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
}
}
@Override
public boolean wasUserDetected() {
// TODO: Update if it needs to be used for fingerprint, i.e. success/reject, error_timeout
return false;
}
@Override
public @LockoutTracker.LockoutMode int handleFailedAttempt(int userId) {
mLockoutFrameworkImpl.addFailedAttemptForUser(userId);

View File

@@ -378,6 +378,11 @@ public class BiometricSchedulerTest {
protected void handleLifecycleAfterAuth(boolean authenticated) {
}
@Override
public boolean wasUserDetected() {
return false;
}
}
private static class TestAuthenticationClient extends AuthenticationClient<Object> {
@@ -407,6 +412,11 @@ public class BiometricSchedulerTest {
protected void handleLifecycleAfterAuth(boolean authenticated) {
}
@Override
public boolean wasUserDetected() {
return false;
}
}
private static class TestClientMonitor2 extends TestClientMonitor {

View File

@@ -32,6 +32,7 @@ import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
@@ -61,6 +62,8 @@ public class CoexCoordinatorTest {
private Context mContext;
@Mock
private CoexCoordinator.Callback mCallback;
@Mock
private CoexCoordinator.ErrorCallback mErrorCallback;
@Before
public void setUp() {
@@ -490,4 +493,82 @@ public class CoexCoordinatorTest {
verify(callback).handleLifecycleAfterAuth();
verify(successfulAuths).remove(eq(auth));
}
@Test
public void testBiometricPrompt_FaceError() {
mCoexCoordinator.reset();
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isBiometricPrompt()).thenReturn(true);
when(client.wasAuthAttempted()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
mErrorCallback);
verify(mErrorCallback).sendHapticFeedback();
}
@Test
public void testKeyguard_faceAuthOnly_errorWhenBypassEnabled() {
testKeyguard_faceAuthOnly(true /* bypassEnabled */);
}
@Test
public void testKeyguard_faceAuthOnly_errorWhenBypassDisabled() {
testKeyguard_faceAuthOnly(false /* bypassEnabled */);
}
private void testKeyguard_faceAuthOnly(boolean bypassEnabled) {
mCoexCoordinator.reset();
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isKeyguard()).thenReturn(true);
when(client.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(client.wasAuthAttempted()).thenReturn(true);
when(client.wasUserDetected()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
mErrorCallback);
verify(mErrorCallback).sendHapticFeedback();
}
@Test
public void testKeyguard_coex_faceErrorWhenBypassEnabled() {
testKeyguard_coex_faceError(true /* bypassEnabled */);
}
@Test
public void testKeyguard_coex_faceErrorWhenBypassDisabled() {
testKeyguard_coex_faceError(false /* bypassEnabled */);
}
private void testKeyguard_coex_faceError(boolean bypassEnabled) {
mCoexCoordinator.reset();
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(faceClient.wasAuthAttempted()).thenReturn(true);
when(faceClient.wasUserDetected()).thenReturn(true);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.onAuthenticationError(faceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
if (bypassEnabled) {
verify(mErrorCallback).sendHapticFeedback();
} else {
verify(mErrorCallback, never()).sendHapticFeedback();
}
}
}