diff options
8 files changed, 192 insertions, 44 deletions
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 705b3aa12391..6f38ed04cd96 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -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; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java index 6d32fdebe582..25d4a38cd475 100644 --- a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java +++ b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java @@ -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) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java index f7fd8d0972f6..d66a27920f49 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java @@ -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); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java index c33b957223a4..33950af2216f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java @@ -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); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 8835c1e02610..37ee76adeece 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -106,6 +106,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) { super.onAuthenticated(identifier, authenticated, token); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index 83f1480f4611..5060744bb33e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -153,6 +153,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); return super.handleFailedAttempt(userId); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index 8592166aae15..f4d14995f7c7 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -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 { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java index 1263f7b84db1..bfb0be760f85 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java @@ -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(); + } + } } |