summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/biometrics/BiometricPrompt.java6
-rw-r--r--core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java3
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationClient.java3
-rw-r--r--services/core/java/com/android/server/biometrics/BiometricService.java357
-rw-r--r--services/core/java/com/android/server/biometrics/BiometricServiceBase.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java742
8 files changed, 980 insertions, 146 deletions
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 2951a4bf328b..14acc6fb834d 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -103,6 +103,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
public static final int DISMISSED_REASON_CONFIRMED = 1;
/**
+ * Dialog is done animating away after user clicked on the button set via
+ * {@link BiometricPrompt.Builder#setNegativeButton(CharSequence, Executor,
+ * DialogInterface.OnClickListener)}.
* @hide
*/
public static final int DISMISSED_REASON_NEGATIVE = 2;
@@ -113,11 +116,14 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
public static final int DISMISSED_REASON_USER_CANCEL = 3;
/**
+ * Authenticated, confirmation not required. Dialog animated away.
* @hide
*/
public static final int DISMISSED_REASON_CONFIRM_NOT_REQUIRED = 4;
/**
+ * Error message shown on SystemUI. When BiometricService receives this, the UI is already
+ * gone.
* @hide
*/
public static final int DISMISSED_REASON_ERROR = 5;
diff --git a/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl
index 180daaf97ada..ca6114e4d842 100644
--- a/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl
@@ -27,8 +27,8 @@ oneway interface IBiometricServiceReceiverInternal {
// Notify BiometricService that authentication was successful. If user confirmation is required,
// the auth token must be submitted into KeyStore.
void onAuthenticationSucceeded(boolean requireConfirmation, in byte[] token);
- // Notify BiometricService that an error has occurred.
- void onAuthenticationFailed(int cookie, boolean requireConfirmation);
+ // Notify BiometricService authentication was rejected.
+ void onAuthenticationFailed();
// Notify BiometricService than an error has occured. Forward to the correct receiver depending
// on the cookie.
void onError(int cookie, int error, String message);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
index 0eea9efcaa4f..0c3cea780d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
@@ -73,15 +73,10 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
case DialogViewCallback.DISMISSED_AUTHENTICATED:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
- // TODO: BiometricService currently sends the result immediately. This should
- // actually happen when the animation is completed.
break;
case DialogViewCallback.DISMISSED_ERROR:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR);
- // TODO: Make sure error isn't received until dialog is dismissed
- // TODO: Similarly, BiometricService currently sends the result immediately.
- // This should happen when the animation is completed.
break;
default:
Log.e(TAG, "Unhandled reason: " + reason);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
index 7a5c3e3da7ec..a1687acf88c6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
@@ -337,6 +337,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
}
mNegativeButton.setVisibility(View.VISIBLE);
+ mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
if (RotationUtils.getRotation(mContext) != RotationUtils.ROTATION_NONE) {
mDialog.getLayoutParams().width = (int) mDialogWidth;
@@ -344,7 +345,6 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
if (mRestoredState == null) {
updateState(STATE_AUTHENTICATING);
- mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
final int hint = getHintStringResourceId();
if (hint != 0) {
mErrorText.setText(hint);
@@ -570,7 +570,6 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
showTemporaryMessage(error);
showTryAgainButton(false /* show */);
- // TODO: Is this still used to synchronize animation and client onError timing?
mHandler.postDelayed(() -> {
animateAway(DialogViewCallback.DISMISSED_ERROR);
}, BiometricPrompt.HIDE_DIALOG_DELAY);
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/AuthenticationClient.java
index 4a9ccdee0522..766e5c4d638f 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationClient.java
@@ -209,8 +209,7 @@ public abstract class AuthenticationClient extends ClientMonitor {
// will show briefly and be replaced by "device locked out" message.
if (listener != null) {
if (isBiometricPrompt()) {
- listener.onAuthenticationFailedInternal(getCookie(),
- getRequireConfirmation());
+ listener.onAuthenticationFailedInternal();
} else {
listener.onAuthenticationFailed(getHalDeviceId());
}
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 90a80dd36cb9..94056694d810 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -27,6 +27,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.AppOpsManager;
+import android.app.IActivityManager;
import android.app.IActivityTaskManager;
import android.app.KeyguardManager;
import android.app.TaskStackListener;
@@ -69,6 +70,7 @@ import android.util.Slog;
import android.util.StatsLog;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.statusbar.IStatusBarService;
import com.android.server.SystemService;
@@ -90,7 +92,7 @@ public class BiometricService extends SystemService {
private static final int MSG_ON_TASK_STACK_CHANGED = 1;
private static final int MSG_ON_AUTHENTICATION_SUCCEEDED = 2;
- private static final int MSG_ON_AUTHENTICATION_FAILED = 3;
+ private static final int MSG_ON_AUTHENTICATION_REJECTED = 3;
private static final int MSG_ON_ERROR = 4;
private static final int MSG_ON_ACQUIRED = 5;
private static final int MSG_ON_DISMISSED = 6;
@@ -101,6 +103,7 @@ public class BiometricService extends SystemService {
private static final int MSG_ON_CONFIRM_DEVICE_CREDENTIAL_SUCCESS = 11;
private static final int MSG_ON_CONFIRM_DEVICE_CREDENTIAL_ERROR = 12;
private static final int MSG_REGISTER_CANCELLATION_CALLBACK = 13;
+ private static final int MSG_ON_AUTHENTICATION_TIMED_OUT = 14;
private static final int[] FEATURE_ID = {
TYPE_FINGERPRINT,
@@ -112,33 +115,41 @@ public class BiometricService extends SystemService {
* Authentication either just called and we have not transitioned to the CALLED state, or
* authentication terminated (success or error).
*/
- private static final int STATE_AUTH_IDLE = 0;
+ static final int STATE_AUTH_IDLE = 0;
/**
* Authentication was called and we are waiting for the <Biometric>Services to return their
* cookies before starting the hardware and showing the BiometricPrompt.
*/
- private static final int STATE_AUTH_CALLED = 1;
+ static final int STATE_AUTH_CALLED = 1;
/**
* Authentication started, BiometricPrompt is showing and the hardware is authenticating.
*/
- private static final int STATE_AUTH_STARTED = 2;
+ static final int STATE_AUTH_STARTED = 2;
/**
* Authentication is paused, waiting for the user to press "try again" button. Only
* passive modalities such as Face or Iris should have this state. Note that for passive
* modalities, the HAL enters the idle state after onAuthenticated(false) which differs from
* fingerprint.
*/
- private static final int STATE_AUTH_PAUSED = 3;
+ static final int STATE_AUTH_PAUSED = 3;
/**
* Authentication is successful, but we're waiting for the user to press "confirm" button.
*/
- private static final int STATE_AUTH_PENDING_CONFIRM = 5;
+ static final int STATE_AUTH_PENDING_CONFIRM = 5;
/**
* Biometric authentication was canceled, but the device is now showing ConfirmDeviceCredential
*/
- private static final int STATE_BIOMETRIC_AUTH_CANCELED_SHOWING_CDC = 6;
+ static final int STATE_BIOMETRIC_AUTH_CANCELED_SHOWING_CDC = 6;
+ /**
+ * Biometric authenticated, waiting for SysUI to finish animation
+ */
+ static final int STATE_AUTHENTICATED_PENDING_SYSUI = 7;
+ /**
+ * Biometric error, waiting for SysUI to finish animation
+ */
+ static final int STATE_ERROR_PENDING_SYSUI = 8;
- private final class AuthSession implements IBinder.DeathRecipient {
+ final class AuthSession implements IBinder.DeathRecipient {
// Map of Authenticator/Cookie pairs. We expect to receive the cookies back from
// <Biometric>Services before we can start authenticating. Pairs that have been returned
// are moved to mModalitiesMatched.
@@ -165,10 +176,13 @@ public class BiometricService extends SystemService {
final boolean mRequireConfirmation;
// The current state, which can be either idle, called, or started
- private int mState = STATE_AUTH_IDLE;
+ int mState = STATE_AUTH_IDLE;
// For explicit confirmation, do not send to keystore until the user has confirmed
// the authentication.
byte[] mTokenEscrow;
+ // Waiting for SystemUI to complete animation
+ int mErrorEscrow;
+ String mErrorStringEscrow;
// Timestamp when authentication started
private long mStartTimeMs;
@@ -251,35 +265,40 @@ public class BiometricService extends SystemService {
}
}
+ private final Injector mInjector;
+ @VisibleForTesting
+ final IBiometricService.Stub mImpl;
private final AppOpsManager mAppOps;
private final boolean mHasFeatureFingerprint;
private final boolean mHasFeatureIris;
private final boolean mHasFeatureFace;
- private final SettingObserver mSettingObserver;
+ @VisibleForTesting
+ SettingObserver mSettingObserver;
private final List<EnabledOnKeyguardCallback> mEnabledOnKeyguardCallbacks;
private final BiometricTaskStackListener mTaskStackListener = new BiometricTaskStackListener();
private final Random mRandom = new Random();
- private IFingerprintService mFingerprintService;
- private IFaceService mFaceService;
- private IActivityTaskManager mActivityTaskManager;
- private IStatusBarService mStatusBarService;
+ @VisibleForTesting
+ IFingerprintService mFingerprintService;
+ @VisibleForTesting
+ IFaceService mFaceService;
+ @VisibleForTesting
+ IActivityTaskManager mActivityTaskManager;
+ @VisibleForTesting
+ IStatusBarService mStatusBarService;
+ @VisibleForTesting
+ KeyStore mKeyStore;
// Get and cache the available authenticator (manager) classes. Used since aidl doesn't support
// polymorphism :/
final ArrayList<Authenticator> mAuthenticators = new ArrayList<>();
- // Cache the current service that's being used. This is the service which
- // cancelAuthentication() must be forwarded to. This is just a cache, and the actual
- // check (is caller the current client) is done in the <Biometric>Service.
- // Since Settings/System (not application) is responsible for changing preference, this
- // should be safe.
- private int mCurrentModality;
-
// The current authentication session, null if idle/done. We need to track both the current
// and pending sessions since errors may be sent to either.
- private AuthSession mCurrentAuthSession;
- private AuthSession mPendingAuthSession;
+ @VisibleForTesting
+ AuthSession mCurrentAuthSession;
+ @VisibleForTesting
+ AuthSession mPendingAuthSession;
// TODO(b/123378871): Remove when moved.
// When BiometricPrompt#setAllowDeviceCredentials is set to true, we need to store the
@@ -289,7 +308,8 @@ public class BiometricService extends SystemService {
// to this receiver.
private IBiometricServiceReceiver mConfirmDeviceCredentialReceiver;
- private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ @VisibleForTesting
+ final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
@@ -307,8 +327,8 @@ public class BiometricService extends SystemService {
break;
}
- case MSG_ON_AUTHENTICATION_FAILED: {
- handleAuthenticationFailed((String) msg.obj /* failureReason */);
+ case MSG_ON_AUTHENTICATION_REJECTED: {
+ handleAuthenticationRejected((String) msg.obj /* failureReason */);
break;
}
@@ -397,6 +417,11 @@ public class BiometricService extends SystemService {
break;
}
+ case MSG_ON_AUTHENTICATION_TIMED_OUT: {
+ handleAuthenticationTimedOut((String) msg.obj /* errorMessage */);
+ break;
+ }
+
default:
Slog.e(TAG, "Unknown message: " + msg);
break;
@@ -422,7 +447,8 @@ public class BiometricService extends SystemService {
}
}
- private final class SettingObserver extends ContentObserver {
+ @VisibleForTesting
+ public static class SettingObserver extends ContentObserver {
private static final boolean DEFAULT_KEYGUARD_ENABLED = true;
private static final boolean DEFAULT_APP_ENABLED = true;
@@ -436,6 +462,7 @@ public class BiometricService extends SystemService {
Settings.Secure.getUriFor(Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION);
private final ContentResolver mContentResolver;
+ private final List<BiometricService.EnabledOnKeyguardCallback> mCallbacks;
private Map<Integer, Boolean> mFaceEnabledOnKeyguard = new HashMap<>();
private Map<Integer, Boolean> mFaceEnabledForApps = new HashMap<>();
@@ -446,13 +473,15 @@ public class BiometricService extends SystemService {
*
* @param handler The handler to run {@link #onChange} on, or null if none.
*/
- SettingObserver(Handler handler) {
+ public SettingObserver(Context context, Handler handler,
+ List<BiometricService.EnabledOnKeyguardCallback> callbacks) {
super(handler);
- mContentResolver = getContext().getContentResolver();
+ mContentResolver = context.getContentResolver();
+ mCallbacks = callbacks;
updateContentObserver();
}
- void updateContentObserver() {
+ public void updateContentObserver() {
mContentResolver.unregisterContentObserver(this);
mContentResolver.registerContentObserver(FACE_UNLOCK_KEYGUARD_ENABLED,
false /* notifyForDescendents */,
@@ -495,7 +524,7 @@ public class BiometricService extends SystemService {
}
}
- boolean getFaceEnabledOnKeyguard() {
+ public boolean getFaceEnabledOnKeyguard() {
final int user = ActivityManager.getCurrentUser();
if (!mFaceEnabledOnKeyguard.containsKey(user)) {
onChange(true /* selfChange */, FACE_UNLOCK_KEYGUARD_ENABLED, user);
@@ -503,22 +532,23 @@ public class BiometricService extends SystemService {
return mFaceEnabledOnKeyguard.get(user);
}
- boolean getFaceEnabledForApps(int userId) {
+ public boolean getFaceEnabledForApps(int userId) {
+ Slog.e(TAG, "getFaceEnabledForApps: " + userId, new Exception());
if (!mFaceEnabledForApps.containsKey(userId)) {
onChange(true /* selfChange */, FACE_UNLOCK_APP_ENABLED, userId);
}
return mFaceEnabledForApps.getOrDefault(userId, DEFAULT_APP_ENABLED);
}
- boolean getFaceAlwaysRequireConfirmation(int userId) {
+ public boolean getFaceAlwaysRequireConfirmation(int userId) {
if (!mFaceAlwaysRequireConfirmation.containsKey(userId)) {
onChange(true /* selfChange */, FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, userId);
}
return mFaceAlwaysRequireConfirmation.get(userId);
}
- void notifyEnabledOnKeyguardCallbacks(int userId) {
- List<EnabledOnKeyguardCallback> callbacks = mEnabledOnKeyguardCallbacks;
+ public void notifyEnabledOnKeyguardCallbacks(int userId) {
+ List<EnabledOnKeyguardCallback> callbacks = mCallbacks;
for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).notify(BiometricSourceType.FACE,
mFaceEnabledOnKeyguard.getOrDefault(userId, DEFAULT_KEYGUARD_ENABLED),
@@ -527,7 +557,7 @@ public class BiometricService extends SystemService {
}
}
- private final class EnabledOnKeyguardCallback implements IBinder.DeathRecipient {
+ final class EnabledOnKeyguardCallback implements IBinder.DeathRecipient {
private final IBiometricEnabledOnKeyguardCallback mCallback;
@@ -559,7 +589,8 @@ public class BiometricService extends SystemService {
}
// Wrap the client's receiver so we can do things with the BiometricDialog first
- private final IBiometricServiceReceiverInternal mInternalReceiver =
+ @VisibleForTesting
+ final IBiometricServiceReceiverInternal mInternalReceiver =
new IBiometricServiceReceiverInternal.Stub() {
@Override
public void onAuthenticationSucceeded(boolean requireConfirmation, byte[] token)
@@ -571,10 +602,11 @@ public class BiometricService extends SystemService {
}
@Override
- public void onAuthenticationFailed(int cookie, boolean requireConfirmation)
+ public void onAuthenticationFailed()
throws RemoteException {
String failureReason = getContext().getString(R.string.biometric_not_recognized);
- mHandler.obtainMessage(MSG_ON_AUTHENTICATION_FAILED, failureReason).sendToTarget();
+ Slog.v(TAG, "onAuthenticationFailed: " + failureReason);
+ mHandler.obtainMessage(MSG_ON_AUTHENTICATION_REJECTED, failureReason).sendToTarget();
}
@Override
@@ -583,7 +615,7 @@ public class BiometricService extends SystemService {
// soft errors and we should allow the user to try authenticating again instead of
// dismissing BiometricPrompt.
if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) {
- mHandler.obtainMessage(MSG_ON_AUTHENTICATION_FAILED, message).sendToTarget();
+ mHandler.obtainMessage(MSG_ON_AUTHENTICATION_TIMED_OUT, message).sendToTarget();
} else {
SomeArgs args = SomeArgs.obtain();
args.argi1 = cookie;
@@ -873,6 +905,48 @@ public class BiometricService extends SystemService {
}
}
+ @VisibleForTesting
+ static class Injector {
+ IActivityManager getActivityManagerService() {
+ return ActivityManager.getService();
+ }
+
+ IActivityTaskManager getActivityTaskManagerService() {
+ return ActivityTaskManager.getService();
+ }
+
+ IStatusBarService getStatusBarService() {
+ return IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ }
+
+ IFingerprintService getFingerprintService() {
+ return IFingerprintService.Stub.asInterface(
+ ServiceManager.getService(Context.FINGERPRINT_SERVICE));
+ }
+
+ IFaceService getFaceService() {
+ return IFaceService.Stub.asInterface(ServiceManager.getService(Context.FACE_SERVICE));
+ }
+
+ SettingObserver getSettingObserver(Context context, Handler handler,
+ List<EnabledOnKeyguardCallback> callbacks) {
+ return new SettingObserver(context, handler, callbacks);
+ }
+
+ KeyStore getKeyStore() {
+ return KeyStore.getInstance();
+ }
+
+ boolean isDebugEnabled(Context context, int userId) {
+ return Utils.isDebugEnabled(context, userId);
+ }
+
+ void publishBinderService(BiometricService service, IBiometricService.Stub impl) {
+ service.publishBinderService(Context.BIOMETRIC_SERVICE, impl);
+ }
+ }
+
/**
* Initializes the system service.
* <p>
@@ -883,11 +957,19 @@ public class BiometricService extends SystemService {
* @param context The system server context.
*/
public BiometricService(Context context) {
+ this(context, new Injector());
+ }
+
+ @VisibleForTesting
+ BiometricService(Context context, Injector injector) {
super(context);
+ mInjector = injector;
+ mImpl = new BiometricServiceWrapper();
mAppOps = context.getSystemService(AppOpsManager.class);
mEnabledOnKeyguardCallbacks = new ArrayList<>();
- mSettingObserver = new SettingObserver(mHandler);
+ mSettingObserver = mInjector.getSettingObserver(context, mHandler,
+ mEnabledOnKeyguardCallbacks);
final PackageManager pm = context.getPackageManager();
mHasFeatureFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
@@ -895,7 +977,7 @@ public class BiometricService extends SystemService {
mHasFeatureFace = pm.hasSystemFeature(PackageManager.FEATURE_FACE);
try {
- ActivityManager.getService().registerUserSwitchObserver(
+ injector.getActivityManagerService().registerUserSwitchObserver(
new UserSwitchObserver() {
@Override
public void onUserSwitchComplete(int newUserId) {
@@ -913,17 +995,15 @@ public class BiometricService extends SystemService {
public void onStart() {
// TODO: maybe get these on-demand
if (mHasFeatureFingerprint) {
- mFingerprintService = IFingerprintService.Stub.asInterface(
- ServiceManager.getService(Context.FINGERPRINT_SERVICE));
+ mFingerprintService = mInjector.getFingerprintService();
}
if (mHasFeatureFace) {
- mFaceService = IFaceService.Stub.asInterface(
- ServiceManager.getService(Context.FACE_SERVICE));
+ mFaceService = mInjector.getFaceService();
}
- mActivityTaskManager = ActivityTaskManager.getService();
- mStatusBarService = IStatusBarService.Stub.asInterface(
- ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ mKeyStore = mInjector.getKeyStore();
+ mActivityTaskManager = mInjector.getActivityTaskManagerService();
+ mStatusBarService = mInjector.getStatusBarService();
// Cache the authenticators
for (int i = 0; i < FEATURE_ID.length; i++) {
@@ -934,7 +1014,7 @@ public class BiometricService extends SystemService {
}
}
- publishBinderService(Context.BIOMETRIC_SERVICE, new BiometricServiceWrapper());
+ mInjector.publishBinderService(this, mImpl);
}
/**
@@ -1094,7 +1174,7 @@ public class BiometricService extends SystemService {
mCurrentAuthSession.mRequireConfirmation,
StatsLog.BIOMETRIC_AUTHENTICATED__STATE__CONFIRMED,
latency,
- Utils.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId));
+ mInjector.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId));
} else {
final long latency = System.currentTimeMillis() - mCurrentAuthSession.mStartTimeMs;
@@ -1122,7 +1202,7 @@ public class BiometricService extends SystemService {
BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT,
error,
0 /* vendorCode */,
- Utils.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId),
+ mInjector.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId),
latency);
}
}
@@ -1170,26 +1250,21 @@ public class BiometricService extends SystemService {
}
private void handleAuthenticationSucceeded(boolean requireConfirmation, byte[] token) {
-
try {
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
// after user dismissed/canceled dialog).
if (mCurrentAuthSession == null) {
- Slog.e(TAG, "onAuthenticationSucceeded(): Auth session is null");
+ Slog.e(TAG, "handleAuthenticationSucceeded: Auth session is null");
return;
}
+ // Store the auth token and submit it to keystore after the dialog is confirmed /
+ // animating away.
+ mCurrentAuthSession.mTokenEscrow = token;
if (!requireConfirmation) {
- mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
- KeyStore.getInstance().addAuthToken(token);
- mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded();
- mCurrentAuthSession.mState = STATE_AUTH_IDLE;
- mCurrentAuthSession = null;
+ mCurrentAuthSession.mState = STATE_AUTHENTICATED_PENDING_SYSUI;
} else {
mCurrentAuthSession.mAuthenticatedTimeMs = System.currentTimeMillis();
- // Store the auth token and submit it to keystore after the confirmation
- // button has been pressed.
- mCurrentAuthSession.mTokenEscrow = token;
mCurrentAuthSession.mState = STATE_AUTH_PENDING_CONFIRM;
}
@@ -1201,12 +1276,13 @@ public class BiometricService extends SystemService {
}
}
- private void handleAuthenticationFailed(String failureReason) {
+ private void handleAuthenticationRejected(String failureReason) {
+ Slog.v(TAG, "handleAuthenticationRejected: " + failureReason);
try {
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
// after user dismissed/canceled dialog).
if (mCurrentAuthSession == null) {
- Slog.e(TAG, "onAuthenticationFailed(): Auth session is null");
+ Slog.e(TAG, "handleAuthenticationRejected: Auth session is null");
return;
}
@@ -1225,9 +1301,26 @@ public class BiometricService extends SystemService {
}
}
+ private void handleAuthenticationTimedOut(String message) {
+ Slog.v(TAG, "handleAuthenticationTimedOut: " + message);
+ try {
+ // Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
+ // after user dismissed/canceled dialog).
+ if (mCurrentAuthSession == null) {
+ Slog.e(TAG, "handleAuthenticationTimedOut: Auth session is null");
+ return;
+ }
+
+ mStatusBarService.onBiometricAuthenticated(false, message);
+ mCurrentAuthSession.mState = STATE_AUTH_PAUSED;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception", e);
+ }
+ }
+
private void handleOnConfirmDeviceCredentialSuccess() {
if (mConfirmDeviceCredentialReceiver == null) {
- Slog.w(TAG, "onCDCASuccess null!");
+ Slog.w(TAG, "handleOnConfirmDeviceCredentialSuccess null!");
return;
}
try {
@@ -1245,7 +1338,8 @@ public class BiometricService extends SystemService {
private void handleOnConfirmDeviceCredentialError(int error, String message) {
if (mConfirmDeviceCredentialReceiver == null) {
- Slog.w(TAG, "onCDCAError null! Error: " + error + " " + message);
+ Slog.w(TAG, "handleOnConfirmDeviceCredentialError null! Error: "
+ + error + " " + message);
return;
}
try {
@@ -1272,7 +1366,7 @@ public class BiometricService extends SystemService {
}
private void handleOnError(int cookie, int error, String message) {
- Slog.d(TAG, "Error: " + error + " cookie: " + cookie);
+ Slog.d(TAG, "handleOnError: " + error + " cookie: " + cookie);
// Errors can either be from the current auth session or the pending auth session.
// The pending auth session may receive errors such as ERROR_LOCKOUT before
// it becomes the current auth session. Similarly, the current auth session may
@@ -1302,27 +1396,16 @@ public class BiometricService extends SystemService {
mCurrentAuthSession = null;
mStatusBarService.hideBiometricDialog();
} else {
- // Send errors after the dialog is dismissed.
- mHandler.postDelayed(() -> {
- try {
- if (mCurrentAuthSession != null) {
- mActivityTaskManager.unregisterTaskStackListener(
- mTaskStackListener);
- mCurrentAuthSession.mClientReceiver.onError(error,
- message);
- mCurrentAuthSession.mState = STATE_AUTH_IDLE;
- mCurrentAuthSession = null;
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "Remote exception", e);
- }
- }, BiometricPrompt.HIDE_DIALOG_DELAY);
+ mCurrentAuthSession.mErrorEscrow = error;
+ mCurrentAuthSession.mErrorStringEscrow = message;
+ mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI;
}
} else if (mCurrentAuthSession.mState == STATE_AUTH_PAUSED) {
// In the "try again" state, we should forward canceled errors to
- // the client and and clean up.
+ // the client and and clean up. The only error we should get here is
+ // ERROR_CANCELED due to another client kicking us out.
mCurrentAuthSession.mClientReceiver.onError(error, message);
- mStatusBarService.onBiometricError(message);
+ mStatusBarService.hideBiometricDialog();
mActivityTaskManager.unregisterTaskStackListener(
mTaskStackListener);
mCurrentAuthSession.mState = STATE_AUTH_IDLE;
@@ -1377,35 +1460,44 @@ public class BiometricService extends SystemService {
logDialogDismissed(reason);
try {
- if (reason != BiometricPrompt.DISMISSED_REASON_CONFIRMED) { // TODO: Check
- // Positive button is used by passive modalities as a "confirm" button,
- // do not send to client
- mCurrentAuthSession.mClientReceiver.onDialogDismissed(reason);
- // Cancel authentication. Skip the token/package check since we are cancelling
- // from system server. The interface is permission protected so this is fine.
- cancelInternal(null /* token */, null /* package */, false /* fromClient */);
- }
- if (reason == BiometricPrompt.DISMISSED_REASON_USER_CANCEL) {
- mCurrentAuthSession.mClientReceiver.onError(
- BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED,
- getContext().getString(
- com.android.internal.R.string.biometric_error_user_canceled));
- } else if (reason == BiometricPrompt.DISMISSED_REASON_CONFIRMED) {
- // Have the service send the token to KeyStore, and send onAuthenticated
- // to the application
- KeyStore.getInstance().addAuthToken(mCurrentAuthSession.mTokenEscrow);
- mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded();
- }
+ switch (reason) {
+ case BiometricPrompt.DISMISSED_REASON_CONFIRMED:
+ case BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED:
+ mKeyStore.addAuthToken(mCurrentAuthSession.mTokenEscrow);
+ mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded();
+ break;
- // Do not clean up yet if we are from ConfirmDeviceCredential. We should be in the
- // STATE_BIOMETRIC_AUTH_CANCELED_SHOWING_CDC. The session should only be removed when
- // ConfirmDeviceCredential is confirmed or canceled.
- // TODO(b/123378871): Remove when moved
- if (!mCurrentAuthSession.isFromConfirmDeviceCredential()) {
- mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
- mCurrentAuthSession.mState = STATE_AUTH_IDLE;
- mCurrentAuthSession = null;
+ case BiometricPrompt.DISMISSED_REASON_NEGATIVE:
+ mCurrentAuthSession.mClientReceiver.onDialogDismissed(reason);
+ // Cancel authentication. Skip the token/package check since we are cancelling
+ // from system server. The interface is permission protected so this is fine.
+ cancelInternal(null /* token */, null /* package */, false /* fromClient */);
+ break;
+
+ case BiometricPrompt.DISMISSED_REASON_USER_CANCEL:
+ mCurrentAuthSession.mClientReceiver.onError(
+ BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED,
+ getContext().getString(R.string.biometric_error_user_canceled));
+ // Cancel authentication. Skip the token/package check since we are cancelling
+ // from system server. The interface is permission protected so this is fine.
+ cancelInternal(null /* token */, null /* package */, false /* fromClient */);
+ break;
+
+ case BiometricPrompt.DISMISSED_REASON_ERROR:
+ mCurrentAuthSession.mClientReceiver.onError(mCurrentAuthSession.mErrorEscrow,
+ mCurrentAuthSession.mErrorStringEscrow);
+ break;
+
+ default:
+ Slog.w(TAG, "Unhandled reason: " + reason);
+ break;
}
+
+ // Dialog is gone, auth session is done.
+ mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
+ mCurrentAuthSession.mState = STATE_AUTH_IDLE;
+ mCurrentAuthSession = null;
+
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
@@ -1517,8 +1609,6 @@ public class BiometricService extends SystemService {
return;
}
- mCurrentModality = modality;
-
// Start preparing for authentication. Authentication starts when
// all modalities requested have invoked onReadyForAuthentication.
authenticateInternal(token, sessionId, userId, receiver, opPackageName, bundle,
@@ -1637,25 +1727,28 @@ public class BiometricService extends SystemService {
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
final int callingUserId = UserHandle.getCallingUserId();
- mHandler.post(() -> {
- try {
- // TODO: For multiple modalities, send a single ERROR_CANCELED only when all
- // drivers have canceled authentication.
- if ((mCurrentModality & TYPE_FINGERPRINT) != 0) {
- mFingerprintService.cancelAuthenticationFromService(token, opPackageName,
- callingUid, callingPid, callingUserId, fromClient);
- }
- if ((mCurrentModality & TYPE_IRIS) != 0) {
- Slog.w(TAG, "Iris unsupported");
- }
- if ((mCurrentModality & TYPE_FACE) != 0) {
- mFaceService.cancelAuthenticationFromService(token, opPackageName,
- callingUid, callingPid, callingUserId, fromClient);
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "Unable to cancel authentication");
+
+ try {
+ if (mCurrentAuthSession.mState != STATE_AUTH_STARTED) {
+ Slog.w(TAG, "Skipping cancelInternal, state: " + mCurrentAuthSession.mState);
+ return;
}
- });
- }
+ // TODO: For multiple modalities, send a single ERROR_CANCELED only when all
+ // drivers have canceled authentication.
+ if ((mCurrentAuthSession.mModality & TYPE_FINGERPRINT) != 0) {
+ mFingerprintService.cancelAuthenticationFromService(token, opPackageName,
+ callingUid, callingPid, callingUserId, fromClient);
+ }
+ if ((mCurrentAuthSession.mModality & TYPE_IRIS) != 0) {
+ Slog.w(TAG, "Iris unsupported");
+ }
+ if ((mCurrentAuthSession.mModality & TYPE_FACE) != 0) {
+ mFaceService.cancelAuthenticationFromService(token, opPackageName,
+ callingUid, callingPid, callingUserId, fromClient);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to cancel authentication");
+ }
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/BiometricServiceBase.java b/services/core/java/com/android/server/biometrics/BiometricServiceBase.java
index f3f9754bd32b..2de18c391d4a 100644
--- a/services/core/java/com/android/server/biometrics/BiometricServiceBase.java
+++ b/services/core/java/com/android/server/biometrics/BiometricServiceBase.java
@@ -420,7 +420,7 @@ public abstract class BiometricServiceBase extends SystemService
throw new UnsupportedOperationException("Stub!");
}
- default void onAuthenticationFailedInternal(int cookie, boolean requireConfirmation)
+ default void onAuthenticationFailedInternal()
throws RemoteException {
throw new UnsupportedOperationException("Stub!");
}
@@ -457,10 +457,10 @@ public abstract class BiometricServiceBase extends SystemService
}
@Override
- public void onAuthenticationFailedInternal(int cookie, boolean requireConfirmation)
+ public void onAuthenticationFailedInternal()
throws RemoteException {
if (getWrapperReceiver() != null) {
- getWrapperReceiver().onAuthenticationFailed(cookie, requireConfirmation);
+ getWrapperReceiver().onAuthenticationFailed();
}
}
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
new file mode 100644
index 000000000000..233db9a293ea
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -0,0 +1,742 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.app.IActivityTaskManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
+import android.hardware.face.FaceManager;
+import android.hardware.face.IFaceService;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.IFingerprintService;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.security.KeyStore;
+
+import com.android.internal.R;
+import com.android.internal.statusbar.IStatusBarService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+@SmallTest
+public class BiometricServiceTest {
+
+ private static final String TAG = "BiometricServiceTest";
+
+ private static final String ERROR_HW_UNAVAILABLE = "hw_unavailable";
+ private static final String ERROR_NOT_RECOGNIZED = "not_recognized";
+ private static final String ERROR_TIMEOUT = "error_timeout";
+ private static final String ERROR_CANCELED = "error_canceled";
+ private static final String ERROR_UNABLE_TO_PROCESS = "error_unable_to_process";
+ private static final String ERROR_USER_CANCELED = "error_user_canceled";
+
+ private static final String FINGERPRINT_ACQUIRED_SENSOR_DIRTY = "sensor_dirty";
+
+ private BiometricService mBiometricService;
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private ContentResolver mContentResolver;
+ @Mock
+ private Resources mResources;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private AppOpsManager mAppOpsManager;
+ @Mock
+ IBiometricServiceReceiver mReceiver1;
+ @Mock
+ IBiometricServiceReceiver mReceiver2;
+ @Mock
+ FingerprintManager mFingerprintManager;
+ @Mock
+ FaceManager mFaceManager;
+
+ private static class MockInjector extends BiometricService.Injector {
+ @Override
+ IActivityManager getActivityManagerService() {
+ return mock(IActivityManager.class);
+ }
+
+ @Override
+ IActivityTaskManager getActivityTaskManagerService() {
+ return mock(IActivityTaskManager.class);
+ }
+
+ @Override
+ IStatusBarService getStatusBarService() {
+ return mock(IStatusBarService.class);
+ }
+
+ @Override
+ IFingerprintService getFingerprintService() {
+ return mock(IFingerprintService.class);
+ }
+
+ @Override
+ IFaceService getFaceService() {
+ return mock(IFaceService.class);
+ }
+
+ @Override
+ BiometricService.SettingObserver getSettingObserver(Context context, Handler handler,
+ List<BiometricService.EnabledOnKeyguardCallback> callbacks) {
+ return mock(BiometricService.SettingObserver.class);
+ }
+
+ @Override
+ KeyStore getKeyStore() {
+ return mock(KeyStore.class);
+ }
+
+ @Override
+ boolean isDebugEnabled(Context context, int userId) {
+ return false;
+ }
+
+ @Override
+ void publishBinderService(BiometricService service, IBiometricService.Stub impl) {
+ // no-op for test
+ }
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
+ when(mContext.getSystemService(Context.FINGERPRINT_SERVICE))
+ .thenReturn(mFingerprintManager);
+ when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager);
+ when(mContext.getContentResolver()).thenReturn(mContentResolver);
+ when(mContext.getResources()).thenReturn(mResources);
+
+ when(mResources.getString(R.string.biometric_error_hw_unavailable))
+ .thenReturn(ERROR_HW_UNAVAILABLE);
+ when(mResources.getString(R.string.biometric_not_recognized))
+ .thenReturn(ERROR_NOT_RECOGNIZED);
+ when(mResources.getString(R.string.biometric_error_user_canceled))
+ .thenReturn(ERROR_USER_CANCELED);
+ }
+
+ @Test
+ public void testAuthenticate_withoutHardware_returnsErrorHardwareNotPresent() throws Exception {
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)).thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false);
+
+ mBiometricService = new BiometricService(mContext, new MockInjector());
+ mBiometricService.onStart();
+
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */);
+ waitForIdle();
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_HW_NOT_PRESENT), eq(ERROR_HW_UNAVAILABLE));
+ }
+
+ @Test
+ public void testAuthenticate_withoutEnrolled_returnsErrorNoBiometrics() throws Exception {
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true);
+ when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+
+ mBiometricService = new BiometricService(mContext, new MockInjector());
+ mBiometricService.onStart();
+
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */);
+ waitForIdle();
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS), any());
+ }
+
+ @Test
+ public void testAuthenticate_whenHalIsDead_returnsErrorHardwareUnavailable() throws Exception {
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+
+ mBiometricService = new BiometricService(mContext, new MockInjector());
+ mBiometricService.onStart();
+
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */);
+ waitForIdle();
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE));
+ }
+
+ @Test
+ public void testAuthenticateFace_respectsUserSetting()
+ throws Exception {
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+ mBiometricService = new BiometricService(mContext, new MockInjector());
+ mBiometricService.onStart();
+
+ // Disabled in user settings receives onError
+ when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(false);
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */);
+ waitForIdle();
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE));
+
+ // Enrolled, not disabled in settings, user requires confirmation in settings
+ resetReceiver();
+ when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(true);
+ when(mBiometricService.mSettingObserver.getFaceAlwaysRequireConfirmation(anyInt()))
+ .thenReturn(true);
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */);
+ waitForIdle();
+ verify(mReceiver1, never()).onError(anyInt(), any(String.class));
+ verify(mBiometricService.mFaceService, times(1)).prepareForAuthentication(
+ eq(true) /* requireConfirmation */,
+ any(IBinder.class),
+ anyLong() /* sessionId */,
+ anyInt() /* userId */,
+ any(IBiometricServiceReceiverInternal.class),
+ anyString() /* opPackageName */,
+ anyInt() /* cookie */,
+ anyInt() /* callingUid */,
+ anyInt() /* callingPid */,
+ anyInt() /* callingUserId */);
+
+ // Enrolled, not disabled in settings, user doesn't require confirmation in settings
+ resetReceiver();
+ when(mBiometricService.mSettingObserver.getFaceAlwaysRequireConfirmation(anyInt()))
+ .thenReturn(false);
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */);
+ waitForIdle();
+ verify(mBiometricService.mFaceService, times(1)).prepareForAuthentication(
+ eq(false) /* requireConfirmation */,
+ any(IBinder.class),
+ anyLong() /* sessionId */,
+ anyInt() /* userId */,
+ any(IBiometricServiceReceiverInternal.class),
+ anyString() /* opPackageName */,
+ anyInt() /* cookie */,
+ anyInt() /* callingUid */,
+ anyInt() /* callingPid */,
+ anyInt() /* callingUserId */);
+ }
+
+ @Test
+ public void testAuthenticate_happyPathWithoutConfirmation() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+ mBiometricService = new BiometricService(mContext, new MockInjector());
+ mBiometricService.onStart();
+
+ // Start testing the happy path
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */);
+ waitForIdle();
+
+ // Creates a pending auth session with the correct initial states
+ assertEquals(mBiometricService.mPendingAuthSession.mState,
+ BiometricService.STATE_AUTH_CALLED);
+
+ // Invokes <Modality>Service#prepareForAuthentication
+ ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mReceiver1, never()).onError(anyInt(), any(String.class));
+ verify(mBiometricService.mFingerprintService, times(1)).prepareForAuthentication(
+ any(IBinder.class),
+ anyLong() /* sessionId */,
+ anyInt() /* userId */,
+ any(IBiometricServiceReceiverInternal.class),
+ anyString() /* opPackageName */,
+ cookieCaptor.capture() /* cookie */,
+ anyInt() /* callingUid */,
+ anyInt() /* callingPid */,
+ anyInt() /* callingUserId */);
+
+ // onReadyForAuthentication, mCurrentAuthSession state OK
+ mBiometricService.mImpl.onReadyForAuthentication(cookieCaptor.getValue(),
+ anyBoolean() /* requireConfirmation */, anyInt() /* userId */);
+ waitForIdle();
+ assertNull(mBiometricService.mPendingAuthSession);
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_STARTED);
+
+ // startPreparedClient invoked
+ verify(mBiometricService.mFingerprintService, times(1))
+ .startPreparedClient(cookieCaptor.getValue());
+
+ // StatusBar showBiometricDialog invoked
+ verify(mBiometricService.mStatusBarService, times(1)).showBiometricDialog(
+ eq(mBiometricService.mCurrentAuthSession.mBundle),
+ any(IBiometricServiceReceiverInternal.class),
+ eq(BiometricAuthenticator.TYPE_FINGERPRINT),
+ anyBoolean() /* requireConfirmation */,
+ anyInt() /* userId */);
+
+ // Hardware authenticated
+ mBiometricService.mInternalReceiver.onAuthenticationSucceeded(
+ false /* requireConfirmation */,
+ new byte[69] /* HAT */);
+ waitForIdle();
+ // Waiting for SystemUI to send dismissed callback
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTHENTICATED_PENDING_SYSUI);
+ // Notify SystemUI hardware authenticated
+ verify(mBiometricService.mStatusBarService, times(1)).onBiometricAuthenticated(
+ eq(true) /* authenticated */, eq(null) /* failureReason */);
+
+ // SystemUI sends callback with dismissed reason
+ mBiometricService.mInternalReceiver.onDialogDismissed(
+ BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
+ waitForIdle();
+ // HAT sent to keystore
+ verify(mBiometricService.mKeyStore, times(1)).addAuthToken(any(byte[].class));
+ // Send onAuthenticated to client
+ verify(mReceiver1, times(1)).onAuthenticationSucceeded();
+ // Current session becomes null
+ assertNull(mBiometricService.mCurrentAuthSession);
+ }
+
+ @Test
+ public void testAuthenticate_happyPathWithConfirmation() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FACE);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ true /* requireConfirmation */);
+
+ // Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not
+ // sent to KeyStore yet
+ mBiometricService.mInternalReceiver.onAuthenticationSucceeded(
+ true /* requireConfirmation */,
+ new byte[69] /* HAT */);
+ waitForIdle();
+ // Waiting for SystemUI to send confirmation callback
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_PENDING_CONFIRM);
+ verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
+
+ // SystemUI sends confirm, HAT is sent to keystore and client is notified.
+ mBiometricService.mInternalReceiver.onDialogDismissed(
+ BiometricPrompt.DISMISSED_REASON_CONFIRMED);
+ waitForIdle();
+ verify(mBiometricService.mKeyStore, times(1)).addAuthToken(any(byte[].class));
+ verify(mReceiver1, times(1)).onAuthenticationSucceeded();
+ }
+
+ @Test
+ public void testRejectFace_whenAuthenticating_notifiesSystemUIAndClient_thenPaused()
+ throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FACE);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onAuthenticationFailed();
+ waitForIdle();
+
+ verify(mBiometricService.mStatusBarService, times(1))
+ .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED));
+ verify(mReceiver1, times(1)).onAuthenticationFailed();
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_PAUSED);
+ }
+
+ @Test
+ public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating()
+ throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onAuthenticationFailed();
+ waitForIdle();
+
+ verify(mBiometricService.mStatusBarService, times(1))
+ .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED));
+ verify(mReceiver1, times(1)).onAuthenticationFailed();
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_STARTED);
+ }
+
+ @Test
+ public void testErrorCanceled_whenAuthenticating_notifiesSystemUIAndClient() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ // Create a new pending auth session but don't start it yet. HAL contract is that previous
+ // one must get ERROR_CANCELED. Simulate that here by creating the pending auth session,
+ // sending ERROR_CANCELED to the current auth session, and then having the second one
+ // onReadyForAuthentication.
+ invokeAuthenticate(mBiometricService.mImpl, mReceiver2, false /* requireConfirmation */);
+ waitForIdle();
+
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_STARTED);
+ mBiometricService.mInternalReceiver.onError(
+ getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
+ BiometricConstants.BIOMETRIC_ERROR_CANCELED, ERROR_CANCELED);
+ waitForIdle();
+
+ // Auth session becomes null
+ assertNull(mBiometricService.mCurrentAuthSession);
+
+ // ERROR_CANCELED sent to first client
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(ERROR_CANCELED));
+ verify(mReceiver2, never()).onError(anyInt(), any(String.class));
+
+ // SystemUI dialog closed
+ verify(mBiometricService.mStatusBarService, times(1)).hideBiometricDialog();
+ }
+
+ @Test
+ public void testErrorHalTimeout_whenAuthenticating_entersPausedState() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FACE);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onError(
+ getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
+ BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
+ ERROR_TIMEOUT);
+ waitForIdle();
+
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_PAUSED);
+ verify(mBiometricService.mStatusBarService, times(1))
+ .onBiometricAuthenticated(eq(false), eq(ERROR_TIMEOUT));
+ // Timeout does not count as fail as per BiometricPrompt documentation.
+ verify(mReceiver1, never()).onAuthenticationFailed();
+
+ // No pending auth session. Pressing try again will create one.
+ assertNull(mBiometricService.mPendingAuthSession);
+
+ // Pressing "Try again" on SystemUI starts a new auth session.
+ mBiometricService.mInternalReceiver.onTryAgainPressed();
+ waitForIdle();
+
+ // The last one is still paused, and a new one has been created.
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_PAUSED);
+ assertEquals(mBiometricService.mPendingAuthSession.mState,
+ BiometricService.STATE_AUTH_CALLED);
+
+ // Test resuming when hardware becomes ready. SystemUI should not be requested to
+ // show another dialog since it's already showing.
+ resetStatusBar();
+ startPendingAuthSession(mBiometricService);
+ waitForIdle();
+ verify(mBiometricService.mStatusBarService, never()).showBiometricDialog(
+ any(Bundle.class),
+ any(IBiometricServiceReceiverInternal.class),
+ anyInt(),
+ anyBoolean() /* requireConfirmation */,
+ anyInt() /* userId */);
+ }
+
+ @Test
+ public void testErrorFromHal_whenPaused_notifiesSystemUIAndClient() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FACE);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireCOnfirmation */);
+
+ mBiometricService.mInternalReceiver.onError(
+ getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
+ BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
+ ERROR_TIMEOUT);
+ mBiometricService.mInternalReceiver.onError(
+ getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
+ BiometricConstants.BIOMETRIC_ERROR_CANCELED,
+ ERROR_CANCELED);
+ waitForIdle();
+
+ // Client receives error immediately
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
+ eq(ERROR_CANCELED));
+ // Dialog is hidden immediately
+ verify(mBiometricService.mStatusBarService, times(1)).hideBiometricDialog();
+ // Auth session is over
+ assertNull(mBiometricService.mCurrentAuthSession);
+ }
+
+ @Test
+ public void testErrorFromHal_whileAuthenticating_waitsForSysUIBeforeNotifyingClient()
+ throws Exception {
+ // For errors that show in SystemUI, BiometricService stays in STATE_ERROR_PENDING_SYSUI
+ // until SystemUI notifies us that the dialog is dismissed at which point the current
+ // session is done.
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onError(
+ getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
+ BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
+ ERROR_UNABLE_TO_PROCESS);
+ waitForIdle();
+
+ // Sends error to SystemUI and does not notify client yet
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_ERROR_PENDING_SYSUI);
+ verify(mBiometricService.mStatusBarService, times(1))
+ .onBiometricError(eq(ERROR_UNABLE_TO_PROCESS));
+ verify(mBiometricService.mStatusBarService, never()).hideBiometricDialog();
+ verify(mReceiver1, never()).onError(anyInt(), anyString());
+
+ // SystemUI animation completed, client is notified, auth session is over
+ mBiometricService.mInternalReceiver
+ .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR);
+ waitForIdle();
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
+ eq(ERROR_UNABLE_TO_PROCESS));
+ assertNull(mBiometricService.mCurrentAuthSession);
+ }
+
+ @Test
+ public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication()
+ throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver
+ .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
+ waitForIdle();
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
+ eq(ERROR_USER_CANCELED));
+ verify(mBiometricService.mFingerprintService, times(1)).cancelAuthenticationFromService(
+ any(),
+ any(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ eq(false) /* fromClient */);
+ assertNull(mBiometricService.mCurrentAuthSession);
+ }
+
+ @Test
+ public void testDismissedReasonNegative_whilePaused_doesntInvokeHalCancel() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FACE);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onError(
+ getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
+ BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
+ ERROR_TIMEOUT);
+ mBiometricService.mInternalReceiver.onDialogDismissed(
+ BiometricPrompt.DISMISSED_REASON_NEGATIVE);
+ waitForIdle();
+
+ verify(mBiometricService.mFaceService, never()).cancelAuthenticationFromService(
+ any(),
+ any(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyBoolean());
+ }
+
+ @Test
+ public void testDismissedReasonUserCancel_whilePaused_doesntInvokeHalCancel() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FACE);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onError(
+ getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
+ BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
+ ERROR_TIMEOUT);
+ mBiometricService.mInternalReceiver.onDialogDismissed(
+ BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
+ waitForIdle();
+
+ verify(mBiometricService.mFaceService, never()).cancelAuthenticationFromService(
+ any(),
+ any(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyBoolean());
+ }
+
+ @Test
+ public void testDismissedReasonUserCancel_whenPendingConfirmation() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FACE);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ true /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onAuthenticationSucceeded(
+ true /* requireConfirmation */,
+ new byte[69] /* HAT */);
+ mBiometricService.mInternalReceiver.onDialogDismissed(
+ BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
+ waitForIdle();
+
+ // doesn't send cancel to HAL
+ verify(mBiometricService.mFaceService, never()).cancelAuthenticationFromService(
+ any(),
+ any(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyBoolean());
+ verify(mReceiver1, times(1)).onError(
+ eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
+ eq(ERROR_USER_CANCELED));
+ assertNull(mBiometricService.mCurrentAuthSession);
+ }
+
+ @Test
+ public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception {
+ setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
+ invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
+ false /* requireConfirmation */);
+
+ mBiometricService.mInternalReceiver.onAcquired(
+ FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
+ FINGERPRINT_ACQUIRED_SENSOR_DIRTY);
+ waitForIdle();
+
+ // Sends to SysUI and stays in authenticating state
+ verify(mBiometricService.mStatusBarService, times(1))
+ .onBiometricHelp(eq(FINGERPRINT_ACQUIRED_SENSOR_DIRTY));
+ assertEquals(mBiometricService.mCurrentAuthSession.mState,
+ BiometricService.STATE_AUTH_STARTED);
+ }
+
+ // Helper methods
+
+ private void setupAuthForOnly(int modality) {
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false);
+
+ if (modality == BiometricAuthenticator.TYPE_FINGERPRINT) {
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+ } else if (modality == BiometricAuthenticator.TYPE_FACE) {
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.isHardwareDetected()).thenReturn(true);
+ } else {
+ fail("Unknown modality: " + modality);
+ }
+
+ mBiometricService = new BiometricService(mContext, new MockInjector());
+ mBiometricService.onStart();
+
+ when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(true);
+ }
+
+ private void resetReceiver() {
+ mReceiver1 = mock(IBiometricServiceReceiver.class);
+ mReceiver2 = mock(IBiometricServiceReceiver.class);
+ }
+
+ private void resetStatusBar() {
+ mBiometricService.mStatusBarService = mock(IStatusBarService.class);
+ }
+
+ private void invokeAuthenticateAndStart(IBiometricService.Stub service,
+ IBiometricServiceReceiver receiver, boolean requireConfirmation) throws Exception {
+ // Request auth, creates a pending session
+ invokeAuthenticate(service, receiver, requireConfirmation);
+ waitForIdle();
+
+ startPendingAuthSession(mBiometricService);
+ waitForIdle();
+ }
+
+ private static void startPendingAuthSession(BiometricService service) throws Exception {
+ // Get the cookie so we can pretend the hardware is ready to authenticate
+ // Currently we only support single modality per auth
+ assertEquals(service.mPendingAuthSession.mModalitiesWaiting.values().size(), 1);
+ final int cookie = service.mPendingAuthSession.mModalitiesWaiting.values()
+ .iterator().next();
+ assertNotEquals(cookie, 0);
+
+ service.mImpl.onReadyForAuthentication(cookie,
+ anyBoolean() /* requireConfirmation */, anyInt() /* userId */);
+ }
+
+ private static void invokeAuthenticate(IBiometricService.Stub service,
+ IBiometricServiceReceiver receiver, boolean requireConfirmation) throws Exception {
+ service.authenticate(
+ new Binder() /* token */,
+ 0 /* sessionId */,
+ 0 /* userId */,
+ receiver,
+ "test" /* packageName */,
+ createTestBiometricPromptBundle(requireConfirmation),
+ null /* IBiometricConfirmDeviceCredentialCallback */);
+ }
+
+ private static Bundle createTestBiometricPromptBundle(boolean requireConfirmation) {
+ final Bundle bundle = new Bundle();
+ bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, requireConfirmation);
+ return bundle;
+ }
+
+ private static int getCookieForCurrentSession(BiometricService.AuthSession session) {
+ assertEquals(session.mModalitiesMatched.values().size(), 1);
+ return session.mModalitiesMatched.values().iterator().next();
+ }
+
+ private static void waitForIdle() {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+}