From 86f1b8e3358f328c7decfe73acee256334b68a44 Mon Sep 17 00:00:00 2001 From: Kevin Chyn Date: Tue, 24 Sep 2019 19:00:49 -0700 Subject: [PATCH] 15/n: Allow Auth UI to start in credential UI If the user is locked out of biometrics, and BiometricPrompt#setDeviceCredentialAllowed(true), the user should be shown the credential UI. This change gives BiometricService the ability to request SystemUI to show AuthCredentialView without first showing AuthBiometricView. Bug: 140127687 Test: atest BiometricServiceTest Test: atest com.android.systemui.biometrics Change-Id: Ic26986ba044b7992641676c3d3b99fc1395a45b7 --- .../hardware/biometrics/Authenticator.java | 35 ++++ .../hardware/biometrics/BiometricPrompt.java | 44 ++--- .../internal/statusbar/IStatusBar.aidl | 12 +- .../internal/statusbar/IStatusBarService.aidl | 12 +- .../biometrics/AuthBiometricView.java | 2 +- .../biometrics/AuthContainerView.java | 150 ++++++++------ .../systemui/biometrics/AuthController.java | 49 +++-- .../biometrics/AuthCredentialView.java | 30 +-- .../android/systemui/biometrics/Utils.java | 17 ++ .../systemui/statusbar/CommandQueue.java | 21 +- .../biometrics/AuthBiometricViewTest.java | 5 +- .../biometrics/AuthContainerViewTest.java | 105 +++++++++- .../biometrics/AuthControllerTest.java | 187 +++++++++++------- .../systemui/statusbar/CommandQueueTest.java | 13 +- .../server/biometrics/BiometricService.java | 104 ++++++++-- .../statusbar/StatusBarManagerService.java | 12 +- .../biometrics/BiometricServiceTest.java | 113 ++++++++++- 17 files changed, 648 insertions(+), 263 deletions(-) create mode 100644 core/java/android/hardware/biometrics/Authenticator.java diff --git a/core/java/android/hardware/biometrics/Authenticator.java b/core/java/android/hardware/biometrics/Authenticator.java new file mode 100644 index 0000000000000..6d7e7488f2d05 --- /dev/null +++ b/core/java/android/hardware/biometrics/Authenticator.java @@ -0,0 +1,35 @@ +/* + * 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 android.hardware.biometrics; + +/** + * Type of authenticators defined on a granularity that the BiometricManager / BiometricPrompt + * supports. + * @hide + */ +public class Authenticator { + + /** + * Device credential, e.g. Pin/Pattern/Password. + */ + public static final int TYPE_CREDENTIAL = 1 << 0; + /** + * Encompasses all biometrics on the device, e.g. Fingerprint/Iris/Face. + */ + public static final int TYPE_BIOMETRIC = 1 << 1; + +} diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 342743da9c352..cf86e25112d2d 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -66,10 +66,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @hide */ public static final String KEY_DESCRIPTION = "description"; - /** - * @hide - */ - public static final String KEY_POSITIVE_TEXT = "positive_text"; /** * @hide */ @@ -79,9 +75,15 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan */ public static final String KEY_REQUIRE_CONFIRMATION = "require_confirmation"; /** + * This is deprecated. Internally we should use {@link #KEY_AUTHENTICATORS_ALLOWED} * @hide */ public static final String KEY_ALLOW_DEVICE_CREDENTIAL = "allow_device_credential"; + /** + * If this key is set, we will ignore {@link #KEY_ALLOW_DEVICE_CREDENTIAL} + * @hide + */ + public static final String KEY_AUTHENTICATORS_ALLOWED = "authenticators_allowed"; /** * Error/help message will show for this amount of time. @@ -202,30 +204,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan return this; } - /** - * Optional: Set the text for the positive button. If not set, the positive button - * will not show. - * @param text - * @return - * @hide - */ - @NonNull public Builder setPositiveButton(@NonNull CharSequence text, - @NonNull @CallbackExecutor Executor executor, - @NonNull DialogInterface.OnClickListener listener) { - if (TextUtils.isEmpty(text)) { - throw new IllegalArgumentException("Text must be set and non-empty"); - } - if (executor == null) { - throw new IllegalArgumentException("Executor must not be null"); - } - if (listener == null) { - throw new IllegalArgumentException("Listener must not be null"); - } - mBundle.putCharSequence(KEY_POSITIVE_TEXT, text); - mPositiveButtonInfo = new ButtonInfo(executor, listener); - return this; - } - /** * Required: Set the text for the negative button. This would typically be used as a * "Cancel" button, but may be also used to show an alternative method for authentication, @@ -306,15 +284,19 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan final CharSequence title = mBundle.getCharSequence(KEY_TITLE); final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT); final boolean useDefaultTitle = mBundle.getBoolean(KEY_USE_DEFAULT_TITLE); - final boolean enableFallback = mBundle.getBoolean(KEY_ALLOW_DEVICE_CREDENTIAL); + final boolean allowCredential = mBundle.getBoolean(KEY_ALLOW_DEVICE_CREDENTIAL); + final Object authenticatorsAllowed = mBundle.get(KEY_AUTHENTICATORS_ALLOWED); if (TextUtils.isEmpty(title) && !useDefaultTitle) { throw new IllegalArgumentException("Title must be set and non-empty"); - } else if (TextUtils.isEmpty(negative) && !enableFallback) { + } else if (TextUtils.isEmpty(negative) && !allowCredential) { throw new IllegalArgumentException("Negative text must be set and non-empty"); - } else if (!TextUtils.isEmpty(negative) && enableFallback) { + } else if (!TextUtils.isEmpty(negative) && allowCredential) { throw new IllegalArgumentException("Can't have both negative button behavior" + " and device credential enabled"); + } else if (authenticatorsAllowed != null && allowCredential) { + throw new IllegalArgumentException("setAuthenticatorsAllowed and" + + " setDeviceCredentialAllowed should not be used simultaneously"); } return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo); } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 122c080df5f1c..c8ba52a63151d 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -151,17 +151,17 @@ oneway interface IStatusBar void showShutdownUi(boolean isReboot, String reason); - // Used to show the dialog when BiometricService starts authentication - void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, - boolean requireConfirmation, int userId, String opPackageName); - // Used to hide the dialog when a biometric is authenticated + // Used to show the authentication dialog (Biometrics, Device Credential) + void showAuthenticationDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, + int biometricModality, boolean requireConfirmation, int userId, String opPackageName); + // Used to notify the authentication dialog that a biometric has been authenticated or rejected void onBiometricAuthenticated(boolean authenticated, String failureReason); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc void onBiometricHelp(String message); // Used to set a message - the dialog will dismiss after a certain amount of time void onBiometricError(int errorCode, String error); - // Used to hide the biometric dialog when the AuthenticationClient is stopped - void hideBiometricDialog(); + // Used to hide the authentication dialog, e.g. when the application cancels authentication + void hideAuthenticationDialog(); /** * Notifies System UI that the display is ready to show system decorations. diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 270ef4dd0ad81..a845b587c49f2 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -99,15 +99,15 @@ interface IStatusBarService void showPinningEnterExitToast(boolean entering); void showPinningEscapeToast(); - // Used to show the dialog when BiometricService starts authentication - void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, - boolean requireConfirmation, int userId, String opPackageName); - // Used to hide the dialog when a biometric is authenticated + // Used to show the authentication dialog (Biometrics, Device Credential) + void showAuthenticationDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, + int biometricModality, boolean requireConfirmation, int userId, String opPackageName); + // Used to notify the authentication dialog that a biometric has been authenticated or rejected void onBiometricAuthenticated(boolean authenticated, String failureReason); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc void onBiometricHelp(String message); // Used to set a message - the dialog will dismiss after a certain amount of time void onBiometricError(int errorCode, String error); - // Used to hide the biometric dialog when the AuthenticationClient is stopped - void hideBiometricDialog(); + // Used to hide the authentication dialog, e.g. when the application cancels authentication + void hideAuthenticationDialog(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index e08707d9941c1..ecc1f695d4f59 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -754,6 +754,6 @@ public abstract class AuthBiometricView extends LinearLayout { } private boolean isDeviceCredentialAllowed() { - return mBiometricPromptBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL); + return Utils.isDeviceCredentialAllowed(mBiometricPromptBundle); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 1a3189b72cfb1..7c6cb085a245f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -24,6 +24,7 @@ import android.content.Context; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.hardware.biometrics.Authenticator; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricPrompt; import android.os.Binder; @@ -74,6 +75,7 @@ public class AuthContainerView extends LinearLayout @interface ContainerState {} final Config mConfig; + private final Injector mInjector; private final IBinder mWindowToken = new Binder(); private final WindowManager mWindowManager; private final AuthPanelController mPanelController; @@ -82,11 +84,11 @@ public class AuthContainerView extends LinearLayout private final CredentialCallback mCredentialCallback; @VisibleForTesting final FrameLayout mFrameLayout; - @VisibleForTesting AuthBiometricView mBiometricView; - @VisibleForTesting AuthCredentialView mCredentialView; + @VisibleForTesting @Nullable AuthBiometricView mBiometricView; + @VisibleForTesting @Nullable AuthCredentialView mCredentialView; private final ImageView mBackgroundView; - private final ScrollView mBiometricScrollView; + @VisibleForTesting final ScrollView mBiometricScrollView; private final View mPanelView; private final float mTranslationY; @@ -107,25 +109,11 @@ public class AuthContainerView extends LinearLayout String mOpPackageName; int mModalityMask; boolean mSkipIntro; - @Builder.InitialView int mInitialView; } public static class Builder { Config mConfig; - /** - * Start the prompt with biometric UI. May flow to credential view if - * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)} is set to true. - */ - public static final int INITIAL_VIEW_BIOMETRIC = 1; - /** - * Start the prompt with credential UI - */ - public static final int INITIAL_VIEW_CREDENTIAL = 2; - @Retention(RetentionPolicy.SOURCE) - @IntDef({INITIAL_VIEW_BIOMETRIC, INITIAL_VIEW_CREDENTIAL}) - @interface InitialView {} - public Builder(Context context) { mConfig = new Config(); mConfig.mContext = context; @@ -161,14 +149,32 @@ public class AuthContainerView extends LinearLayout return this; } - public Builder setInitialView(@InitialView int initialView) { - mConfig.mInitialView = initialView; - return this; - } - public AuthContainerView build(int modalityMask) { mConfig.mModalityMask = modalityMask; - return new AuthContainerView(mConfig); + return new AuthContainerView(mConfig, new Injector()); + } + } + + public static class Injector { + ScrollView getBiometricScrollView(FrameLayout parent) { + return parent.findViewById(R.id.biometric_scrollview); + } + + FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) { + return (FrameLayout) factory.inflate( + R.layout.auth_container_view, root, false /* attachToRoot */); + } + + AuthPanelController getPanelController(Context context, View panelView) { + return new AuthPanelController(context, panelView); + } + + ImageView getBackgroundView(FrameLayout parent) { + return parent.findViewById(R.id.background); + } + + View getPanelView(FrameLayout parent) { + return parent.findViewById(R.id.panel); } } @@ -194,7 +200,7 @@ public class AuthContainerView extends LinearLayout break; case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL: mConfig.mCallback.onDeviceCredentialPressed(); - addCredentialView(false /* animatePanel */); + addCredentialView(false /* animatePanel */, true /* animateContents */); break; default: Log.e(TAG, "Unhandled action: " + action); @@ -210,10 +216,12 @@ public class AuthContainerView extends LinearLayout } @VisibleForTesting - AuthContainerView(Config config) { + AuthContainerView(Config config, Injector injector) { super(config.mContext); mConfig = config; + mInjector = injector; + mWindowManager = mContext.getSystemService(WindowManager.class); mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); @@ -224,29 +232,30 @@ public class AuthContainerView extends LinearLayout mCredentialCallback = new CredentialCallback(); final LayoutInflater factory = LayoutInflater.from(mContext); - mFrameLayout = (FrameLayout) factory.inflate( - R.layout.auth_container_view, this, false /* attachToRoot */); + mFrameLayout = mInjector.inflateContainerView(factory, this); - mPanelView = mFrameLayout.findViewById(R.id.panel); - mPanelController = new AuthPanelController(mContext, mPanelView); + mPanelView = mInjector.getPanelView(mFrameLayout); + mPanelController = mInjector.getPanelController(mContext, mPanelView); - // TODO: Update with new controllers if multi-modal authentication can occur simultaneously - if (config.mModalityMask == BiometricAuthenticator.TYPE_FINGERPRINT) { - mBiometricView = (AuthBiometricFingerprintView) - factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false); - } else if (config.mModalityMask == BiometricAuthenticator.TYPE_FACE) { - mBiometricView = (AuthBiometricFaceView) - factory.inflate(R.layout.auth_biometric_face_view, null, false); - } else { - Log.e(TAG, "Unsupported modality mask: " + config.mModalityMask); - mBiometricView = null; - mBackgroundView = null; - mBiometricScrollView = null; - return; + // Inflate biometric view only if necessary. + if (Utils.isBiometricAllowed(mConfig.mBiometricPromptBundle)) { + if (config.mModalityMask == BiometricAuthenticator.TYPE_FINGERPRINT) { + mBiometricView = (AuthBiometricFingerprintView) + factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false); + } else if (config.mModalityMask == BiometricAuthenticator.TYPE_FACE) { + mBiometricView = (AuthBiometricFaceView) + factory.inflate(R.layout.auth_biometric_face_view, null, false); + } else { + Log.e(TAG, "Unsupported biometric modality: " + config.mModalityMask); + mBiometricView = null; + mBackgroundView = null; + mBiometricScrollView = null; + return; + } } - mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview); - mBackgroundView = mFrameLayout.findViewById(R.id.background); + mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout); + mBackgroundView = mInjector.getBackgroundView(mFrameLayout); UserManager userManager = mContext.getSystemService(UserManager.class); DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); @@ -277,8 +286,7 @@ public class AuthContainerView extends LinearLayout @Override public boolean isAllowDeviceCredentials() { - return mConfig.mBiometricPromptBundle - .getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL); + return Utils.isDeviceCredentialAllowed(mConfig.mBiometricPromptBundle); } private void addBiometricView() { @@ -297,7 +305,7 @@ public class AuthContainerView extends LinearLayout * it should own the panel expansion. * @param animatePanel if the credential view needs to own the panel expansion animation */ - private void addCredentialView(boolean animatePanel) { + private void addCredentialView(boolean animatePanel, boolean animateContents) { final LayoutInflater factory = LayoutInflater.from(mContext); mCredentialView = (AuthCredentialView) factory.inflate( R.layout.auth_credential_view, null, false); @@ -305,6 +313,7 @@ public class AuthContainerView extends LinearLayout mCredentialView.setCallback(mCredentialCallback); mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle); mCredentialView.setPanelController(mPanelController, animatePanel); + mCredentialView.setShouldAnimateContents(animateContents); mFrameLayout.addView(mCredentialView); } @@ -317,23 +326,22 @@ public class AuthContainerView extends LinearLayout @Override public void onAttachedToWindow() { super.onAttachedToWindow(); + onAttachedToWindowInternal(); + } + + @VisibleForTesting + void onAttachedToWindowInternal() { mWakefulnessLifecycle.addObserver(this); - Log.v(TAG, "Initial view: " + mConfig.mInitialView); - - switch (mConfig.mInitialView) { - case Builder.INITIAL_VIEW_BIOMETRIC: - addBiometricView(); - break; - case Builder.INITIAL_VIEW_CREDENTIAL: - addCredentialView(true /* animatePanel */); - break; - default: - Log.e(TAG, "Initial view not supported: " + mConfig.mInitialView); - break; + if (Utils.isBiometricAllowed(mConfig.mBiometricPromptBundle)) { + addBiometricView(); + } else if (Utils.isDeviceCredentialAllowed(mConfig.mBiometricPromptBundle)) { + addCredentialView(true /* animatePanel */, false /* animateContents */); + } else { + throw new IllegalStateException("Unknown configuration: " + + Utils.getAuthenticators(mConfig.mBiometricPromptBundle)); } - if (mConfig.mSkipIntro) { mContainerState = STATE_SHOWING; } else { @@ -358,6 +366,15 @@ public class AuthContainerView extends LinearLayout .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); + if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { + mCredentialView.setY(mTranslationY); + mCredentialView.animate() + .translationY(0) + .setDuration(ANIMATION_DURATION_SHOW_MS) + .setInterpolator(mLinearOutSlowIn) + .withLayer() + .start(); + } animate() .alpha(1f) .setDuration(ANIMATION_DURATION_SHOW_MS) @@ -381,7 +398,9 @@ public class AuthContainerView extends LinearLayout @Override public void show(WindowManager wm, @Nullable Bundle savedState) { - mBiometricView.restoreState(savedState); + if (mBiometricView != null) { + mBiometricView.restoreState(savedState); + } wm.addView(this, getLayoutParams(mWindowToken)); } @@ -427,7 +446,10 @@ public class AuthContainerView extends LinearLayout outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING, mBiometricView != null && mCredentialView == null); outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null); - mBiometricView.onSaveState(outState); + + if (mBiometricView != null) { + mBiometricView.onSaveState(outState); + } } @Override @@ -525,7 +547,9 @@ public class AuthContainerView extends LinearLayout return; } mContainerState = STATE_SHOWING; - mBiometricView.onDialogAnimatedIn(); + if (mBiometricView != null) { + mBiometricView.onDialogAnimatedIn(); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 74e170d0ad987..4c2afb0a14ca2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -23,6 +23,7 @@ import android.app.TaskStackListener; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.hardware.biometrics.Authenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.IBiometricServiceReceiverInternal; @@ -200,16 +201,19 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } @Override - public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, - int type, boolean requireConfirmation, int userId, String opPackageName) { + public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, + int biometricModality, boolean requireConfirmation, int userId, String opPackageName) { + final int authenticators = Utils.getAuthenticators(bundle); + if (DEBUG) { - Log.d(TAG, "showBiometricDialog, type: " + type + Log.d(TAG, "showAuthenticationDialog, authenticators: " + authenticators + + ", biometricModality: " + biometricModality + ", requireConfirmation: " + requireConfirmation); } SomeArgs args = SomeArgs.obtain(); args.arg1 = bundle; args.arg2 = receiver; - args.argi1 = type; + args.argi1 = biometricModality; args.arg3 = requireConfirmation; args.argi2 = userId; args.arg4 = opPackageName; @@ -219,8 +223,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, Log.w(TAG, "mCurrentDialog: " + mCurrentDialog); skipAnimation = true; } - showDialog(args, skipAnimation, null /* savedState */, - AuthContainerView.Builder.INITIAL_VIEW_BIOMETRIC); + + showDialog(args, skipAnimation, null /* savedState */); } @Override @@ -256,14 +260,13 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } @Override - public void hideBiometricDialog() { - if (DEBUG) Log.d(TAG, "hideBiometricDialog"); + public void hideAuthenticationDialog() { + if (DEBUG) Log.d(TAG, "hideAuthenticationDialog"); mCurrentDialog.dismissFromSystemServer(); } - private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState, - @AuthContainerView.Builder.InitialView int initialView) { + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; final int type = args.argi1; final Bundle biometricPromptBundle = (Bundle) args.arg1; @@ -278,8 +281,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, userId, type, opPackageName, - skipAnimation, - initialView); + skipAnimation); if (newDialog == null) { Log.e(TAG, "Unsupported type: " + type); @@ -287,12 +289,11 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } if (DEBUG) { - Log.d(TAG, "showDialog, " + Log.d(TAG, "showDialog: " + args + " savedState: " + savedState + " mCurrentDialog: " + mCurrentDialog + " newDialog: " + newDialog - + " type: " + type - + " initialView: " + initialView); + + " type: " + type); } if (mCurrentDialog != null) { @@ -334,21 +335,20 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, != AuthContainerView.STATE_ANIMATING_OUT) { final boolean credentialShowing = savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING); + if (credentialShowing) { + // TODO: Clean this up + Bundle bundle = (Bundle) mCurrentDialogArgs.arg1; + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, + Authenticator.TYPE_CREDENTIAL); + } - // We can assume if credential is showing, then biometric doesn't need to be shown, - // since credential is always after biometric. - final int initialView = credentialShowing - ? AuthContainerView.Builder.INITIAL_VIEW_CREDENTIAL - : AuthContainerView.Builder.INITIAL_VIEW_BIOMETRIC; - - showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState, initialView); + showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState); } } } protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, - int userId, int type, String opPackageName, boolean skipIntro, - @AuthContainerView.Builder.InitialView int initialView) { + int userId, int type, String opPackageName, boolean skipIntro) { return new AuthContainerView.Builder(mContext) .setCallback(this) .setBiometricPromptBundle(biometricPromptBundle) @@ -356,7 +356,6 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, .setUserId(userId) .setOpPackageName(opPackageName) .setSkipIntro(skipIntro) - .setInitialView(initialView) .build(type); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index 8eac8f591e5c4..1ba88c6add097 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -59,6 +59,7 @@ public class AuthCredentialView extends LinearLayout { private Bundle mBiometricPromptBundle; private AuthPanelController mPanelController; private boolean mShouldAnimatePanel; + private boolean mShouldAnimateContents; private TextView mTitleView; private TextView mSubtitleView; @@ -220,6 +221,10 @@ public class AuthCredentialView extends LinearLayout { mShouldAnimatePanel = animatePanel; } + void setShouldAnimateContents(boolean animateContents) { + mShouldAnimateContents = animateContents; + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -230,18 +235,21 @@ public class AuthCredentialView extends LinearLayout { setTextOrHide(mDescriptionView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION)); - setTranslationY(getResources() - .getDimension(R.dimen.biometric_dialog_credential_translation_offset)); - setAlpha(0); + // Only animate this if we're transitioning from a biometric view. + if (mShouldAnimateContents) { + setTranslationY(getResources() + .getDimension(R.dimen.biometric_dialog_credential_translation_offset)); + setAlpha(0); - postOnAnimation(() -> { - animate().translationY(0) - .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS) - .alpha(1.f) - .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) - .withLayer() - .start(); - }); + postOnAnimation(() -> { + animate().translationY(0) + .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS) + .alpha(1.f) + .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) + .withLayer() + .start(); + }); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java index e00cf6abafaa0..485e667aae985 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java @@ -19,6 +19,9 @@ package com.android.systemui.biometrics; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; import android.content.Context; +import android.hardware.biometrics.Authenticator; +import android.hardware.biometrics.BiometricPrompt; +import android.os.Bundle; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; @@ -46,4 +49,18 @@ public class Utils { view.sendAccessibilityEventUnchecked(event); view.notifySubtreeAccessibilityStateChanged(view, view, CONTENT_CHANGE_TYPE_SUBTREE); } + + static boolean isDeviceCredentialAllowed(Bundle biometricPromptBundle) { + final int authenticators = getAuthenticators(biometricPromptBundle); + return (authenticators & Authenticator.TYPE_CREDENTIAL) != 0; + } + + static boolean isBiometricAllowed(Bundle biometricPromptBundle) { + final int authenticators = getAuthenticators(biometricPromptBundle); + return (authenticators & Authenticator.TYPE_BIOMETRIC) != 0; + } + + static int getAuthenticators(Bundle biometricPromptBundle) { + return biometricPromptBundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 39b65c4a963d7..36e04fe42ced3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -270,12 +270,13 @@ 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, String opPackageName) { } + default void showAuthenticationDialog(Bundle bundle, + IBiometricServiceReceiverInternal receiver, int biometricModality, + boolean requireConfirmation, int userId, String opPackageName) { } default void onBiometricAuthenticated(boolean authenticated, String failureReason) { } default void onBiometricHelp(String message) { } default void onBiometricError(int errorCode, String error) { } - default void hideBiometricDialog() { } + default void hideAuthenticationDialog() { } /** * @see IStatusBar#onDisplayReady(int) @@ -740,13 +741,13 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } @Override - public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, - int type, boolean requireConfirmation, int userId, String opPackageName) { + public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, + int biometricModality, boolean requireConfirmation, int userId, String opPackageName) { synchronized (mLock) { SomeArgs args = SomeArgs.obtain(); args.arg1 = bundle; args.arg2 = receiver; - args.argi1 = type; + args.argi1 = biometricModality; args.arg3 = requireConfirmation; args.argi2 = userId; args.arg4 = opPackageName; @@ -780,7 +781,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } @Override - public void hideBiometricDialog() { + public void hideAuthenticationDialog() { synchronized (mLock) { mHandler.obtainMessage(MSG_BIOMETRIC_HIDE).sendToTarget(); } @@ -1032,10 +1033,10 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED); SomeArgs someArgs = (SomeArgs) msg.obj; for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).showBiometricDialog( + mCallbacks.get(i).showAuthenticationDialog( (Bundle) someArgs.arg1, (IBiometricServiceReceiverInternal) someArgs.arg2, - someArgs.argi1 /* type */, + someArgs.argi1 /* biometricModality */, (boolean) someArgs.arg3 /* requireConfirmation */, someArgs.argi2 /* userId */, (String) someArgs.arg4 /* opPackageName */); @@ -1065,7 +1066,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< break; case MSG_BIOMETRIC_HIDE: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).hideBiometricDialog(); + mCallbacks.get(i).hideAuthenticationDialog(); } break; case MSG_SHOW_CHARGING_ANIMATION: diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java index d1109400e4b5d..2c85424bac792 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.content.Context; +import android.hardware.biometrics.Authenticator; import android.hardware.biometrics.BiometricPrompt; import android.os.Bundle; import android.test.suitebuilder.annotation.SmallTest; @@ -291,11 +292,13 @@ public class AuthBiometricViewTest extends SysuiTestCase { private Bundle buildBiometricPromptBundle(boolean allowDeviceCredential) { Bundle bundle = new Bundle(); bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title"); + int authenticators = Authenticator.TYPE_BIOMETRIC; if (allowDeviceCredential) { - bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true); + authenticators |= Authenticator.TYPE_CREDENTIAL; } else { bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative"); } + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); return bundle; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java index aeceba7add08d..8550390da4507 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java @@ -17,14 +17,29 @@ package com.android.systemui.biometrics; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.content.Context; +import android.hardware.biometrics.Authenticator; import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricPrompt; +import android.os.Bundle; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ScrollView; import com.android.systemui.SysuiTestCase; @@ -46,16 +61,12 @@ public class AuthContainerViewTest extends SysuiTestCase { @Before public void setup() { MockitoAnnotations.initMocks(this); - - AuthContainerView.Config config = new AuthContainerView.Config(); - config.mContext = mContext; - config.mCallback = mCallback; - config.mModalityMask |= BiometricAuthenticator.TYPE_FINGERPRINT; - mAuthContainer = new TestableAuthContainer(config); } @Test public void testActionAuthenticated_sendsDismissedAuthenticated() { + initializeContainer(Authenticator.TYPE_BIOMETRIC); + mAuthContainer.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_AUTHENTICATED); verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED)); @@ -63,6 +74,8 @@ public class AuthContainerViewTest extends SysuiTestCase { @Test public void testActionUserCanceled_sendsDismissedUserCanceled() { + initializeContainer(Authenticator.TYPE_BIOMETRIC); + mAuthContainer.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_USER_CANCELED); verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_USER_CANCELED)); @@ -70,6 +83,8 @@ public class AuthContainerViewTest extends SysuiTestCase { @Test public void testActionButtonNegative_sendsDismissedButtonNegative() { + initializeContainer(Authenticator.TYPE_BIOMETRIC); + mAuthContainer.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE); verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE)); @@ -77,6 +92,8 @@ public class AuthContainerViewTest extends SysuiTestCase { @Test public void testActionTryAgain_sendsTryAgain() { + initializeContainer(Authenticator.TYPE_BIOMETRIC); + mAuthContainer.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN); verify(mCallback).onTryAgainPressed(); @@ -84,6 +101,8 @@ public class AuthContainerViewTest extends SysuiTestCase { @Test public void testActionError_sendsDismissedError() { + initializeContainer(Authenticator.TYPE_BIOMETRIC); + mAuthContainer.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_ERROR); verify(mCallback).onDismissed(AuthDialogCallback.DISMISSED_ERROR); @@ -91,25 +110,68 @@ public class AuthContainerViewTest extends SysuiTestCase { @Test public void testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() { + initializeContainer( + Authenticator.TYPE_BIOMETRIC | Authenticator.TYPE_CREDENTIAL); + mAuthContainer.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL); verify(mCallback).onDeviceCredentialPressed(); // Credential view is attached to the frame layout waitForIdleSync(); - assertEquals(mAuthContainer.mFrameLayout, mAuthContainer.mCredentialView.getParent()); + assertNotNull(mAuthContainer.mCredentialView); + verify(mAuthContainer.mFrameLayout).addView(eq(mAuthContainer.mCredentialView)); } @Test public void testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() { + initializeContainer( + Authenticator.TYPE_BIOMETRIC | Authenticator.TYPE_CREDENTIAL); + mAuthContainer.mBiometricView = mock(AuthBiometricView.class); mAuthContainer.animateToCredentialUI(); verify(mAuthContainer.mBiometricView).startTransitionToCredentialUI(); } + @Test + public void testShowBiometricUI() { + initializeContainer(Authenticator.TYPE_BIOMETRIC); + + assertNotEquals(null, mAuthContainer.mBiometricView); + + mAuthContainer.onAttachedToWindowInternal(); + verify(mAuthContainer.mBiometricScrollView).addView(mAuthContainer.mBiometricView); + // Credential view is not added + verify(mAuthContainer.mFrameLayout, never()).addView(any()); + } + + @Test + public void testShowCredentialUI_doesNotInflateBiometricUI() { + initializeContainer(Authenticator.TYPE_CREDENTIAL); + + mAuthContainer.onAttachedToWindowInternal(); + + assertNull(null, mAuthContainer.mBiometricView); + assertNotNull(mAuthContainer.mCredentialView); + verify(mAuthContainer.mFrameLayout).addView(mAuthContainer.mCredentialView); + } + + private void initializeContainer(int authenticators) { + AuthContainerView.Config config = new AuthContainerView.Config(); + config.mContext = mContext; + config.mCallback = mCallback; + config.mModalityMask |= BiometricAuthenticator.TYPE_FINGERPRINT; + + Bundle bundle = new Bundle(); + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); + config.mBiometricPromptBundle = bundle; + + mAuthContainer = new TestableAuthContainer(config); + } + private class TestableAuthContainer extends AuthContainerView { TestableAuthContainer(AuthContainerView.Config config) { - super(config); + super(config, new MockInjector()); } @Override @@ -117,4 +179,31 @@ public class AuthContainerViewTest extends SysuiTestCase { mConfig.mCallback.onDismissed(reason); } } + + private final class MockInjector extends AuthContainerView.Injector { + @Override + public ScrollView getBiometricScrollView(FrameLayout parent) { + return mock(ScrollView.class); + } + + @Override + public FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) { + return mock(FrameLayout.class); + } + + @Override + public AuthPanelController getPanelController(Context context, View view) { + return mock(AuthPanelController.class); + } + + @Override + public ImageView getBackgroundView(FrameLayout parent) { + return mock(ImageView.class); + } + + @Override + public View getPanelView(FrameLayout parent) { + return mock(View.class); + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index b8f735e09e81e..dcdb5c3f8e9e7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -35,6 +36,7 @@ import android.app.IActivityTaskManager; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.hardware.biometrics.Authenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.IBiometricServiceReceiverInternal; @@ -72,7 +74,7 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private AuthDialog mDialog2; - private TestableBiometricDialogImpl mBiometricDialogImpl; + private TestableAuthController mAuthController; @Before @@ -93,63 +95,66 @@ public class AuthControllerTest extends SysuiTestCase { when(mDialog1.getOpPackageName()).thenReturn("Dialog1"); when(mDialog2.getOpPackageName()).thenReturn("Dialog2"); - mBiometricDialogImpl = new TestableBiometricDialogImpl(new MockInjector()); - mBiometricDialogImpl.mContext = context; - mBiometricDialogImpl.mComponents = mContext.getComponents(); + when(mDialog1.isAllowDeviceCredentials()).thenReturn(false); + when(mDialog2.isAllowDeviceCredentials()).thenReturn(false); - mBiometricDialogImpl.start(); + mAuthController = new TestableAuthController(new MockInjector()); + mAuthController.mContext = context; + mAuthController.mComponents = mContext.getComponents(); + + mAuthController.start(); } // Callback tests @Test public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED); verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } @Test public void testSendsReasonNegative_whenDismissedByButtonNegative() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE); verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE); } @Test public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE); verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED); } @Test public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED); verify(mReceiver).onDialogDismissed( BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED); } @Test public void testSendsReasonError_whenDismissedByError() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_ERROR); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_ERROR); verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR); } @Test public void testSendsReasonServerRequested_whenDismissedByServer() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER); verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED); } @Test public void testSendsReasonCredentialConfirmed_whenDeviceCredentialAuthenticated() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED); } @@ -158,22 +163,22 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testShowInvoked_whenSystemRequested() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); verify(mDialog1).show(any(), any()); } @Test - public void testOnAuthenticationSucceededInvoked_whenSystemRequested() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onBiometricAuthenticated(true, null /* failureReason */); + public void testOnAuthenticationSucceededInvoked_whenSystemRequested() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onBiometricAuthenticated(true, null /* failureReason */); verify(mDialog1).onAuthenticationSucceeded(); } @Test - public void testOnAuthenticationFailedInvoked_whenSystemRequested() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testOnAuthenticationFailedInvoked_whenSystemRequested() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); final String failureReason = "failure reason"; - mBiometricDialogImpl.onBiometricAuthenticated(false, failureReason); + mAuthController.onBiometricAuthenticated(false, failureReason); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(mDialog1).onAuthenticationFailed(captor.capture()); @@ -182,10 +187,10 @@ public class AuthControllerTest extends SysuiTestCase { } @Test - public void testOnHelpInvoked_whenSystemRequested() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testOnHelpInvoked_whenSystemRequested() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); final String helpMessage = "help"; - mBiometricDialogImpl.onBiometricHelp(helpMessage); + mAuthController.onBiometricHelp(helpMessage); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(mDialog1).onHelp(captor.capture()); @@ -194,11 +199,11 @@ public class AuthControllerTest extends SysuiTestCase { } @Test - public void testOnErrorInvoked_whenSystemRequested() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testOnErrorInvoked_whenSystemRequested() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); final int error = 1; final String errMessage = "error message"; - mBiometricDialogImpl.onBiometricError(error, errMessage); + mAuthController.onBiometricError(error, errMessage); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(mDialog1).onError(captor.capture()); @@ -207,83 +212,82 @@ public class AuthControllerTest extends SysuiTestCase { } @Test - public void testErrorLockout_whenCredentialAllowed_AnimatesToCredentialUI() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testErrorLockout_whenCredentialAllowed_AnimatesToCredentialUI() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT; final String errorString = "lockout"; when(mDialog1.isAllowDeviceCredentials()).thenReturn(true); - mBiometricDialogImpl.onBiometricError(error, errorString); + mAuthController.onBiometricError(error, errorString); verify(mDialog1, never()).onError(anyString()); verify(mDialog1).animateToCredentialUI(); } @Test - public void testErrorLockoutPermanent_whenCredentialAllowed_AnimatesToCredentialUI() - throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testErrorLockoutPermanent_whenCredentialAllowed_AnimatesToCredentialUI() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT; final String errorString = "lockout_permanent"; when(mDialog1.isAllowDeviceCredentials()).thenReturn(true); - mBiometricDialogImpl.onBiometricError(error, errorString); + mAuthController.onBiometricError(error, errorString); verify(mDialog1, never()).onError(anyString()); verify(mDialog1).animateToCredentialUI(); } @Test - public void testErrorLockout_whenCredentialNotAllowed_sendsOnError() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testErrorLockout_whenCredentialNotAllowed_sendsOnError() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT; final String errorString = "lockout"; when(mDialog1.isAllowDeviceCredentials()).thenReturn(false); - mBiometricDialogImpl.onBiometricError(error, errorString); + mAuthController.onBiometricError(error, errorString); verify(mDialog1).onError(eq(errorString)); verify(mDialog1, never()).animateToCredentialUI(); } @Test - public void testErrorLockoutPermanent_whenCredentialNotAllowed_sendsOnError() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testErrorLockoutPermanent_whenCredentialNotAllowed_sendsOnError() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); final int error = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT; final String errorString = "lockout_permanent"; when(mDialog1.isAllowDeviceCredentials()).thenReturn(false); - mBiometricDialogImpl.onBiometricError(error, errorString); + mAuthController.onBiometricError(error, errorString); verify(mDialog1).onError(eq(errorString)); verify(mDialog1, never()).animateToCredentialUI(); } @Test - public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.hideBiometricDialog(); + public void testDismissWithoutCallbackInvoked_whenSystemRequested() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.hideAuthenticationDialog(); verify(mDialog1).dismissFromSystemServer(); } @Test - public void testClientNotified_whenDismissedBySystemServer() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.hideBiometricDialog(); + public void testClientNotified_whenDismissedBySystemServer() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.hideAuthenticationDialog(); verify(mDialog1).dismissFromSystemServer(); - assertNotNull(mBiometricDialogImpl.mCurrentDialog); - assertNotNull(mBiometricDialogImpl.mReceiver); + assertNotNull(mAuthController.mCurrentDialog); + assertNotNull(mAuthController.mReceiver); } // Corner case tests @Test - public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); verify(mDialog1).show(any(), any()); - showDialog(BiometricPrompt.TYPE_FACE); + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); // First dialog should be dismissed without animation verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */); @@ -293,11 +297,20 @@ public class AuthControllerTest extends SysuiTestCase { } @Test - public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + public void testConfigurationPersists_whenOnConfigurationChanged() { + showDialog(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); verify(mDialog1).show(any(), any()); - mBiometricDialogImpl.onConfigurationChanged(new Configuration()); + // Return that the UI is in "showing" state + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + Bundle savedState = (Bundle) args[0]; + savedState.putInt( + AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING); + return null; // onSaveState returns void + }).when(mDialog1).onSaveState(any()); + + mAuthController.onConfigurationChanged(new Configuration()); ArgumentCaptor captor = ArgumentCaptor.forClass(Bundle.class); verify(mDialog1).onSaveState(captor.capture()); @@ -313,38 +326,64 @@ public class AuthControllerTest extends SysuiTestCase { assertEquals(captor.getValue(), captor2.getValue()); } + @Test + public void testConfigurationPersists_whenBiometricFallbackToCredential() { + showDialog(Authenticator.TYPE_CREDENTIAL | Authenticator.TYPE_BIOMETRIC, + BiometricPrompt.TYPE_FACE); + verify(mDialog1).show(any(), any()); + + // Pretend that the UI is now showing device credential UI. + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + Bundle savedState = (Bundle) args[0]; + savedState.putInt( + AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING); + savedState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, true); + return null; // onSaveState returns void + }).when(mDialog1).onSaveState(any()); + + mAuthController.onConfigurationChanged(new Configuration()); + + // Check that the new dialog was initialized to the credential UI. + ArgumentCaptor captor = ArgumentCaptor.forClass(Bundle.class); + verify(mDialog2).show(any(), captor.capture()); + assertEquals(Authenticator.TYPE_CREDENTIAL, + mAuthController.mLastBiometricPromptBundle + .getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED)); + } + @Test public void testClientNotified_whenTaskStackChangesDuringAuthentication() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); + showDialog(Authenticator.TYPE_BIOMETRIC, 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); + when(mAuthController.mActivityTaskManager.getTasks(anyInt())).thenReturn(tasks); - mBiometricDialogImpl.mTaskStackListener.onTaskStackChanged(); + mAuthController.mTaskStackListener.onTaskStackChanged(); waitForIdleSync(); - assertNull(mBiometricDialogImpl.mCurrentDialog); - assertNull(mBiometricDialogImpl.mReceiver); + assertNull(mAuthController.mCurrentDialog); + assertNull(mAuthController.mReceiver); verify(mDialog1).dismissWithoutCallback(true /* animate */); verify(mReceiver).onDialogDismissed(eq(BiometricPrompt.DISMISSED_REASON_USER_CANCEL)); } // Helpers - private void showDialog(int type) { - mBiometricDialogImpl.showBiometricDialog(createTestDialogBundle(), + private void showDialog(int authenticators, int biometricModality) { + mAuthController.showAuthenticationDialog(createTestDialogBundle(authenticators), mReceiver /* receiver */, - type, + biometricModality, true /* requireConfirmation */, 0 /* userId */, "testPackage"); } - private Bundle createTestDialogBundle() { + private Bundle createTestDialogBundle(int authenticators) { Bundle bundle = new Bundle(); bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title"); @@ -356,20 +395,26 @@ public class AuthControllerTest extends SysuiTestCase { // by user settings, and should be tested in BiometricService. bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true); + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); + return bundle; } - private final class TestableBiometricDialogImpl extends AuthController { + private final class TestableAuthController extends AuthController { private int mBuildCount = 0; + private Bundle mLastBiometricPromptBundle; - public TestableBiometricDialogImpl(Injector injector) { + public TestableAuthController(Injector injector) { super(injector); } @Override protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, int userId, int type, String opPackageName, - boolean skipIntro, @AuthContainerView.Builder.InitialView int initialView) { + boolean skipIntro) { + + mLastBiometricPromptBundle = biometricPromptBundle; + AuthDialog dialog; if (mBuildCount == 0) { dialog = mDialog1; 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 fcd0e23919b75..1bd01e166ddb8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -367,12 +367,13 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - public void testShowBiometricDialog() { + public void testShowAuthenticationDialog() { Bundle bundle = new Bundle(); String packageName = "test"; - mCommandQueue.showBiometricDialog(bundle, null /* receiver */, 1, true, 3, packageName); + mCommandQueue.showAuthenticationDialog(bundle, null /* receiver */, 1, true, 3, + packageName); waitForIdleSync(); - verify(mCallbacks).showBiometricDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3), + verify(mCallbacks).showAuthenticationDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3), eq(packageName)); } @@ -402,9 +403,9 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - public void testHideBiometricDialog() { - mCommandQueue.hideBiometricDialog(); + public void testHideAuthenticationDialog() { + mCommandQueue.hideAuthenticationDialog(); waitForIdleSync(); - verify(mCallbacks).hideBiometricDialog(); + verify(mCallbacks).hideAuthenticationDialog(); } } diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 08af2a046aebb..1003bf7856e7b 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -31,6 +31,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.database.ContentObserver; +import android.hardware.biometrics.Authenticator; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; @@ -211,7 +212,8 @@ public class BiometricService extends SystemService { } boolean isAllowDeviceCredential() { - return mBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, false); + final int authenticators = mBundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED); + return (authenticators & Authenticator.TYPE_CREDENTIAL) != 0; } } @@ -237,7 +239,7 @@ public class BiometricService extends SystemService { // Get and cache the available authenticator (manager) classes. Used since aidl doesn't support // polymorphism :/ - final ArrayList mAuthenticators = new ArrayList<>(); + final ArrayList mAuthenticators = new ArrayList<>(); // 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. @@ -346,11 +348,11 @@ public class BiometricService extends SystemService { } }; - private final class Authenticator { + private final class AuthenticatorWrapper { final int mType; final BiometricAuthenticator mAuthenticator; - Authenticator(int type, BiometricAuthenticator authenticator) { + AuthenticatorWrapper(int type, BiometricAuthenticator authenticator) { mType = type; mAuthenticator = authenticator; } @@ -607,6 +609,12 @@ public class BiometricService extends SystemService { return; } + if (bundle.get(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED) != null) { + checkInternalPermission(); + } + + combineAuthenticatorBundles(bundle); + // Check the usage of this in system server. Need to remove this check if it becomes // a public API. final boolean useDefaultTitle = @@ -840,8 +848,8 @@ public class BiometricService extends SystemService { // Cache the authenticators for (int featureId : FEATURE_ID) { if (hasFeature(featureId)) { - Authenticator authenticator = - new Authenticator(featureId, getAuthenticator(featureId)); + AuthenticatorWrapper authenticator = + new AuthenticatorWrapper(featureId, getAuthenticator(featureId)); mAuthenticators.add(authenticator); } } @@ -879,7 +887,7 @@ public class BiometricService extends SystemService { int modality = TYPE_NONE; int firstHwAvailable = TYPE_NONE; - for (Authenticator authenticatorWrapper : mAuthenticators) { + for (AuthenticatorWrapper authenticatorWrapper : mAuthenticators) { modality = authenticatorWrapper.getType(); BiometricAuthenticator authenticator = authenticatorWrapper.getAuthenticator(); if (authenticator.isHardwareDetected()) { @@ -1145,7 +1153,7 @@ public class BiometricService extends SystemService { } else { mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI; if (error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) { - mStatusBarService.hideBiometricDialog(); + mStatusBarService.hideAuthenticationDialog(); } else { mStatusBarService.onBiometricError(error, message); } @@ -1155,7 +1163,7 @@ public class BiometricService extends SystemService { // 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.hideBiometricDialog(); + mStatusBarService.hideAuthenticationDialog(); mCurrentAuthSession = null; } else if (mCurrentAuthSession.mState == STATE_SHOWING_DEVICE_CREDENTIAL) { Slog.d(TAG, "Biometric canceled, ignoring from state: " @@ -1167,8 +1175,32 @@ public class BiometricService extends SystemService { } else if (mPendingAuthSession != null && mPendingAuthSession.containsCookie(cookie)) { if (mPendingAuthSession.mState == STATE_AUTH_CALLED) { - mPendingAuthSession.mClientReceiver.onError(error, message); - mPendingAuthSession = null; + // If any error is received while preparing the auth session (lockout, etc), + // and if device credential is allowed, just show the credential UI. + if (mPendingAuthSession.isAllowDeviceCredential()) { + int authenticators = mPendingAuthSession.mBundle + .getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, 0); + // Disallow biometric and notify SystemUI to show the authentication prompt. + authenticators &= ~Authenticator.TYPE_BIOMETRIC; + mPendingAuthSession.mBundle.putInt( + BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, + authenticators); + + mCurrentAuthSession = mPendingAuthSession; + mCurrentAuthSession.mState = STATE_SHOWING_DEVICE_CREDENTIAL; + mPendingAuthSession = null; + + mStatusBarService.showAuthenticationDialog( + mCurrentAuthSession.mBundle, + mInternalReceiver, + 0 /* biometricModality */, + false /* requireConfirmation */, + mCurrentAuthSession.mUserId, + mCurrentAuthSession.mOpPackageName); + } else { + mPendingAuthSession.mClientReceiver.onError(error, message); + mPendingAuthSession = null; + } } else { Slog.e(TAG, "Impossible pending session error state: " + mPendingAuthSession.mState); @@ -1286,8 +1318,20 @@ public class BiometricService extends SystemService { mCurrentAuthSession.mState = STATE_SHOWING_DEVICE_CREDENTIAL; } + /** + * Invoked when each service has notified that its client is ready to be started. When + * all biometrics are ready, this invokes the SystemUI dialog through StatusBar. + */ private void handleOnReadyForAuthentication(int cookie, boolean requireConfirmation, int userId) { + if (mPendingAuthSession == null) { + // Only should happen if a biometric was locked out when authenticate() was invoked. + // In that case, if device credentials are allowed, the UI is already showing. If not + // allowed, the error has already been returned to the caller. + Slog.w(TAG, "Pending auth session null"); + return; + } + Iterator it = mPendingAuthSession.mModalitiesWaiting.entrySet().iterator(); while (it.hasNext()) { Map.Entry pair = (Map.Entry) it.next(); @@ -1329,7 +1373,7 @@ public class BiometricService extends SystemService { } if (!continuing) { - mStatusBarService.showBiometricDialog(mCurrentAuthSession.mBundle, + mStatusBarService.showAuthenticationDialog(mCurrentAuthSession.mBundle, mInternalReceiver, modality, requireConfirmation, userId, mCurrentAuthSession.mOpPackageName); } @@ -1452,7 +1496,7 @@ public class BiometricService extends SystemService { ); mCurrentAuthSession = null; - mStatusBarService.hideBiometricDialog(); + mStatusBarService.hideAuthenticationDialog(); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } @@ -1492,4 +1536,38 @@ public class BiometricService extends SystemService { Slog.e(TAG, "Unable to cancel authentication"); } } + + + /** + * Combine {@link BiometricPrompt#KEY_ALLOW_DEVICE_CREDENTIAL} with + * {@link BiometricPrompt#KEY_AUTHENTICATORS_ALLOWED}, as the former is not flexible + * enough. + */ + @VisibleForTesting + static void combineAuthenticatorBundles(Bundle bundle) { + boolean biometricEnabled = true; // enabled by default + boolean credentialEnabled = false; // disabled by default + if (bundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, false)) { + credentialEnabled = true; + } + if (bundle.get(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED) != null) { + final int authenticatorFlags = + bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED); + biometricEnabled = (authenticatorFlags & Authenticator.TYPE_BIOMETRIC) != 0; + // Using both KEY_ALLOW_DEVICE_CREDENTIAL and KEY_AUTHENTICATORS_ALLOWED together + // is not supported. Default to overwriting. + credentialEnabled = (authenticatorFlags & Authenticator.TYPE_CREDENTIAL) != 0; + } + + bundle.remove(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL); + + int authenticators = 0; + if (biometricEnabled) { + authenticators |= Authenticator.TYPE_BIOMETRIC; + } + if (credentialEnabled) { + authenticators |= Authenticator.TYPE_CREDENTIAL; + } + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); + } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 644228df159f1..3439d38419735 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -609,13 +609,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, - int type, boolean requireConfirmation, int userId, String opPackageName) { + public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, + int biometricModality, boolean requireConfirmation, int userId, String opPackageName) { enforceBiometricDialog(); if (mBar != null) { try { - mBar.showBiometricDialog(bundle, receiver, type, requireConfirmation, userId, - opPackageName); + mBar.showAuthenticationDialog(bundle, receiver, biometricModality, + requireConfirmation, userId, opPackageName); } catch (RemoteException ex) { } } @@ -655,11 +655,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void hideBiometricDialog() { + public void hideAuthenticationDialog() { enforceBiometricDialog(); if (mBar != null) { try { - mBar.hideBiometricDialog(); + mBar.hideAuthenticationDialog(); } 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 e474ea90fab1b..555b927551aa3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -39,6 +39,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.hardware.biometrics.Authenticator; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; @@ -319,7 +320,7 @@ public class BiometricServiceTest { .startPreparedClient(cookieCaptor.getValue()); // StatusBar showBiometricDialog invoked - verify(mBiometricService.mStatusBarService).showBiometricDialog( + verify(mBiometricService.mStatusBarService).showAuthenticationDialog( eq(mBiometricService.mCurrentAuthSession.mBundle), any(IBiometricServiceReceiverInternal.class), eq(BiometricAuthenticator.TYPE_FINGERPRINT), @@ -439,7 +440,7 @@ public class BiometricServiceTest { verify(mReceiver2, never()).onError(anyInt(), any(String.class)); // SystemUI dialog closed - verify(mBiometricService.mStatusBarService).hideBiometricDialog(); + verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); // After SystemUI notifies that the animation has completed mBiometricService.mInternalReceiver @@ -488,7 +489,7 @@ public class BiometricServiceTest { resetStatusBar(); startPendingAuthSession(mBiometricService); waitForIdle(); - verify(mBiometricService.mStatusBarService, never()).showBiometricDialog( + verify(mBiometricService.mStatusBarService, never()).showAuthenticationDialog( any(Bundle.class), any(IBiometricServiceReceiverInternal.class), anyInt(), @@ -518,7 +519,7 @@ public class BiometricServiceTest { eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(ERROR_CANCELED)); // Dialog is hidden immediately - verify(mBiometricService.mStatusBarService).hideBiometricDialog(); + verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); // Auth session is over assertNull(mBiometricService.mCurrentAuthSession); } @@ -545,7 +546,7 @@ public class BiometricServiceTest { verify(mBiometricService.mStatusBarService).onBiometricError( eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(ERROR_UNABLE_TO_PROCESS)); - verify(mBiometricService.mStatusBarService, never()).hideBiometricDialog(); + verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog(); verify(mReceiver1, never()).onError(anyInt(), anyString()); // SystemUI animation completed, client is notified, auth session is over @@ -558,6 +559,103 @@ public class BiometricServiceTest { assertNull(mBiometricService.mCurrentAuthSession); } + @Test + public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, true /* allowDeviceCredential */); + waitForIdle(); + + mBiometricService.mInternalReceiver.onError( + getCookieForPendingSession(mBiometricService.mPendingAuthSession), + BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, + ERROR_LOCKOUT); + waitForIdle(); + + // Pending auth session becomes current auth session, since device credential should + // be shown now. + assertNull(mBiometricService.mPendingAuthSession); + assertNotNull(mBiometricService.mCurrentAuthSession); + assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL, + mBiometricService.mCurrentAuthSession.mState); + assertEquals(Authenticator.TYPE_CREDENTIAL, + mBiometricService.mCurrentAuthSession.mBundle.getInt( + BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED)); + verify(mBiometricService.mStatusBarService).showAuthenticationDialog( + eq(mBiometricService.mCurrentAuthSession.mBundle), + any(IBiometricServiceReceiverInternal.class), + eq(0 /* biometricModality */), + anyBoolean() /* requireConfirmation */, + anyInt() /* userId */, + eq(TEST_PACKAGE_NAME)); + } + + @Test + public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed() + throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, false /* allowDeviceCredential */); + waitForIdle(); + + mBiometricService.mInternalReceiver.onError( + getCookieForPendingSession(mBiometricService.mPendingAuthSession), + BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, + ERROR_LOCKOUT); + waitForIdle(); + + // Error is sent to client + assertNull(mBiometricService.mPendingAuthSession); + assertNull(mBiometricService.mCurrentAuthSession); + } + + @Test + public void testCombineAuthenticatorBundle_keyAllowDeviceCredentialAlwaysRemoved() { + Bundle bundle; + int authenticators; + + // In: + // KEY_ALLOW_DEVICE_CREDENTIAL = true + // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL + // Out: + // KEY_ALLOW_DEVICE_CREDENTIAL = null + // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL + bundle = new Bundle(); + bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true); + authenticators = Authenticator.TYPE_CREDENTIAL | Authenticator.TYPE_BIOMETRIC; + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); + BiometricService.combineAuthenticatorBundles(bundle); + assertNull(bundle.get(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL)); + assertEquals(authenticators, bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED)); + + // In: + // KEY_ALLOW_DEVICE_CREDENTIAL = true + // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC + // Out: + // KEY_ALLOW_DEVICE_CREDENTIAL = null + // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL + bundle = new Bundle(); + bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true); + authenticators = Authenticator.TYPE_BIOMETRIC; + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); + BiometricService.combineAuthenticatorBundles(bundle); + assertNull(bundle.get(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL)); + assertEquals(authenticators, bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED)); + + // In: + // KEY_ALLOW_DEVICE_CREDENTIAL = null + // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL + // Out: + // KEY_ALLOW_DEVICE_CREDENTIAL = null + // KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL + bundle = new Bundle(); + authenticators = Authenticator.TYPE_BIOMETRIC | Authenticator.TYPE_CREDENTIAL; + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); + BiometricService.combineAuthenticatorBundles(bundle); + assertNull(bundle.get(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL)); + assertEquals(authenticators, bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED)); + } + @Test public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI() throws Exception { @@ -827,6 +925,11 @@ public class BiometricServiceTest { return session.mModalitiesMatched.values().iterator().next(); } + private static int getCookieForPendingSession(BiometricService.AuthSession session) { + assertEquals(session.mModalitiesWaiting.values().size(), 1); + return session.mModalitiesWaiting.values().iterator().next(); + } + private static void waitForIdle() { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); }