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 f97cb8a67d811..f732a147e4b71 100644 --- a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java +++ b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java @@ -268,11 +268,18 @@ public class CoexCoordinator { AuthenticationClient udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null); AuthenticationClient face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null); if (isCurrentFaceAuth(client)) { - // UDFPS should still be running in this case, do not vibrate. However, we - // should notify the callback and finish the client, so that Keyguard and - // BiometricScheduler do not get stuck. - Slog.d(TAG, "Face rejected in multi-sensor auth, udfps: " + udfps); - callback.handleLifecycleAfterAuth(); + if (isUdfpsActivelyAuthing(udfps)) { + // UDFPS should still be running in this case, do not vibrate. However, we + // should notify the callback and finish the client, so that Keyguard and + // BiometricScheduler do not get stuck. + Slog.d(TAG, "Face rejected in multi-sensor auth, udfps: " + udfps); + callback.handleLifecycleAfterAuth(); + } else { + // UDFPS is not actively authenticating (finger not touching, already + // rejected, etc). + callback.sendHapticFeedback(); + callback.handleLifecycleAfterAuth(); + } } else if (isCurrentUdfps(client)) { // Face should either be running, or have already finished SuccessfulAuth auth = popSuccessfulFaceAuthIfExists(currentTimeMillis); 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 fb05825a122b3..a169ebd0320f7 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 @@ -258,6 +258,74 @@ public class CoexCoordinatorTest { verify(mCallback).handleLifecycleAfterAuth(); } + @Test + public void testKeyguard_faceRejectedWhenUdfpsTouching_thenUdfpsRejected() { + mCoexCoordinator.reset(); + + AuthenticationClient faceClient = mock(AuthenticationClient.class); + when(faceClient.isKeyguard()).thenReturn(true); + when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + + AuthenticationClient udfpsClient = mock(AuthenticationClient.class, + withSettings().extraInterfaces(Udfps.class)); + when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(udfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true); + + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); + + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient, + LockoutTracker.LOCKOUT_NONE, mCallback); + verify(mCallback, never()).sendHapticFeedback(); + verify(mCallback).handleLifecycleAfterAuth(); + + // BiometricScheduler removes the face authentication client after rejection + mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, faceClient); + + // Then UDFPS rejected + CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class); + mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, udfpsClient, + LockoutTracker.LOCKOUT_NONE, udfpsCallback); + verify(udfpsCallback).sendHapticFeedback(); + verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); + verify(mCallback, never()).sendHapticFeedback(); + } + + @Test + public void testKeyguard_udfpsRejected_thenFaceRejected() { + mCoexCoordinator.reset(); + + AuthenticationClient faceClient = mock(AuthenticationClient.class); + when(faceClient.isKeyguard()).thenReturn(true); + when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + + AuthenticationClient udfpsClient = mock(AuthenticationClient.class, + withSettings().extraInterfaces(Udfps.class)); + when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(udfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true); + + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); + + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, udfpsClient, + LockoutTracker.LOCKOUT_NONE, mCallback); + // Client becomes paused, but finger does not necessarily lift, since we suppress the haptic + when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED_PAUSED); + verify(mCallback, never()).sendHapticFeedback(); + verify(mCallback).handleLifecycleAfterAuth(); + + // Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since + // unlike face, its lifecycle becomes "paused" instead of "finished". + CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class); + mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, faceClient, + LockoutTracker.LOCKOUT_NONE, faceCallback); + verify(faceCallback).sendHapticFeedback(); + verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); + verify(mCallback, never()).sendHapticFeedback(); + } + @Test public void testNonKeyguard_rejectAndNotLockedOut() { mCoexCoordinator.reset();