diff options
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(); + } +} |