diff options
21 files changed, 338 insertions, 249 deletions
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 9a27a99437bc..55c90ce2a32f 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -69,8 +69,6 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan private static final int MSG_SET_FEATURE_COMPLETED = 107; private static final int MSG_CHALLENGE_GENERATED = 108; private static final int MSG_FACE_DETECTED = 109; - private static final int MSG_CHALLENGE_INTERRUPTED = 110; - private static final int MSG_CHALLENGE_INTERRUPT_FINISHED = 111; private static final int MSG_AUTHENTICATION_FRAME = 112; private static final int MSG_ENROLLMENT_FRAME = 113; @@ -102,8 +100,8 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan @Override // binder call public void onAuthenticationSucceeded(Face face, int userId, boolean isStrongBiometric) { - mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, isStrongBiometric ? 1 : 0, - face).sendToTarget(); + mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, + isStrongBiometric ? 1 : 0, face).sendToTarget(); } @Override // binder call @@ -142,22 +140,12 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } @Override - public void onChallengeGenerated(int sensorId, long challenge) { - mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, 0, challenge) + public void onChallengeGenerated(int sensorId, int userId, long challenge) { + mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, userId, challenge) .sendToTarget(); } @Override - public void onChallengeInterrupted(int sensorId) { - mHandler.obtainMessage(MSG_CHALLENGE_INTERRUPTED, sensorId).sendToTarget(); - } - - @Override - public void onChallengeInterruptFinished(int sensorId) { - mHandler.obtainMessage(MSG_CHALLENGE_INTERRUPT_FINISHED, sensorId).sendToTarget(); - } - - @Override public void onAuthenticationFrame(FaceAuthenticationFrame frame) { mHandler.obtainMessage(MSG_AUTHENTICATION_FRAME, frame).sendToTarget(); } @@ -434,16 +422,14 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan * * @see com.android.server.locksettings.LockSettingsService * - * TODO(b/171335732): should take userId - * * @hide */ @RequiresPermission(MANAGE_BIOMETRIC) - public void generateChallenge(int sensorId, GenerateChallengeCallback callback) { + public void generateChallenge(int sensorId, int userId, GenerateChallengeCallback callback) { if (mService != null) { try { mGenerateChallengeCallback = callback; - mService.generateChallenge(mToken, sensorId, 0 /* userId */, mServiceReceiver, + mService.generateChallenge(mToken, sensorId, userId, mServiceReceiver, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -452,12 +438,13 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } /** - * Same as {@link #generateChallenge(int, GenerateChallengeCallback)}, but assumes the first - * enumerated sensor. + * Same as {@link #generateChallenge(int, int, GenerateChallengeCallback)}, but assumes the + * first enumerated sensor. + * * @hide */ @RequiresPermission(MANAGE_BIOMETRIC) - public void generateChallenge(GenerateChallengeCallback callback) { + public void generateChallenge(int userId, GenerateChallengeCallback callback) { final List<FaceSensorPropertiesInternal> faceSensorProperties = getSensorPropertiesInternal(); if (faceSensorProperties.isEmpty()) { @@ -466,7 +453,7 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } final int sensorId = faceSensorProperties.get(0).sensorId; - generateChallenge(sensorId, callback); + generateChallenge(sensorId, userId, callback); } /** @@ -1120,25 +1107,16 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } /** - * Callback structure provided to {@link #generateChallenge(int, GenerateChallengeCallback)}. + * Callback structure provided to {@link #generateChallenge(int, int, + * GenerateChallengeCallback)}. + * * @hide */ public interface GenerateChallengeCallback { /** * Invoked when a challenge has been generated. */ - void onGenerateChallengeResult(int sensorId, long challenge); - - /** - * Invoked if the challenge has not been revoked and a subsequent caller/owner invokes - * {@link #generateChallenge(int, GenerateChallengeCallback)}, but - */ - default void onChallengeInterrupted(int sensorId) {} - - /** - * Invoked when the interrupting client has finished (e.g. revoked its challenge). - */ - default void onChallengeInterruptFinished(int sensorId) {} + void onGenerateChallengeResult(int sensorId, int userId, long challenge); } private class OnEnrollCancelListener implements OnCancelListener { @@ -1212,18 +1190,13 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan args.recycle(); break; case MSG_CHALLENGE_GENERATED: - sendChallengeGenerated(msg.arg1 /* sensorId */, (long) msg.obj /* challenge */); + sendChallengeGenerated(msg.arg1 /* sensorId */, msg.arg2 /* userId */, + (long) msg.obj /* challenge */); break; case MSG_FACE_DETECTED: sendFaceDetected(msg.arg1 /* sensorId */, msg.arg2 /* userId */, (boolean) msg.obj /* isStrongBiometric */); break; - case MSG_CHALLENGE_INTERRUPTED: - sendChallengeInterrupted((int) msg.obj /* sensorId */); - break; - case MSG_CHALLENGE_INTERRUPT_FINISHED: - sendChallengeInterruptFinished((int) msg.obj /* sensorId */); - break; case MSG_AUTHENTICATION_FRAME: sendAuthenticationFrame((FaceAuthenticationFrame) msg.obj /* frame */); break; @@ -1251,11 +1224,11 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan mGetFeatureCallback.onCompleted(success, features, featureState); } - private void sendChallengeGenerated(int sensorId, long challenge) { + private void sendChallengeGenerated(int sensorId, int userId, long challenge) { if (mGenerateChallengeCallback == null) { return; } - mGenerateChallengeCallback.onGenerateChallengeResult(sensorId, challenge); + mGenerateChallengeCallback.onGenerateChallengeResult(sensorId, userId, challenge); } private void sendFaceDetected(int sensorId, int userId, boolean isStrongBiometric) { @@ -1266,22 +1239,6 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan mFaceDetectionCallback.onFaceDetected(sensorId, userId, isStrongBiometric); } - private void sendChallengeInterrupted(int sensorId) { - if (mGenerateChallengeCallback == null) { - Slog.e(TAG, "sendChallengeInterrupted, callback null"); - return; - } - mGenerateChallengeCallback.onChallengeInterrupted(sensorId); - } - - private void sendChallengeInterruptFinished(int sensorId) { - if (mGenerateChallengeCallback == null) { - Slog.e(TAG, "sendChallengeInterruptFinished, callback null"); - return; - } - mGenerateChallengeCallback.onChallengeInterruptFinished(sensorId); - } - private void sendRemovedResult(Face face, int remaining) { if (mRemovalCallback == null) { return; diff --git a/core/java/android/hardware/face/FaceServiceReceiver.java b/core/java/android/hardware/face/FaceServiceReceiver.java index 9e62ca5e466b..9e7859277bd2 100644 --- a/core/java/android/hardware/face/FaceServiceReceiver.java +++ b/core/java/android/hardware/face/FaceServiceReceiver.java @@ -72,17 +72,8 @@ public class FaceServiceReceiver extends IFaceServiceReceiver.Stub { } @Override - public void onChallengeGenerated(int sensorId, long challenge) throws RemoteException { - - } - - @Override - public void onChallengeInterrupted(int sensorId) throws RemoteException { - - } - - @Override - public void onChallengeInterruptFinished(int sensorId) throws RemoteException { + public void onChallengeGenerated(int sensorId, int userId, long challenge) + throws RemoteException { } diff --git a/core/java/android/hardware/face/IFaceServiceReceiver.aidl b/core/java/android/hardware/face/IFaceServiceReceiver.aidl index 0ccb39583554..c4d9bf26c3ea 100644 --- a/core/java/android/hardware/face/IFaceServiceReceiver.aidl +++ b/core/java/android/hardware/face/IFaceServiceReceiver.aidl @@ -33,9 +33,7 @@ oneway interface IFaceServiceReceiver { void onRemoved(in Face face, int remaining); void onFeatureSet(boolean success, int feature); void onFeatureGet(boolean success, in int[] features, in boolean[] featureState); - void onChallengeGenerated(int sensorId, long challenge); - void onChallengeInterrupted(int sensorId); - void onChallengeInterruptFinished(int sensorId); + void onChallengeGenerated(int sensorId, int userId, long challenge); void onAuthenticationFrame(in FaceAuthenticationFrame frame); void onEnrollmentFrame(in FaceEnrollFrame frame); } diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index b52955d035b5..8aeb5cd8f428 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -475,10 +475,13 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** + * Callbacks for generate challenge operations. + * * @hide */ public interface GenerateChallengeCallback { - void onChallengeGenerated(int sensorId, long challenge); + /** Called when a challenged has been generated. */ + void onChallengeGenerated(int sensorId, int userId, long challenge); } /** @@ -1124,7 +1127,8 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing sendRemovedResult((Fingerprint) msg.obj, msg.arg1 /* remaining */); break; case MSG_CHALLENGE_GENERATED: - sendChallengeGenerated(msg.arg1 /* sensorId */, (long) msg.obj /* challenge */); + sendChallengeGenerated(msg.arg1 /* sensorId */, msg.arg2 /* userId */, + (long) msg.obj /* challenge */); break; case MSG_FINGERPRINT_DETECTED: sendFingerprintDetected(msg.arg1 /* sensorId */, msg.arg2 /* userId */, @@ -1233,12 +1237,12 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } } - private void sendChallengeGenerated(int sensorId, long challenge) { + private void sendChallengeGenerated(int sensorId, int userId, long challenge) { if (mGenerateChallengeCallback == null) { Slog.e(TAG, "sendChallengeGenerated, callback null"); return; } - mGenerateChallengeCallback.onChallengeGenerated(sensorId, challenge); + mGenerateChallengeCallback.onChallengeGenerated(sensorId, userId, challenge); } private void sendFingerprintDetected(int sensorId, int userId, boolean isStrongBiometric) { @@ -1454,8 +1458,8 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } @Override // binder call - public void onChallengeGenerated(int sensorId, long challenge) { - mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, 0, challenge) + public void onChallengeGenerated(int sensorId, int userId, long challenge) { + mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, userId, challenge) .sendToTarget(); } diff --git a/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java b/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java index 798e87beb52a..a9779b51321b 100644 --- a/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java +++ b/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java @@ -61,7 +61,8 @@ public class FingerprintServiceReceiver extends IFingerprintServiceReceiver.Stub } @Override - public void onChallengeGenerated(int sensorId, long challenge) throws RemoteException { + public void onChallengeGenerated(int sensorId, int userId, long challenge) + throws RemoteException { } diff --git a/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl b/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl index 1bd284d1ec05..9cea1fed629d 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl @@ -29,7 +29,7 @@ oneway interface IFingerprintServiceReceiver { void onAuthenticationFailed(); void onError(int error, int vendorCode); void onRemoved(in Fingerprint fp, int remaining); - void onChallengeGenerated(int sensorId, long challenge); + void onChallengeGenerated(int sensorId, int userId, long challenge); void onUdfpsPointerDown(int sensorId); void onUdfpsPointerUp(int sensorId); } diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java index 677ea5dc8f03..6482a2eead42 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java @@ -46,6 +46,7 @@ public abstract class BaseClientMonitor extends LoggableMonitor * Interface that ClientMonitor holders should use to receive callbacks. */ public interface Callback { + /** * Invoked when the ClientMonitor operation has been started (e.g. reached the head of * the queue and becomes the current operation). @@ -222,6 +223,7 @@ public abstract class BaseClientMonitor extends LoggableMonitor + this.getClass().getSimpleName() + ", " + getProtoEnum() + ", " + getOwnerString() - + ", " + getCookie() + "}"; + + ", " + getCookie() + + ", " + getTargetUserId() + "}"; } } diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java index 62a9769aee1a..f1c786b4977c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java +++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java @@ -132,11 +132,13 @@ public class ClientMonitorCallbackConverter { } } - public void onChallengeGenerated(int sensorId, long challenge) throws RemoteException { + /** Called when a challenged has been generated. */ + public void onChallengeGenerated(int sensorId, int userId, long challenge) + throws RemoteException { if (mFaceServiceReceiver != null) { - mFaceServiceReceiver.onChallengeGenerated(sensorId, challenge); + mFaceServiceReceiver.onChallengeGenerated(sensorId, userId, challenge); } else if (mFingerprintServiceReceiver != null) { - mFingerprintServiceReceiver.onChallengeGenerated(sensorId, challenge); + mFingerprintServiceReceiver.onChallengeGenerated(sensorId, userId, challenge); } } @@ -153,18 +155,6 @@ public class ClientMonitorCallbackConverter { } } - public void onChallengeInterrupted(int sensorId) throws RemoteException { - if (mFaceServiceReceiver != null) { - mFaceServiceReceiver.onChallengeInterrupted(sensorId); - } - } - - public void onChallengeInterruptFinished(int sensorId) throws RemoteException { - if (mFaceServiceReceiver != null) { - mFaceServiceReceiver.onChallengeInterruptFinished(sensorId); - } - } - // Fingerprint-specific callbacks for FingerprintManager only public void onUdfpsPointerDown(int sensorId) throws RemoteException { diff --git a/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java index 1fcad62e3a07..3d74f369efde 100644 --- a/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java @@ -40,7 +40,7 @@ public abstract class GenerateChallengeClient<T> extends HalClientMonitor<T> { @Override public void unableToStart() { try { - getListener().onChallengeGenerated(getSensorId(), 0L); + getListener().onChallengeGenerated(getSensorId(), getTargetUserId(), 0L); } catch (RemoteException e) { Slog.e(TAG, "Unable to send error", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java index 87269237bc85..57c1c74a51a8 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java @@ -110,17 +110,7 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { } @Override - public void onChallengeGenerated(int sensorId, long challenge) { - - } - - @Override - public void onChallengeInterrupted(int sensorId) { - - } - - @Override - public void onChallengeInterruptFinished(int sensorId) { + public void onChallengeGenerated(int sensorId, int userId, long challenge) { } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java index 904c39922a06..d76036bf432d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java @@ -52,7 +52,7 @@ public class FaceGenerateChallengeClient extends GenerateChallengeClient<ISessio void onChallengeGenerated(int sensorId, int userId, long challenge) { try { - getListener().onChallengeGenerated(sensorId, challenge); + getListener().onChallengeGenerated(sensorId, userId, challenge); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Unable to send challenge", e); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java index f8067670f61f..d0580c712610 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java @@ -99,17 +99,7 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { } @Override - public void onChallengeGenerated(int sensorId, long challenge) { - - } - - @Override - public void onChallengeInterrupted(int sensorId) { - - } - - @Override - public void onChallengeInterruptFinished(int sensorId) { + public void onChallengeGenerated(int sensorId, int userId, long challenge) { } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index f908fba8693c..a5bb0f430609 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -80,6 +80,7 @@ import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; +import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -93,7 +94,13 @@ import java.util.Map; public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { private static final String TAG = "Face10"; + private static final int ENROLL_TIMEOUT_SEC = 75; + private static final int GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS = 60 * 1000; + private static final int GENERATE_CHALLENGE_COUNTER_TTL_MILLIS = + FaceGenerateChallengeClient.CHALLENGE_TIMEOUT_SEC * 1000; + @VisibleForTesting + public static Clock sSystemClock = Clock.systemUTC(); private boolean mTestHalEnabled; @@ -102,19 +109,15 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @NonNull private final BiometricScheduler mScheduler; @NonNull private final Handler mHandler; @NonNull private final HalClientMonitor.LazyDaemon<IBiometricsFace> mLazyDaemon; - @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher; @NonNull private final LockoutHalImpl mLockoutTracker; @NonNull private final UsageStats mUsageStats; @NonNull private final Map<Integer, Long> mAuthenticatorIds; @Nullable private IBiometricsFace mDaemon; @NonNull private final HalResultController mHalResultController; - // If a challenge is generated, keep track of its owner. Since IBiometricsFace@1.0 only - // supports a single in-flight challenge, we must notify the interrupted owner that its - // challenge is no longer valid. The interrupted owner will be notified when the interrupter - // has finished. - @Nullable private FaceGenerateChallengeClient mCurrentChallengeOwner; private int mCurrentUserId = UserHandle.USER_NULL; private final int mSensorId; + private final List<Long> mGeneratedChallengeCount = new ArrayList<>(); + private FaceGenerateChallengeClient mGeneratedChallengeCache = null; private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() { @Override @@ -335,7 +338,6 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { mAuthenticatorIds = new HashMap<>(); mLazyDaemon = Face10.this::getDaemon; mLockoutTracker = new LockoutHalImpl(); - mLockoutResetDispatcher = lockoutResetDispatcher; mHalResultController = new HalResultController(sensorProps.sensorId, context, mHandler, mScheduler, mLockoutTracker, lockoutResetDispatcher); mHalResultController.setCallback(() -> { @@ -480,56 +482,56 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { return getDaemon() != null; } + private boolean isGeneratedChallengeCacheValid() { + return mGeneratedChallengeCache != null + && sSystemClock.millis() - mGeneratedChallengeCache.getCreatedAt() + < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS; + } + + private void incrementChallengeCount() { + mGeneratedChallengeCount.add(0, sSystemClock.millis()); + } + + private int decrementChallengeCount() { + final long now = sSystemClock.millis(); + // ignore values that are old in case generate/revoke calls are not matched + // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing + mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS); + if (!mGeneratedChallengeCount.isEmpty()) { + mGeneratedChallengeCount.remove(0); + } + return mGeneratedChallengeCount.size(); + } + /** - * {@link IBiometricsFace} only supports a single in-flight challenge. In cases where two - * callers both need challenges (e.g. resetLockout right before enrollment), we need to ensure - * that either: - * 1) generateChallenge/operation/revokeChallenge is complete before the next generateChallenge - * is processed by the scheduler, or - * 2) the generateChallenge callback provides a mechanism for notifying the caller that its - * challenge has been invalidated by a subsequent caller, as well as a mechanism for - * notifying the previous caller that the interrupting operation is complete (e.g. the - * interrupting client's challenge has been revoked, so that the interrupted client can - * start retry logic if necessary). See - * {@link - *android.hardware.face.FaceManager.GenerateChallengeCallback#onChallengeInterruptFinished(int)} - * The only case of conflicting challenges is currently resetLockout --> enroll. So, the second - * option seems better as it prioritizes the new operation, which is user-facing. + * {@link IBiometricsFace} only supports a single in-flight challenge but there are cases where + * two callers both need challenges (e.g. resetLockout right before enrollment). */ @Override public void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token, @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { mHandler.post(() -> { - if (mCurrentChallengeOwner != null) { - final ClientMonitorCallbackConverter listener = - mCurrentChallengeOwner.getListener(); - Slog.w(TAG, "Current challenge owner: " + mCurrentChallengeOwner - + ", listener: " + listener - + ", interrupted by: " + opPackageName); - if (listener != null) { - try { - listener.onChallengeInterrupted(mSensorId); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to notify challenge interrupted", e); - } - } + incrementChallengeCount(); + + if (isGeneratedChallengeCacheValid()) { + Slog.d(TAG, "Current challenge is cached and will be reused"); + mGeneratedChallengeCache.reuseResult(receiver); + return; } scheduleUpdateActiveUserWithoutHandler(userId); final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext, mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId, - opPackageName, mSensorId, mCurrentChallengeOwner); + opPackageName, mSensorId, sSystemClock.millis()); + mGeneratedChallengeCache = client; mScheduler.scheduleClientMonitor(client, new BaseClientMonitor.Callback() { @Override public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { if (client != clientMonitor) { Slog.e(TAG, "scheduleGenerateChallenge onClientStarted, mismatched client." + " Expecting: " + client + ", received: " + clientMonitor); - return; } - Slog.d(TAG, "Current challenge owner: " + client); - mCurrentChallengeOwner = client; } }); }); @@ -539,14 +541,16 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { public void scheduleRevokeChallenge(int sensorId, int userId, @NonNull IBinder token, @NonNull String opPackageName, long challenge) { mHandler.post(() -> { - if (mCurrentChallengeOwner != null - && !mCurrentChallengeOwner.getOwnerString().contentEquals(opPackageName)) { - Slog.e(TAG, "scheduleRevokeChallenge, package: " + opPackageName - + " attempting to revoke challenge owned by: " - + mCurrentChallengeOwner.getOwnerString()); + final boolean shouldRevoke = decrementChallengeCount() == 0; + if (!shouldRevoke) { + Slog.w(TAG, "scheduleRevokeChallenge skipped - challenge still in use: " + + mGeneratedChallengeCount); return; } + Slog.d(TAG, "scheduleRevokeChallenge executing - no active clients"); + mGeneratedChallengeCache = null; + final FaceRevokeChallengeClient client = new FaceRevokeChallengeClient(mContext, mLazyDaemon, token, userId, opPackageName, mSensorId); mScheduler.scheduleClientMonitor(client, new BaseClientMonitor.Callback() { @@ -556,33 +560,6 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { if (client != clientMonitor) { Slog.e(TAG, "scheduleRevokeChallenge, mismatched client." + "Expecting: " + client + ", received: " + clientMonitor); - return; - } - - if (mCurrentChallengeOwner == null) { - // Can happen if revoke is incorrectly called, for example without a - // preceding generateChallenge - Slog.w(TAG, "Current challenge owner is null"); - return; - } - - final FaceGenerateChallengeClient previousChallengeOwner = - mCurrentChallengeOwner.getInterruptedClient(); - mCurrentChallengeOwner = null; - - Slog.d(TAG, "Previous challenge owner: " + previousChallengeOwner); - if (previousChallengeOwner != null) { - final ClientMonitorCallbackConverter listener = - previousChallengeOwner.getListener(); - if (listener == null) { - Slog.w(TAG, "Listener is null"); - } else { - try { - listener.onChallengeInterruptFinished(mSensorId); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to notify interrupt finished", e); - } - } } } }); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java index 3e0064e496c7..f418104834e3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java @@ -17,16 +17,20 @@ package com.android.server.biometrics.sensors.face.hidl; import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.face.V1_0.IBiometricsFace; +import android.hardware.face.IFaceServiceReceiver; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; +import com.android.internal.util.Preconditions; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.GenerateChallengeClient; +import java.util.ArrayList; +import java.util.List; + /** * Face-specific generateChallenge client supporting the * {@link android.hardware.biometrics.face.V1_0} HIDL interface. @@ -34,40 +38,70 @@ import com.android.server.biometrics.sensors.GenerateChallengeClient; public class FaceGenerateChallengeClient extends GenerateChallengeClient<IBiometricsFace> { private static final String TAG = "FaceGenerateChallengeClient"; - private static final int CHALLENGE_TIMEOUT_SEC = 600; // 10 minutes + static final int CHALLENGE_TIMEOUT_SEC = 600; // 10 minutes + private static final Callback EMPTY_CALLBACK = new Callback() { + }; - // If `this` FaceGenerateChallengeClient was invoked while an existing in-flight challenge - // was not revoked yet, store a reference to the interrupted client here. Notify the interrupted - // client when `this` challenge is revoked. - @Nullable private final FaceGenerateChallengeClient mInterruptedClient; + private final long mCreatedAt; + private List<IFaceServiceReceiver> mWaiting; + private Long mChallengeResult; FaceGenerateChallengeClient(@NonNull Context context, @NonNull LazyDaemon<IBiometricsFace> lazyDaemon, @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener, int userId, @NonNull String owner, - int sensorId, @Nullable FaceGenerateChallengeClient interruptedClient) { + int sensorId, long now) { super(context, lazyDaemon, token, listener, userId, owner, sensorId); - mInterruptedClient = interruptedClient; - } - - @Nullable - public FaceGenerateChallengeClient getInterruptedClient() { - return mInterruptedClient; + mCreatedAt = now; + mWaiting = new ArrayList<>(); } @Override protected void startHalOperation() { + mChallengeResult = null; try { - final long challenge = getFreshDaemon().generateChallenge(CHALLENGE_TIMEOUT_SEC).value; - try { - getListener().onChallengeGenerated(getSensorId(), challenge); - mCallback.onClientFinished(this, true /* success */); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception", e); - mCallback.onClientFinished(this, false /* success */); + mChallengeResult = getFreshDaemon().generateChallenge(CHALLENGE_TIMEOUT_SEC).value; + // send the result to the original caller via mCallback and any waiting callers + // that called reuseResult + sendChallengeResult(getListener(), mCallback); + for (IFaceServiceReceiver receiver : mWaiting) { + sendChallengeResult(new ClientMonitorCallbackConverter(receiver), EMPTY_CALLBACK); } } catch (RemoteException e) { Slog.e(TAG, "generateChallenge failed", e); mCallback.onClientFinished(this, false /* success */); + } finally { + mWaiting = null; + } + } + + /** @return An arbitrary time value for caching provided to the constructor. */ + public long getCreatedAt() { + return mCreatedAt; + } + + /** + * Reuse the result of this operation when it is available. The receiver will be notified + * immediately if a challenge has already been generated. + * + * @param receiver receiver to be notified of challenge result + */ + public void reuseResult(@NonNull IFaceServiceReceiver receiver) { + if (mWaiting != null) { + mWaiting.add(receiver); + } else { + sendChallengeResult(new ClientMonitorCallbackConverter(receiver), EMPTY_CALLBACK); + } + } + + private void sendChallengeResult(@NonNull ClientMonitorCallbackConverter receiver, + @NonNull Callback ownerCallback) { + Preconditions.checkState(mChallengeResult != null, "result not available"); + try { + receiver.onChallengeGenerated(getSensorId(), getTargetUserId(), mChallengeResult); + ownerCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); + ownerCallback.onClientFinished(this, false /* success */); } } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java index e34afc09eec1..29f2f20b8a75 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java @@ -100,7 +100,7 @@ class BiometricTestSessionImpl extends ITestSession.Stub { } @Override - public void onChallengeGenerated(int sensorId, long challenge) { + public void onChallengeGenerated(int sensorId, int userId, long challenge) { } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java index 293b57d0e890..6d0148190a60 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java @@ -53,7 +53,7 @@ class FingerprintGenerateChallengeClient extends GenerateChallengeClient<ISessio void onChallengeGenerated(int sensorId, int userId, long challenge) { try { - getListener().onChallengeGenerated(sensorId, challenge); + getListener().onChallengeGenerated(sensorId, userId, challenge); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Unable to send challenge", e); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java index ad4f679f075f..c00daffb867f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java @@ -101,7 +101,7 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { } @Override - public void onChallengeGenerated(int sensorId, long challenge) { + public void onChallengeGenerated(int sensorId, int userId, long challenge) { } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java index 3584397eea81..db2f0455a475 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java @@ -48,7 +48,7 @@ public class FingerprintGenerateChallengeClient try { final long challenge = getFreshDaemon().preEnroll(); try { - getListener().onChallengeGenerated(getSensorId(), challenge); + getListener().onChallengeGenerated(getSensorId(), getTargetUserId(), challenge); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); diff --git a/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java b/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java index 5abc438579e8..2bdeab4703a8 100644 --- a/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java +++ b/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java @@ -98,19 +98,7 @@ public class BiometricDeferredQueue { } @Override - public void onChallengeInterrupted(int sensorId) { - Slog.w(TAG, "Challenge interrupted, sensor: " + sensorId); - // Consider re-attempting generateChallenge/resetLockout/revokeChallenge - // when onChallengeInterruptFinished is invoked - } - - @Override - public void onChallengeInterruptFinished(int sensorId) { - Slog.w(TAG, "Challenge interrupt finished, sensor: " + sensorId); - } - - @Override - public void onGenerateChallengeResult(int sensorId, long challenge) { + public void onGenerateChallengeResult(int sensorId, int userId, long challenge) { if (!sensorIds.contains(sensorId)) { Slog.e(TAG, "Unknown sensorId received: " + sensorId); return; @@ -128,10 +116,6 @@ public class BiometricDeferredQueue { } sensorIds.remove(sensorId); - // Challenge is only required for IBiometricsFace@1.0 (and not IFace AIDL). The - // IBiometricsFace@1.0 HAL does not require userId to revokeChallenge, so passing - // in 0 is OK. - final int userId = 0; faceManager.revokeChallenge(sensorId, userId, challenge); if (sensorIds.isEmpty()) { @@ -234,18 +218,12 @@ public class BiometricDeferredQueue { } } - /** - * For devices on {@link android.hardware.biometrics.face.V1_0} which only support a single - * in-flight challenge, we generate a single challenge to reset lockout for all profiles. This - * hopefully reduces/eliminates issues such as overwritten challenge, incorrectly revoked - * challenge, or other race conditions. - */ private void processPendingLockoutsForFace(List<UserAuthInfo> pendingResetLockouts) { if (mFaceManager != null) { if (mFaceResetLockoutTask != null) { // This code will need to be updated if this problem ever occurs. - Slog.w(TAG, "mFaceGenerateChallengeCallback not null, previous operation may be" - + " stuck"); + Slog.w(TAG, + "mFaceGenerateChallengeCallback not null, previous operation may be stuck"); } final List<FaceSensorPropertiesInternal> faceSensorProperties = mFaceManager.getSensorPropertiesInternal(); @@ -258,12 +236,13 @@ public class BiometricDeferredQueue { mSpManager, sensorIds, pendingResetLockouts); for (final FaceSensorPropertiesInternal prop : faceSensorProperties) { if (prop.resetLockoutRequiresHardwareAuthToken) { - if (prop.resetLockoutRequiresChallenge) { - // Generate a challenge for each sensor. The challenge does not need to be - // per-user, since the HAT returned by gatekeeper contains userId. - mFaceManager.generateChallenge(prop.sensorId, mFaceResetLockoutTask); - } else { - for (UserAuthInfo user : pendingResetLockouts) { + for (UserAuthInfo user : pendingResetLockouts) { + if (prop.resetLockoutRequiresChallenge) { + Slog.d(TAG, "Generating challenge for sensor: " + prop.sensorId + + ", user: " + user.userId); + mFaceManager.generateChallenge(prop.sensorId, user.userId, + mFaceResetLockoutTask); + } else { Slog.d(TAG, "Resetting face lockout for sensor: " + prop.sensorId + ", user: " + user.userId); final byte[] hat = requestHatFromGatekeeperPassword(mSpManager, user, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java index 0b59be65b887..39c51d5f5e5e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java @@ -18,6 +18,10 @@ package com.android.server.biometrics.sensors.face.hidl; import static junit.framework.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -26,6 +30,7 @@ import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceServiceReceiver; import android.os.Binder; import android.os.IBinder; import android.os.UserManager; @@ -42,8 +47,12 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; +import java.util.stream.IntStream; @Presubmit @SmallTest @@ -51,6 +60,7 @@ public class Face10Test { private static final String TAG = "Face10Test"; private static final int SENSOR_ID = 1; + private static final int USER_ID = 20; @Mock private Context mContext; @@ -86,10 +96,18 @@ public class Face10Test { FaceSensorProperties.TYPE_UNKNOWN, supportsFaceDetection, supportsSelfIllumination, resetLockoutRequiresChallenge); + Face10.sSystemClock = Clock.fixed(Instant.ofEpochMilli(100), ZoneId.of("PST")); mFace10 = new Face10(mContext, sensorProps, mLockoutResetDispatcher, mScheduler); mBinder = new Binder(); } + private void tick(long seconds) { + waitForIdle(); + Face10.sSystemClock = Clock.fixed(Instant.ofEpochSecond( + Face10.sSystemClock.instant().getEpochSecond() + seconds), + ZoneId.of("PST")); + } + @Test public void getAuthenticatorId_doesNotCrashWhenIdNotFound() { assertEquals(0, mFace10.getAuthenticatorId(0 /* sensorId */, 111 /* userId */)); @@ -104,6 +122,59 @@ public class Face10Test { } @Test + public void scheduleGenerateChallenge_cachesResult() { + final IFaceServiceReceiver[] mocks = IntStream.range(0, 3) + .mapToObj(i -> mock(IFaceServiceReceiver.class)) + .toArray(IFaceServiceReceiver[]::new); + for (IFaceServiceReceiver mock : mocks) { + mFace10.scheduleGenerateChallenge(SENSOR_ID, USER_ID, mBinder, mock, TAG); + tick(10); + } + tick(120); + mFace10.scheduleGenerateChallenge( + SENSOR_ID, USER_ID, mBinder, mock(IFaceServiceReceiver.class), TAG); + waitForIdle(); + + verify(mScheduler, times(2)) + .scheduleClientMonitor(isA(FaceGenerateChallengeClient.class), any()); + } + + @Test + public void scheduleRevokeChallenge_waitsUntilEmpty() { + final long challenge = 22; + final IFaceServiceReceiver[] mocks = IntStream.range(0, 3) + .mapToObj(i -> mock(IFaceServiceReceiver.class)) + .toArray(IFaceServiceReceiver[]::new); + for (IFaceServiceReceiver mock : mocks) { + mFace10.scheduleGenerateChallenge(SENSOR_ID, USER_ID, mBinder, mock, TAG); + tick(10); + } + for (IFaceServiceReceiver mock : mocks) { + mFace10.scheduleRevokeChallenge(SENSOR_ID, USER_ID, mBinder, TAG, challenge); + tick(10); + } + waitForIdle(); + + verify(mScheduler).scheduleClientMonitor(isA(FaceRevokeChallengeClient.class), any()); + } + + @Test + public void scheduleRevokeChallenge_doesNotWaitForever() { + mFace10.scheduleGenerateChallenge( + SENSOR_ID, USER_ID, mBinder, mock(IFaceServiceReceiver.class), TAG); + mFace10.scheduleGenerateChallenge( + SENSOR_ID, USER_ID, mBinder, mock(IFaceServiceReceiver.class), TAG); + tick(10000); + mFace10.scheduleGenerateChallenge( + SENSOR_ID, USER_ID, mBinder, mock(IFaceServiceReceiver.class), TAG); + mFace10.scheduleRevokeChallenge( + SENSOR_ID, USER_ID, mBinder, TAG, 8 /* challenge */); + waitForIdle(); + + verify(mScheduler).scheduleClientMonitor(isA(FaceRevokeChallengeClient.class), any()); + } + + @Test public void halServiceDied_resetsScheduler() { // It's difficult to test the linkToDeath --> serviceDied path, so let's just invoke // serviceDied directly. diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClientTest.java new file mode 100644 index 000000000000..55dc03595b3d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClientTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.hidl; + +import static junit.framework.Assert.assertEquals; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.biometrics.face.V1_0.IBiometricsFace; +import android.hardware.biometrics.face.V1_0.OptionalUint64; +import android.hardware.face.IFaceServiceReceiver; +import android.os.Binder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; + +import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@Presubmit +@SmallTest +public class FaceGenerateChallengeClientTest { + + private static final String TAG = "FaceGenerateChallengeClientTest"; + private static final int USER_ID = 2; + private static final int SENSOR_ID = 4; + private static final long START_TIME = 5000; + private static final long CHALLENGE = 200; + + private final Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock + private IBiometricsFace mIBiometricsFace; + @Mock + private IFaceServiceReceiver mClientReceiver; + @Mock + private IFaceServiceReceiver mOtherReceiver; + @Mock + private BaseClientMonitor.Callback mMonitorCallback; + + private FaceGenerateChallengeClient mClient; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + + final OptionalUint64 challenge = new OptionalUint64(); + challenge.value = CHALLENGE; + when(mIBiometricsFace.generateChallenge(anyInt())).thenReturn(challenge); + + mClient = new FaceGenerateChallengeClient(mContext, () -> mIBiometricsFace, new Binder(), + new ClientMonitorCallbackConverter(mClientReceiver), USER_ID, + TAG, SENSOR_ID, START_TIME); + } + + @Test + public void getCreatedAt() { + assertEquals(START_TIME, mClient.getCreatedAt()); + } + + @Test + public void reuseResult_whenNotReady() throws Exception { + mClient.reuseResult(mOtherReceiver); + verify(mOtherReceiver, never()).onChallengeGenerated(anyInt(), anyInt(), anyInt()); + } + + @Test + public void reuseResult_whenReady() throws Exception { + mClient.start(mMonitorCallback); + mClient.reuseResult(mOtherReceiver); + verify(mOtherReceiver).onChallengeGenerated(eq(SENSOR_ID), eq(USER_ID), eq(CHALLENGE)); + } + + @Test + public void reuseResult_whenReallyReady() throws Exception { + mClient.reuseResult(mOtherReceiver); + mClient.start(mMonitorCallback); + verify(mOtherReceiver).onChallengeGenerated(eq(SENSOR_ID), eq(USER_ID), eq(CHALLENGE)); + } +} |