summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java4
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java53
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java1
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java192
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java22
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java22
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java28
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java2
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java31
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java10
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java147
11 files changed, 475 insertions, 37 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index e6e2ac980889..2a0f9aead16f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -3315,6 +3315,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
pw.println(" trustManaged=" + getUserTrustIsManaged(userId));
pw.println(" udfpsEnrolled=" + isUdfpsEnrolled());
pw.println(" mFingerprintLockedOut=" + mFingerprintLockedOut);
+ pw.println(" mFingerprintLockedOutPermanent=" + mFingerprintLockedOutPermanent);
pw.println(" enabledByUser=" + mBiometricEnabledForUser.get(userId));
if (isUdfpsEnrolled()) {
pw.println(" shouldListenForUdfps=" + shouldListenForFingerprint(true));
@@ -3336,8 +3337,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
+ getStrongAuthTracker().hasUserAuthenticatedSinceBoot());
pw.println(" disabled(DPM)=" + isFaceDisabled(userId));
pw.println(" possible=" + isUnlockWithFacePossible(userId));
+ pw.println(" listening: actual=" + mFaceRunningState
+ + " expected=(" + (shouldListenForFace() ? 1 : 0));
pw.println(" strongAuthFlags=" + Integer.toHexString(strongAuthFlags));
pw.println(" trustManaged=" + getUserTrustIsManaged(userId));
+ pw.println(" mFaceLockedOutPermanent=" + mFaceLockedOutPermanent);
pw.println(" enabledByUser=" + mBiometricEnabledForUser.get(userId));
pw.println(" mSecureCameraLaunched=" + mSecureCameraLaunched);
}
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 28c949d4ed87..6463e04a4ff6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics.sensors;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
@@ -30,6 +31,7 @@ import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.security.KeyStore;
import android.util.EventLog;
import android.util.Slog;
@@ -48,6 +50,18 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
private static final String TAG = "Biometrics/AuthenticationClient";
+ // New, has not started yet
+ public static final int STATE_NEW = 0;
+ // Framework/HAL have started this operation
+ public static final int STATE_STARTED = 1;
+ // Operation is started, but requires some user action (such as finger lift & re-touch)
+ public static final int STATE_STARTED_PAUSED = 2;
+ // Done, errored, canceled, etc. HAL/framework are not running this sensor anymore.
+ public static final int STATE_STOPPED = 3;
+
+ @IntDef({STATE_NEW, STATE_STARTED, STATE_STARTED_PAUSED, STATE_STOPPED})
+ @interface State {}
+
private final boolean mIsStrongBiometric;
private final boolean mRequireConfirmation;
private final ActivityTaskManager mActivityTaskManager;
@@ -63,6 +77,20 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
protected 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.
+ protected @State int mState = STATE_NEW;
+
+ /**
+ * Handles lifecycle, e.g. {@link BiometricScheduler},
+ * {@link com.android.server.biometrics.sensors.BaseClientMonitor.Callback} after authentication
+ * results are known. Note that this happens asynchronously from (but shortly after)
+ * {@link #onAuthenticated(BiometricAuthenticator.Identifier, boolean, ArrayList)} and allows
+ * {@link CoexCoordinator} a chance to invoke/delay this event.
+ * @param authenticated
+ */
+ protected abstract void handleLifecycleAfterAuth(boolean authenticated);
+
public AuthenticationClient(@NonNull Context context, @NonNull LazyDaemon<T> lazyDaemon,
@NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener,
int targetUserId, long operationId, boolean restricted, @NonNull String owner,
@@ -221,7 +249,8 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
}
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
- coordinator.onAuthenticationSucceeded(this, new CoexCoordinator.Callback() {
+ coordinator.onAuthenticationSucceeded(SystemClock.uptimeMillis(), this,
+ new CoexCoordinator.Callback() {
@Override
public void sendAuthenticationResult(boolean addAuthTokenIfStrong) {
if (addAuthTokenIfStrong && mIsStrongBiometric) {
@@ -262,6 +291,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
vibrateSuccess();
}
}
+
+ @Override
+ public void handleLifecycleAfterAuth() {
+ AuthenticationClient.this.handleLifecycleAfterAuth(true /* authenticated */);
+ }
});
} else {
// Allow system-defined limit of number of attempts before giving up
@@ -272,7 +306,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
}
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
- coordinator.onAuthenticationRejected(this, lockoutMode,
+ coordinator.onAuthenticationRejected(SystemClock.uptimeMillis(), this, lockoutMode,
new CoexCoordinator.Callback() {
@Override
public void sendAuthenticationResult(boolean addAuthTokenIfStrong) {
@@ -291,6 +325,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
vibrateError();
}
}
+
+ @Override
+ public void handleLifecycleAfterAuth() {
+ AuthenticationClient.this.handleLifecycleAfterAuth(false /* authenticated */);
+ }
});
}
}
@@ -307,6 +346,12 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
}
}
+ @Override
+ public void onError(int errorCode, int vendorCode) {
+ super.onError(errorCode, vendorCode);
+ mState = STATE_STOPPED;
+ }
+
/**
* Start authentication
*/
@@ -345,6 +390,10 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
}
}
+ public @State int getState() {
+ return mState;
+ }
+
@Override
public int getProtoEnum() {
return BiometricsProto.CM_AUTHENTICATE;
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index 1ac91672c7dd..b20316e4c6df 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -355,7 +355,6 @@ public class BiometricScheduler {
/**
* Creates a new scheduler.
- * @param context system_server context.
* @param tag for the specific instance of the scheduler. Should be unique.
* @param sensorType the sensorType that this scheduler is handling.
* @param gestureAvailabilityDispatcher may be null if the sensor does not support gestures
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 7638a51a3710..f97cb8a67d81 100644
--- a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java
@@ -22,12 +22,17 @@ import static com.android.server.biometrics.sensors.BiometricScheduler.sensorTyp
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.sensors.BiometricScheduler.SensorType;
import com.android.server.biometrics.sensors.fingerprint.Udfps;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.Map;
/**
@@ -43,6 +48,9 @@ public class CoexCoordinator {
"com.android.server.biometrics.sensors.CoexCoordinator.enable";
private static final boolean DEBUG = true;
+ // Successful authentications should be used within this amount of time.
+ static final long SUCCESSFUL_AUTH_VALID_DURATION_MS = 5000;
+
/**
* Callback interface notifying the owner of "results" from the CoexCoordinator's business
* logic.
@@ -58,10 +66,69 @@ public class CoexCoordinator {
* Requests the owner to initiate a vibration for this event.
*/
void sendHapticFeedback();
+
+ /**
+ * Requests the owner to handle the AuthenticationClient's lifecycle (e.g. finish and remove
+ * from scheduler if auth was successful).
+ */
+ void handleLifecycleAfterAuth();
}
private static CoexCoordinator sInstance;
+ @VisibleForTesting
+ public static class SuccessfulAuth {
+ final long mAuthTimestamp;
+ final @SensorType int mSensorType;
+ final AuthenticationClient<?> mAuthenticationClient;
+ final Callback mCallback;
+ final CleanupRunnable mCleanupRunnable;
+
+ public static class CleanupRunnable implements Runnable {
+ @NonNull final LinkedList<SuccessfulAuth> mSuccessfulAuths;
+ @NonNull final SuccessfulAuth mAuth;
+ @NonNull final Callback mCallback;
+
+ public CleanupRunnable(@NonNull LinkedList<SuccessfulAuth> successfulAuths,
+ @NonNull SuccessfulAuth auth, @NonNull Callback callback) {
+ mSuccessfulAuths = successfulAuths;
+ mAuth = auth;
+ mCallback = callback;
+ }
+
+ @Override
+ public void run() {
+ final boolean removed = mSuccessfulAuths.remove(mAuth);
+ Slog.w(TAG, "Removing stale successfulAuth: " + mAuth.toString()
+ + ", success: " + removed);
+ mCallback.handleLifecycleAfterAuth();
+ }
+ }
+
+ public SuccessfulAuth(@NonNull Handler handler,
+ @NonNull LinkedList<SuccessfulAuth> successfulAuths,
+ long currentTimeMillis,
+ @SensorType int sensorType,
+ @NonNull AuthenticationClient<?> authenticationClient,
+ @NonNull Callback callback) {
+ mAuthTimestamp = currentTimeMillis;
+ mSensorType = sensorType;
+ mAuthenticationClient = authenticationClient;
+ mCallback = callback;
+
+ mCleanupRunnable = new CleanupRunnable(successfulAuths, this, callback);
+
+ handler.postDelayed(mCleanupRunnable, SUCCESSFUL_AUTH_VALID_DURATION_MS);
+ }
+
+ @Override
+ public String toString() {
+ return "SensorType: " + sensorTypeToString(mSensorType)
+ + ", mAuthTimestamp: " + mAuthTimestamp
+ + ", authenticationClient: " + mAuthenticationClient;
+ }
+ }
+
/**
* @return a singleton instance.
*/
@@ -85,11 +152,15 @@ public class CoexCoordinator {
// SensorType to AuthenticationClient map
private final Map<Integer, AuthenticationClient<?>> mClientMap;
+ @VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths;
private boolean mAdvancedLogicEnabled;
+ private final Handler mHandler;
private CoexCoordinator() {
// Singleton
mClientMap = new HashMap<>();
+ mSuccessfulAuths = new LinkedList<>();
+ mHandler = new Handler(Looper.getMainLooper());
}
public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType,
@@ -121,34 +192,43 @@ public class CoexCoordinator {
mClientMap.remove(sensorType);
}
- public void onAuthenticationSucceeded(@NonNull AuthenticationClient<?> client,
+ public void onAuthenticationSucceeded(long currentTimeMillis,
+ @NonNull AuthenticationClient<?> client,
@NonNull Callback callback) {
if (client.isBiometricPrompt()) {
callback.sendHapticFeedback();
// For BP, BiometricService will add the authToken to Keystore.
callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
+ callback.handleLifecycleAfterAuth();
} else if (isUnknownClient(client)) {
// Client doesn't exist in our map for some reason. Give the user feedback so the
// device doesn't feel like it's stuck. All other cases below can assume that the
// client exists in our map.
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
+ callback.handleLifecycleAfterAuth();
} else if (mAdvancedLogicEnabled && client.isKeyguard()) {
if (isSingleAuthOnly(client)) {
// Single sensor authentication
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
+ callback.handleLifecycleAfterAuth();
} else {
// Multi sensor authentication
AuthenticationClient<?> udfps = mClientMap.getOrDefault(SENSOR_TYPE_UDFPS, null);
AuthenticationClient<?> face = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null);
if (isCurrentFaceAuth(client)) {
- if (isPointerDown(udfps)) {
- // Face auth success while UDFPS pointer down. No callback, no haptic.
- // Feedback will be provided after UDFPS result.
+ if (isUdfpsActivelyAuthing(udfps)) {
+ // Face auth success while UDFPS is actively authing. No callback, no haptic
+ // Feedback will be provided after UDFPS result:
+ // 1) UDFPS succeeds - simply remove this from the queue
+ // 2) UDFPS rejected - use this face auth success to notify clients
+ mSuccessfulAuths.add(new SuccessfulAuth(mHandler, mSuccessfulAuths,
+ currentTimeMillis, SENSOR_TYPE_FACE, client, callback));
} else {
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
+ callback.handleLifecycleAfterAuth();
}
} else if (isCurrentUdfps(client)) {
if (isFaceScanning()) {
@@ -156,8 +236,12 @@ public class CoexCoordinator {
// Cancel face auth and/or prevent it from invoking haptics/callbacks after
face.cancel();
}
+
+ removeAndFinishAllFaceFromQueue();
+
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
+ callback.handleLifecycleAfterAuth();
}
}
} else {
@@ -165,13 +249,68 @@ public class CoexCoordinator {
// FingerprintManager for highlighting fingers
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
+ callback.handleLifecycleAfterAuth();
}
}
- public void onAuthenticationRejected(@NonNull AuthenticationClient<?> client,
+ public void onAuthenticationRejected(long currentTimeMillis,
+ @NonNull AuthenticationClient<?> client,
@LockoutTracker.LockoutMode int lockoutMode,
@NonNull Callback callback) {
- callback.sendHapticFeedback();
+ final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
+
+ if (keyguardAdvancedLogic) {
+ if (isSingleAuthOnly(client)) {
+ callback.sendHapticFeedback();
+ callback.handleLifecycleAfterAuth();
+ } else {
+ // Multi sensor authentication
+ 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();
+ } else if (isCurrentUdfps(client)) {
+ // Face should either be running, or have already finished
+ SuccessfulAuth auth = popSuccessfulFaceAuthIfExists(currentTimeMillis);
+ if (auth != null) {
+ Slog.d(TAG, "Using recent auth: " + auth);
+ callback.handleLifecycleAfterAuth();
+
+ auth.mCallback.sendHapticFeedback();
+ auth.mCallback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
+ auth.mCallback.handleLifecycleAfterAuth();
+ } else if (isFaceScanning()) {
+ // UDFPS rejected but face is still scanning
+ Slog.d(TAG, "UDFPS rejected in multi-sensor auth, face: " + face);
+ callback.handleLifecycleAfterAuth();
+
+ // TODO(b/193089985): Enforce/ensure that face auth finishes (whether
+ // accept/reject) within X amount of time. Otherwise users will be stuck
+ // waiting with their finger down for a long time.
+ } else {
+ // Face not scanning, and was not found in the queue. Most likely, face
+ // auth was too long ago.
+ Slog.d(TAG, "UDFPS rejected in multi-sensor auth, face not scanning");
+ callback.sendHapticFeedback();
+ callback.handleLifecycleAfterAuth();
+ }
+ } else {
+ Slog.d(TAG, "Unknown client rejected: " + client);
+ callback.sendHapticFeedback();
+ callback.handleLifecycleAfterAuth();
+ }
+ }
+ } else {
+ callback.sendHapticFeedback();
+ callback.handleLifecycleAfterAuth();
+ }
+
+ // Always notify keyguard, otherwise the cached "running" state in KeyguardUpdateMonitor
+ // will get stuck.
if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
// Don't send onAuthenticationFailed if we're in lockout, it causes a
// janky UI on Keyguard/BiometricPrompt since "authentication failed"
@@ -180,6 +319,30 @@ public class CoexCoordinator {
}
}
+ @Nullable
+ 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);
+ mSuccessfulAuths.remove(auth);
+ } else if (auth.mSensorType == SENSOR_TYPE_FACE) {
+ mSuccessfulAuths.remove(auth);
+ return auth;
+ }
+ }
+ return null;
+ }
+
+ private void removeAndFinishAllFaceFromQueue() {
+ for (SuccessfulAuth auth : mSuccessfulAuths) {
+ if (auth.mSensorType == SENSOR_TYPE_FACE) {
+ Slog.d(TAG, "Removing from queue and finishing: " + auth);
+ auth.mCallback.handleLifecycleAfterAuth();
+ mSuccessfulAuths.remove(auth);
+ }
+ }
+ }
+
private boolean isCurrentFaceAuth(@NonNull AuthenticationClient<?> client) {
return client == mClientMap.getOrDefault(SENSOR_TYPE_FACE, null);
}
@@ -189,12 +352,13 @@ public class CoexCoordinator {
}
private boolean isFaceScanning() {
- return mClientMap.containsKey(SENSOR_TYPE_FACE);
+ AuthenticationClient<?> client = mClientMap.getOrDefault(SENSOR_TYPE_FACE, null);
+ return client != null && client.getState() == AuthenticationClient.STATE_STARTED;
}
- private static boolean isPointerDown(@Nullable AuthenticationClient<?> client) {
+ private static boolean isUdfpsActivelyAuthing(@Nullable AuthenticationClient<?> client) {
if (client instanceof Udfps) {
- return ((Udfps) client).isPointerDown();
+ return client.getState() == AuthenticationClient.STATE_STARTED;
}
return false;
}
@@ -221,7 +385,15 @@ public class CoexCoordinator {
return true;
}
+ @Override
public String toString() {
- return "Enabled: " + mAdvancedLogicEnabled;
+ StringBuilder sb = new StringBuilder();
+ sb.append("Enabled: ").append(mAdvancedLogicEnabled);
+ sb.append(", Queue size: " ).append(mSuccessfulAuths.size());
+ for (SuccessfulAuth auth : mSuccessfulAuths) {
+ sb.append(", Auth: ").append(auth.toString());
+ }
+
+ return sb.toString();
}
}
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 0525d2da6988..35c17459804d 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
@@ -89,6 +89,12 @@ class FaceAuthenticationClient extends AuthenticationClient<ISession> implements
R.array.config_face_acquire_vendor_keyguard_ignorelist);
}
+ @Override
+ public void start(@NonNull Callback callback) {
+ super.start(callback);
+ mState = STATE_STARTED;
+ }
+
@NonNull
@Override
protected Callback wrapCallbackForStart(@NonNull Callback callback) {
@@ -128,10 +134,20 @@ class FaceAuthenticationClient extends AuthenticationClient<ISession> implements
}
@Override
+ protected void handleLifecycleAfterAuth(boolean authenticated) {
+ // For face, the authentication lifecycle ends either when
+ // 1) Authenticated == true
+ // 2) Error occurred
+ // 3) Authenticated == false
+ mCallback.onClientFinished(this, true /* success */);
+ }
+
+ @Override
public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
boolean authenticated, ArrayList<Byte> token) {
super.onAuthenticated(identifier, authenticated, token);
+ mState = STATE_STOPPED;
mUsageStats.addEvent(new UsageStats.AuthenticationEvent(
getStartTimeMs(),
System.currentTimeMillis() - getStartTimeMs() /* latency */,
@@ -139,12 +155,6 @@ class FaceAuthenticationClient extends AuthenticationClient<ISession> implements
0 /* error */,
0 /* vendorError */,
getTargetUserId()));
-
- // For face, the authentication lifecycle ends either when
- // 1) Authenticated == true
- // 2) Error occurred
- // 3) Authenticated == false
- mCallback.onClientFinished(this, true /* success */);
}
@Override
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 5731d73dfd49..e65245b98829 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
@@ -79,6 +79,12 @@ class FaceAuthenticationClient extends AuthenticationClient<IBiometricsFace> {
R.array.config_face_acquire_vendor_keyguard_ignorelist);
}
+ @Override
+ public void start(@NonNull Callback callback) {
+ super.start(callback);
+ mState = STATE_STARTED;
+ }
+
@NonNull
@Override
protected Callback wrapCallbackForStart(@NonNull Callback callback) {
@@ -115,10 +121,20 @@ class FaceAuthenticationClient extends AuthenticationClient<IBiometricsFace> {
}
@Override
+ protected void handleLifecycleAfterAuth(boolean authenticated) {
+ // For face, the authentication lifecycle ends either when
+ // 1) Authenticated == true
+ // 2) Error occurred
+ // 3) Authenticated == false
+ mCallback.onClientFinished(this, true /* success */);
+ }
+
+ @Override
public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
boolean authenticated, ArrayList<Byte> token) {
super.onAuthenticated(identifier, authenticated, token);
+ mState = STATE_STOPPED;
mUsageStats.addEvent(new UsageStats.AuthenticationEvent(
getStartTimeMs(),
System.currentTimeMillis() - getStartTimeMs() /* latency */,
@@ -126,12 +142,6 @@ class FaceAuthenticationClient extends AuthenticationClient<IBiometricsFace> {
0 /* error */,
0 /* vendorError */,
getTargetUserId()));
-
- // For face, the authentication lifecycle ends either when
- // 1) Authenticated == true
- // 2) Error occurred
- // 3) Authenticated == false
- mCallback.onClientFinished(this, true /* success */);
}
@Override
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 639814bf549f..14d18225d674 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
@@ -54,6 +54,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<ISession> imp
@NonNull private final LockoutCache mLockoutCache;
@Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
+ @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
+
@Nullable private ICancellationSignal mCancellationSignal;
private boolean mIsPointerDown;
@@ -72,6 +74,19 @@ class FingerprintAuthenticationClient extends AuthenticationClient<ISession> imp
lockoutCache, allowBackgroundAuthentication, true /* shouldVibrate */);
mLockoutCache = lockoutCache;
mUdfpsOverlayController = udfpsOverlayController;
+ mSensorProps = sensorProps;
+ }
+
+ @Override
+ public void start(@NonNull Callback callback) {
+ super.start(callback);
+
+ if (mSensorProps.isAnyUdfpsType()) {
+ // UDFPS requires user to touch before becoming "active"
+ mState = STATE_STARTED_PAUSED;
+ } else {
+ mState = STATE_STARTED;
+ }
}
@NonNull
@@ -81,13 +96,22 @@ class FingerprintAuthenticationClient extends AuthenticationClient<ISession> imp
}
@Override
+ protected void handleLifecycleAfterAuth(boolean authenticated) {
+ if (authenticated) {
+ mCallback.onClientFinished(this, true /* success */);
+ }
+ }
+
+ @Override
public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
boolean authenticated, ArrayList<Byte> token) {
super.onAuthenticated(identifier, authenticated, token);
if (authenticated) {
+ mState = STATE_STOPPED;
UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
- mCallback.onClientFinished(this, true /* success */);
+ } else {
+ mState = STATE_STARTED_PAUSED;
}
}
@@ -145,6 +169,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<ISession> imp
public void onPointerDown(int x, int y, float minor, float major) {
try {
mIsPointerDown = true;
+ mState = STATE_STARTED;
getFreshDaemon().onPointerDown(0 /* pointerId */, x, y, minor, major);
if (getListener() != null) {
getListener().onUdfpsPointerDown(getSensorId());
@@ -158,6 +183,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<ISession> imp
public void onPointerUp() {
try {
mIsPointerDown = false;
+ mState = STATE_STARTED_PAUSED;
getFreshDaemon().onPointerUp(0 /* pointerId */);
if (getListener() != null) {
getListener().onUdfpsPointerUp(getSensorId());
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index a6385a541b03..2f5b5c7b9727 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -622,7 +622,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
opPackageName, cookie, false /* requireConfirmation */,
mSensorProperties.sensorId, isStrongBiometric, statsClient,
mTaskStackListener, mLockoutTracker, mUdfpsOverlayController,
- allowBackgroundAuthentication);
+ allowBackgroundAuthentication, mSensorProperties);
mScheduler.scheduleClientMonitor(client, fingerprintStateCallback);
});
}
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 95a54d3591a3..9347244d7c77 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
@@ -25,6 +25,7 @@ import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IUdfpsOverlayController;
import android.os.IBinder;
import android.os.RemoteException;
@@ -52,6 +53,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
private final LockoutFrameworkImpl mLockoutFrameworkImpl;
@Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
+ @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
+
private boolean mIsPointerDown;
FingerprintAuthenticationClient(@NonNull Context context,
@@ -62,13 +65,27 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
@NonNull TaskStackListener taskStackListener,
@NonNull LockoutFrameworkImpl lockoutTracker,
@Nullable IUdfpsOverlayController udfpsOverlayController,
- boolean allowBackgroundAuthentication) {
+ boolean allowBackgroundAuthentication,
+ @NonNull FingerprintSensorPropertiesInternal sensorProps) {
super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
owner, cookie, requireConfirmation, sensorId, isStrongBiometric,
BiometricsProtoEnums.MODALITY_FINGERPRINT, statsClient, taskStackListener,
lockoutTracker, allowBackgroundAuthentication, true /* shouldVibrate */);
mLockoutFrameworkImpl = lockoutTracker;
mUdfpsOverlayController = udfpsOverlayController;
+ mSensorProps = sensorProps;
+ }
+
+ @Override
+ public void start(@NonNull Callback callback) {
+ super.start(callback);
+
+ if (mSensorProps.isAnyUdfpsType()) {
+ // UDFPS requires user to touch before becoming "active"
+ mState = STATE_STARTED_PAUSED;
+ } else {
+ mState = STATE_STARTED;
+ }
}
@NonNull
@@ -88,10 +105,11 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
// Note that authentication doesn't end when Authenticated == false
if (authenticated) {
+ mState = STATE_STOPPED;
resetFailedAttempts(getTargetUserId());
UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
- mCallback.onClientFinished(this, true /* success */);
} else {
+ mState = STATE_STARTED_PAUSED;
final @LockoutTracker.LockoutMode int lockoutMode =
mLockoutFrameworkImpl.getLockoutModeForUser(getTargetUserId());
if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {
@@ -125,6 +143,13 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
}
@Override
+ protected void handleLifecycleAfterAuth(boolean authenticated) {
+ if (authenticated) {
+ mCallback.onClientFinished(this, true /* success */);
+ }
+ }
+
+ @Override
public @LockoutTracker.LockoutMode int handleFailedAttempt(int userId) {
mLockoutFrameworkImpl.addFailedAttemptForUser(userId);
return super.handleFailedAttempt(userId);
@@ -162,6 +187,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
@Override
public void onPointerDown(int x, int y, float minor, float major) {
mIsPointerDown = true;
+ mState = STATE_STARTED;
UdfpsHelper.onFingerDown(getFreshDaemon(), x, y, minor, major);
if (getListener() != null) {
try {
@@ -175,6 +201,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
@Override
public void onPointerUp() {
mIsPointerDown = false;
+ mState = STATE_STARTED_PAUSED;
UdfpsHelper.onFingerUp(getFreshDaemon());
if (getListener() != null) {
try {
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 a8bf0c751e87..1fe41234849f 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
@@ -372,6 +372,11 @@ public class BiometricSchedulerTest {
protected void startHalOperation() {
}
+
+ @Override
+ protected void handleLifecycleAfterAuth(boolean authenticated) {
+
+ }
}
private static class TestAuthenticationClient extends AuthenticationClient<Object> {
@@ -395,6 +400,11 @@ public class BiometricSchedulerTest {
protected void startHalOperation() {
}
+
+ @Override
+ protected void handleLifecycleAfterAuth(boolean authenticated) {
+
+ }
}
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 9c42f4558450..fb05825a122b 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
@@ -19,6 +19,9 @@ package com.android.server.biometrics.sensors;
import static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_FACE;
import static com.android.server.biometrics.sensors.BiometricScheduler.SENSOR_TYPE_UDFPS;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -28,8 +31,11 @@ import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
import android.platform.test.annotations.Presubmit;
+import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.server.biometrics.sensors.fingerprint.Udfps;
@@ -39,6 +45,8 @@ import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.LinkedList;
+
@Presubmit
@SmallTest
public class CoexCoordinatorTest {
@@ -46,6 +54,7 @@ public class CoexCoordinatorTest {
private static final String TAG = "CoexCoordinatorTest";
private CoexCoordinator mCoexCoordinator;
+ private Handler mHandler;
@Mock
private Context mContext;
@@ -55,6 +64,9 @@ public class CoexCoordinatorTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+
+ mHandler = new Handler(Looper.getMainLooper());
+
mCoexCoordinator = CoexCoordinator.getInstance();
mCoexCoordinator.setAdvancedLogicEnabled(true);
}
@@ -68,9 +80,10 @@ public class CoexCoordinatorTest {
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
- mCoexCoordinator.onAuthenticationSucceeded(client, mCallback);
+ mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
+ verify(mCallback).handleLifecycleAfterAuth();
}
@Test
@@ -82,9 +95,11 @@ public class CoexCoordinatorTest {
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
- mCoexCoordinator.onAuthenticationRejected(client, LockoutTracker.LOCKOUT_NONE, mCallback);
+ mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
+ client, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
+ verify(mCallback).handleLifecycleAfterAuth();
}
@Test
@@ -96,9 +111,11 @@ public class CoexCoordinatorTest {
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
- mCoexCoordinator.onAuthenticationRejected(client, LockoutTracker.LOCKOUT_TIMED, mCallback);
+ mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
+ client, LockoutTracker.LOCKOUT_TIMED, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
+ verify(mCallback).handleLifecycleAfterAuth();
}
@Test
@@ -110,9 +127,10 @@ public class CoexCoordinatorTest {
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
- mCoexCoordinator.onAuthenticationSucceeded(client, mCallback);
+ mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
+ verify(mCallback).handleLifecycleAfterAuth();
}
@Test
@@ -130,13 +148,33 @@ public class CoexCoordinatorTest {
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
- mCoexCoordinator.onAuthenticationSucceeded(faceClient, mCallback);
+ mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
+ mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
+ verify(mCallback).handleLifecycleAfterAuth();
+ }
+
+ @Test
+ public void testKeyguard_faceAuth_udfpsTouching_faceSuccess_thenUdfpsRejectedWithinBounds() {
+ testKeyguard_faceAuth_udfpsTouching_faceSuccess(false /* thenUdfpsAccepted */,
+ 0 /* udfpsRejectedAfterMs */);
+ }
+
+ @Test
+ public void testKeyguard_faceAuth_udfpsTouching_faceSuccess_thenUdfpsRejectedAfterBounds() {
+ testKeyguard_faceAuth_udfpsTouching_faceSuccess(false /* thenUdfpsAccepted */,
+ CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS + 1 /* udfpsRejectedAfterMs */);
}
@Test
- public void testKeyguard_faceAuth_udfpsTouching_faceSuccess() {
+ public void testKeyguard_faceAuth_udfpsTouching_faceSuccess_thenUdfpsAccepted() {
+ testKeyguard_faceAuth_udfpsTouching_faceSuccess(true /* thenUdfpsAccepted */,
+ 0 /* udfpsRejectedAfterMs */);
+ }
+
+ private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted,
+ long udfpsRejectedAfterMs) {
mCoexCoordinator.reset();
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
@@ -146,13 +184,54 @@ public class CoexCoordinatorTest {
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
+ when (udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
- mCoexCoordinator.onAuthenticationSucceeded(faceClient, mCallback);
+ mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
+ mCallback);
verify(mCallback, never()).sendHapticFeedback();
verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
+ // CoexCoordinator requests the system to hold onto this AuthenticationClient until
+ // UDFPS result is known
+ verify(mCallback, never()).handleLifecycleAfterAuth();
+
+ // Reset the mock
+ CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
+ assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size());
+ assertEquals(faceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
+ if (thenUdfpsAccepted) {
+ mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
+ udfpsCallback);
+ verify(udfpsCallback).sendHapticFeedback();
+ verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */);
+ verify(udfpsCallback).handleLifecycleAfterAuth();
+
+ assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
+ } else {
+ mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, udfpsClient,
+ LockoutTracker.LOCKOUT_NONE, udfpsCallback);
+ if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) {
+ verify(udfpsCallback, never()).sendHapticFeedback();
+
+ verify(mCallback).sendHapticFeedback();
+ verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
+ verify(mCallback).handleLifecycleAfterAuth();
+
+ assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
+ } else {
+ assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
+
+ verify(mCallback, never()).sendHapticFeedback();
+ verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
+
+ verify(udfpsCallback).sendHapticFeedback();
+ verify(udfpsCallback)
+ .sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
+ verify(udfpsCallback).handleLifecycleAfterAuth();
+ }
+ }
}
@Test
@@ -161,6 +240,7 @@ public class CoexCoordinatorTest {
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));
@@ -170,9 +250,60 @@ public class CoexCoordinatorTest {
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
- mCoexCoordinator.onAuthenticationSucceeded(udfpsClient, mCallback);
+ mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
+ mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true));
verify(faceClient).cancel();
+ verify(mCallback).handleLifecycleAfterAuth();
+ }
+
+ @Test
+ public void testNonKeyguard_rejectAndNotLockedOut() {
+ mCoexCoordinator.reset();
+
+ AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
+ when(faceClient.isKeyguard()).thenReturn(false);
+ when(faceClient.isBiometricPrompt()).thenReturn(true);
+
+ mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
+ mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
+ LockoutTracker.LOCKOUT_NONE, mCallback);
+
+ verify(mCallback).sendHapticFeedback();
+ verify(mCallback).sendAuthenticationResult(eq(false));
+ verify(mCallback).handleLifecycleAfterAuth();
+ }
+
+ @Test
+ public void testNonKeyguard_rejectLockedOut() {
+ mCoexCoordinator.reset();
+
+ AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
+ when(faceClient.isKeyguard()).thenReturn(false);
+ when(faceClient.isBiometricPrompt()).thenReturn(true);
+
+ mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
+ mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
+ LockoutTracker.LOCKOUT_TIMED, mCallback);
+
+ verify(mCallback).sendHapticFeedback();
+ verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
+ verify(mCallback).handleLifecycleAfterAuth();
+ }
+
+ @Test
+ public void testCleanupRunnable() {
+ LinkedList<CoexCoordinator.SuccessfulAuth> successfulAuths = mock(LinkedList.class);
+ CoexCoordinator.SuccessfulAuth auth = mock(CoexCoordinator.SuccessfulAuth.class);
+ CoexCoordinator.Callback callback = mock(CoexCoordinator.Callback.class);
+ CoexCoordinator.SuccessfulAuth.CleanupRunnable runnable =
+ new CoexCoordinator.SuccessfulAuth.CleanupRunnable(successfulAuths, auth, callback);
+ runnable.run();
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(callback).handleLifecycleAfterAuth();
+ verify(successfulAuths).remove(eq(auth));
}
}