From c53d981e244d49d38ba3d5080b0d8d204a7dcc4f Mon Sep 17 00:00:00 2001 From: Kevin Chyn Date: Tue, 30 Jul 2019 18:10:30 -0700 Subject: [PATCH 1/4] 1/n: Update BiometricDialog (SystemUI side) with cleaner lifecycle 1) Clean up BiometricDialogImpl. As a side-effect of 2, which cleans up the dialog lifecycle, we no longer need to have a handler here. This greatly simplifies the code here. 2) Clean up interface between BiometricDialogImpl and the UI (BiometricDialogView). 3) Clean up interface between BimetricService and BiometricDialogImpl. SystemUI is now responsible for dismissing the dialog. Test: atest BiometricDialogImplTest Bug: 138628043 Change-Id: Ic1fea4c05c27dfc7eb6fc661f517f0380b9fff99 --- .../hardware/biometrics/BiometricPrompt.java | 14 +- .../systemui/biometrics/BiometricDialog.java | 100 +++++ .../biometrics/BiometricDialogImpl.java | 406 ++++++------------ .../biometrics/DialogViewCallback.java | 31 +- .../{ => ui}/BiometricDialogView.java | 326 +++++++++----- .../biometrics/{ => ui}/FaceDialogView.java | 11 +- .../{ => ui}/FingerprintDialogView.java | 7 +- .../biometrics/BiometricDialogImplTest.java | 278 ++++++++++++ .../systemui/statusbar/CommandQueueTest.java | 39 ++ .../server/biometrics/BiometricService.java | 8 +- 10 files changed, 814 insertions(+), 406 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java rename packages/SystemUI/src/com/android/systemui/biometrics/{ => ui}/BiometricDialogView.java (75%) rename packages/SystemUI/src/com/android/systemui/biometrics/{ => ui}/FaceDialogView.java (98%) rename packages/SystemUI/src/com/android/systemui/biometrics/{ => ui}/FingerprintDialogView.java (96%) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 1142a07bc66c2..2951a4bf328b9 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -100,7 +100,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan /** * @hide */ - public static final int DISMISSED_REASON_POSITIVE = 1; + public static final int DISMISSED_REASON_CONFIRMED = 1; /** * @hide @@ -112,6 +112,16 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan */ public static final int DISMISSED_REASON_USER_CANCEL = 3; + /** + * @hide + */ + public static final int DISMISSED_REASON_CONFIRM_NOT_REQUIRED = 4; + + /** + * @hide + */ + public static final int DISMISSED_REASON_ERROR = 5; + private static class ButtonInfo { Executor executor; DialogInterface.OnClickListener listener; @@ -362,7 +372,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/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..ca9d372d14446 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java @@ -0,0 +1,100 @@ +/* + * 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. Only used when the system detects a case + * where the error won't come from the UI (e.g. task stack changed). + * @param animate + */ + void dismissWithoutCallback(boolean animate); + + /** + * 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); +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java index e66a8fa96298d..0eea9efcaa4f5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java @@ -19,132 +19,88 @@ package com.android.systemui.biometrics; 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.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; /** - * 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; - + // TODO: These should just be saved from onSaveState private SomeArgs mCurrentDialogArgs; - private BiometricDialogView mCurrentDialog; + private BiometricDialog mCurrentDialog; + private WindowManager mWindowManager; private IBiometricServiceReceiverInternal mReceiver; - private boolean mDialogShowing; - private Callback mCallback = new Callback(); - private WakefulnessLifecycle mWakefulnessLifecycle; - private Handler mHandler = new Handler(Looper.getMainLooper()) { - @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(); + @Override + public void onTryAgainPressed() { + try { + mReceiver.onTryAgainPressed(); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException when handling try again", e); } } - 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(); - } + @Override + public void onDismissed(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); + // TODO: BiometricService currently sends the result immediately. This should + // actually happen when the animation is completed. + break; + + case DialogViewCallback.DISMISSED_ERROR: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR); + // TODO: Make sure error isn't received until dialog is dismissed + // TODO: Similarly, BiometricService currently sends the result immediately. + // This should happen when the animation is completed. + break; + default: + Log.e(TAG, "Unhandled reason: " + reason); + break; } - }; + } + + private void sendResultAndCleanUp(int result) { + if (mReceiver == null) { + Log.e(TAG, "Receiver is null"); + return; + } + try { + mReceiver.onDialogDismissed(result); + } catch (RemoteException e) { + Log.w(TAG, "Remote exception", e); + } + onDialogDismissed(); + } @Override public void start() { @@ -154,8 +110,6 @@ 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); } } @@ -166,18 +120,19 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba 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(); + + boolean skipAnimation = false; + if (mCurrentDialog != null) { + Log.w(TAG, "mCurrentDialog: " + mCurrentDialog); + skipAnimation = true; + } + showDialog(args, skipAnimation /* skipAnimation */, null /* savedState */); } @Override @@ -185,185 +140,106 @@ 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"); + + // TODO: I think we need to remove this interface + mCurrentDialog.dismissWithoutCallback(true /* animate */); + } + + private void showDialog(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. + final BiometricDialog newDialog = buildDialog( + (Bundle) args.arg1 /* bundle */, + (boolean) args.arg3 /* requireConfirmation */, + args.argi2 /* userId */, + type); + + 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() { + if (DEBUG) Log.d(TAG, "onDialogDismissed"); + 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) { + return new BiometricDialogView.Builder(mContext) + .setCallback(this) + .setBiometricPromptBundle(biometricPromptBundle) + .setRequireConfirmation(requireConfirmation) + .setUserId(userId) + .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..acad62ac13007 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java @@ -21,31 +21,22 @@ package com.android.systemui.biometrics; * 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; /** - * 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(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 75% 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..7a5c3e3da7ec5 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,29 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +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 +82,7 @@ public abstract class BiometricDialogView extends LinearLayout { protected static final int STATE_PENDING_CONFIRMATION = 3; protected static final int STATE_AUTHENTICATED = 4; + private final WakefulnessLifecycle mWakefulnessLifecycle; private final AccessibilityManager mAccessibilityManager; private final IBinder mWindowToken = new Binder(); private final Interpolator mLinearOutSlowIn; @@ -105,7 +111,6 @@ public abstract class BiometricDialogView extends LinearLayout { private Bundle mRestoredState; private int mState = STATE_IDLE; - private boolean mAnimatingAway; private boolean mWasForceRemoved; private boolean mSkipIntro; protected boolean mRequireConfirmation; @@ -141,6 +146,14 @@ public abstract class BiometricDialogView extends LinearLayout { } }; + private 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 +168,63 @@ 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; + + 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 BiometricDialogView build(int type) { + BiometricDialogView dialog; + if (type == TYPE_FINGERPRINT) { + dialog = new FingerprintDialogView(mContext, mCallback); + } else if (type == TYPE_FACE) { + dialog = new FaceDialogView(mContext, mCallback); + } else { + return null; + } + dialog.setBundle(mBundle); + dialog.setRequireConfirmation(mRequireConfirmation); + dialog.setUserId(mUserId); + return dialog; + } + } + + protected BiometricDialogView(Context context, DialogViewCallback callback) { super(context); + mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); + mCallback = callback; mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); @@ -178,19 +246,19 @@ public abstract class BiometricDialogView extends LinearLayout { addView(mLayout); mLayout.setOnKeyListener(new View.OnKeyListener() { - boolean downPressed = false; + boolean mDownPressed = 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; + if (event.getAction() == KeyEvent.ACTION_DOWN && mDownPressed == false) { + mDownPressed = true; } else if (event.getAction() == KeyEvent.ACTION_DOWN) { - downPressed = false; - } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) { - downPressed = false; - mCallback.onUserCanceled(); + mDownPressed = false; + } else if (event.getAction() == KeyEvent.ACTION_UP && mDownPressed == true) { + mDownPressed = false; + animateAway(DialogViewCallback.DISMISSED_USER_CANCELED); } return true; } @@ -219,16 +287,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 +316,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)) { @@ -346,34 +405,48 @@ 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(int reason) { + animateAway(true /* sendReason */, reason); + } + /** + * Animate the dialog away + * @param reason one of the {@link DialogViewCallback} codes + */ + private void animateAway(boolean sendReason, 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,50 +470,29 @@ 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; - } - // Shows an error/help message protected void showTemporaryMessage(String message) { mHandler.removeMessages(MSG_RESET_MESSAGE); @@ -452,17 +504,58 @@ 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); + } else { + mLayout.animate().cancel(); + mDialog.animate().cancel(); + mWindowManager.removeView(BiometricDialogView.this); + mWasForceRemoved = true; + } + } + + @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 +564,57 @@ 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 + + // TODO: Is this still used to synchronize animation and client onError timing? + 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); + } + } + + protected void updateState(int newState) { if (newState == STATE_PENDING_CONFIRMATION) { mHandler.removeMessages(MSG_RESET_MESSAGE); mErrorText.setTextColor(mTextColor); @@ -505,48 +641,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 +667,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 98% 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..cfa17ee689229 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; @@ -34,6 +34,7 @@ import android.view.View; import android.view.ViewOutlineProvider; 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: @@ -152,7 +153,7 @@ public class FaceDialogView extends BiometricDialogView { announceAccessibilityEvent(); }; - public FaceDialogView(Context context, + protected FaceDialogView(Context context, DialogViewCallback callback) { super(context, callback); mIconController = new IconController(); @@ -339,8 +340,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 96% 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..1c339ba6db546 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,7 +33,7 @@ public class FingerprintDialogView extends BiometricDialogView { private static final String TAG = "FingerprintDialogView"; - public FingerprintDialogView(Context context, + protected FingerprintDialogView(Context context, DialogViewCallback callback) { super(context, callback); } 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..3122051772c47 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java @@ -0,0 +1,278 @@ +/* + * 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 org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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; + +@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); + + mBiometricDialogImpl = new TestableBiometricDialogImpl(); + 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, times(1)) + .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, times(1)) + .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE); + } + + @Test + public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BUTTON_POSITIVE); + verify(mReceiver, times(1)) + .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED); + } + + @Test + public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception { + // TODO: Modify BiometricService / BiometricDialog + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_AUTHENTICATED); + verify(mReceiver, times(1)) + .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + } + + @Test + public void testSendsReasonError_whenDismissedByError() throws Exception { + // TODO: Modify BiometricService / BiometricDialog + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_ERROR); + verify(mReceiver, times(1)) + .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR); + } + + // Statusbar tests + + @Test + public void testShowInvoked_whenSystemRequested() + throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */); + } + + @Test + public void testOnAuthenticationSucceededInvoked_whenSystemRequested() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.onBiometricAuthenticated(true, null /* failureReason */); + verify(mDialog1, times(1)).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, times(1)).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, times(1)).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, times(1)).onError(captor.capture()); + + assertEquals(captor.getValue(), errMessage); + } + + @Test + public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + mBiometricDialogImpl.hideBiometricDialog(); + + verify(mDialog1, times(1)).dismissWithoutCallback(eq(true) /* animate */); + } + + // Corner case tests + + @Test + public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */); + + showDialog(BiometricPrompt.TYPE_FACE); + + // First dialog should be dismissed without animation + verify(mDialog1, times(1)).dismissWithoutCallback(eq(false) /* animate */); + + // Second dialog should be shown without animation + verify(mDialog2, times(1)).show(any(), eq(true)) /* skipIntro */; + } + + @Test + public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception { + showDialog(BiometricPrompt.TYPE_FACE); + verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */); + + mBiometricDialogImpl.onConfigurationChanged(new Configuration()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Bundle.class); + verify(mDialog1, times(1)).onSaveState(captor.capture()); + + // Old dialog doesn't animate + verify(mDialog1, times(1)).dismissWithoutCallback(eq(false) /* animate */); + + // Saved state is restored into new dialog + ArgumentCaptor captor2 = ArgumentCaptor.forClass(Bundle.class); + verify(mDialog2, times(1)).restoreState(captor2.capture()); + + // Dialog for new configuration skips intro + verify(mDialog2, times(1)).show(any(), eq(true) /* skipIntro */); + + // TODO: This should check all values we want to save/restore + assertEquals(captor.getValue(), captor2.getValue()); + } + + + // Helpers + + private void showDialog(int type) { + mBiometricDialogImpl.showBiometricDialog(createTestDialogBundle(), + mReceiver /* receiver */, + type, + true /* requireConfirmation */, + 0 /* userId */); + } + + 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; + + @Override + protected BiometricDialog buildDialog(Bundle biometricPromptBundle, + boolean requireConfirmation, int userId, int type) { + BiometricDialog dialog; + if (mBuildCount == 0) { + dialog = mDialog1; + } else if (mBuildCount == 1) { + dialog = mDialog2; + } else { + dialog = null; + } + mBuildCount++; + return dialog; + } + } +} + 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..f68c11b4371bf 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,43 @@ public class CommandQueueTest extends SysuiTestCase { waitForIdleSync(); verify(mCallbacks).onRecentsAnimationStateChanged(eq(true)); } + + @Test + public void testShowBiometricDialog() { + Bundle bundle = new Bundle(); + mCommandQueue.showBiometricDialog(bundle, null /* receiver */, 1, true, 3); + waitForIdleSync(); + verify(mCallbacks).showBiometricDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3)); + } + + @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/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index af2f24f959b4d..90a80dd36cb9d 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -1068,7 +1068,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). @@ -1370,14 +1370,14 @@ 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) { + if (reason != BiometricPrompt.DISMISSED_REASON_CONFIRMED) { // TODO: Check // Positive button is used by passive modalities as a "confirm" button, // do not send to client mCurrentAuthSession.mClientReceiver.onDialogDismissed(reason); @@ -1390,7 +1390,7 @@ public class BiometricService extends SystemService { BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED, getContext().getString( com.android.internal.R.string.biometric_error_user_canceled)); - } else if (reason == BiometricPrompt.DISMISSED_REASON_POSITIVE) { + } else if (reason == BiometricPrompt.DISMISSED_REASON_CONFIRMED) { // Have the service send the token to KeyStore, and send onAuthenticated // to the application KeyStore.getInstance().addAuthToken(mCurrentAuthSession.mTokenEscrow); From 9f8b2fc74cd89aaa5c21f8525bfd5a1e6a14499c Mon Sep 17 00:00:00 2001 From: Kevin Chyn Date: Fri, 2 Aug 2019 18:24:32 -0700 Subject: [PATCH 2/4] 2/n: Update BiometricService with cleaner lifecycle Hiding of the BiometricDialog is (almost) all handled by SystemUI now. BiometricService should only send results to the client when it receives onDialogDismissed() from SystemUI Bug: 135082347 Test: atest BiometricServiceTest Change-Id: Ie91fd54680b942e421b7e187f1107ef7323ea719 --- .../hardware/biometrics/BiometricPrompt.java | 6 + .../IBiometricServiceReceiverInternal.aidl | 4 +- .../biometrics/BiometricDialogImpl.java | 5 - .../biometrics/ui/BiometricDialogView.java | 3 +- .../biometrics/AuthenticationClient.java | 3 +- .../server/biometrics/BiometricService.java | 359 +++++---- .../biometrics/BiometricServiceBase.java | 6 +- .../biometrics/BiometricServiceTest.java | 742 ++++++++++++++++++ 8 files changed, 981 insertions(+), 147 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 2951a4bf328b9..14acc6fb834d9 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -103,6 +103,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public static final int DISMISSED_REASON_CONFIRMED = 1; /** + * Dialog is done animating away after user clicked on the button set via + * {@link BiometricPrompt.Builder#setNegativeButton(CharSequence, Executor, + * DialogInterface.OnClickListener)}. * @hide */ public static final int DISMISSED_REASON_NEGATIVE = 2; @@ -113,11 +116,14 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public static final int DISMISSED_REASON_USER_CANCEL = 3; /** + * Authenticated, confirmation not required. Dialog animated away. * @hide */ public static final int DISMISSED_REASON_CONFIRM_NOT_REQUIRED = 4; /** + * Error message shown on SystemUI. When BiometricService receives this, the UI is already + * gone. * @hide */ public static final int DISMISSED_REASON_ERROR = 5; diff --git a/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl index 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/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java index 0eea9efcaa4f5..0c3cea780d4e3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java @@ -73,15 +73,10 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba case DialogViewCallback.DISMISSED_AUTHENTICATED: sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); - // TODO: BiometricService currently sends the result immediately. This should - // actually happen when the animation is completed. break; case DialogViewCallback.DISMISSED_ERROR: sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR); - // TODO: Make sure error isn't received until dialog is dismissed - // TODO: Similarly, BiometricService currently sends the result immediately. - // This should happen when the animation is completed. break; default: Log.e(TAG, "Unhandled reason: " + reason); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java index 7a5c3e3da7ec5..a1687acf88c65 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java @@ -337,6 +337,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet } mNegativeButton.setVisibility(View.VISIBLE); + mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); if (RotationUtils.getRotation(mContext) != RotationUtils.ROTATION_NONE) { mDialog.getLayoutParams().width = (int) mDialogWidth; @@ -344,7 +345,6 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet if (mRestoredState == null) { updateState(STATE_AUTHENTICATING); - mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); final int hint = getHintStringResourceId(); if (hint != 0) { mErrorText.setText(hint); @@ -570,7 +570,6 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet showTemporaryMessage(error); showTryAgainButton(false /* show */); - // TODO: Is this still used to synchronize animation and client onError timing? mHandler.postDelayed(() -> { animateAway(DialogViewCallback.DISMISSED_ERROR); }, BiometricPrompt.HIDE_DIALOG_DELAY); diff --git a/services/core/java/com/android/server/biometrics/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/AuthenticationClient.java index 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 90a80dd36cb9d..94056694d8106 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -27,6 +27,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.AppOpsManager; +import android.app.IActivityManager; import android.app.IActivityTaskManager; import android.app.KeyguardManager; import android.app.TaskStackListener; @@ -69,6 +70,7 @@ import android.util.Slog; import android.util.StatsLog; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.statusbar.IStatusBarService; import com.android.server.SystemService; @@ -90,7 +92,7 @@ public class BiometricService extends SystemService { private static final int MSG_ON_TASK_STACK_CHANGED = 1; private static final int MSG_ON_AUTHENTICATION_SUCCEEDED = 2; - private static final int MSG_ON_AUTHENTICATION_FAILED = 3; + private static final int MSG_ON_AUTHENTICATION_REJECTED = 3; private static final int MSG_ON_ERROR = 4; private static final int MSG_ON_ACQUIRED = 5; private static final int MSG_ON_DISMISSED = 6; @@ -101,6 +103,7 @@ public class BiometricService extends SystemService { private static final int MSG_ON_CONFIRM_DEVICE_CREDENTIAL_SUCCESS = 11; private static final int MSG_ON_CONFIRM_DEVICE_CREDENTIAL_ERROR = 12; private static final int MSG_REGISTER_CANCELLATION_CALLBACK = 13; + private static final int MSG_ON_AUTHENTICATION_TIMED_OUT = 14; private static final int[] FEATURE_ID = { TYPE_FINGERPRINT, @@ -112,33 +115,41 @@ public class BiometricService extends SystemService { * Authentication either just called and we have not transitioned to the CALLED state, or * authentication terminated (success or error). */ - private static final int STATE_AUTH_IDLE = 0; + static final int STATE_AUTH_IDLE = 0; /** * Authentication was called and we are waiting for the 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 +176,13 @@ public class BiometricService extends SystemService { final boolean mRequireConfirmation; // The current state, which can be either idle, called, or started - private int mState = STATE_AUTH_IDLE; + int mState = STATE_AUTH_IDLE; // For explicit confirmation, do not send to keystore until the user has confirmed // the authentication. byte[] mTokenEscrow; + // Waiting for SystemUI to complete animation + int mErrorEscrow; + String mErrorStringEscrow; // Timestamp when authentication started private long mStartTimeMs; @@ -251,35 +265,40 @@ public class BiometricService extends SystemService { } } + private final Injector mInjector; + @VisibleForTesting + final IBiometricService.Stub mImpl; private final AppOpsManager mAppOps; private final boolean mHasFeatureFingerprint; private final boolean mHasFeatureIris; private final boolean mHasFeatureFace; - private final SettingObserver mSettingObserver; + @VisibleForTesting + SettingObserver mSettingObserver; private final List mEnabledOnKeyguardCallbacks; private final BiometricTaskStackListener mTaskStackListener = new BiometricTaskStackListener(); private final Random mRandom = new Random(); - private IFingerprintService mFingerprintService; - private IFaceService mFaceService; - private IActivityTaskManager mActivityTaskManager; - private IStatusBarService mStatusBarService; + @VisibleForTesting + IFingerprintService mFingerprintService; + @VisibleForTesting + IFaceService mFaceService; + @VisibleForTesting + IActivityTaskManager mActivityTaskManager; + @VisibleForTesting + IStatusBarService mStatusBarService; + @VisibleForTesting + KeyStore mKeyStore; // Get and cache the available authenticator (manager) classes. Used since aidl doesn't support // polymorphism :/ final ArrayList 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,7 +308,8 @@ public class BiometricService extends SystemService { // to this receiver. private IBiometricServiceReceiver mConfirmDeviceCredentialReceiver; - private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @VisibleForTesting + final Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -307,8 +327,8 @@ public class BiometricService extends SystemService { break; } - case MSG_ON_AUTHENTICATION_FAILED: { - handleAuthenticationFailed((String) msg.obj /* failureReason */); + case MSG_ON_AUTHENTICATION_REJECTED: { + handleAuthenticationRejected((String) msg.obj /* failureReason */); break; } @@ -397,6 +417,11 @@ public class BiometricService extends SystemService { break; } + case MSG_ON_AUTHENTICATION_TIMED_OUT: { + handleAuthenticationTimedOut((String) msg.obj /* errorMessage */); + break; + } + default: Slog.e(TAG, "Unknown message: " + msg); break; @@ -422,7 +447,8 @@ public class BiometricService extends SystemService { } } - private final class SettingObserver extends ContentObserver { + @VisibleForTesting + public static class SettingObserver extends ContentObserver { private static final boolean DEFAULT_KEYGUARD_ENABLED = true; private static final boolean DEFAULT_APP_ENABLED = true; @@ -436,6 +462,7 @@ public class BiometricService extends SystemService { Settings.Secure.getUriFor(Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION); private final ContentResolver mContentResolver; + private final List mCallbacks; private Map mFaceEnabledOnKeyguard = new HashMap<>(); private Map mFaceEnabledForApps = new HashMap<>(); @@ -446,13 +473,15 @@ public class BiometricService extends SystemService { * * @param handler The handler to run {@link #onChange} on, or null if none. */ - SettingObserver(Handler handler) { + public SettingObserver(Context context, Handler handler, + List callbacks) { super(handler); - mContentResolver = getContext().getContentResolver(); + mContentResolver = context.getContentResolver(); + mCallbacks = callbacks; updateContentObserver(); } - void updateContentObserver() { + public void updateContentObserver() { mContentResolver.unregisterContentObserver(this); mContentResolver.registerContentObserver(FACE_UNLOCK_KEYGUARD_ENABLED, false /* notifyForDescendents */, @@ -495,7 +524,7 @@ public class BiometricService extends SystemService { } } - boolean getFaceEnabledOnKeyguard() { + public boolean getFaceEnabledOnKeyguard() { final int user = ActivityManager.getCurrentUser(); if (!mFaceEnabledOnKeyguard.containsKey(user)) { onChange(true /* selfChange */, FACE_UNLOCK_KEYGUARD_ENABLED, user); @@ -503,22 +532,23 @@ public class BiometricService extends SystemService { return mFaceEnabledOnKeyguard.get(user); } - boolean getFaceEnabledForApps(int userId) { + public boolean getFaceEnabledForApps(int userId) { + Slog.e(TAG, "getFaceEnabledForApps: " + userId, new Exception()); if (!mFaceEnabledForApps.containsKey(userId)) { onChange(true /* selfChange */, FACE_UNLOCK_APP_ENABLED, userId); } return mFaceEnabledForApps.getOrDefault(userId, DEFAULT_APP_ENABLED); } - boolean getFaceAlwaysRequireConfirmation(int userId) { + public boolean getFaceAlwaysRequireConfirmation(int userId) { if (!mFaceAlwaysRequireConfirmation.containsKey(userId)) { onChange(true /* selfChange */, FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, userId); } return mFaceAlwaysRequireConfirmation.get(userId); } - void notifyEnabledOnKeyguardCallbacks(int userId) { - List 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 +557,7 @@ public class BiometricService extends SystemService { } } - private final class EnabledOnKeyguardCallback implements IBinder.DeathRecipient { + final class EnabledOnKeyguardCallback implements IBinder.DeathRecipient { private final IBiometricEnabledOnKeyguardCallback mCallback; @@ -559,7 +589,8 @@ public class BiometricService extends SystemService { } // Wrap the client's receiver so we can do things with the BiometricDialog first - private final IBiometricServiceReceiverInternal mInternalReceiver = + @VisibleForTesting + final IBiometricServiceReceiverInternal mInternalReceiver = new IBiometricServiceReceiverInternal.Stub() { @Override public void onAuthenticationSucceeded(boolean requireConfirmation, byte[] token) @@ -571,10 +602,11 @@ public class BiometricService extends SystemService { } @Override - public void onAuthenticationFailed(int cookie, boolean requireConfirmation) + public void onAuthenticationFailed() throws RemoteException { String failureReason = getContext().getString(R.string.biometric_not_recognized); - mHandler.obtainMessage(MSG_ON_AUTHENTICATION_FAILED, failureReason).sendToTarget(); + Slog.v(TAG, "onAuthenticationFailed: " + failureReason); + mHandler.obtainMessage(MSG_ON_AUTHENTICATION_REJECTED, failureReason).sendToTarget(); } @Override @@ -583,7 +615,7 @@ public class BiometricService extends SystemService { // soft errors and we should allow the user to try authenticating again instead of // dismissing BiometricPrompt. if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) { - mHandler.obtainMessage(MSG_ON_AUTHENTICATION_FAILED, message).sendToTarget(); + mHandler.obtainMessage(MSG_ON_AUTHENTICATION_TIMED_OUT, message).sendToTarget(); } else { SomeArgs args = SomeArgs.obtain(); args.argi1 = cookie; @@ -873,6 +905,48 @@ public class BiometricService extends SystemService { } } + @VisibleForTesting + static class Injector { + IActivityManager getActivityManagerService() { + return ActivityManager.getService(); + } + + IActivityTaskManager getActivityTaskManagerService() { + return ActivityTaskManager.getService(); + } + + IStatusBarService getStatusBarService() { + return IStatusBarService.Stub.asInterface( + ServiceManager.getService(Context.STATUS_BAR_SERVICE)); + } + + IFingerprintService getFingerprintService() { + return IFingerprintService.Stub.asInterface( + ServiceManager.getService(Context.FINGERPRINT_SERVICE)); + } + + IFaceService getFaceService() { + return IFaceService.Stub.asInterface(ServiceManager.getService(Context.FACE_SERVICE)); + } + + SettingObserver getSettingObserver(Context context, Handler handler, + List 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 +957,19 @@ public class BiometricService extends SystemService { * @param context The system server context. */ public BiometricService(Context context) { + this(context, new Injector()); + } + + @VisibleForTesting + BiometricService(Context context, Injector injector) { super(context); + mInjector = injector; + mImpl = new BiometricServiceWrapper(); mAppOps = context.getSystemService(AppOpsManager.class); mEnabledOnKeyguardCallbacks = new ArrayList<>(); - mSettingObserver = new SettingObserver(mHandler); + mSettingObserver = mInjector.getSettingObserver(context, mHandler, + mEnabledOnKeyguardCallbacks); final PackageManager pm = context.getPackageManager(); mHasFeatureFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT); @@ -895,7 +977,7 @@ public class BiometricService extends SystemService { mHasFeatureFace = pm.hasSystemFeature(PackageManager.FEATURE_FACE); try { - ActivityManager.getService().registerUserSwitchObserver( + injector.getActivityManagerService().registerUserSwitchObserver( new UserSwitchObserver() { @Override public void onUserSwitchComplete(int newUserId) { @@ -913,17 +995,15 @@ public class BiometricService extends SystemService { public void onStart() { // TODO: maybe get these on-demand if (mHasFeatureFingerprint) { - mFingerprintService = IFingerprintService.Stub.asInterface( - ServiceManager.getService(Context.FINGERPRINT_SERVICE)); + mFingerprintService = mInjector.getFingerprintService(); } if (mHasFeatureFace) { - mFaceService = IFaceService.Stub.asInterface( - ServiceManager.getService(Context.FACE_SERVICE)); + mFaceService = mInjector.getFaceService(); } - mActivityTaskManager = ActivityTaskManager.getService(); - mStatusBarService = IStatusBarService.Stub.asInterface( - ServiceManager.getService(Context.STATUS_BAR_SERVICE)); + mKeyStore = mInjector.getKeyStore(); + mActivityTaskManager = mInjector.getActivityTaskManagerService(); + mStatusBarService = mInjector.getStatusBarService(); // Cache the authenticators for (int i = 0; i < FEATURE_ID.length; i++) { @@ -934,7 +1014,7 @@ public class BiometricService extends SystemService { } } - publishBinderService(Context.BIOMETRIC_SERVICE, new BiometricServiceWrapper()); + mInjector.publishBinderService(this, mImpl); } /** @@ -1094,7 +1174,7 @@ public class BiometricService extends SystemService { mCurrentAuthSession.mRequireConfirmation, StatsLog.BIOMETRIC_AUTHENTICATED__STATE__CONFIRMED, latency, - Utils.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId)); + mInjector.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId)); } else { final long latency = System.currentTimeMillis() - mCurrentAuthSession.mStartTimeMs; @@ -1122,7 +1202,7 @@ public class BiometricService extends SystemService { BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT, error, 0 /* vendorCode */, - Utils.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId), + mInjector.isDebugEnabled(getContext(), mCurrentAuthSession.mUserId), latency); } } @@ -1170,26 +1250,21 @@ public class BiometricService extends SystemService { } private void handleAuthenticationSucceeded(boolean requireConfirmation, byte[] token) { - try { // Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded // after user dismissed/canceled dialog). if (mCurrentAuthSession == null) { - Slog.e(TAG, "onAuthenticationSucceeded(): Auth session is null"); + Slog.e(TAG, "handleAuthenticationSucceeded: Auth session is null"); return; } + // Store the auth token and submit it to keystore after the dialog is confirmed / + // animating away. + mCurrentAuthSession.mTokenEscrow = token; if (!requireConfirmation) { - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); - KeyStore.getInstance().addAuthToken(token); - mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded(); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; - mCurrentAuthSession = null; + mCurrentAuthSession.mState = STATE_AUTHENTICATED_PENDING_SYSUI; } else { mCurrentAuthSession.mAuthenticatedTimeMs = System.currentTimeMillis(); - // Store the auth token and submit it to keystore after the confirmation - // button has been pressed. - mCurrentAuthSession.mTokenEscrow = token; mCurrentAuthSession.mState = STATE_AUTH_PENDING_CONFIRM; } @@ -1201,12 +1276,13 @@ public class BiometricService extends SystemService { } } - private void handleAuthenticationFailed(String failureReason) { + private void handleAuthenticationRejected(String failureReason) { + Slog.v(TAG, "handleAuthenticationRejected: " + failureReason); try { // Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded // after user dismissed/canceled dialog). if (mCurrentAuthSession == null) { - Slog.e(TAG, "onAuthenticationFailed(): Auth session is null"); + Slog.e(TAG, "handleAuthenticationRejected: Auth session is null"); return; } @@ -1225,9 +1301,26 @@ public class BiometricService extends SystemService { } } + private void handleAuthenticationTimedOut(String message) { + Slog.v(TAG, "handleAuthenticationTimedOut: " + message); + try { + // Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded + // after user dismissed/canceled dialog). + if (mCurrentAuthSession == null) { + Slog.e(TAG, "handleAuthenticationTimedOut: Auth session is null"); + return; + } + + mStatusBarService.onBiometricAuthenticated(false, message); + mCurrentAuthSession.mState = STATE_AUTH_PAUSED; + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); + } + } + private void handleOnConfirmDeviceCredentialSuccess() { if (mConfirmDeviceCredentialReceiver == null) { - Slog.w(TAG, "onCDCASuccess null!"); + Slog.w(TAG, "handleOnConfirmDeviceCredentialSuccess null!"); return; } try { @@ -1245,7 +1338,8 @@ public class BiometricService extends SystemService { private void handleOnConfirmDeviceCredentialError(int error, String message) { if (mConfirmDeviceCredentialReceiver == null) { - Slog.w(TAG, "onCDCAError null! Error: " + error + " " + message); + Slog.w(TAG, "handleOnConfirmDeviceCredentialError null! Error: " + + error + " " + message); return; } try { @@ -1272,7 +1366,7 @@ public class BiometricService extends SystemService { } private void handleOnError(int cookie, int error, String message) { - Slog.d(TAG, "Error: " + error + " cookie: " + cookie); + Slog.d(TAG, "handleOnError: " + error + " cookie: " + cookie); // Errors can either be from the current auth session or the pending auth session. // The pending auth session may receive errors such as ERROR_LOCKOUT before // it becomes the current auth session. Similarly, the current auth session may @@ -1302,27 +1396,16 @@ public class BiometricService extends SystemService { mCurrentAuthSession = null; mStatusBarService.hideBiometricDialog(); } else { - // Send errors after the dialog is dismissed. - mHandler.postDelayed(() -> { - try { - if (mCurrentAuthSession != null) { - mActivityTaskManager.unregisterTaskStackListener( - mTaskStackListener); - mCurrentAuthSession.mClientReceiver.onError(error, - message); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; - mCurrentAuthSession = null; - } - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception", e); - } - }, BiometricPrompt.HIDE_DIALOG_DELAY); + mCurrentAuthSession.mErrorEscrow = error; + mCurrentAuthSession.mErrorStringEscrow = message; + mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI; } } else if (mCurrentAuthSession.mState == STATE_AUTH_PAUSED) { // In the "try again" state, we should forward canceled errors to - // the client and and clean up. + // the client and and clean up. The only error we should get here is + // ERROR_CANCELED due to another client kicking us out. mCurrentAuthSession.mClientReceiver.onError(error, message); - mStatusBarService.onBiometricError(message); + mStatusBarService.hideBiometricDialog(); mActivityTaskManager.unregisterTaskStackListener( mTaskStackListener); mCurrentAuthSession.mState = STATE_AUTH_IDLE; @@ -1377,35 +1460,44 @@ public class BiometricService extends SystemService { logDialogDismissed(reason); try { - if (reason != BiometricPrompt.DISMISSED_REASON_CONFIRMED) { // TODO: Check - // Positive button is used by passive modalities as a "confirm" button, - // do not send to client - mCurrentAuthSession.mClientReceiver.onDialogDismissed(reason); - // Cancel authentication. Skip the token/package check since we are cancelling - // from system server. The interface is permission protected so this is fine. - cancelInternal(null /* token */, null /* package */, false /* fromClient */); - } - if (reason == BiometricPrompt.DISMISSED_REASON_USER_CANCEL) { - mCurrentAuthSession.mClientReceiver.onError( - BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED, - getContext().getString( - com.android.internal.R.string.biometric_error_user_canceled)); - } else if (reason == BiometricPrompt.DISMISSED_REASON_CONFIRMED) { - // Have the service send the token to KeyStore, and send onAuthenticated - // to the application - KeyStore.getInstance().addAuthToken(mCurrentAuthSession.mTokenEscrow); - mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded(); + switch (reason) { + case BiometricPrompt.DISMISSED_REASON_CONFIRMED: + case BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED: + mKeyStore.addAuthToken(mCurrentAuthSession.mTokenEscrow); + mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded(); + break; + + case BiometricPrompt.DISMISSED_REASON_NEGATIVE: + mCurrentAuthSession.mClientReceiver.onDialogDismissed(reason); + // Cancel authentication. Skip the token/package check since we are cancelling + // from system server. The interface is permission protected so this is fine. + cancelInternal(null /* token */, null /* package */, false /* fromClient */); + break; + + case BiometricPrompt.DISMISSED_REASON_USER_CANCEL: + mCurrentAuthSession.mClientReceiver.onError( + BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED, + getContext().getString(R.string.biometric_error_user_canceled)); + // Cancel authentication. Skip the token/package check since we are cancelling + // from system server. The interface is permission protected so this is fine. + cancelInternal(null /* token */, null /* package */, false /* fromClient */); + break; + + case BiometricPrompt.DISMISSED_REASON_ERROR: + mCurrentAuthSession.mClientReceiver.onError(mCurrentAuthSession.mErrorEscrow, + mCurrentAuthSession.mErrorStringEscrow); + break; + + default: + Slog.w(TAG, "Unhandled reason: " + reason); + break; } - // 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. + mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); + mCurrentAuthSession.mState = STATE_AUTH_IDLE; + mCurrentAuthSession = null; + } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } @@ -1517,8 +1609,6 @@ public class BiometricService extends SystemService { return; } - mCurrentModality = modality; - // Start preparing for authentication. Authentication starts when // all modalities requested have invoked onReadyForAuthentication. authenticateInternal(token, sessionId, userId, receiver, opPackageName, bundle, @@ -1637,25 +1727,28 @@ public class BiometricService extends SystemService { final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); final int callingUserId = UserHandle.getCallingUserId(); - mHandler.post(() -> { - try { - // TODO: For multiple modalities, send a single ERROR_CANCELED only when all - // drivers have canceled authentication. - if ((mCurrentModality & TYPE_FINGERPRINT) != 0) { - mFingerprintService.cancelAuthenticationFromService(token, opPackageName, - callingUid, callingPid, callingUserId, fromClient); - } - if ((mCurrentModality & TYPE_IRIS) != 0) { - Slog.w(TAG, "Iris unsupported"); - } - if ((mCurrentModality & TYPE_FACE) != 0) { - mFaceService.cancelAuthenticationFromService(token, opPackageName, - callingUid, callingPid, callingUserId, fromClient); - } - } catch (RemoteException e) { - Slog.e(TAG, "Unable to cancel authentication"); - } - }); - } + try { + if (mCurrentAuthSession.mState != STATE_AUTH_STARTED) { + Slog.w(TAG, "Skipping cancelInternal, state: " + mCurrentAuthSession.mState); + return; + } + + // TODO: For multiple modalities, send a single ERROR_CANCELED only when all + // drivers have canceled authentication. + if ((mCurrentAuthSession.mModality & TYPE_FINGERPRINT) != 0) { + mFingerprintService.cancelAuthenticationFromService(token, opPackageName, + callingUid, callingPid, callingUserId, fromClient); + } + if ((mCurrentAuthSession.mModality & TYPE_IRIS) != 0) { + Slog.w(TAG, "Iris unsupported"); + } + if ((mCurrentAuthSession.mModality & TYPE_FACE) != 0) { + mFaceService.cancelAuthenticationFromService(token, opPackageName, + callingUid, callingPid, callingUserId, fromClient); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to cancel authentication"); + } + } } diff --git a/services/core/java/com/android/server/biometrics/BiometricServiceBase.java b/services/core/java/com/android/server/biometrics/BiometricServiceBase.java index 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/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..233db9a293eaa --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -0,0 +1,742 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +import static junit.framework.Assert.assertEquals; + +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.app.IActivityManager; +import android.app.IActivityTaskManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.IBiometricService; +import android.hardware.biometrics.IBiometricServiceReceiver; +import android.hardware.biometrics.IBiometricServiceReceiverInternal; +import android.hardware.face.FaceManager; +import android.hardware.face.IFaceService; +import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.IFingerprintService; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.security.KeyStore; + +import com.android.internal.R; +import com.android.internal.statusbar.IStatusBarService; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +@SmallTest +public class BiometricServiceTest { + + private static final String TAG = "BiometricServiceTest"; + + private static final String ERROR_HW_UNAVAILABLE = "hw_unavailable"; + private static final String ERROR_NOT_RECOGNIZED = "not_recognized"; + private static final String ERROR_TIMEOUT = "error_timeout"; + private static final String ERROR_CANCELED = "error_canceled"; + private static final String ERROR_UNABLE_TO_PROCESS = "error_unable_to_process"; + private static final String ERROR_USER_CANCELED = "error_user_canceled"; + + private static final String FINGERPRINT_ACQUIRED_SENSOR_DIRTY = "sensor_dirty"; + + private BiometricService mBiometricService; + + @Mock + private Context mContext; + @Mock + private ContentResolver mContentResolver; + @Mock + private Resources mResources; + @Mock + private PackageManager mPackageManager; + @Mock + private AppOpsManager mAppOpsManager; + @Mock + IBiometricServiceReceiver mReceiver1; + @Mock + IBiometricServiceReceiver mReceiver2; + @Mock + FingerprintManager mFingerprintManager; + @Mock + FaceManager mFaceManager; + + private static class MockInjector extends BiometricService.Injector { + @Override + IActivityManager getActivityManagerService() { + return mock(IActivityManager.class); + } + + @Override + IActivityTaskManager getActivityTaskManagerService() { + return mock(IActivityTaskManager.class); + } + + @Override + IStatusBarService getStatusBarService() { + return mock(IStatusBarService.class); + } + + @Override + IFingerprintService getFingerprintService() { + return mock(IFingerprintService.class); + } + + @Override + IFaceService getFaceService() { + return mock(IFaceService.class); + } + + @Override + BiometricService.SettingObserver getSettingObserver(Context context, Handler handler, + List callbacks) { + return mock(BiometricService.SettingObserver.class); + } + + @Override + KeyStore getKeyStore() { + return mock(KeyStore.class); + } + + @Override + boolean isDebugEnabled(Context context, int userId) { + return false; + } + + @Override + void publishBinderService(BiometricService service, IBiometricService.Stub impl) { + // no-op for test + } + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager); + when(mContext.getSystemService(Context.FINGERPRINT_SERVICE)) + .thenReturn(mFingerprintManager); + when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + when(mContext.getResources()).thenReturn(mResources); + + when(mResources.getString(R.string.biometric_error_hw_unavailable)) + .thenReturn(ERROR_HW_UNAVAILABLE); + when(mResources.getString(R.string.biometric_not_recognized)) + .thenReturn(ERROR_NOT_RECOGNIZED); + when(mResources.getString(R.string.biometric_error_user_canceled)) + .thenReturn(ERROR_USER_CANCELED); + } + + @Test + public void testAuthenticate_withoutHardware_returnsErrorHardwareNotPresent() throws Exception { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false); + + mBiometricService = new BiometricService(mContext, new MockInjector()); + mBiometricService.onStart(); + + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + waitForIdle(); + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_HW_NOT_PRESENT), eq(ERROR_HW_UNAVAILABLE)); + } + + @Test + public void testAuthenticate_withoutEnrolled_returnsErrorNoBiometrics() throws Exception { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + + mBiometricService = new BiometricService(mContext, new MockInjector()); + mBiometricService.onStart(); + + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + waitForIdle(); + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS), any()); + } + + @Test + public void testAuthenticate_whenHalIsDead_returnsErrorHardwareUnavailable() throws Exception { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + + mBiometricService = new BiometricService(mContext, new MockInjector()); + mBiometricService.onStart(); + + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + waitForIdle(); + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE)); + } + + @Test + public void testAuthenticateFace_respectsUserSetting() + throws Exception { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + mBiometricService = new BiometricService(mContext, new MockInjector()); + mBiometricService.onStart(); + + // Disabled in user settings receives onError + when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(false); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + waitForIdle(); + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE)); + + // Enrolled, not disabled in settings, user requires confirmation in settings + resetReceiver(); + when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(true); + when(mBiometricService.mSettingObserver.getFaceAlwaysRequireConfirmation(anyInt())) + .thenReturn(true); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + waitForIdle(); + verify(mReceiver1, never()).onError(anyInt(), any(String.class)); + verify(mBiometricService.mFaceService, times(1)).prepareForAuthentication( + eq(true) /* requireConfirmation */, + any(IBinder.class), + anyLong() /* sessionId */, + anyInt() /* userId */, + any(IBiometricServiceReceiverInternal.class), + anyString() /* opPackageName */, + anyInt() /* cookie */, + anyInt() /* callingUid */, + anyInt() /* callingPid */, + anyInt() /* callingUserId */); + + // Enrolled, not disabled in settings, user doesn't require confirmation in settings + resetReceiver(); + when(mBiometricService.mSettingObserver.getFaceAlwaysRequireConfirmation(anyInt())) + .thenReturn(false); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + waitForIdle(); + verify(mBiometricService.mFaceService, times(1)).prepareForAuthentication( + eq(false) /* requireConfirmation */, + any(IBinder.class), + anyLong() /* sessionId */, + anyInt() /* userId */, + any(IBiometricServiceReceiverInternal.class), + anyString() /* opPackageName */, + anyInt() /* cookie */, + anyInt() /* callingUid */, + anyInt() /* callingPid */, + anyInt() /* callingUserId */); + } + + @Test + public void testAuthenticate_happyPathWithoutConfirmation() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + mBiometricService = new BiometricService(mContext, new MockInjector()); + mBiometricService.onStart(); + + // Start testing the happy path + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + waitForIdle(); + + // Creates a pending auth session with the correct initial states + assertEquals(mBiometricService.mPendingAuthSession.mState, + BiometricService.STATE_AUTH_CALLED); + + // Invokes Service#prepareForAuthentication + ArgumentCaptor cookieCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mReceiver1, never()).onError(anyInt(), any(String.class)); + verify(mBiometricService.mFingerprintService, times(1)).prepareForAuthentication( + any(IBinder.class), + anyLong() /* sessionId */, + anyInt() /* userId */, + any(IBiometricServiceReceiverInternal.class), + anyString() /* opPackageName */, + cookieCaptor.capture() /* cookie */, + anyInt() /* callingUid */, + anyInt() /* callingPid */, + anyInt() /* callingUserId */); + + // onReadyForAuthentication, mCurrentAuthSession state OK + mBiometricService.mImpl.onReadyForAuthentication(cookieCaptor.getValue(), + anyBoolean() /* requireConfirmation */, anyInt() /* userId */); + waitForIdle(); + assertNull(mBiometricService.mPendingAuthSession); + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_STARTED); + + // startPreparedClient invoked + verify(mBiometricService.mFingerprintService, times(1)) + .startPreparedClient(cookieCaptor.getValue()); + + // StatusBar showBiometricDialog invoked + verify(mBiometricService.mStatusBarService, times(1)).showBiometricDialog( + eq(mBiometricService.mCurrentAuthSession.mBundle), + any(IBiometricServiceReceiverInternal.class), + eq(BiometricAuthenticator.TYPE_FINGERPRINT), + anyBoolean() /* requireConfirmation */, + anyInt() /* userId */); + + // Hardware authenticated + mBiometricService.mInternalReceiver.onAuthenticationSucceeded( + false /* requireConfirmation */, + new byte[69] /* HAT */); + waitForIdle(); + // Waiting for SystemUI to send dismissed callback + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTHENTICATED_PENDING_SYSUI); + // Notify SystemUI hardware authenticated + verify(mBiometricService.mStatusBarService, times(1)).onBiometricAuthenticated( + eq(true) /* authenticated */, eq(null) /* failureReason */); + + // SystemUI sends callback with dismissed reason + mBiometricService.mInternalReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + waitForIdle(); + // HAT sent to keystore + verify(mBiometricService.mKeyStore, times(1)).addAuthToken(any(byte[].class)); + // Send onAuthenticated to client + verify(mReceiver1, times(1)).onAuthenticationSucceeded(); + // Current session becomes null + assertNull(mBiometricService.mCurrentAuthSession); + } + + @Test + public void testAuthenticate_happyPathWithConfirmation() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + true /* requireConfirmation */); + + // Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not + // sent to KeyStore yet + mBiometricService.mInternalReceiver.onAuthenticationSucceeded( + true /* requireConfirmation */, + new byte[69] /* HAT */); + waitForIdle(); + // Waiting for SystemUI to send confirmation callback + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_PENDING_CONFIRM); + verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); + + // SystemUI sends confirm, HAT is sent to keystore and client is notified. + mBiometricService.mInternalReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_CONFIRMED); + waitForIdle(); + verify(mBiometricService.mKeyStore, times(1)).addAuthToken(any(byte[].class)); + verify(mReceiver1, times(1)).onAuthenticationSucceeded(); + } + + @Test + public void testRejectFace_whenAuthenticating_notifiesSystemUIAndClient_thenPaused() + throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onAuthenticationFailed(); + waitForIdle(); + + verify(mBiometricService.mStatusBarService, times(1)) + .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED)); + verify(mReceiver1, times(1)).onAuthenticationFailed(); + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_PAUSED); + } + + @Test + public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating() + throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onAuthenticationFailed(); + waitForIdle(); + + verify(mBiometricService.mStatusBarService, times(1)) + .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED)); + verify(mReceiver1, times(1)).onAuthenticationFailed(); + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_STARTED); + } + + @Test + public void testErrorCanceled_whenAuthenticating_notifiesSystemUIAndClient() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + // Create a new pending auth session but don't start it yet. HAL contract is that previous + // one must get ERROR_CANCELED. Simulate that here by creating the pending auth session, + // sending ERROR_CANCELED to the current auth session, and then having the second one + // onReadyForAuthentication. + invokeAuthenticate(mBiometricService.mImpl, mReceiver2, false /* requireConfirmation */); + waitForIdle(); + + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_STARTED); + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_CANCELED, ERROR_CANCELED); + waitForIdle(); + + // Auth session becomes null + assertNull(mBiometricService.mCurrentAuthSession); + + // ERROR_CANCELED sent to first client + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(ERROR_CANCELED)); + verify(mReceiver2, never()).onError(anyInt(), any(String.class)); + + // SystemUI dialog closed + verify(mBiometricService.mStatusBarService, times(1)).hideBiometricDialog(); + } + + @Test + public void testErrorHalTimeout_whenAuthenticating_entersPausedState() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, + ERROR_TIMEOUT); + waitForIdle(); + + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_PAUSED); + verify(mBiometricService.mStatusBarService, times(1)) + .onBiometricAuthenticated(eq(false), eq(ERROR_TIMEOUT)); + // Timeout does not count as fail as per BiometricPrompt documentation. + verify(mReceiver1, never()).onAuthenticationFailed(); + + // No pending auth session. Pressing try again will create one. + assertNull(mBiometricService.mPendingAuthSession); + + // Pressing "Try again" on SystemUI starts a new auth session. + mBiometricService.mInternalReceiver.onTryAgainPressed(); + waitForIdle(); + + // The last one is still paused, and a new one has been created. + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_PAUSED); + assertEquals(mBiometricService.mPendingAuthSession.mState, + BiometricService.STATE_AUTH_CALLED); + + // Test resuming when hardware becomes ready. SystemUI should not be requested to + // show another dialog since it's already showing. + resetStatusBar(); + startPendingAuthSession(mBiometricService); + waitForIdle(); + verify(mBiometricService.mStatusBarService, never()).showBiometricDialog( + any(Bundle.class), + any(IBiometricServiceReceiverInternal.class), + anyInt(), + anyBoolean() /* requireConfirmation */, + anyInt() /* userId */); + } + + @Test + public void testErrorFromHal_whenPaused_notifiesSystemUIAndClient() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireCOnfirmation */); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, + ERROR_TIMEOUT); + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_CANCELED, + ERROR_CANCELED); + waitForIdle(); + + // Client receives error immediately + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), + eq(ERROR_CANCELED)); + // Dialog is hidden immediately + verify(mBiometricService.mStatusBarService, times(1)).hideBiometricDialog(); + // Auth session is over + assertNull(mBiometricService.mCurrentAuthSession); + } + + @Test + public void testErrorFromHal_whileAuthenticating_waitsForSysUIBeforeNotifyingClient() + throws Exception { + // For errors that show in SystemUI, BiometricService stays in STATE_ERROR_PENDING_SYSUI + // until SystemUI notifies us that the dialog is dismissed at which point the current + // session is done. + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS, + ERROR_UNABLE_TO_PROCESS); + waitForIdle(); + + // Sends error to SystemUI and does not notify client yet + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_ERROR_PENDING_SYSUI); + verify(mBiometricService.mStatusBarService, times(1)) + .onBiometricError(eq(ERROR_UNABLE_TO_PROCESS)); + verify(mBiometricService.mStatusBarService, never()).hideBiometricDialog(); + verify(mReceiver1, never()).onError(anyInt(), anyString()); + + // SystemUI animation completed, client is notified, auth session is over + mBiometricService.mInternalReceiver + .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR); + waitForIdle(); + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), + eq(ERROR_UNABLE_TO_PROCESS)); + assertNull(mBiometricService.mCurrentAuthSession); + } + + @Test + public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication() + throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver + .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); + waitForIdle(); + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), + eq(ERROR_USER_CANCELED)); + verify(mBiometricService.mFingerprintService, times(1)).cancelAuthenticationFromService( + any(), + any(), + anyInt(), + anyInt(), + anyInt(), + eq(false) /* fromClient */); + assertNull(mBiometricService.mCurrentAuthSession); + } + + @Test + public void testDismissedReasonNegative_whilePaused_doesntInvokeHalCancel() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, + ERROR_TIMEOUT); + mBiometricService.mInternalReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_NEGATIVE); + waitForIdle(); + + verify(mBiometricService.mFaceService, never()).cancelAuthenticationFromService( + any(), + any(), + anyInt(), + anyInt(), + anyInt(), + anyBoolean()); + } + + @Test + public void testDismissedReasonUserCancel_whilePaused_doesntInvokeHalCancel() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, + ERROR_TIMEOUT); + mBiometricService.mInternalReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_USER_CANCEL); + waitForIdle(); + + verify(mBiometricService.mFaceService, never()).cancelAuthenticationFromService( + any(), + any(), + anyInt(), + anyInt(), + anyInt(), + anyBoolean()); + } + + @Test + public void testDismissedReasonUserCancel_whenPendingConfirmation() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + true /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onAuthenticationSucceeded( + true /* requireConfirmation */, + new byte[69] /* HAT */); + mBiometricService.mInternalReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_USER_CANCEL); + waitForIdle(); + + // doesn't send cancel to HAL + verify(mBiometricService.mFaceService, never()).cancelAuthenticationFromService( + any(), + any(), + anyInt(), + anyInt(), + anyInt(), + anyBoolean()); + verify(mReceiver1, times(1)).onError( + eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), + eq(ERROR_USER_CANCELED)); + assertNull(mBiometricService.mCurrentAuthSession); + } + + @Test + public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */); + + mBiometricService.mInternalReceiver.onAcquired( + FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, + FINGERPRINT_ACQUIRED_SENSOR_DIRTY); + waitForIdle(); + + // Sends to SysUI and stays in authenticating state + verify(mBiometricService.mStatusBarService, times(1)) + .onBiometricHelp(eq(FINGERPRINT_ACQUIRED_SENSOR_DIRTY)); + assertEquals(mBiometricService.mCurrentAuthSession.mState, + BiometricService.STATE_AUTH_STARTED); + } + + // Helper methods + + private void setupAuthForOnly(int modality) { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false); + + if (modality == BiometricAuthenticator.TYPE_FINGERPRINT) { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + } else if (modality == BiometricAuthenticator.TYPE_FACE) { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + } else { + fail("Unknown modality: " + modality); + } + + mBiometricService = new BiometricService(mContext, new MockInjector()); + mBiometricService.onStart(); + + when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(true); + } + + private void resetReceiver() { + mReceiver1 = mock(IBiometricServiceReceiver.class); + mReceiver2 = mock(IBiometricServiceReceiver.class); + } + + private void resetStatusBar() { + mBiometricService.mStatusBarService = mock(IStatusBarService.class); + } + + private void invokeAuthenticateAndStart(IBiometricService.Stub service, + IBiometricServiceReceiver receiver, boolean requireConfirmation) throws Exception { + // Request auth, creates a pending session + invokeAuthenticate(service, receiver, requireConfirmation); + waitForIdle(); + + startPendingAuthSession(mBiometricService); + waitForIdle(); + } + + private static void startPendingAuthSession(BiometricService service) throws Exception { + // Get the cookie so we can pretend the hardware is ready to authenticate + // Currently we only support single modality per auth + assertEquals(service.mPendingAuthSession.mModalitiesWaiting.values().size(), 1); + final int cookie = service.mPendingAuthSession.mModalitiesWaiting.values() + .iterator().next(); + assertNotEquals(cookie, 0); + + service.mImpl.onReadyForAuthentication(cookie, + anyBoolean() /* requireConfirmation */, anyInt() /* userId */); + } + + private static void invokeAuthenticate(IBiometricService.Stub service, + IBiometricServiceReceiver receiver, boolean requireConfirmation) throws Exception { + service.authenticate( + new Binder() /* token */, + 0 /* sessionId */, + 0 /* userId */, + receiver, + "test" /* packageName */, + createTestBiometricPromptBundle(requireConfirmation), + null /* IBiometricConfirmDeviceCredentialCallback */); + } + + private static Bundle createTestBiometricPromptBundle(boolean requireConfirmation) { + final Bundle bundle = new Bundle(); + bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, requireConfirmation); + return bundle; + } + + private static int getCookieForCurrentSession(BiometricService.AuthSession session) { + assertEquals(session.mModalitiesMatched.values().size(), 1); + return session.mModalitiesMatched.values().iterator().next(); + } + + private static void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } +} From 050315f61a27fc399e17a8396fa6b999ee98f211 Mon Sep 17 00:00:00 2001 From: Kevin Chyn Date: Thu, 8 Aug 2019 14:22:54 -0700 Subject: [PATCH 3/4] 3/n: Move task stack listener to SystemUI Simplifies the dismissal of the dialog, and allows for better synchronization of when the client should receive the error. The only time that BiometricService should dismiss the dialog is when authentication is canceled due to another client, which is almost always due to allowed-but-weird app behavior. Bug: 135082347 Test: atest BiometricServiceTest Test: atest BiometricDialogImplTest Test: atest CommandQueueTest Change-Id: I10daa798115e51af8a854759e30033c28e6636ba --- .../hardware/biometrics/BiometricPrompt.java | 6 + .../internal/statusbar/IStatusBar.aidl | 2 +- .../internal/statusbar/IStatusBarService.aidl | 2 +- .../systemui/biometrics/BiometricDialog.java | 14 ++- .../biometrics/BiometricDialogImpl.java | 116 +++++++++++++++--- .../biometrics/DialogViewCallback.java | 13 +- .../biometrics/ui/BiometricDialogView.java | 37 ++++-- .../systemui/statusbar/CommandQueue.java | 8 +- .../biometrics/BiometricDialogImplTest.java | 111 ++++++++++++----- .../systemui/statusbar/CommandQueueTest.java | 6 +- .../server/biometrics/BiometricService.java | 83 ++----------- .../statusbar/StatusBarManagerService.java | 5 +- .../biometrics/BiometricServiceTest.java | 94 +++++++------- 13 files changed, 312 insertions(+), 185 deletions(-) diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 14acc6fb834d9..fb6b231632f19 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -128,6 +128,12 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan */ 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; 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 index ca9d372d14446..d4baefd645123 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java @@ -57,12 +57,15 @@ public interface BiometricDialog { void show(WindowManager wm, boolean skipIntroAnimation); /** - * Dismiss the dialog without sending a callback. Only used when the system detects a case - * where the error won't come from the UI (e.g. task stack changed). - * @param animate + * 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. */ @@ -97,4 +100,9 @@ public interface BiometricDialog { * @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 0c3cea780d4e3..a8e572216315d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java @@ -16,21 +16,30 @@ 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.BiometricPrompt; import android.hardware.biometrics.IBiometricServiceReceiverInternal; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; 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.SystemUI; import com.android.systemui.biometrics.ui.BiometricDialogView; import com.android.systemui.statusbar.CommandQueue; +import java.util.List; + /** * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the * appropriate biometric UI (e.g. BiometricDialogView). @@ -40,12 +49,51 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba private static final String TAG = "BiometricDialogImpl"; private static final boolean DEBUG = true; + private final Injector mInjector; + // TODO: These should just be saved from onSaveState private SomeArgs mCurrentDialogArgs; - private BiometricDialog mCurrentDialog; + @VisibleForTesting + BiometricDialog mCurrentDialog; + private Handler mHandler = new Handler(Looper.getMainLooper()); private WindowManager mWindowManager; - private IBiometricServiceReceiverInternal mReceiver; + @VisibleForTesting + IActivityTaskManager mActivityTaskManager; + @VisibleForTesting + BiometricTaskStackListener mTaskStackListener; + @VisibleForTesting + IBiometricServiceReceiverInternal mReceiver; + + public class BiometricTaskStackListener extends TaskStackListener { + @Override + public void onTaskStackChanged() { + mHandler.post(mTaskStackChangedRunnable); + } + } + + 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() { @@ -57,7 +105,7 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba } @Override - public void onDismissed(int reason) { + public void onDismissed(@DismissedReason int reason) { switch (reason) { case DialogViewCallback.DISMISSED_USER_CANCELED: sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); @@ -78,23 +126,43 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba 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(int result) { + private void sendResultAndCleanUp(@DismissedReason int reason) { if (mReceiver == null) { Log.e(TAG, "Receiver is null"); return; } try { - mReceiver.onDialogDismissed(result); + mReceiver.onDialogDismissed(reason); } catch (RemoteException e) { Log.w(TAG, "Remote exception", e); } - onDialogDismissed(); + onDialogDismissed(reason); + } + + public static class Injector { + IActivityTaskManager getActivityTaskManager() { + return ActivityTaskManager.getService(); + } + } + + public BiometricDialogImpl() { + this(new Injector()); + } + + @VisibleForTesting + BiometricDialogImpl(Injector injector) { + mInjector = injector; } @Override @@ -105,12 +173,20 @@ 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); + 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); @@ -121,13 +197,14 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba args.argi1 = type; args.arg3 = requireConfirmation; args.argi2 = userId; + args.arg4 = opPackageName; boolean skipAnimation = false; if (mCurrentDialog != null) { Log.w(TAG, "mCurrentDialog: " + mCurrentDialog); skipAnimation = true; } - showDialog(args, skipAnimation /* skipAnimation */, null /* savedState */); + showDialog(args, skipAnimation, null /* savedState */); } @Override @@ -159,20 +236,24 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba public void hideBiometricDialog() { if (DEBUG) Log.d(TAG, "hideBiometricDialog"); - // TODO: I think we need to remove this interface - mCurrentDialog.dismissWithoutCallback(true /* animate */); + 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( - (Bundle) args.arg1 /* bundle */, - (boolean) args.arg3 /* requireConfirmation */, - args.argi2 /* userId */, - type); + biometricPromptBundle, + requireConfirmation, + userId, + type, + opPackageName); if (newDialog == null) { Log.e(TAG, "Unsupported type: " + type); @@ -204,8 +285,8 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba mCurrentDialog.show(mWindowManager, skipAnimation); } - private void onDialogDismissed() { - if (DEBUG) Log.d(TAG, "onDialogDismissed"); + private void onDialogDismissed(@DismissedReason int reason) { + if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason); if (mCurrentDialog == null) { Log.w(TAG, "Dialog already dismissed"); } @@ -229,12 +310,13 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba } protected BiometricDialog buildDialog(Bundle biometricPromptBundle, - boolean requireConfirmation, int userId, int type) { + 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 acad62ac13007..b65d1e823a9b1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java @@ -16,6 +16,8 @@ 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). @@ -28,12 +30,21 @@ public interface DialogViewCallback { 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 the dialog is dismissed * @param reason */ - void onDismissed(int reason); + void onDismissed(@DismissedReason int reason); /** * Invoked when the "try again" button is clicked diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java index a1687acf88c65..66718c157bed8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java @@ -109,6 +109,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet private Bundle mBundle; private Bundle mRestoredState; + private String mOpPackageName; private int mState = STATE_IDLE; private boolean mWasForceRemoved; @@ -180,6 +181,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet private Bundle mBundle; private boolean mRequireConfirmation; private int mUserId; + private String mOpPackageName; public Builder(Context context) { mContext = context; @@ -205,6 +207,11 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet return this; } + public Builder setOpPackageName(String opPackageName) { + mOpPackageName = opPackageName; + return this; + } + public BiometricDialogView build(int type) { BiometricDialogView dialog; if (type == TYPE_FINGERPRINT) { @@ -217,6 +224,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet dialog.setBundle(mBundle); dialog.setRequireConfirmation(mRequireConfirmation); dialog.setUserId(mUserId); + dialog.setOpPackageName(mOpPackageName); return dialog; } } @@ -246,18 +254,12 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet addView(mLayout); mLayout.setOnKeyListener(new View.OnKeyListener() { - boolean mDownPressed = false; @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (keyCode != KeyEvent.KEYCODE_BACK) { return false; } - if (event.getAction() == KeyEvent.ACTION_DOWN && mDownPressed == false) { - mDownPressed = true; - } else if (event.getAction() == KeyEvent.ACTION_DOWN) { - mDownPressed = false; - } else if (event.getAction() == KeyEvent.ACTION_UP && mDownPressed == true) { - mDownPressed = false; + if (event.getAction() == KeyEvent.ACTION_UP) { animateAway(DialogViewCallback.DISMISSED_USER_CANCELED); } return true; @@ -421,14 +423,15 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet }); } - private void animateAway(int reason) { + 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, int reason) { + private void animateAway(boolean sendReason, @DialogViewCallback.DismissedReason int reason) { if (!mCompletedAnimatingIn) { Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn"); mPendingDismissDialog = true; @@ -493,6 +496,10 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet mUserId = userId; } + private void setOpPackageName(String opPackageName) { + mOpPackageName = opPackageName; + } + // Shows an error/help message protected void showTemporaryMessage(String message) { mHandler.removeMessages(MSG_RESET_MESSAGE); @@ -518,7 +525,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet @Override public void dismissWithoutCallback(boolean animate) { if (animate) { - animateAway(false /* sendReason */, 0); + animateAway(false /* sendReason */, 0 /* reason */); } else { mLayout.animate().cancel(); mDialog.animate().cancel(); @@ -527,6 +534,11 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet } } + @Override + public void dismissFromSystemServer() { + animateAway(DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER); + } + @Override public void onAuthenticationSucceeded() { announceForAccessibility(getResources().getText(getAuthenticatedAccessibilityResourceId())); @@ -613,6 +625,11 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet } } + @Override + public String getOpPackageName() { + return mOpPackageName; + } + protected void updateState(int newState) { if (newState == STATE_PENDING_CONFIRMATION) { mHandler.removeMessages(MSG_RESET_MESSAGE); 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 index 3122051772c47..8f2f8b1c0e632 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java @@ -17,15 +17,20 @@ 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.times; 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; @@ -47,6 +52,9 @@ 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 @@ -79,7 +87,10 @@ public class BiometricDialogImplTest extends SysuiTestCase { when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) .thenReturn(true); - mBiometricDialogImpl = new TestableBiometricDialogImpl(); + when(mDialog1.getOpPackageName()).thenReturn("Dialog1"); + when(mDialog2.getOpPackageName()).thenReturn("Dialog2"); + + mBiometricDialogImpl = new TestableBiometricDialogImpl(new MockInjector()); mBiometricDialogImpl.mContext = context; mBiometricDialogImpl.mComponents = mContext.getComponents(); @@ -92,42 +103,42 @@ public class BiometricDialogImplTest extends SysuiTestCase { public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception { showDialog(BiometricPrompt.TYPE_FACE); mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_USER_CANCELED); - verify(mReceiver, times(1)) - .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); + 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, times(1)) - .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_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, times(1)) - .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED); } @Test public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception { - // TODO: Modify BiometricService / BiometricDialog showDialog(BiometricPrompt.TYPE_FACE); mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_AUTHENTICATED); - verify(mReceiver, times(1)) - .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); } @Test public void testSendsReasonError_whenDismissedByError() throws Exception { - // TODO: Modify BiometricService / BiometricDialog showDialog(BiometricPrompt.TYPE_FACE); mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_ERROR); - verify(mReceiver, times(1)) - .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_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 @@ -136,14 +147,14 @@ public class BiometricDialogImplTest extends SysuiTestCase { public void testShowInvoked_whenSystemRequested() throws Exception { showDialog(BiometricPrompt.TYPE_FACE); - verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */); + 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, times(1)).onAuthenticationSucceeded(); + verify(mDialog1).onAuthenticationSucceeded(); } @Test @@ -153,7 +164,7 @@ public class BiometricDialogImplTest extends SysuiTestCase { mBiometricDialogImpl.onBiometricAuthenticated(false, failureReason); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(mDialog1, times(1)).onAuthenticationFailed(captor.capture()); + verify(mDialog1).onAuthenticationFailed(captor.capture()); assertEquals(captor.getValue(), failureReason); } @@ -165,7 +176,7 @@ public class BiometricDialogImplTest extends SysuiTestCase { mBiometricDialogImpl.onBiometricHelp(helpMessage); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(mDialog1, times(1)).onHelp(captor.capture()); + verify(mDialog1).onHelp(captor.capture()); assertEquals(captor.getValue(), helpMessage); } @@ -177,7 +188,7 @@ public class BiometricDialogImplTest extends SysuiTestCase { mBiometricDialogImpl.onBiometricError(errMessage); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(mDialog1, times(1)).onError(captor.capture()); + verify(mDialog1).onError(captor.capture()); assertEquals(captor.getValue(), errMessage); } @@ -186,8 +197,17 @@ public class BiometricDialogImplTest extends SysuiTestCase { public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception { showDialog(BiometricPrompt.TYPE_FACE); mBiometricDialogImpl.hideBiometricDialog(); + verify(mDialog1).dismissFromSystemServer(); + } - verify(mDialog1, times(1)).dismissWithoutCallback(eq(true) /* animate */); + @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 @@ -195,41 +215,60 @@ public class BiometricDialogImplTest extends SysuiTestCase { @Test public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception { showDialog(BiometricPrompt.TYPE_FACE); - verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */); + verify(mDialog1).show(any(), eq(false) /* skipIntro */); showDialog(BiometricPrompt.TYPE_FACE); // First dialog should be dismissed without animation - verify(mDialog1, times(1)).dismissWithoutCallback(eq(false) /* animate */); + verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */); // Second dialog should be shown without animation - verify(mDialog2, times(1)).show(any(), eq(true)) /* skipIntro */; + verify(mDialog2).show(any(), eq(true)) /* skipIntro */; } @Test public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception { showDialog(BiometricPrompt.TYPE_FACE); - verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */); + verify(mDialog1).show(any(), eq(false) /* skipIntro */); mBiometricDialogImpl.onConfigurationChanged(new Configuration()); ArgumentCaptor captor = ArgumentCaptor.forClass(Bundle.class); - verify(mDialog1, times(1)).onSaveState(captor.capture()); + verify(mDialog1).onSaveState(captor.capture()); // Old dialog doesn't animate - verify(mDialog1, times(1)).dismissWithoutCallback(eq(false) /* animate */); + verify(mDialog1).dismissWithoutCallback(eq(false /* animate */)); // Saved state is restored into new dialog ArgumentCaptor captor2 = ArgumentCaptor.forClass(Bundle.class); - verify(mDialog2, times(1)).restoreState(captor2.capture()); + verify(mDialog2).restoreState(captor2.capture()); // Dialog for new configuration skips intro - verify(mDialog2, times(1)).show(any(), eq(true) /* skipIntro */); + 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 @@ -238,7 +277,8 @@ public class BiometricDialogImplTest extends SysuiTestCase { mReceiver /* receiver */, type, true /* requireConfirmation */, - 0 /* userId */); + 0 /* userId */, + "testPackage"); } private Bundle createTestDialogBundle() { @@ -259,9 +299,13 @@ public class BiometricDialogImplTest extends SysuiTestCase { 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) { + boolean requireConfirmation, int userId, int type, String opPackageName) { BiometricDialog dialog; if (mBuildCount == 0) { dialog = mDialog1; @@ -274,5 +318,12 @@ public class BiometricDialogImplTest extends SysuiTestCase { 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/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index f68c11b4371bf..b252a0d95f940 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -369,9 +369,11 @@ public class CommandQueueTest extends SysuiTestCase { @Test public void testShowBiometricDialog() { Bundle bundle = new Bundle(); - mCommandQueue.showBiometricDialog(bundle, null /* receiver */, 1, true, 3); + 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)); + verify(mCallbacks).showBiometricDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3), + eq(packageName)); } @Test diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 94056694d8106..24e6a75e68440 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -25,12 +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.IActivityManager; -import android.app.IActivityTaskManager; import android.app.KeyguardManager; -import android.app.TaskStackListener; import android.app.UserSwitchObserver; import android.content.ContentResolver; import android.content.Context; @@ -90,7 +87,6 @@ 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_REJECTED = 3; private static final int MSG_ON_ERROR = 4; @@ -258,13 +254,6 @@ 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; @@ -275,7 +264,6 @@ public class BiometricService extends SystemService { @VisibleForTesting SettingObserver mSettingObserver; private final List mEnabledOnKeyguardCallbacks; - private final BiometricTaskStackListener mTaskStackListener = new BiometricTaskStackListener(); private final Random mRandom = new Random(); @VisibleForTesting @@ -283,8 +271,6 @@ public class BiometricService extends SystemService { @VisibleForTesting IFaceService mFaceService; @VisibleForTesting - IActivityTaskManager mActivityTaskManager; - @VisibleForTesting IStatusBarService mStatusBarService; @VisibleForTesting KeyStore mKeyStore; @@ -313,11 +299,6 @@ public class BiometricService extends SystemService { @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( @@ -911,10 +892,6 @@ public class BiometricService extends SystemService { return ActivityManager.getService(); } - IActivityTaskManager getActivityTaskManagerService() { - return ActivityTaskManager.getService(); - } - IStatusBarService getStatusBarService() { return IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); @@ -1002,7 +979,6 @@ public class BiometricService extends SystemService { } mKeyStore = mInjector.getKeyStore(); - mActivityTaskManager = mInjector.getActivityTaskManagerService(); mStatusBarService = mInjector.getStatusBarService(); // Cache the authenticators @@ -1225,30 +1201,6 @@ 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 @@ -1324,10 +1276,8 @@ public class BiometricService extends SystemService { return; } try { - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); mConfirmDeviceCredentialReceiver.onAuthenticationSucceeded(); if (mCurrentAuthSession != null) { - mCurrentAuthSession.mState = STATE_AUTH_IDLE; mCurrentAuthSession = null; } } catch (RemoteException e) { @@ -1343,10 +1293,8 @@ public class BiometricService extends SystemService { return; } try { - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); mConfirmDeviceCredentialReceiver.onError(error, message); if (mCurrentAuthSession != null) { - mCurrentAuthSession.mState = STATE_AUTH_IDLE; mCurrentAuthSession = null; } } catch (RemoteException e) { @@ -1376,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 @@ -1387,18 +1338,11 @@ 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 { - mCurrentAuthSession.mErrorEscrow = error; - mCurrentAuthSession.mErrorStringEscrow = message; - mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI; + mStatusBarService.onBiometricError(message); } } else if (mCurrentAuthSession.mState == STATE_AUTH_PAUSED) { // In the "try again" state, we should forward canceled errors to @@ -1406,9 +1350,6 @@ public class BiometricService extends SystemService { // ERROR_CANCELED due to another client kicking us out. mCurrentAuthSession.mClientReceiver.onError(error, message); mStatusBarService.hideBiometricDialog(); - mActivityTaskManager.unregisterTaskStackListener( - mTaskStackListener); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; mCurrentAuthSession = null; } else { Slog.e(TAG, "Impossible session error state: " @@ -1418,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: " @@ -1483,6 +1423,7 @@ public class BiometricService extends SystemService { 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); @@ -1494,8 +1435,6 @@ public class BiometricService extends SystemService { } // Dialog is gone, auth session is done. - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); - mCurrentAuthSession.mState = STATE_AUTH_IDLE; mCurrentAuthSession = null; } catch (RemoteException e) { @@ -1564,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); @@ -1700,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) { @@ -1729,7 +1667,10 @@ public class BiometricService extends SystemService { final int callingUserId = UserHandle.getCallingUserId(); try { - if (mCurrentAuthSession.mState != STATE_AUTH_STARTED) { + 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; } 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 index 233db9a293eaa..ccf3a908364a2 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -17,6 +17,7 @@ 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; @@ -29,13 +30,11 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.app.IActivityManager; -import android.app.IActivityTaskManager; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; @@ -75,6 +74,8 @@ 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"; @@ -111,11 +112,6 @@ public class BiometricServiceTest { return mock(IActivityManager.class); } - @Override - IActivityTaskManager getActivityTaskManagerService() { - return mock(IActivityTaskManager.class); - } - @Override IStatusBarService getStatusBarService() { return mock(IStatusBarService.class); @@ -185,7 +181,7 @@ public class BiometricServiceTest { invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); waitForIdle(); - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_HW_NOT_PRESENT), eq(ERROR_HW_UNAVAILABLE)); } @@ -199,7 +195,7 @@ public class BiometricServiceTest { invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); waitForIdle(); - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS), any()); } @@ -214,7 +210,7 @@ public class BiometricServiceTest { invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); waitForIdle(); - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE)); } @@ -232,7 +228,7 @@ public class BiometricServiceTest { when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(false); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); waitForIdle(); - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE)); // Enrolled, not disabled in settings, user requires confirmation in settings @@ -243,7 +239,7 @@ public class BiometricServiceTest { invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); waitForIdle(); verify(mReceiver1, never()).onError(anyInt(), any(String.class)); - verify(mBiometricService.mFaceService, times(1)).prepareForAuthentication( + verify(mBiometricService.mFaceService).prepareForAuthentication( eq(true) /* requireConfirmation */, any(IBinder.class), anyLong() /* sessionId */, @@ -261,7 +257,7 @@ public class BiometricServiceTest { .thenReturn(false); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); waitForIdle(); - verify(mBiometricService.mFaceService, times(1)).prepareForAuthentication( + verify(mBiometricService.mFaceService).prepareForAuthentication( eq(false) /* requireConfirmation */, any(IBinder.class), anyLong() /* sessionId */, @@ -291,7 +287,7 @@ public class BiometricServiceTest { // Invokes Service#prepareForAuthentication ArgumentCaptor cookieCaptor = ArgumentCaptor.forClass(Integer.class); verify(mReceiver1, never()).onError(anyInt(), any(String.class)); - verify(mBiometricService.mFingerprintService, times(1)).prepareForAuthentication( + verify(mBiometricService.mFingerprintService).prepareForAuthentication( any(IBinder.class), anyLong() /* sessionId */, anyInt() /* userId */, @@ -311,16 +307,17 @@ public class BiometricServiceTest { BiometricService.STATE_AUTH_STARTED); // startPreparedClient invoked - verify(mBiometricService.mFingerprintService, times(1)) + verify(mBiometricService.mFingerprintService) .startPreparedClient(cookieCaptor.getValue()); // StatusBar showBiometricDialog invoked - verify(mBiometricService.mStatusBarService, times(1)).showBiometricDialog( + verify(mBiometricService.mStatusBarService).showBiometricDialog( eq(mBiometricService.mCurrentAuthSession.mBundle), any(IBiometricServiceReceiverInternal.class), eq(BiometricAuthenticator.TYPE_FINGERPRINT), anyBoolean() /* requireConfirmation */, - anyInt() /* userId */); + anyInt() /* userId */, + eq(TEST_PACKAGE_NAME)); // Hardware authenticated mBiometricService.mInternalReceiver.onAuthenticationSucceeded( @@ -331,7 +328,7 @@ public class BiometricServiceTest { assertEquals(mBiometricService.mCurrentAuthSession.mState, BiometricService.STATE_AUTHENTICATED_PENDING_SYSUI); // Notify SystemUI hardware authenticated - verify(mBiometricService.mStatusBarService, times(1)).onBiometricAuthenticated( + verify(mBiometricService.mStatusBarService).onBiometricAuthenticated( eq(true) /* authenticated */, eq(null) /* failureReason */); // SystemUI sends callback with dismissed reason @@ -339,9 +336,9 @@ public class BiometricServiceTest { BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); waitForIdle(); // HAT sent to keystore - verify(mBiometricService.mKeyStore, times(1)).addAuthToken(any(byte[].class)); + verify(mBiometricService.mKeyStore).addAuthToken(any(byte[].class)); // Send onAuthenticated to client - verify(mReceiver1, times(1)).onAuthenticationSucceeded(); + verify(mReceiver1).onAuthenticationSucceeded(); // Current session becomes null assertNull(mBiometricService.mCurrentAuthSession); } @@ -367,8 +364,8 @@ public class BiometricServiceTest { mBiometricService.mInternalReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_CONFIRMED); waitForIdle(); - verify(mBiometricService.mKeyStore, times(1)).addAuthToken(any(byte[].class)); - verify(mReceiver1, times(1)).onAuthenticationSucceeded(); + verify(mBiometricService.mKeyStore).addAuthToken(any(byte[].class)); + verify(mReceiver1).onAuthenticationSucceeded(); } @Test @@ -381,9 +378,9 @@ public class BiometricServiceTest { mBiometricService.mInternalReceiver.onAuthenticationFailed(); waitForIdle(); - verify(mBiometricService.mStatusBarService, times(1)) + verify(mBiometricService.mStatusBarService) .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED)); - verify(mReceiver1, times(1)).onAuthenticationFailed(); + verify(mReceiver1).onAuthenticationFailed(); assertEquals(mBiometricService.mCurrentAuthSession.mState, BiometricService.STATE_AUTH_PAUSED); } @@ -398,9 +395,9 @@ public class BiometricServiceTest { mBiometricService.mInternalReceiver.onAuthenticationFailed(); waitForIdle(); - verify(mBiometricService.mStatusBarService, times(1)) + verify(mBiometricService.mStatusBarService) .onBiometricAuthenticated(eq(false), eq(ERROR_NOT_RECOGNIZED)); - verify(mReceiver1, times(1)).onAuthenticationFailed(); + verify(mReceiver1).onAuthenticationFailed(); assertEquals(mBiometricService.mCurrentAuthSession.mState, BiometricService.STATE_AUTH_STARTED); } @@ -425,16 +422,24 @@ public class BiometricServiceTest { BiometricConstants.BIOMETRIC_ERROR_CANCELED, ERROR_CANCELED); waitForIdle(); - // Auth session becomes null - assertNull(mBiometricService.mCurrentAuthSession); - - // ERROR_CANCELED sent to first client - verify(mReceiver1, times(1)).onError( - eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(ERROR_CANCELED)); + // 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, times(1)).hideBiometricDialog(); + 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 @@ -451,7 +456,7 @@ public class BiometricServiceTest { assertEquals(mBiometricService.mCurrentAuthSession.mState, BiometricService.STATE_AUTH_PAUSED); - verify(mBiometricService.mStatusBarService, times(1)) + verify(mBiometricService.mStatusBarService) .onBiometricAuthenticated(eq(false), eq(ERROR_TIMEOUT)); // Timeout does not count as fail as per BiometricPrompt documentation. verify(mReceiver1, never()).onAuthenticationFailed(); @@ -479,7 +484,8 @@ public class BiometricServiceTest { any(IBiometricServiceReceiverInternal.class), anyInt(), anyBoolean() /* requireConfirmation */, - anyInt() /* userId */); + anyInt() /* userId */, + anyString()); } @Test @@ -499,11 +505,11 @@ public class BiometricServiceTest { waitForIdle(); // Client receives error immediately - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(ERROR_CANCELED)); // Dialog is hidden immediately - verify(mBiometricService.mStatusBarService, times(1)).hideBiometricDialog(); + verify(mBiometricService.mStatusBarService).hideBiometricDialog(); // Auth session is over assertNull(mBiometricService.mCurrentAuthSession); } @@ -527,7 +533,7 @@ public class BiometricServiceTest { // Sends error to SystemUI and does not notify client yet assertEquals(mBiometricService.mCurrentAuthSession.mState, BiometricService.STATE_ERROR_PENDING_SYSUI); - verify(mBiometricService.mStatusBarService, times(1)) + verify(mBiometricService.mStatusBarService) .onBiometricError(eq(ERROR_UNABLE_TO_PROCESS)); verify(mBiometricService.mStatusBarService, never()).hideBiometricDialog(); verify(mReceiver1, never()).onError(anyInt(), anyString()); @@ -536,7 +542,7 @@ public class BiometricServiceTest { mBiometricService.mInternalReceiver .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR); waitForIdle(); - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(ERROR_UNABLE_TO_PROCESS)); assertNull(mBiometricService.mCurrentAuthSession); @@ -552,10 +558,10 @@ public class BiometricServiceTest { mBiometricService.mInternalReceiver .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); waitForIdle(); - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(ERROR_USER_CANCELED)); - verify(mBiometricService.mFingerprintService, times(1)).cancelAuthenticationFromService( + verify(mBiometricService.mFingerprintService).cancelAuthenticationFromService( any(), any(), anyInt(), @@ -632,7 +638,7 @@ public class BiometricServiceTest { anyInt(), anyInt(), anyBoolean()); - verify(mReceiver1, times(1)).onError( + verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(ERROR_USER_CANCELED)); assertNull(mBiometricService.mCurrentAuthSession); @@ -650,7 +656,7 @@ public class BiometricServiceTest { waitForIdle(); // Sends to SysUI and stays in authenticating state - verify(mBiometricService.mStatusBarService, times(1)) + verify(mBiometricService.mStatusBarService) .onBiometricHelp(eq(FINGERPRINT_ACQUIRED_SENSOR_DIRTY)); assertEquals(mBiometricService.mCurrentAuthSession.mState, BiometricService.STATE_AUTH_STARTED); @@ -720,7 +726,7 @@ public class BiometricServiceTest { 0 /* sessionId */, 0 /* userId */, receiver, - "test" /* packageName */, + TEST_PACKAGE_NAME /* packageName */, createTestBiometricPromptBundle(requireConfirmation), null /* IBiometricConfirmDeviceCredentialCallback */); } From 27fafb76551cfec876c01cfadfa7f9da6056312e Mon Sep 17 00:00:00 2001 From: Kevin Chyn Date: Fri, 9 Aug 2019 17:35:41 -0700 Subject: [PATCH 4/4] 4/n: Make BiometricDialogView testable Fixes: 138628043 Test: Manual BiometricPrompt test Test: atest BiometricDialogViewTest Change-Id: I105f44aad5ffe5f86a68ae688f7b56a5dd42c37d --- .../biometrics/ui/BiometricDialogView.java | 49 +++-- .../biometrics/ui/FaceDialogView.java | 12 +- .../biometrics/ui/FingerprintDialogView.java | 6 +- .../ui/BiometricDialogViewTest.java | 204 ++++++++++++++++++ 4 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/BiometricDialogViewTest.java diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java index 66718c157bed8..2b4dde55ef72d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java @@ -47,6 +47,7 @@ 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; @@ -82,7 +83,8 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet protected static final int STATE_PENDING_CONFIRMATION = 3; protected static final int STATE_AUTHENTICATED = 4; - private final WakefulnessLifecycle mWakefulnessLifecycle; + @VisibleForTesting + final WakefulnessLifecycle mWakefulnessLifecycle; private final AccessibilityManager mAccessibilityManager; private final IBinder mWindowToken = new Binder(); private final Interpolator mLinearOutSlowIn; @@ -96,14 +98,22 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet 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; @@ -147,7 +157,8 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet } }; - private final WakefulnessLifecycle.Observer mWakefulnessObserver = + @VisibleForTesting + final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { @Override public void onStartedGoingToSleep() { @@ -213,11 +224,15 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet } 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); + dialog = new FingerprintDialogView(mContext, mCallback, injector); } else if (type == TYPE_FACE) { - dialog = new FaceDialogView(mContext, mCallback); + dialog = new FaceDialogView(mContext, mCallback, injector); } else { return null; } @@ -229,9 +244,15 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet } } - protected BiometricDialogView(Context context, DialogViewCallback callback) { + public static class Injector { + public WakefulnessLifecycle getWakefulnessLifecycle() { + return Dependency.get(WakefulnessLifecycle.class); + } + } + + protected BiometricDialogView(Context context, DialogViewCallback callback, Injector injector) { super(context); - mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); + mWakefulnessLifecycle = injector.getWakefulnessLifecycle(); mCallback = callback; mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java index cfa17ee689229..bd8714812ab49 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java @@ -33,6 +33,7 @@ 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; @@ -53,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; @@ -153,13 +155,13 @@ public class FaceDialogView extends BiometricDialogView { announceAccessibilityEvent(); }; - protected 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; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java index 1c339ba6db546..e597080f89fd2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java @@ -33,9 +33,9 @@ public class FingerprintDialogView extends BiometricDialogView { private static final String TAG = "FingerprintDialogView"; - protected 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/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; + } +}