diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 1142a07bc66c2..fb6b231632f19 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -100,9 +100,12 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan /** * @hide */ - public static final int DISMISSED_REASON_POSITIVE = 1; + 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; @@ -112,6 +115,25 @@ 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; + + /** + * Dialog dismissal requested by BiometricService. + * @hide + */ + public static final int DISMISSED_REASON_SERVER_REQUESTED = 6; + private static class ButtonInfo { Executor executor; DialogInterface.OnClickListener listener; @@ -362,7 +384,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan @Override public void onDialogDismissed(int reason) throws RemoteException { // Check the reason and invoke OnClickListener(s) if necessary - if (reason == DISMISSED_REASON_POSITIVE) { + if (reason == DISMISSED_REASON_CONFIRMED) { mPositiveButtonInfo.executor.execute(() -> { mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE); }); diff --git a/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl index 180daaf97adaa..ca6114e4d8429 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/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index cf0394d466ead..9441825a1ed6d 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -153,7 +153,7 @@ oneway interface IStatusBar // Used to show the dialog when BiometricService starts authentication void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, - boolean requireConfirmation, int userId); + boolean requireConfirmation, int userId, String opPackageName); // Used to hide the dialog when a biometric is authenticated void onBiometricAuthenticated(boolean authenticated, String failureReason); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 85ae18e867934..4c3a177a013b7 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -101,7 +101,7 @@ interface IStatusBarService // Used to show the dialog when BiometricService starts authentication void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, - boolean requireConfirmation, int userId); + boolean requireConfirmation, int userId, String opPackageName); // Used to hide the dialog when a biometric is authenticated void onBiometricAuthenticated(boolean authenticated, String failureReason); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java new file mode 100644 index 0000000000000..d4baefd645123 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java @@ -0,0 +1,108 @@ +/* + * 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.systemui.biometrics; + +import android.hardware.biometrics.BiometricPrompt; +import android.os.Bundle; +import android.view.WindowManager; + +import com.android.systemui.biometrics.ui.BiometricDialogView; + +/** + * Interface for the biometric dialog UI. + */ +public interface BiometricDialog { + + // TODO: Clean up save/restore state + String[] KEYS_TO_BACKUP = { + BiometricPrompt.KEY_TITLE, + BiometricPrompt.KEY_USE_DEFAULT_TITLE, + BiometricPrompt.KEY_SUBTITLE, + BiometricPrompt.KEY_DESCRIPTION, + BiometricPrompt.KEY_POSITIVE_TEXT, + BiometricPrompt.KEY_NEGATIVE_TEXT, + BiometricPrompt.KEY_REQUIRE_CONFIRMATION, + BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, + BiometricPrompt.KEY_FROM_CONFIRM_DEVICE_CREDENTIAL, + + BiometricDialogView.KEY_TRY_AGAIN_VISIBILITY, + BiometricDialogView.KEY_CONFIRM_VISIBILITY, + BiometricDialogView.KEY_CONFIRM_ENABLED, + BiometricDialogView.KEY_STATE, + BiometricDialogView.KEY_ERROR_TEXT_VISIBILITY, + BiometricDialogView.KEY_ERROR_TEXT_STRING, + BiometricDialogView.KEY_ERROR_TEXT_IS_TEMPORARY, + BiometricDialogView.KEY_ERROR_TEXT_COLOR, + }; + + /** + * Show the dialog. + * @param wm + * @param skipIntroAnimation + */ + void show(WindowManager wm, boolean skipIntroAnimation); + + /** + * Dismiss the dialog without sending a callback. + */ + void dismissWithoutCallback(boolean animate); + + /** + * Dismiss the dialog. Animate away. + */ + void dismissFromSystemServer(); + + /** + * Biometric authenticated. May be pending user confirmation, or completed. + */ + void onAuthenticationSucceeded(); + + /** + * Authentication failed (reject, timeout). Dialog stays showing. + * @param failureReason + */ + void onAuthenticationFailed(String failureReason); + + /** + * Authentication rejected, or help message received. + * @param help + */ + void onHelp(String help); + + /** + * Authentication failed. Dialog going away. + * @param error + */ + void onError(String error); + + /** + * Save the current state. + * @param outState + */ + void onSaveState(Bundle outState); + + /** + * Restore a previous state. + * @param savedState + */ + void restoreState(Bundle savedState); + + /** + * Get the client's package name + */ + String getOpPackageName(); +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java index e66a8fa96298d..a8e572216315d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java @@ -16,136 +16,155 @@ package com.android.systemui.biometrics; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; +import android.app.TaskStackListener; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.IBiometricServiceReceiverInternal; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.WindowManager; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; -import com.android.systemui.Dependency; import com.android.systemui.SystemUI; -import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.biometrics.ui.BiometricDialogView; import com.android.systemui.statusbar.CommandQueue; +import java.util.List; + /** - * Receives messages sent from AuthenticationClient and shows the appropriate biometric UI (e.g. - * BiometricDialogView). + * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the + * appropriate biometric UI (e.g. BiometricDialogView). */ -public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks { +public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks, + DialogViewCallback { private static final String TAG = "BiometricDialogImpl"; private static final boolean DEBUG = true; - private static final int MSG_SHOW_DIALOG = 1; - private static final int MSG_BIOMETRIC_AUTHENTICATED = 2; - private static final int MSG_BIOMETRIC_HELP = 3; - private static final int MSG_BIOMETRIC_ERROR = 4; - private static final int MSG_HIDE_DIALOG = 5; - private static final int MSG_BUTTON_NEGATIVE = 6; - private static final int MSG_USER_CANCELED = 7; - private static final int MSG_BUTTON_POSITIVE = 8; - private static final int MSG_TRY_AGAIN_PRESSED = 9; + private final Injector mInjector; + // TODO: These should just be saved from onSaveState private SomeArgs mCurrentDialogArgs; - private BiometricDialogView mCurrentDialog; + @VisibleForTesting + BiometricDialog mCurrentDialog; + + private Handler mHandler = new Handler(Looper.getMainLooper()); private WindowManager mWindowManager; - private IBiometricServiceReceiverInternal mReceiver; - private boolean mDialogShowing; - private Callback mCallback = new Callback(); - private WakefulnessLifecycle mWakefulnessLifecycle; + @VisibleForTesting + IActivityTaskManager mActivityTaskManager; + @VisibleForTesting + BiometricTaskStackListener mTaskStackListener; + @VisibleForTesting + IBiometricServiceReceiverInternal mReceiver; - private Handler mHandler = new Handler(Looper.getMainLooper()) { + public class BiometricTaskStackListener extends TaskStackListener { @Override - public void handleMessage(Message msg) { - switch(msg.what) { - case MSG_SHOW_DIALOG: - handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */, - null /* savedState */); - break; - case MSG_BIOMETRIC_AUTHENTICATED: { - SomeArgs args = (SomeArgs) msg.obj; - handleBiometricAuthenticated((boolean) args.arg1 /* authenticated */, - (String) args.arg2 /* failureReason */); - args.recycle(); - break; - } - case MSG_BIOMETRIC_HELP: { - SomeArgs args = (SomeArgs) msg.obj; - handleBiometricHelp((String) args.arg1 /* message */); - args.recycle(); - break; - } - case MSG_BIOMETRIC_ERROR: - handleBiometricError((String) msg.obj); - break; - case MSG_HIDE_DIALOG: - handleHideDialog((Boolean) msg.obj); - break; - case MSG_BUTTON_NEGATIVE: - handleButtonNegative(); - break; - case MSG_USER_CANCELED: - handleUserCanceled(); - break; - case MSG_BUTTON_POSITIVE: - handleButtonPositive(); - break; - case MSG_TRY_AGAIN_PRESSED: - handleTryAgainPressed(); - break; - default: - Log.w(TAG, "Unknown message: " + msg.what); - break; - } - } - }; - - private class Callback implements DialogViewCallback { - @Override - public void onUserCanceled() { - mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget(); - } - - @Override - public void onErrorShown() { - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE_DIALOG, - false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY); - } - - @Override - public void onNegativePressed() { - mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget(); - } - - @Override - public void onPositivePressed() { - mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget(); - } - - @Override - public void onTryAgainPressed() { - mHandler.obtainMessage(MSG_TRY_AGAIN_PRESSED).sendToTarget(); + public void onTaskStackChanged() { + mHandler.post(mTaskStackChangedRunnable); } } - final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { - @Override - public void onStartedGoingToSleep() { - if (mDialogShowing) { - if (DEBUG) Log.d(TAG, "User canceled due to screen off"); - mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget(); + private final Runnable mTaskStackChangedRunnable = () -> { + if (mCurrentDialog != null) { + try { + final String clientPackage = mCurrentDialog.getOpPackageName(); + Log.w(TAG, "Task stack changed, current client: " + clientPackage); + final List runningTasks = + mActivityTaskManager.getTasks(1); + if (!runningTasks.isEmpty()) { + final String topPackage = runningTasks.get(0).topActivity.getPackageName(); + if (!topPackage.contentEquals(clientPackage)) { + Log.w(TAG, "Evicting client due to: " + topPackage); + mCurrentDialog.dismissWithoutCallback(true /* animate */); + mCurrentDialog = null; + mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); + mReceiver = null; + } + } + } catch (RemoteException e) { + Log.e(TAG, "Remote exception", e); } } }; + @Override + public void onTryAgainPressed() { + try { + mReceiver.onTryAgainPressed(); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException when handling try again", e); + } + } + + @Override + public void onDismissed(@DismissedReason int reason) { + switch (reason) { + case DialogViewCallback.DISMISSED_USER_CANCELED: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); + break; + + case DialogViewCallback.DISMISSED_BUTTON_NEGATIVE: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE); + break; + + case DialogViewCallback.DISMISSED_BUTTON_POSITIVE: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED); + break; + + case DialogViewCallback.DISMISSED_AUTHENTICATED: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + break; + + case DialogViewCallback.DISMISSED_ERROR: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR); + break; + + case DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED); + break; + + default: + Log.e(TAG, "Unhandled reason: " + reason); + break; + } + } + + private void sendResultAndCleanUp(@DismissedReason int reason) { + if (mReceiver == null) { + Log.e(TAG, "Receiver is null"); + return; + } + try { + mReceiver.onDialogDismissed(reason); + } catch (RemoteException e) { + Log.w(TAG, "Remote exception", e); + } + onDialogDismissed(reason); + } + + public static class Injector { + IActivityTaskManager getActivityTaskManager() { + return ActivityTaskManager.getService(); + } + } + + public BiometricDialogImpl() { + this(new Injector()); + } + + @VisibleForTesting + BiometricDialogImpl(Injector injector) { + mInjector = injector; + } + @Override public void start() { final PackageManager pm = mContext.getPackageManager(); @@ -154,30 +173,38 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) { getComponent(CommandQueue.class).addCallback(this); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); - mWakefulnessLifecycle.addObserver(mWakefulnessObserver); + mActivityTaskManager = mInjector.getActivityTaskManager(); + + try { + mTaskStackListener = new BiometricTaskStackListener(); + mActivityTaskManager.registerTaskStackListener(mTaskStackListener); + } catch (RemoteException e) { + Log.w(TAG, "Unable to register task stack listener", e); + } } } @Override public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, - int type, boolean requireConfirmation, int userId) { + int type, boolean requireConfirmation, int userId, String opPackageName) { if (DEBUG) { Log.d(TAG, "showBiometricDialog, type: " + type + ", requireConfirmation: " + requireConfirmation); } - // Remove these messages as they are part of the previous client - mHandler.removeMessages(MSG_BIOMETRIC_ERROR); - mHandler.removeMessages(MSG_BIOMETRIC_HELP); - mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED); - mHandler.removeMessages(MSG_HIDE_DIALOG); SomeArgs args = SomeArgs.obtain(); args.arg1 = bundle; args.arg2 = receiver; args.argi1 = type; args.arg3 = requireConfirmation; args.argi2 = userId; - mHandler.obtainMessage(MSG_SHOW_DIALOG, args).sendToTarget(); + args.arg4 = opPackageName; + + boolean skipAnimation = false; + if (mCurrentDialog != null) { + Log.w(TAG, "mCurrentDialog: " + mCurrentDialog); + skipAnimation = true; + } + showDialog(args, skipAnimation, null /* savedState */); } @Override @@ -185,185 +212,111 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: " + authenticated + " reason: " + failureReason); - SomeArgs args = SomeArgs.obtain(); - args.arg1 = authenticated; - args.arg2 = failureReason; - mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget(); - } - - @Override - public void onBiometricHelp(String message) { - if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message); - SomeArgs args = SomeArgs.obtain(); - args.arg1 = message; - mHandler.obtainMessage(MSG_BIOMETRIC_HELP, args).sendToTarget(); - } - - @Override - public void onBiometricError(String error) { - if (DEBUG) Log.d(TAG, "onBiometricError: " + error); - mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget(); - } - - @Override - public void hideBiometricDialog() { - if (DEBUG) Log.d(TAG, "hideBiometricDialog"); - mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget(); - } - - private void handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { - mCurrentDialogArgs = args; - final int type = args.argi1; - - // Create a new dialog but do not replace the current one yet. - BiometricDialogView newDialog; - if (type == BiometricAuthenticator.TYPE_FINGERPRINT) { - newDialog = new FingerprintDialogView(mContext, mCallback); - } else if (type == BiometricAuthenticator.TYPE_FACE) { - newDialog = new FaceDialogView(mContext, mCallback); - } else { - Log.e(TAG, "Unsupported type: " + type); - return; - } - - if (DEBUG) Log.d(TAG, "handleShowDialog, " - + " savedState: " + savedState - + " mCurrentDialog: " + mCurrentDialog - + " newDialog: " + newDialog - + " type: " + type); - - if (savedState != null) { - // SavedState is only non-null if it's from onConfigurationChanged. Restore the state - // even though it may be removed / re-created again - newDialog.restoreState(savedState); - } else if (mCurrentDialog != null && mDialogShowing) { - // If somehow we're asked to show a dialog, the old one doesn't need to be animated - // away. This can happen if the app cancels and re-starts auth during configuration - // change. This is ugly because we also have to do things on onConfigurationChanged - // here. - mCurrentDialog.forceRemove(); - } - - mReceiver = (IBiometricServiceReceiverInternal) args.arg2; - newDialog.setBundle((Bundle) args.arg1); - newDialog.setRequireConfirmation((boolean) args.arg3); - newDialog.setUserId(args.argi2); - newDialog.setSkipIntro(skipAnimation); - mCurrentDialog = newDialog; - mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams()); - mDialogShowing = true; - } - - private void handleBiometricAuthenticated(boolean authenticated, String failureReason) { - if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated: " + authenticated); - if (authenticated) { - mCurrentDialog.announceForAccessibility( - mContext.getResources() - .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId())); - if (mCurrentDialog.requiresConfirmation()) { - mCurrentDialog.updateState(BiometricDialogView.STATE_PENDING_CONFIRMATION); - } else { - mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED); - mHandler.postDelayed(() -> { - handleHideDialog(false /* userCanceled */); - }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs()); - } + mCurrentDialog.onAuthenticationSucceeded(); } else { mCurrentDialog.onAuthenticationFailed(failureReason); } } - private void handleBiometricHelp(String message) { - if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message); - mCurrentDialog.onHelpReceived(message); + @Override + public void onBiometricHelp(String message) { + if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message); + + mCurrentDialog.onHelp(message); } - private void handleBiometricError(String error) { - if (DEBUG) Log.d(TAG, "handleBiometricError: " + error); - if (!mDialogShowing) { - if (DEBUG) Log.d(TAG, "Dialog already dismissed"); - return; - } - mCurrentDialog.onErrorReceived(error); + @Override + public void onBiometricError(String error) { + if (DEBUG) Log.d(TAG, "onBiometricError: " + error); + mCurrentDialog.onError(error); } - private void handleHideDialog(boolean userCanceled) { - if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled); - if (!mDialogShowing) { - // This can happen if there's a race and we get called from both - // onAuthenticated and onError, etc. - Log.w(TAG, "Dialog already dismissed, userCanceled: " + userCanceled); + @Override + public void hideBiometricDialog() { + if (DEBUG) Log.d(TAG, "hideBiometricDialog"); + + mCurrentDialog.dismissFromSystemServer(); + } + + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { + mCurrentDialogArgs = args; + final int type = args.argi1; + final Bundle biometricPromptBundle = (Bundle) args.arg1; + final boolean requireConfirmation = (boolean) args.arg3; + final int userId = args.argi2; + final String opPackageName = (String) args.arg4; + + // Create a new dialog but do not replace the current one yet. + final BiometricDialog newDialog = buildDialog( + biometricPromptBundle, + requireConfirmation, + userId, + type, + opPackageName); + + if (newDialog == null) { + Log.e(TAG, "Unsupported type: " + type); return; } - if (userCanceled) { - try { - mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException when hiding dialog", e); - } + + if (DEBUG) { + Log.d(TAG, "showDialog, " + + " savedState: " + savedState + + " mCurrentDialog: " + mCurrentDialog + + " newDialog: " + newDialog + + " type: " + type); + } + + if (savedState != null) { + // SavedState is only non-null if it's from onConfigurationChanged. Restore the state + // even though it may be removed / re-created again + newDialog.restoreState(savedState); + } else if (mCurrentDialog != null) { + // If somehow we're asked to show a dialog, the old one doesn't need to be animated + // away. This can happen if the app cancels and re-starts auth during configuration + // change. This is ugly because we also have to do things on onConfigurationChanged + // here. + mCurrentDialog.dismissWithoutCallback(false /* animate */); + } + + mReceiver = (IBiometricServiceReceiverInternal) args.arg2; + mCurrentDialog = newDialog; + mCurrentDialog.show(mWindowManager, skipAnimation); + } + + private void onDialogDismissed(@DismissedReason int reason) { + if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason); + if (mCurrentDialog == null) { + Log.w(TAG, "Dialog already dismissed"); } mReceiver = null; - mDialogShowing = false; - mCurrentDialog.startDismiss(); - } - - private void handleButtonNegative() { - if (mReceiver == null) { - Log.e(TAG, "Receiver is null"); - return; - } - try { - mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE); - } catch (RemoteException e) { - Log.e(TAG, "Remote exception when handling negative button", e); - } - handleHideDialog(false /* userCanceled */); - } - - private void handleButtonPositive() { - if (mReceiver == null) { - Log.e(TAG, "Receiver is null"); - return; - } - try { - mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE); - } catch (RemoteException e) { - Log.e(TAG, "Remote exception when handling positive button", e); - } - handleHideDialog(false /* userCanceled */); - } - - private void handleUserCanceled() { - handleHideDialog(true /* userCanceled */); - } - - private void handleTryAgainPressed() { - try { - mReceiver.onTryAgainPressed(); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException when handling try again", e); - } + mCurrentDialog = null; } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - final boolean wasShowing = mDialogShowing; // Save the state of the current dialog (buttons showing, etc) - final Bundle savedState = new Bundle(); if (mCurrentDialog != null) { + final Bundle savedState = new Bundle(); mCurrentDialog.onSaveState(savedState); - } + mCurrentDialog.dismissWithoutCallback(false /* animate */); + mCurrentDialog = null; - if (mDialogShowing) { - mCurrentDialog.forceRemove(); - mDialogShowing = false; - } - - if (wasShowing) { - handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState); + showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState); } } + + protected BiometricDialog buildDialog(Bundle biometricPromptBundle, + boolean requireConfirmation, int userId, int type, String opPackageName) { + return new BiometricDialogView.Builder(mContext) + .setCallback(this) + .setBiometricPromptBundle(biometricPromptBundle) + .setRequireConfirmation(requireConfirmation) + .setUserId(userId) + .setOpPackageName(opPackageName) + .build(type); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java index 24fd22e2ee80f..b65d1e823a9b1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java @@ -16,36 +16,38 @@ package com.android.systemui.biometrics; +import android.annotation.IntDef; + /** * Callback interface for dialog views. These should be implemented by the controller (e.g. * FingerprintDialogImpl) and passed into their views (e.g. FingerprintDialogView). */ public interface DialogViewCallback { - /** - * Invoked when the user cancels authentication by tapping outside the prompt, etc. The dialog - * should be dismissed. - */ - void onUserCanceled(); + + int DISMISSED_USER_CANCELED = 1; + int DISMISSED_BUTTON_NEGATIVE = 2; + int DISMISSED_BUTTON_POSITIVE = 3; + + int DISMISSED_AUTHENTICATED = 4; + int DISMISSED_ERROR = 5; + int DISMISSED_BY_SYSTEM_SERVER = 6; + + @IntDef({DISMISSED_USER_CANCELED, + DISMISSED_BUTTON_NEGATIVE, + DISMISSED_BUTTON_POSITIVE, + DISMISSED_AUTHENTICATED, + DISMISSED_ERROR, + DISMISSED_BY_SYSTEM_SERVER}) + @interface DismissedReason {} /** - * Invoked when an error is shown. The dialog should be dismissed after a set amount of time. + * Invoked when the dialog is dismissed + * @param reason */ - void onErrorShown(); + void onDismissed(@DismissedReason int reason); /** - * Invoked when the negative button is pressed. The client should be notified and the dialog - * should be dismissed. - */ - void onNegativePressed(); - - /** - * Invoked when the positive button is pressed. The client should be notified and the dialog - * should be dismissed. - */ - void onPositivePressed(); - - /** - * Invoked when the "try again" button is pressed. + * Invoked when the "try again" button is clicked */ void onTryAgainPressed(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java similarity index 70% rename from packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java index ce67577ea4837..2b4dde55ef72d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java @@ -11,10 +11,10 @@ * 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 + * limitations under the License. */ -package com.android.systemui.biometrics; +package com.android.systemui.biometrics.ui; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; @@ -23,6 +23,7 @@ import android.content.Context; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricPrompt; import android.os.Binder; import android.os.Bundle; @@ -46,25 +47,30 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; +import com.android.systemui.biometrics.BiometricDialog; +import com.android.systemui.biometrics.DialogViewCallback; +import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.util.leak.RotationUtils; /** * Abstract base class. Shows a dialog for BiometricPrompt. */ -public abstract class BiometricDialogView extends LinearLayout { +public abstract class BiometricDialogView extends LinearLayout implements BiometricDialog { private static final String TAG = "BiometricDialogView"; - private static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility"; - private static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility"; - private static final String KEY_CONFIRM_ENABLED = "key_confirm_enabled"; - private static final String KEY_STATE = "key_state"; - private static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility"; - private static final String KEY_ERROR_TEXT_STRING = "key_error_text_string"; - private static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary"; - private static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color"; + public static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility"; + public static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility"; + public static final String KEY_CONFIRM_ENABLED = "key_confirm_enabled"; + public static final String KEY_STATE = "key_state"; + public static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility"; + public static final String KEY_ERROR_TEXT_STRING = "key_error_text_string"; + public static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary"; + public static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color"; private static final int ANIMATION_DURATION_SHOW = 250; // ms private static final int ANIMATION_DURATION_AWAY = 350; // ms @@ -77,6 +83,8 @@ public abstract class BiometricDialogView extends LinearLayout { protected static final int STATE_PENDING_CONFIRMATION = 3; protected static final int STATE_AUTHENTICATED = 4; + @VisibleForTesting + final WakefulnessLifecycle mWakefulnessLifecycle; private final AccessibilityManager mAccessibilityManager; private final IBinder mWindowToken = new Binder(); private final Interpolator mLinearOutSlowIn; @@ -90,22 +98,30 @@ public abstract class BiometricDialogView extends LinearLayout { protected final ViewGroup mLayout; protected final LinearLayout mDialog; - protected final TextView mTitleText; - protected final TextView mSubtitleText; - protected final TextView mDescriptionText; - protected final ImageView mBiometricIcon; - protected final TextView mErrorText; - protected final Button mPositiveButton; - protected final Button mNegativeButton; - protected final Button mTryAgainButton; + @VisibleForTesting + final TextView mTitleText; + @VisibleForTesting + final TextView mSubtitleText; + @VisibleForTesting + final TextView mDescriptionText; + @VisibleForTesting + final ImageView mBiometricIcon; + @VisibleForTesting + final TextView mErrorText; + @VisibleForTesting + final Button mPositiveButton; + @VisibleForTesting + final Button mNegativeButton; + @VisibleForTesting + final Button mTryAgainButton; protected final int mTextColor; private Bundle mBundle; private Bundle mRestoredState; + private String mOpPackageName; private int mState = STATE_IDLE; - private boolean mAnimatingAway; private boolean mWasForceRemoved; private boolean mSkipIntro; protected boolean mRequireConfirmation; @@ -141,6 +157,15 @@ public abstract class BiometricDialogView extends LinearLayout { } }; + @VisibleForTesting + final WakefulnessLifecycle.Observer mWakefulnessObserver = + new WakefulnessLifecycle.Observer() { + @Override + public void onStartedGoingToSleep() { + animateAway(DialogViewCallback.DISMISSED_USER_CANCELED); + } + }; + protected Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { @@ -155,8 +180,80 @@ public abstract class BiometricDialogView extends LinearLayout { } }; - public BiometricDialogView(Context context, DialogViewCallback callback) { + /** + * Builds the dialog with specified parameters. + */ + public static class Builder { + public static final int TYPE_FINGERPRINT = BiometricAuthenticator.TYPE_FINGERPRINT; + public static final int TYPE_FACE = BiometricAuthenticator.TYPE_FACE; + + private Context mContext; + private DialogViewCallback mCallback; + private Bundle mBundle; + private boolean mRequireConfirmation; + private int mUserId; + private String mOpPackageName; + + public Builder(Context context) { + mContext = context; + } + + public Builder setCallback(DialogViewCallback callback) { + mCallback = callback; + return this; + } + + public Builder setBiometricPromptBundle(Bundle bundle) { + mBundle = bundle; + return this; + } + + public Builder setRequireConfirmation(boolean requireConfirmation) { + mRequireConfirmation = requireConfirmation; + return this; + } + + public Builder setUserId(int userId) { + mUserId = userId; + return this; + } + + public Builder setOpPackageName(String opPackageName) { + mOpPackageName = opPackageName; + return this; + } + + public BiometricDialogView build(int type) { + return build(type, new Injector()); + } + + public BiometricDialogView build(int type, Injector injector) { + BiometricDialogView dialog; + if (type == TYPE_FINGERPRINT) { + dialog = new FingerprintDialogView(mContext, mCallback, injector); + } else if (type == TYPE_FACE) { + dialog = new FaceDialogView(mContext, mCallback, injector); + } else { + return null; + } + dialog.setBundle(mBundle); + dialog.setRequireConfirmation(mRequireConfirmation); + dialog.setUserId(mUserId); + dialog.setOpPackageName(mOpPackageName); + return dialog; + } + } + + public static class Injector { + public WakefulnessLifecycle getWakefulnessLifecycle() { + return Dependency.get(WakefulnessLifecycle.class); + } + } + + protected BiometricDialogView(Context context, DialogViewCallback callback, Injector injector) { super(context); + mWakefulnessLifecycle = injector.getWakefulnessLifecycle(); + mCallback = callback; mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); @@ -178,19 +275,13 @@ public abstract class BiometricDialogView extends LinearLayout { addView(mLayout); mLayout.setOnKeyListener(new View.OnKeyListener() { - boolean downPressed = false; @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (keyCode != KeyEvent.KEYCODE_BACK) { return false; } - if (event.getAction() == KeyEvent.ACTION_DOWN && downPressed == false) { - downPressed = true; - } else if (event.getAction() == KeyEvent.ACTION_DOWN) { - downPressed = false; - } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) { - downPressed = false; - mCallback.onUserCanceled(); + if (event.getAction() == KeyEvent.ACTION_UP) { + animateAway(DialogViewCallback.DISMISSED_USER_CANCELED); } return true; } @@ -219,16 +310,16 @@ public abstract class BiometricDialogView extends LinearLayout { mNegativeButton.setOnClickListener((View v) -> { if (mState == STATE_PENDING_CONFIRMATION || mState == STATE_AUTHENTICATED) { - mCallback.onUserCanceled(); + animateAway(DialogViewCallback.DISMISSED_USER_CANCELED); } else { - mCallback.onNegativePressed(); + animateAway(DialogViewCallback.DISMISSED_BUTTON_NEGATIVE); } }); mPositiveButton.setOnClickListener((View v) -> { updateState(STATE_AUTHENTICATED); mHandler.postDelayed(() -> { - mCallback.onPositivePressed(); + animateAway(DialogViewCallback.DISMISSED_BUTTON_POSITIVE); }, getDelayAfterAuthenticatedDurationMs()); }); @@ -248,21 +339,12 @@ public abstract class BiometricDialogView extends LinearLayout { mLayout.requestFocus(); } - public void onSaveState(Bundle bundle) { - bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility()); - bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility()); - bundle.putBoolean(KEY_CONFIRM_ENABLED, mPositiveButton.isEnabled()); - bundle.putInt(KEY_STATE, mState); - bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility()); - bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText()); - bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE)); - bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor()); - } - @Override public void onAttachedToWindow() { super.onAttachedToWindow(); + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); + final ImageView backgroundView = mLayout.findViewById(R.id.background); if (mUserManager.isManagedProfile(mUserId)) { @@ -278,6 +360,7 @@ public abstract class BiometricDialogView extends LinearLayout { } mNegativeButton.setVisibility(View.VISIBLE); + mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); if (RotationUtils.getRotation(mContext) != RotationUtils.ROTATION_NONE) { mDialog.getLayoutParams().width = (int) mDialogWidth; @@ -285,7 +368,6 @@ public abstract class BiometricDialogView extends LinearLayout { if (mRestoredState == null) { updateState(STATE_AUTHENTICATING); - mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); final int hint = getHintStringResourceId(); if (hint != 0) { mErrorText.setText(hint); @@ -346,34 +428,49 @@ public abstract class BiometricDialogView extends LinearLayout { mSkipIntro = false; } + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); + } + private void setDismissesDialog(View v) { v.setClickable(true); v.setOnClickListener(v1 -> { if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) { - mCallback.onUserCanceled(); + animateAway(DialogViewCallback.DISMISSED_USER_CANCELED); } }); } - public void startDismiss() { + private void animateAway(@DialogViewCallback.DismissedReason int reason) { + animateAway(true /* sendReason */, reason); + } + + /** + * Animate the dialog away + * @param reason one of the {@link DialogViewCallback} codes + */ + private void animateAway(boolean sendReason, @DialogViewCallback.DismissedReason int reason) { if (!mCompletedAnimatingIn) { Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn"); mPendingDismissDialog = true; return; } - mAnimatingAway = true; - // This is where final cleanup should occur. final Runnable endActionRunnable = new Runnable() { @Override public void run() { mWindowManager.removeView(BiometricDialogView.this); - mAnimatingAway = false; // Set the icons / text back to normal state handleResetMessage(); showTryAgainButton(false /* show */); updateState(STATE_IDLE); + if (sendReason) { + mCallback.onDismissed(reason); + } } }; @@ -397,48 +494,31 @@ public abstract class BiometricDialogView extends LinearLayout { }); } - /** - * Force remove the window, cancelling any animation that's happening. This should only be - * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method - * will cause the dialog to show without an animation the next time it's attached. - */ - public void forceRemove() { - mLayout.animate().cancel(); - mDialog.animate().cancel(); - mWindowManager.removeView(BiometricDialogView.this); - mAnimatingAway = false; - mWasForceRemoved = true; - } - /** * Skip the intro animation */ - public void setSkipIntro(boolean skip) { + private void setSkipIntro(boolean skip) { mSkipIntro = skip; } - public boolean isAnimatingAway() { - return mAnimatingAway; - } - - public void setBundle(Bundle bundle) { + private void setBundle(Bundle bundle) { mBundle = bundle; } - public void setRequireConfirmation(boolean requireConfirmation) { + private void setRequireConfirmation(boolean requireConfirmation) { mRequireConfirmation = requireConfirmation; } - public boolean requiresConfirmation() { + protected boolean requiresConfirmation() { return mRequireConfirmation; } - public void setUserId(int userId) { + private void setUserId(int userId) { mUserId = userId; } - public ViewGroup getLayout() { - return mLayout; + private void setOpPackageName(String opPackageName) { + mOpPackageName = opPackageName; } // Shows an error/help message @@ -452,17 +532,63 @@ public abstract class BiometricDialogView extends LinearLayout { BiometricPrompt.HIDE_DIALOG_DELAY); } + @Override + public void show(WindowManager wm, boolean skipIntroAnimation) { + setSkipIntro(skipIntroAnimation); + wm.addView(this, getLayoutParams(mWindowToken)); + } + + /** + * Force remove the window, cancelling any animation that's happening. This should only be + * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method + * will cause the dialog to show without an animation the next time it's attached. + */ + @Override + public void dismissWithoutCallback(boolean animate) { + if (animate) { + animateAway(false /* sendReason */, 0 /* reason */); + } else { + mLayout.animate().cancel(); + mDialog.animate().cancel(); + mWindowManager.removeView(BiometricDialogView.this); + mWasForceRemoved = true; + } + } + + @Override + public void dismissFromSystemServer() { + animateAway(DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER); + } + + @Override + public void onAuthenticationSucceeded() { + announceForAccessibility(getResources().getText(getAuthenticatedAccessibilityResourceId())); + + if (requiresConfirmation()) { + updateState(STATE_PENDING_CONFIRMATION); + } else { + mHandler.postDelayed(() -> { + animateAway(DialogViewCallback.DISMISSED_AUTHENTICATED); + }, getDelayAfterAuthenticatedDurationMs()); + + updateState(STATE_AUTHENTICATED); + } + } + + + @Override + public void onAuthenticationFailed(String message) { + updateState(STATE_ERROR); + showTemporaryMessage(message); + } + /** * Transient help message (acquire) is received, dialog stays showing. Sensor stays in * "authenticating" state. * @param message */ - public void onHelpReceived(String message) { - updateState(STATE_ERROR); - showTemporaryMessage(message); - } - - public void onAuthenticationFailed(String message) { + @Override + public void onHelp(String message) { updateState(STATE_ERROR); showTemporaryMessage(message); } @@ -471,14 +597,61 @@ public abstract class BiometricDialogView extends LinearLayout { * Hard error is received, dialog will be dismissed soon. * @param error */ - public void onErrorReceived(String error) { + @Override + public void onError(String error) { updateState(STATE_ERROR); showTemporaryMessage(error); showTryAgainButton(false /* show */); - mCallback.onErrorShown(); // TODO: Split between fp and face + + mHandler.postDelayed(() -> { + animateAway(DialogViewCallback.DISMISSED_ERROR); + }, BiometricPrompt.HIDE_DIALOG_DELAY); } - public void updateState(int newState) { + + @Override + public void onSaveState(Bundle bundle) { + bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility()); + bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility()); + bundle.putBoolean(KEY_CONFIRM_ENABLED, mPositiveButton.isEnabled()); + bundle.putInt(KEY_STATE, mState); + bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility()); + bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText()); + bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE)); + bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor()); + } + + @Override + public void restoreState(Bundle bundle) { + mRestoredState = bundle; + final int tryAgainVisibility = bundle.getInt(KEY_TRY_AGAIN_VISIBILITY); + mTryAgainButton.setVisibility(tryAgainVisibility); + final int confirmVisibility = bundle.getInt(KEY_CONFIRM_VISIBILITY); + mPositiveButton.setVisibility(confirmVisibility); + final boolean confirmEnabled = bundle.getBoolean(KEY_CONFIRM_ENABLED); + mPositiveButton.setEnabled(confirmEnabled); + mState = bundle.getInt(KEY_STATE); + mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); + mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); + final int errorTextVisibility = bundle.getInt(KEY_ERROR_TEXT_VISIBILITY); + mErrorText.setVisibility(errorTextVisibility); + if (errorTextVisibility == View.INVISIBLE || tryAgainVisibility == View.INVISIBLE + || confirmVisibility == View.INVISIBLE) { + announceAccessibilityEvent(); + } + mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR)); + if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE), + BiometricPrompt.HIDE_DIALOG_DELAY); + } + } + + @Override + public String getOpPackageName() { + return mOpPackageName; + } + + protected void updateState(int newState) { if (newState == STATE_PENDING_CONFIRMATION) { mHandler.removeMessages(MSG_RESET_MESSAGE); mErrorText.setTextColor(mTextColor); @@ -505,48 +678,24 @@ public abstract class BiometricDialogView extends LinearLayout { mState = newState; } - public void showTryAgainButton(boolean show) { + protected void showTryAgainButton(boolean show) { } - public void onDialogAnimatedIn() { + protected void onDialogAnimatedIn() { mCompletedAnimatingIn = true; if (mPendingDismissDialog) { Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); - startDismiss(); + animateAway(false /* sendReason */, 0); mPendingDismissDialog = false; } } - public void restoreState(Bundle bundle) { - mRestoredState = bundle; - final int tryAgainVisibility = bundle.getInt(KEY_TRY_AGAIN_VISIBILITY); - mTryAgainButton.setVisibility(tryAgainVisibility); - final int confirmVisibility = bundle.getInt(KEY_CONFIRM_VISIBILITY); - mPositiveButton.setVisibility(confirmVisibility); - final boolean confirmEnabled = bundle.getBoolean(KEY_CONFIRM_ENABLED); - mPositiveButton.setEnabled(confirmEnabled); - mState = bundle.getInt(KEY_STATE); - mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); - mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); - final int errorTextVisibility = bundle.getInt(KEY_ERROR_TEXT_VISIBILITY); - mErrorText.setVisibility(errorTextVisibility); - if (errorTextVisibility == View.INVISIBLE || tryAgainVisibility == View.INVISIBLE - || confirmVisibility == View.INVISIBLE) { - announceAccessibilityEvent(); - } - mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR)); - if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) { - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE), - BiometricPrompt.HIDE_DIALOG_DELAY); - } - } - - protected int getState() { - return mState; - } - - public WindowManager.LayoutParams getLayoutParams() { + /** + * @param windowToken token for the window + * @return + */ + public static WindowManager.LayoutParams getLayoutParams(IBinder windowToken) { final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, @@ -555,7 +704,7 @@ public abstract class BiometricDialogView extends LinearLayout { PixelFormat.TRANSLUCENT); lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; lp.setTitle("BiometricDialogView"); - lp.token = mWindowToken; + lp.token = windowToken; return lp; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java similarity index 97% rename from packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java index ae6cb5ce23d39..bd8714812ab49 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java @@ -11,10 +11,10 @@ * 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 + * limitations under the License. */ -package com.android.systemui.biometrics; +package com.android.systemui.biometrics.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -33,7 +33,9 @@ import android.util.Log; import android.view.View; import android.view.ViewOutlineProvider; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; +import com.android.systemui.biometrics.DialogViewCallback; /** * This class loads the view for the system-provided dialog. The view consists of: @@ -52,7 +54,8 @@ public class FaceDialogView extends BiometricDialogView { private static final int TEXT_ANIMATE_DISTANCE = 32; // dp private static final int SIZE_UNKNOWN = 0; - private static final int SIZE_SMALL = 1; + @VisibleForTesting + static final int SIZE_SMALL = 1; private static final int SIZE_GROWING = 2; private static final int SIZE_BIG = 3; @@ -152,13 +155,13 @@ public class FaceDialogView extends BiometricDialogView { announceAccessibilityEvent(); }; - public FaceDialogView(Context context, - DialogViewCallback callback) { - super(context, callback); + protected FaceDialogView(Context context, DialogViewCallback callback, Injector injector) { + super(context, callback, injector); mIconController = new IconController(); } - private void updateSize(int newSize) { + @VisibleForTesting + void updateSize(int newSize) { final float padding = dpToPixels(IMPLICIT_Y_PADDING); final float iconSmallPositionY = mDialog.getHeight() - mBiometricIcon.getHeight() - padding; @@ -339,8 +342,8 @@ public class FaceDialogView extends BiometricDialogView { } @Override - public void onErrorReceived(String error) { - super.onErrorReceived(error); + public void onError(String error) { + super.onError(error); // All error messages will cause the dialog to go from small -> big. Error messages // are messages such as lockout, auth failed, etc. if (mSize == SIZE_SMALL) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java similarity index 93% rename from packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java index 183933eb395c8..e597080f89fd2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java @@ -11,10 +11,10 @@ * 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 + * limitations under the License. */ -package com.android.systemui.biometrics; +package com.android.systemui.biometrics.ui; import android.content.Context; import android.graphics.drawable.AnimatedVectorDrawable; @@ -22,6 +22,7 @@ import android.graphics.drawable.Drawable; import android.util.Log; import com.android.systemui.R; +import com.android.systemui.biometrics.DialogViewCallback; /** * This class loads the view for the system-provided dialog. The view consists of: @@ -32,9 +33,9 @@ public class FingerprintDialogView extends BiometricDialogView { private static final String TAG = "FingerprintDialogView"; - public FingerprintDialogView(Context context, - DialogViewCallback callback) { - super(context, callback); + protected FingerprintDialogView(Context context, DialogViewCallback callback, + Injector injector) { + super(context, callback, injector); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index fa0fe136b8ebd..134d4b87a159d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -271,7 +271,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< default void onRotationProposal(int rotation, boolean isValid) { } default void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, - int type, boolean requireConfirmation, int userId) { } + int type, boolean requireConfirmation, int userId, String opPackageName) { } default void onBiometricAuthenticated(boolean authenticated, String failureReason) { } default void onBiometricHelp(String message) { } default void onBiometricError(String error) { } @@ -741,7 +741,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< @Override public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, - int type, boolean requireConfirmation, int userId) { + int type, boolean requireConfirmation, int userId, String opPackageName) { synchronized (mLock) { SomeArgs args = SomeArgs.obtain(); args.arg1 = bundle; @@ -749,6 +749,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< args.argi1 = type; args.arg3 = requireConfirmation; args.argi2 = userId; + args.arg4 = opPackageName; mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args) .sendToTarget(); } @@ -1036,7 +1037,8 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< (IBiometricServiceReceiverInternal) someArgs.arg2, someArgs.argi1 /* type */, (boolean) someArgs.arg3 /* requireConfirmation */, - someArgs.argi2 /* userId */); + someArgs.argi2 /* userId */, + (String) someArgs.arg4 /* opPackageName */); } someArgs.recycle(); break; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java new file mode 100644 index 0000000000000..8f2f8b1c0e632 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java @@ -0,0 +1,329 @@ +/* + * 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.systemui.biometrics; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static junit.framework.TestCase.assertNotNull; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.IActivityTaskManager; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.IBiometricServiceReceiverInternal; +import android.os.Bundle; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper.RunWithLooper; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.phone.StatusBar; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +@SmallTest +public class BiometricDialogImplTest extends SysuiTestCase { + + @Mock + private PackageManager mPackageManager; + @Mock + private IBiometricServiceReceiverInternal mReceiver; + @Mock + private BiometricDialog mDialog1; + @Mock + private BiometricDialog mDialog2; + + private TestableBiometricDialogImpl mBiometricDialogImpl; + + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + TestableContext context = spy(mContext); + + mContext.putComponent(StatusBar.class, mock(StatusBar.class)); + mContext.putComponent(CommandQueue.class, mock(CommandQueue.class)); + + when(context.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + + when(mDialog1.getOpPackageName()).thenReturn("Dialog1"); + when(mDialog2.getOpPackageName()).thenReturn("Dialog2"); + + mBiometricDialogImpl = new TestableBiometricDialogImpl(new MockInjector()); + mBiometricDialogImpl.mContext = context; + mBiometricDialogImpl.mComponents = mContext.getComponents(); + + mBiometricDialogImpl.start(); + } + + // Callback tests + + @Test + public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_USER_CANCELED); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); + } + + @Test + public void testSendsReasonNegative_whenDismissedByButtonNegative() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BUTTON_NEGATIVE); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE); + } + + @Test + public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BUTTON_POSITIVE); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED); + } + + @Test + public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_AUTHENTICATED); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + } + + @Test + public void testSendsReasonError_whenDismissedByError() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_ERROR); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR); + } + + @Test + public void testSendsReasonDismissedBySystemServer_whenDismissedByServer() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED); + } + + // Statusbar tests + + @Test + public void testShowInvoked_whenSystemRequested() + throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + verify(mDialog1).show(any(), eq(false) /* skipIntro */); + } + + @Test + public void testOnAuthenticationSucceededInvoked_whenSystemRequested() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onBiometricAuthenticated(true, null /* failureReason */); + verify(mDialog1).onAuthenticationSucceeded(); + } + + @Test + public void testOnAuthenticationFailedInvoked_whenSystemRequested() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + final String failureReason = "failure reason"; + mBiometricDialogImpl.onBiometricAuthenticated(false, failureReason); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(mDialog1).onAuthenticationFailed(captor.capture()); + + assertEquals(captor.getValue(), failureReason); + } + + @Test + public void testOnHelpInvoked_whenSystemRequested() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + final String helpMessage = "help"; + mBiometricDialogImpl.onBiometricHelp(helpMessage); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(mDialog1).onHelp(captor.capture()); + + assertEquals(captor.getValue(), helpMessage); + } + + @Test + public void testOnErrorInvoked_whenSystemRequested() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + final String errMessage = "error message"; + mBiometricDialogImpl.onBiometricError(errMessage); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(mDialog1).onError(captor.capture()); + + assertEquals(captor.getValue(), errMessage); + } + + @Test + public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.hideBiometricDialog(); + verify(mDialog1).dismissFromSystemServer(); + } + + @Test + public void testClientNotified_whenDismissedBySystemServer() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.hideBiometricDialog(); + verify(mDialog1).dismissFromSystemServer(); + + assertNotNull(mBiometricDialogImpl.mCurrentDialog); + assertNotNull(mBiometricDialogImpl.mReceiver); + } + + // Corner case tests + + @Test + public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + verify(mDialog1).show(any(), eq(false) /* skipIntro */); + + showDialog(BiometricPrompt.TYPE_FACE); + + // First dialog should be dismissed without animation + verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */); + + // Second dialog should be shown without animation + verify(mDialog2).show(any(), eq(true)) /* skipIntro */; + } + + @Test + public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + verify(mDialog1).show(any(), eq(false) /* skipIntro */); + + mBiometricDialogImpl.onConfigurationChanged(new Configuration()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Bundle.class); + verify(mDialog1).onSaveState(captor.capture()); + + // Old dialog doesn't animate + verify(mDialog1).dismissWithoutCallback(eq(false /* animate */)); + + // Saved state is restored into new dialog + ArgumentCaptor captor2 = ArgumentCaptor.forClass(Bundle.class); + verify(mDialog2).restoreState(captor2.capture()); + + // Dialog for new configuration skips intro + verify(mDialog2).show(any(), eq(true) /* skipIntro */); + + // TODO: This should check all values we want to save/restore + assertEquals(captor.getValue(), captor2.getValue()); + } + + @Test + public void testClientNotified_whenTaskStackChangesDuringAuthentication() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + + List tasks = new ArrayList<>(); + ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); + taskInfo.topActivity = mock(ComponentName.class); + when(taskInfo.topActivity.getPackageName()).thenReturn("other_package"); + tasks.add(taskInfo); + when(mBiometricDialogImpl.mActivityTaskManager.getTasks(anyInt())).thenReturn(tasks); + + mBiometricDialogImpl.mTaskStackListener.onTaskStackChanged(); + waitForIdleSync(); + + assertNull(mBiometricDialogImpl.mCurrentDialog); + assertNull(mBiometricDialogImpl.mReceiver); + verify(mDialog1).dismissWithoutCallback(true /* animate */); + verify(mReceiver).onDialogDismissed(eq(BiometricPrompt.DISMISSED_REASON_USER_CANCEL)); + } + + // Helpers + + private void showDialog(int type) { + mBiometricDialogImpl.showBiometricDialog(createTestDialogBundle(), + mReceiver /* receiver */, + type, + true /* requireConfirmation */, + 0 /* userId */, + "testPackage"); + } + + private Bundle createTestDialogBundle() { + Bundle bundle = new Bundle(); + + bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title"); + bundle.putCharSequence(BiometricPrompt.KEY_SUBTITLE, "Subtitle"); + bundle.putCharSequence(BiometricPrompt.KEY_DESCRIPTION, "Description"); + bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative Button"); + + // RequireConfirmation is a hint to BiometricService. This can be forced to be required + // by user settings, and should be tested in BiometricService. + bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true); + + return bundle; + } + + private final class TestableBiometricDialogImpl extends BiometricDialogImpl { + private int mBuildCount = 0; + + public TestableBiometricDialogImpl(Injector injector) { + super(injector); + } + + @Override + protected BiometricDialog buildDialog(Bundle biometricPromptBundle, + boolean requireConfirmation, int userId, int type, String opPackageName) { + BiometricDialog dialog; + if (mBuildCount == 0) { + dialog = mDialog1; + } else if (mBuildCount == 1) { + dialog = mDialog2; + } else { + dialog = null; + } + mBuildCount++; + return dialog; + } + } + + private final class MockInjector extends BiometricDialogImpl.Injector { + @Override + IActivityTaskManager getActivityTaskManager() { + return mock(IActivityTaskManager.class); + } + } +} + diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/BiometricDialogViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/BiometricDialogViewTest.java new file mode 100644 index 0000000000000..bbdd837bb4462 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/BiometricDialogViewTest.java @@ -0,0 +1,204 @@ +/* + * 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.systemui.biometrics.ui; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotSame; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.spy; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.hardware.biometrics.BiometricPrompt; +import android.os.Bundle; +import android.os.UserManager; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper.RunWithLooper; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.DialogViewCallback; +import com.android.systemui.keyguard.WakefulnessLifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +@SmallTest +public class BiometricDialogViewTest extends SysuiTestCase { + + FaceDialogView mFaceDialogView; + + private static final String TITLE = "Title"; + private static final String SUBTITLE = "Subtitle"; + private static final String DESCRIPTION = "Description"; + private static final String NEGATIVE_BUTTON = "Negative Button"; + + private static final String TEST_HELP = "Help"; + + TestableContext mTestableContext; + @Mock + private DialogViewCallback mCallback; + @Mock + private UserManager mUserManager; + @Mock + private DevicePolicyManager mDpm; + + private static class Injector extends BiometricDialogView.Injector { + @Override + public WakefulnessLifecycle getWakefulnessLifecycle() { + final WakefulnessLifecycle lifecycle = new WakefulnessLifecycle(); + lifecycle.dispatchFinishedWakingUp(); + return lifecycle; + } + } + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTestableContext = spy(mContext); + mTestableContext.addMockSystemService(UserManager.class, mUserManager); + mTestableContext.addMockSystemService(DevicePolicyManager.class, mDpm); + } + + @Test + public void testContentStates_confirmationRequired_authenticated() { + mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback, + true /* requireConfirmation */); + mFaceDialogView.onAttachedToWindow(); + + // When starting authentication + assertEquals(View.VISIBLE, mFaceDialogView.mTitleText.getVisibility()); + assertEquals(View.VISIBLE, mFaceDialogView.mSubtitleText.getVisibility()); + assertEquals(View.VISIBLE, mFaceDialogView.mDescriptionText.getVisibility()); + assertEquals(View.INVISIBLE, mFaceDialogView.mErrorText.getVisibility()); + assertEquals(View.VISIBLE, mFaceDialogView.mPositiveButton.getVisibility()); + assertEquals(View.VISIBLE, mFaceDialogView.mNegativeButton.getVisibility()); + assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility()); + + // Contents are as expected + assertTrue(TITLE.contentEquals(mFaceDialogView.mTitleText.getText())); + assertTrue(SUBTITLE.contentEquals(mFaceDialogView.mSubtitleText.getText())); + assertTrue(DESCRIPTION.contentEquals(mFaceDialogView.mDescriptionText.getText())); + assertTrue(mFaceDialogView.mPositiveButton.getText().toString() + .contentEquals(mContext.getString(R.string.biometric_dialog_confirm))); + assertTrue(NEGATIVE_BUTTON.contentEquals(mFaceDialogView.mNegativeButton.getText())); + assertTrue(mFaceDialogView.mTryAgainButton.getText().toString() + .contentEquals(mContext.getString(R.string.biometric_dialog_try_again))); + + // When help message is received + mFaceDialogView.onHelp(TEST_HELP); + assertEquals(mFaceDialogView.mErrorText.getVisibility(), View.VISIBLE); + assertTrue(TEST_HELP.contentEquals(mFaceDialogView.mErrorText.getText())); + + // When authenticated, confirm button comes out + mFaceDialogView.onAuthenticationSucceeded(); + assertEquals(View.VISIBLE, mFaceDialogView.mPositiveButton.getVisibility()); + assertEquals(true, mFaceDialogView.mPositiveButton.isEnabled()); + } + + @Test + public void testContentStates_confirmationNotRequired_authenticated() { + mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback, + false /* requireConfirmation */); + mFaceDialogView.onAttachedToWindow(); + mFaceDialogView.updateSize(FaceDialogView.SIZE_SMALL); + + assertEquals(View.INVISIBLE, mFaceDialogView.mTitleText.getVisibility()); + assertNotSame(View.VISIBLE, mFaceDialogView.mSubtitleText.getVisibility()); + assertNotSame(View.VISIBLE, mFaceDialogView.mDescriptionText.getVisibility()); + assertEquals(View.INVISIBLE, mFaceDialogView.mErrorText.getVisibility()); + assertEquals(View.GONE, mFaceDialogView.mPositiveButton.getVisibility()); + assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility()); + assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility()); + } + + @Test + public void testContentStates_confirmationNotRequired_help() { + mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback, + false /* requireConfirmation */); + mFaceDialogView.onAttachedToWindow(); + + mFaceDialogView.onHelp(TEST_HELP); + assertEquals(mFaceDialogView.mErrorText.getVisibility(), View.VISIBLE); + assertTrue(TEST_HELP.contentEquals(mFaceDialogView.mErrorText.getText())); + } + + @Test + public void testBack_sendsUserCanceled() { + // TODO: Need robolectric framework to wait for handler to complete + } + + @Test + public void testScreenOff_sendsUserCanceled() { + // TODO: Need robolectric framework to wait for handler to complete + } + + @Test + public void testRestoreState_contentStatesCorrect() { + mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback, + false /* requireConfirmation */); + mFaceDialogView.onAttachedToWindow(); + mFaceDialogView.onAuthenticationFailed(TEST_HELP); + + final Bundle bundle = new Bundle(); + mFaceDialogView.onSaveState(bundle); + + mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback, + false /* requireConfirmation */); + mFaceDialogView.restoreState(bundle); + mFaceDialogView.onAttachedToWindow(); + + assertEquals(View.VISIBLE, mFaceDialogView.mTryAgainButton.getVisibility()); + } + + private FaceDialogView buildFaceDialogView(Context context, DialogViewCallback callback, + boolean requireConfirmation) { + return (FaceDialogView) new BiometricDialogView.Builder(context) + .setCallback(callback) + .setBiometricPromptBundle(createTestDialogBundle()) + .setRequireConfirmation(requireConfirmation) + .setUserId(0) + .setOpPackageName("test_package") + .build(BiometricDialogView.Builder.TYPE_FACE, new Injector()); + } + + private Bundle createTestDialogBundle() { + Bundle bundle = new Bundle(); + + bundle.putCharSequence(BiometricPrompt.KEY_TITLE, TITLE); + bundle.putCharSequence(BiometricPrompt.KEY_SUBTITLE, SUBTITLE); + bundle.putCharSequence(BiometricPrompt.KEY_DESCRIPTION, DESCRIPTION); + bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, NEGATIVE_BUTTON); + + // RequireConfirmation is a hint to BiometricService. This can be forced to be required + // by user settings, and should be tested in BiometricService. + bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true); + + return bundle; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index 1e1f21568377b..b252a0d95f940 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -365,4 +365,45 @@ public class CommandQueueTest extends SysuiTestCase { waitForIdleSync(); verify(mCallbacks).onRecentsAnimationStateChanged(eq(true)); } + + @Test + public void testShowBiometricDialog() { + Bundle bundle = new Bundle(); + String packageName = "test"; + mCommandQueue.showBiometricDialog(bundle, null /* receiver */, 1, true, 3, packageName); + waitForIdleSync(); + verify(mCallbacks).showBiometricDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3), + eq(packageName)); + } + + @Test + public void testOnBiometricAuthenticated() { + String failureReason = "test_failure_reason"; + mCommandQueue.onBiometricAuthenticated(true /* authenticated */, failureReason); + waitForIdleSync(); + verify(mCallbacks).onBiometricAuthenticated(eq(true), eq(failureReason)); + } + + @Test + public void testOnBiometricHelp() { + String helpMessage = "test_help_message"; + mCommandQueue.onBiometricHelp(helpMessage); + waitForIdleSync(); + verify(mCallbacks).onBiometricHelp(eq(helpMessage)); + } + + @Test + public void testOnBiometricError() { + String errorMessage = "test_error_message"; + mCommandQueue.onBiometricError(errorMessage); + waitForIdleSync(); + verify(mCallbacks).onBiometricError(eq(errorMessage)); + } + + @Test + public void testHideBiometricDialog() { + mCommandQueue.hideBiometricDialog(); + waitForIdleSync(); + verify(mCallbacks).hideBiometricDialog(); + } } diff --git a/services/core/java/com/android/server/biometrics/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/AuthenticationClient.java index 4a9ccdee05223..766e5c4d638f6 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 af2f24f959b4d..24e6a75e68440 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -25,11 +25,9 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_IRIS; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; import android.app.ActivityManager; -import android.app.ActivityTaskManager; import android.app.AppOpsManager; -import android.app.IActivityTaskManager; +import android.app.IActivityManager; import android.app.KeyguardManager; -import android.app.TaskStackListener; import android.app.UserSwitchObserver; import android.content.ContentResolver; import android.content.Context; @@ -69,6 +67,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; @@ -88,9 +87,8 @@ public class BiometricService extends SystemService { private static final String TAG = "BiometricService"; private static final boolean DEBUG = true; - 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 +99,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 +111,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 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 // Services before we can start authenticating. Pairs that have been returned // are moved to mModalitiesMatched. @@ -165,10 +172,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; @@ -244,42 +254,37 @@ public class BiometricService extends SystemService { } } - private final class BiometricTaskStackListener extends TaskStackListener { - @Override - public void onTaskStackChanged() { - mHandler.sendEmptyMessage(MSG_ON_TASK_STACK_CHANGED); - } - } - + 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 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 + IStatusBarService mStatusBarService; + @VisibleForTesting + KeyStore mKeyStore; // Get and cache the available authenticator (manager) classes. Used since aidl doesn't support // polymorphism :/ final ArrayList 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 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,15 +294,11 @@ 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) { - case MSG_ON_TASK_STACK_CHANGED: { - handleTaskStackChanged(); - break; - } - case MSG_ON_AUTHENTICATION_SUCCEEDED: { SomeArgs args = (SomeArgs) msg.obj; handleAuthenticationSucceeded( @@ -307,8 +308,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 +398,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 +428,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 +443,7 @@ public class BiometricService extends SystemService { Settings.Secure.getUriFor(Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION); private final ContentResolver mContentResolver; + private final List mCallbacks; private Map mFaceEnabledOnKeyguard = new HashMap<>(); private Map mFaceEnabledForApps = new HashMap<>(); @@ -446,13 +454,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 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 +505,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 +513,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 callbacks = mEnabledOnKeyguardCallbacks; + public void notifyEnabledOnKeyguardCallbacks(int userId) { + List callbacks = mCallbacks; for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).notify(BiometricSourceType.FACE, mFaceEnabledOnKeyguard.getOrDefault(userId, DEFAULT_KEYGUARD_ENABLED), @@ -527,7 +538,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 +570,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 +583,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 +596,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 +886,44 @@ public class BiometricService extends SystemService { } } + @VisibleForTesting + static class Injector { + IActivityManager getActivityManagerService() { + return ActivityManager.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 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. *

@@ -883,11 +934,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 +954,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 +972,14 @@ 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(); + mStatusBarService = mInjector.getStatusBarService(); // Cache the authenticators for (int i = 0; i < FEATURE_ID.length; i++) { @@ -934,7 +990,7 @@ public class BiometricService extends SystemService { } } - publishBinderService(Context.BIOMETRIC_SERVICE, new BiometricServiceWrapper()); + mInjector.publishBinderService(this, mImpl); } /** @@ -1068,7 +1124,7 @@ public class BiometricService extends SystemService { } private void logDialogDismissed(int reason) { - if (reason == BiometricPrompt.DISMISSED_REASON_POSITIVE) { + if (reason == BiometricPrompt.DISMISSED_REASON_CONFIRMED) { // Explicit auth, authentication confirmed. // Latency in this case is authenticated -> confirmed. Service // should have the first half (first acquired -> authenticated). @@ -1094,7 +1150,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 +1178,7 @@ public class BiometricService extends SystemService { BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT, error, 0 /* vendorCode */, - Utils.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId), + mInjector.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId), latency); } } @@ -1145,51 +1201,22 @@ public class BiometricService extends SystemService { return modality; } - private void handleTaskStackChanged() { - try { - final List runningTasks = - mActivityTaskManager.getTasks(1); - if (!runningTasks.isEmpty()) { - final String topPackage = runningTasks.get(0).topActivity.getPackageName(); - if (mCurrentAuthSession != null - && !topPackage.contentEquals(mCurrentAuthSession.mOpPackageName)) { - mStatusBarService.hideBiometricDialog(); - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); - mCurrentAuthSession.mClientReceiver.onError( - BiometricConstants.BIOMETRIC_ERROR_CANCELED, - getContext().getString( - com.android.internal.R.string.biometric_error_canceled) - ); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; - mCurrentAuthSession = null; - } - } - } catch (RemoteException e) { - Slog.e(TAG, "Unable to get running tasks", e); - } - } - 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 +1228,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,16 +1253,31 @@ 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 { - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); mConfirmDeviceCredentialReceiver.onAuthenticationSucceeded(); if (mCurrentAuthSession != null) { - mCurrentAuthSession.mState = STATE_AUTH_IDLE; mCurrentAuthSession = null; } } catch (RemoteException e) { @@ -1245,14 +1288,13 @@ 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 { - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); mConfirmDeviceCredentialReceiver.onError(error, message); if (mCurrentAuthSession != null) { - mCurrentAuthSession.mState = STATE_AUTH_IDLE; mCurrentAuthSession = null; } } catch (RemoteException e) { @@ -1272,7 +1314,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 @@ -1282,6 +1324,9 @@ public class BiometricService extends SystemService { try { if (mCurrentAuthSession != null && mCurrentAuthSession.containsCookie(cookie)) { + mCurrentAuthSession.mErrorEscrow = error; + mCurrentAuthSession.mErrorStringEscrow = message; + if (mCurrentAuthSession.isFromConfirmDeviceCredential()) { // If we were invoked by ConfirmDeviceCredential, do not delete the current // auth session since we still need to respond to cancel signal while @@ -1293,39 +1338,18 @@ public class BiometricService extends SystemService { mCurrentAuthSession.mState = STATE_BIOMETRIC_AUTH_CANCELED_SHOWING_CDC; mStatusBarService.hideBiometricDialog(); } else if (mCurrentAuthSession.mState == STATE_AUTH_STARTED) { - mStatusBarService.onBiometricError(message); + mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI; if (error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) { - mActivityTaskManager.unregisterTaskStackListener( - mTaskStackListener); - mCurrentAuthSession.mClientReceiver.onError(error, message); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; - 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); + mStatusBarService.onBiometricError(message); } } 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); - mActivityTaskManager.unregisterTaskStackListener( - mTaskStackListener); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; + mStatusBarService.hideBiometricDialog(); mCurrentAuthSession = null; } else { Slog.e(TAG, "Impossible session error state: " @@ -1335,7 +1359,6 @@ public class BiometricService extends SystemService { && mPendingAuthSession.containsCookie(cookie)) { if (mPendingAuthSession.mState == STATE_AUTH_CALLED) { mPendingAuthSession.mClientReceiver.onError(error, message); - mPendingAuthSession.mState = STATE_AUTH_IDLE; mPendingAuthSession = null; } else { Slog.e(TAG, "Impossible pending session error state: " @@ -1370,42 +1393,50 @@ public class BiometricService extends SystemService { private void handleOnDismissed(int reason) { if (mCurrentAuthSession == null) { - Slog.e(TAG, "onDialogDismissed: " + reason + ", auth session null"); + Slog.e(TAG, "onDismissed: " + reason + ", auth session null"); return; } logDialogDismissed(reason); try { - if (reason != BiometricPrompt.DISMISSED_REASON_POSITIVE) { - // 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_POSITIVE) { - // 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; + + 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_SERVER_REQUESTED: + case BiometricPrompt.DISMISSED_REASON_ERROR: + mCurrentAuthSession.mClientReceiver.onError(mCurrentAuthSession.mErrorEscrow, + mCurrentAuthSession.mErrorStringEscrow); + break; + + default: + Slog.w(TAG, "Unhandled reason: " + reason); + 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; - } + // Dialog is gone, auth session is done. + mCurrentAuthSession = null; + } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } @@ -1472,8 +1503,8 @@ public class BiometricService extends SystemService { if (!continuing) { mStatusBarService.showBiometricDialog(mCurrentAuthSession.mBundle, - mInternalReceiver, modality, requireConfirmation, userId); - mActivityTaskManager.registerTaskStackListener(mTaskStackListener); + mInternalReceiver, modality, requireConfirmation, userId, + mCurrentAuthSession.mOpPackageName); } } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); @@ -1517,8 +1548,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, @@ -1610,7 +1639,6 @@ public class BiometricService extends SystemService { com.android.internal.R.string.biometric_error_user_canceled) ); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; mCurrentAuthSession = null; mStatusBarService.hideBiometricDialog(); } catch (RemoteException e) { @@ -1637,25 +1665,31 @@ 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 == null) { + Slog.w(TAG, "Skipping cancelInternal"); + return; + } else 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 f3f9754bd32b5..2de18c391d4a1 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/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index d67048fe548d0..8897eca85d7ad 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -610,11 +610,12 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, - int type, boolean requireConfirmation, int userId) { + int type, boolean requireConfirmation, int userId, String opPackageName) { enforceBiometricDialog(); if (mBar != null) { try { - mBar.showBiometricDialog(bundle, receiver, type, requireConfirmation, userId); + mBar.showBiometricDialog(bundle, receiver, type, requireConfirmation, userId, + opPackageName); } catch (RemoteException ex) { } } 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 0000000000000..ccf3a908364a2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -0,0 +1,748 @@ +/* + * 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 junit.framework.TestCase.assertNotNull; + +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.verify; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.app.IActivityManager; +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 TEST_PACKAGE_NAME = "test_package"; + + 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 + 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 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).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).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).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).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).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).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 Service#prepareForAuthentication + ArgumentCaptor cookieCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mReceiver1, never()).onError(anyInt(), any(String.class)); + verify(mBiometricService.mFingerprintService).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) + .startPreparedClient(cookieCaptor.getValue()); + + // StatusBar showBiometricDialog invoked + verify(mBiometricService.mStatusBarService).showBiometricDialog( + eq(mBiometricService.mCurrentAuthSession.mBundle), + any(IBiometricServiceReceiverInternal.class), + eq(BiometricAuthenticator.TYPE_FINGERPRINT), + anyBoolean() /* requireConfirmation */, + anyInt() /* userId */, + eq(TEST_PACKAGE_NAME)); + + // 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).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).addAuthToken(any(byte[].class)); + // Send onAuthenticated to client + verify(mReceiver1).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).addAuthToken(any(byte[].class)); + verify(mReceiver1).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) + .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED)); + verify(mReceiver1).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) + .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED)); + verify(mReceiver1).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 doesn't become null until SystemUI responds that the animation is completed + assertNotNull(mBiometricService.mCurrentAuthSession); + // ERROR_CANCELED is not sent until SystemUI responded that animation is completed + verify(mReceiver1, never()).onError( + anyInt(), anyString()); + verify(mReceiver2, never()).onError(anyInt(), any(String.class)); + + // SystemUI dialog closed + verify(mBiometricService.mStatusBarService).hideBiometricDialog(); + + // After SystemUI notifies that the animation has completed + mBiometricService.mInternalReceiver + .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED); + waitForIdle(); + verify(mReceiver1).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), + eq(ERROR_CANCELED)); + assertNull(mBiometricService.mCurrentAuthSession); + } + + @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) + .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 */, + anyString()); + } + + @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).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), + eq(ERROR_CANCELED)); + // Dialog is hidden immediately + verify(mBiometricService.mStatusBarService).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) + .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).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).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), + eq(ERROR_USER_CANCELED)); + verify(mBiometricService.mFingerprintService).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).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) + .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_PACKAGE_NAME /* 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(); + } +}