Update error haptics for coex
Fixes: 193089985 Test: atest com.android.server.biometrics Change-Id: I9daecabd1b5609261584146dd3ecb8f175312813
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user