diff options
3 files changed, 56 insertions, 12 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 f4327e8a104d..705b3aa12391 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -180,7 +180,8 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T> + ", isBP: " + isBiometricPrompt() + ", listener: " + listener + ", requireConfirmation: " + mRequireConfirmation - + ", user: " + getTargetUserId()); + + ", user: " + getTargetUserId() + + ", clientMonitor: " + toString()); final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId()); if (isCryptoOperation()) { @@ -304,6 +305,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T> public void handleLifecycleAfterAuth() { AuthenticationClient.this.handleLifecycleAfterAuth(true /* authenticated */); } + + @Override + public void sendAuthenticationCanceled() { + sendCancelOnly(listener); + } }); } else { // Allow system-defined limit of number of attempts before giving up @@ -338,10 +344,30 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T> public void handleLifecycleAfterAuth() { AuthenticationClient.this.handleLifecycleAfterAuth(false /* authenticated */); } + + @Override + public void sendAuthenticationCanceled() { + sendCancelOnly(listener); + } }); } } + private void sendCancelOnly(@Nullable ClientMonitorCallbackConverter listener) { + if (listener == null) { + Slog.e(TAG, "Unable to sendAuthenticationCanceled, listener null"); + return; + } + try { + listener.onError(getSensorId(), + getCookie(), + BiometricConstants.BIOMETRIC_ERROR_CANCELED, + 0 /* vendorCode */); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); + } + } + @Override public void onAcquired(int acquiredInfo, int vendorCode) { super.onAcquired(acquiredInfo, vendorCode); 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 a15ecada3cae..6d32fdebe582 100644 --- a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java +++ b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java @@ -73,6 +73,11 @@ public class CoexCoordinator { * from scheduler if auth was successful). */ void handleLifecycleAfterAuth(); + + /** + * Requests the owner to notify the caller that authentication was canceled. + */ + void sendAuthenticationCanceled(); } private static CoexCoordinator sInstance; @@ -356,7 +361,11 @@ public class CoexCoordinator { private SuccessfulAuth popSuccessfulFaceAuthIfExists(long currentTimeMillis) { for (SuccessfulAuth auth : mSuccessfulAuths) { if (currentTimeMillis - auth.mAuthTimestamp >= SUCCESSFUL_AUTH_VALID_DURATION_MS) { - Slog.d(TAG, "Removing stale auth: " + auth); + // TODO(b/193089985): This removes the auth but does not notify the client with + // an appropriate lifecycle event (such as ERROR_CANCELED), and violates the + // API contract. However, this might be OK for now since the validity duration + // is way longer than the time it takes to auth with fingerprint. + Slog.e(TAG, "Removing stale auth: " + auth); mSuccessfulAuths.remove(auth); } else if (auth.mSensorType == SENSOR_TYPE_FACE) { mSuccessfulAuths.remove(auth); @@ -367,9 +376,13 @@ public class CoexCoordinator { } private void removeAndFinishAllFaceFromQueue() { + // Note that these auth are all successful, but have never notified the client (e.g. + // keyguard). To comply with the authentication lifecycle, we must notify the client that + // auth is "done". The safest thing to do is to send ERROR_CANCELED. for (SuccessfulAuth auth : mSuccessfulAuths) { if (auth.mSensorType == SENSOR_TYPE_FACE) { - Slog.d(TAG, "Removing from queue and finishing: " + auth); + Slog.d(TAG, "Removing from queue, canceling, and finishing: " + auth); + auth.mCallback.sendAuthenticationCanceled(); auth.mCallback.handleLifecycleAfterAuth(); mSuccessfulAuths.remove(auth); } 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 7f5f3c2df077..1263f7b84db1 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 @@ -255,13 +255,16 @@ public class CoexCoordinatorTest { mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); + // For easier reading + final CoexCoordinator.Callback faceCallback = mCallback; + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient, - mCallback); - verify(mCallback, never()).sendHapticFeedback(); - verify(mCallback, never()).sendAuthenticationResult(anyBoolean()); + faceCallback); + verify(faceCallback, never()).sendHapticFeedback(); + verify(faceCallback, never()).sendAuthenticationResult(anyBoolean()); // CoexCoordinator requests the system to hold onto this AuthenticationClient until // UDFPS result is known - verify(mCallback, never()).handleLifecycleAfterAuth(); + verify(faceCallback, never()).handleLifecycleAfterAuth(); // Reset the mock CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class); @@ -274,6 +277,8 @@ public class CoexCoordinatorTest { verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */); verify(udfpsCallback).handleLifecycleAfterAuth(); + verify(faceCallback).sendAuthenticationCanceled(); + assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty()); } else { mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, udfpsClient, @@ -281,16 +286,16 @@ public class CoexCoordinatorTest { if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) { verify(udfpsCallback, never()).sendHapticFeedback(); - verify(mCallback).sendHapticFeedback(); - verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */); - verify(mCallback).handleLifecycleAfterAuth(); + verify(faceCallback).sendHapticFeedback(); + verify(faceCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */); + verify(faceCallback).handleLifecycleAfterAuth(); assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty()); } else { assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty()); - verify(mCallback, never()).sendHapticFeedback(); - verify(mCallback, never()).sendAuthenticationResult(anyBoolean()); + verify(faceCallback, never()).sendHapticFeedback(); + verify(faceCallback, never()).sendAuthenticationResult(anyBoolean()); verify(udfpsCallback).sendHapticFeedback(); verify(udfpsCallback) |