diff options
35 files changed, 2329 insertions, 684 deletions
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 9b667a118ebc..b1565ab8a501 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -87,12 +87,6 @@ public class KeyguardManager { "android.app.action.CONFIRM_FRP_CREDENTIAL"; /** - * @hide - */ - public static final String EXTRA_BIOMETRIC_PROMPT_BUNDLE = - "android.app.extra.BIOMETRIC_PROMPT_BUNDLE"; - - /** * A CharSequence dialog title to show to the user when used with a * {@link #ACTION_CONFIRM_DEVICE_CREDENTIAL}. * @hide diff --git a/core/java/android/hardware/biometrics/IBiometricConfirmDeviceCredentialCallback.aidl b/core/java/android/hardware/biometrics/Authenticator.java index 8b35852efd31..6d7e7488f2d0 100644 --- a/core/java/android/hardware/biometrics/IBiometricConfirmDeviceCredentialCallback.aidl +++ b/core/java/android/hardware/biometrics/Authenticator.java @@ -17,10 +17,19 @@ package android.hardware.biometrics; /** - * Communication channel between ConfirmDeviceCredential / ConfirmLock* and BiometricService. + * Type of authenticators defined on a granularity that the BiometricManager / BiometricPrompt + * supports. * @hide */ -interface IBiometricConfirmDeviceCredentialCallback { - // Invoked when authentication should be canceled. - oneway void cancel(); -}
\ No newline at end of file +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/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index d8110f33d723..cbe8a052db2f 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -201,55 +201,5 @@ public class BiometricManager { } } - /** - * TODO(b/123378871): Remove when moved. - * @hide - */ - @RequiresPermission(USE_BIOMETRIC_INTERNAL) - public void onConfirmDeviceCredentialSuccess() { - if (mService != null) { - try { - mService.onConfirmDeviceCredentialSuccess(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } else { - Slog.w(TAG, "onConfirmDeviceCredentialSuccess(): Service not connected"); - } - } - - /** - * TODO(b/123378871): Remove when moved. - * @hide - */ - @RequiresPermission(USE_BIOMETRIC_INTERNAL) - public void onConfirmDeviceCredentialError(int error, String message) { - if (mService != null) { - try { - mService.onConfirmDeviceCredentialError(error, message); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } else { - Slog.w(TAG, "onConfirmDeviceCredentialError(): Service not connected"); - } - } - - /** - * TODO(b/123378871): Remove when moved. - * @hide - */ - @RequiresPermission(USE_BIOMETRIC_INTERNAL) - public void registerCancellationCallback(IBiometricConfirmDeviceCredentialCallback callback) { - if (mService != null) { - try { - mService.registerCancellationCallback(callback); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } else { - Slog.w(TAG, "registerCancellationCallback(): Service not connected"); - } - } } diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index fb6b231632f1..cf86e25112d2 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -69,24 +69,21 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan /** * @hide */ - public static final String KEY_POSITIVE_TEXT = "positive_text"; - /** - * @hide - */ public static final String KEY_NEGATIVE_TEXT = "negative_text"; /** * @hide */ 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_FROM_CONFIRM_DEVICE_CREDENTIAL - = "from_confirm_device_credential"; + public static final String KEY_AUTHENTICATORS_ALLOWED = "authenticators_allowed"; /** * Error/help message will show for this amount of time. @@ -100,7 +97,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan /** * @hide */ - public static final int DISMISSED_REASON_CONFIRMED = 1; + public static final int DISMISSED_REASON_BIOMETRIC_CONFIRMED = 1; /** * Dialog is done animating away after user clicked on the button set via @@ -119,7 +116,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * Authenticated, confirmation not required. Dialog animated away. * @hide */ - public static final int DISMISSED_REASON_CONFIRM_NOT_REQUIRED = 4; + public static final int DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED = 4; /** * Error message shown on SystemUI. When BiometricService receives this, the UI is already @@ -134,6 +131,11 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan */ public static final int DISMISSED_REASON_SERVER_REQUESTED = 6; + /** + * @hide + */ + public static final int DISMISSED_REASON_CREDENTIAL_CONFIRMED = 7; + private static class ButtonInfo { Executor executor; DialogInterface.OnClickListener listener; @@ -203,30 +205,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** - * 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, * such as screen that asks for a backup password. @@ -298,17 +276,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** - * TODO(123378871): Remove when moved. - * @return - * @hide - */ - @RequiresPermission(USE_BIOMETRIC_INTERNAL) - @NonNull public Builder setFromConfirmDeviceCredential() { - mBundle.putBoolean(KEY_FROM_CONFIRM_DEVICE_CREDENTIAL, true); - return this; - } - - /** * Creates a {@link BiometricPrompt}. * @return a {@link BiometricPrompt} * @throws IllegalArgumentException if any of the required fields are not set. @@ -317,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); } @@ -384,7 +355,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_CONFIRMED) { + if (reason == DISMISSED_REASON_BIOMETRIC_CONFIRMED) { mPositiveButtonInfo.executor.execute(() -> { mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE); }); @@ -532,8 +503,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public void authenticateUser(@NonNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull AuthenticationCallback callback, - int userId, - IBiometricConfirmDeviceCredentialCallback confirmDeviceCredentialCallback) { + int userId) { if (cancel == null) { throw new IllegalArgumentException("Must supply a cancellation signal"); } @@ -543,8 +513,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan if (callback == null) { throw new IllegalArgumentException("Must supply a callback"); } - authenticateInternal(null /* crypto */, cancel, executor, callback, userId, - confirmDeviceCredentialCallback); + authenticateInternal(null /* crypto */, cancel, executor, callback, userId); } /** @@ -595,8 +564,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan if (mBundle.getBoolean(KEY_ALLOW_DEVICE_CREDENTIAL)) { throw new IllegalArgumentException("Device credential not supported with crypto"); } - authenticateInternal(crypto, cancel, executor, callback, mContext.getUserId(), - null /* confirmDeviceCredentialCallback */); + authenticateInternal(crypto, cancel, executor, callback, mContext.getUserId()); } /** @@ -638,8 +606,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan if (callback == null) { throw new IllegalArgumentException("Must supply a callback"); } - authenticateInternal(null /* crypto */, cancel, executor, callback, mContext.getUserId(), - null /* confirmDeviceCredentialCallback */); + authenticateInternal(null /* crypto */, cancel, executor, callback, mContext.getUserId()); } private void cancelAuthentication() { @@ -656,8 +623,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan @NonNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull AuthenticationCallback callback, - int userId, - IBiometricConfirmDeviceCredentialCallback confirmDeviceCredentialCallback) { + int userId) { try { if (cancel.isCanceled()) { Log.w(TAG, "Authentication already canceled"); @@ -672,7 +638,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan final long sessionId = crypto != null ? crypto.getOpId() : 0; if (BiometricManager.hasBiometrics(mContext)) { mService.authenticate(mToken, sessionId, userId, mBiometricServiceReceiver, - mContext.getOpPackageName(), mBundle, confirmDeviceCredentialCallback); + mContext.getOpPackageName(), mBundle); } else { mExecutor.execute(() -> { callback.onAuthenticationError(BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT, diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl index f0a0b2f0235f..6a3bf38a97e1 100644 --- a/core/java/android/hardware/biometrics/IBiometricService.aidl +++ b/core/java/android/hardware/biometrics/IBiometricService.aidl @@ -17,7 +17,6 @@ package android.hardware.biometrics; import android.os.Bundle; -import android.hardware.biometrics.IBiometricConfirmDeviceCredentialCallback; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricServiceReceiver; @@ -31,10 +30,8 @@ import android.hardware.biometrics.IBiometricServiceReceiver; interface IBiometricService { // Requests authentication. The service choose the appropriate biometric to use, and show // the corresponding BiometricDialog. - // TODO(b/123378871): Remove callback when moved. void authenticate(IBinder token, long sessionId, int userId, - IBiometricServiceReceiver receiver, String opPackageName, in Bundle bundle, - IBiometricConfirmDeviceCredentialCallback callback); + IBiometricServiceReceiver receiver, String opPackageName, in Bundle bundle); // Cancel authentication for the given sessionId void cancelAuthentication(IBinder token, String opPackageName); @@ -57,16 +54,4 @@ interface IBiometricService { // Reset the lockout when user authenticates with strong auth (e.g. PIN, pattern or password) void resetLockout(in byte [] token); - - // TODO(b/123378871): Remove when moved. - // CDCA needs to send results to BiometricService if it was invoked using BiometricPrompt's - // setAllowDeviceCredential method, since there's no way for us to intercept onActivityResult. - // CDCA is launched from BiometricService (startActivityAsUser) instead of *ForResult. - void onConfirmDeviceCredentialSuccess(); - // TODO(b/123378871): Remove when moved. - void onConfirmDeviceCredentialError(int error, String message); - // TODO(b/123378871): Remove when moved. - // When ConfirmLock* is invoked from BiometricPrompt, it needs to register a callback so that - // it can receive the cancellation signal. - void registerCancellationCallback(IBiometricConfirmDeviceCredentialCallback callback); } diff --git a/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl index ca6114e4d842..66b6e896fc13 100644 --- a/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl +++ b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl @@ -38,4 +38,6 @@ oneway interface IBiometricServiceReceiverInternal { void onDialogDismissed(int reason); // Notifies that the user has pressed the "try again" button on SystemUI void onTryAgainPressed(); + // Notifies that the user has pressed the "use password" button on SystemUI + void onDeviceCredentialPressed(); } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 9441825a1ed6..c8ba52a63151 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(String error); - // Used to hide the biometric dialog when the AuthenticationClient is stopped - void hideBiometricDialog(); + void onBiometricError(int errorCode, String error); + // 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 4c3a177a013b..a845b587c49f 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(String error); - // Used to hide the biometric dialog when the AuthenticationClient is stopped - void hideBiometricDialog(); + void onBiometricError(int errorCode, String error); + // Used to hide the authentication dialog, e.g. when the application cancels authentication + void hideAuthenticationDialog(); } diff --git a/packages/SystemUI/res/drawable/auth_dialog_lock.xml b/packages/SystemUI/res/drawable/auth_dialog_lock.xml new file mode 100644 index 000000000000..8146c16e4aaf --- /dev/null +++ b/packages/SystemUI/res/drawable/auth_dialog_lock.xml @@ -0,0 +1,27 @@ +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="32dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?android:attr/colorAccent" + android:pathData="M12,15m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/> + <path + android:fillColor="?android:attr/colorAccent" + android:pathData="M18,8h-1.5V5.5C16.5,3.01 14.49,1 12,1S7.5,3.01 7.5,5.5V8H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10C20,8.9 19.1,8 18,8zM9.5,5.5C9.5,4.12 10.62,3 12,3c1.38,0 2.5,1.12 2.5,2.5V8h-5V5.5zM18,20H6V10h1.5h9H18V20z"/> +</vector> diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml new file mode 100644 index 000000000000..c3fa39e5a87f --- /dev/null +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -0,0 +1,113 @@ +<!-- + ~ 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. + --> + +<com.android.systemui.biometrics.AuthCredentialPatternView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:elevation="@dimen/biometric_dialog_elevation"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical"> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + + <ImageView + android:layout_width="32dp" + android:layout_height="32dp" + android:background="@drawable/auth_dialog_lock"/> + + <TextView + android:id="@+id/title" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="12dp" + android:textSize="20sp" + android:gravity="center" + android:textColor="?android:attr/textColorPrimary"/> + + <TextView + android:id="@+id/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:textSize="16sp" + android:gravity="center" + android:textColor="?android:attr/textColorPrimary"/> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:gravity="center" + android:textSize="16sp" + android:textColor="?android:attr/textColorPrimary"/> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + + <TextView + android:id="@+id/error" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:textSize="16sp" + android:gravity="center" + android:textColor="?android:attr/colorError"/> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + + </LinearLayout> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center"> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="40dp" + android:layout_marginRight="40dp" + android:layout_gravity="center" + android:clipChildren="false" + android:clipToPadding="false" + style="@style/LockPatternStyleBiometricPrompt"/> + + </LinearLayout> + +</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_container_view.xml b/packages/SystemUI/res/layout/auth_container_view.xml index 23199aacc093..3db01a4e7f3a 100644 --- a/packages/SystemUI/res/layout/auth_container_view.xml +++ b/packages/SystemUI/res/layout/auth_container_view.xml @@ -34,7 +34,7 @@ android:elevation="@dimen/biometric_dialog_elevation"/> <ScrollView - android:id="@+id/scrollview" + android:id="@+id/biometric_scrollview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal|bottom" diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml new file mode 100644 index 000000000000..4aed0333e9ca --- /dev/null +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -0,0 +1,101 @@ +<!-- + ~ 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. + --> + +<com.android.systemui.biometrics.AuthCredentialPasswordView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center_horizontal" + android:elevation="@dimen/biometric_dialog_elevation"> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + + <ImageView + android:layout_width="32dp" + android:layout_height="32dp" + android:background="@drawable/auth_dialog_lock"/> + + <TextView + android:id="@+id/title" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="12dp" + android:textSize="20sp" + android:gravity="center" + android:textColor="?android:attr/textColorPrimary"/> + + <TextView + android:id="@+id/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:textSize="16sp" + android:gravity="center" + android:textColor="?android:attr/textColorPrimary"/> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:gravity="center" + android:textSize="16sp" + android:textColor="?android:attr/textColorPrimary"/> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + + <TextView + android:id="@+id/error" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:textSize="16sp" + android:gravity="center" + android:textColor="?android:attr/colorError"/> + + <EditText + android:id="@+id/lockPassword" + android:layout_marginBottom="20dp" + android:layout_marginLeft="100dp" + android:layout_marginRight="100dp" + android:layout_width="208dp" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:inputType="textPassword" + android:maxLength="500" + android:textSize="16sp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:imeOptions="flagForceAscii" + style="@style/LockPatternStyleBiometricPrompt"/> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="5"/> + +</com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml new file mode 100644 index 000000000000..c9edcd606277 --- /dev/null +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -0,0 +1,97 @@ +<!-- + ~ 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. + --> + +<com.android.systemui.biometrics.AuthCredentialPatternView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center_horizontal" + android:elevation="@dimen/biometric_dialog_elevation"> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + + <ImageView + android:layout_width="32dp" + android:layout_height="32dp" + android:background="@drawable/auth_dialog_lock"/> + + <TextView + android:id="@+id/title" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="12dp" + android:textSize="20sp" + android:gravity="center" + android:textColor="?android:attr/textColorPrimary"/> + + <TextView + android:id="@+id/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:textSize="16sp" + android:gravity="center" + android:textColor="?android:attr/textColorPrimary"/> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:gravity="center" + android:textSize="16sp" + android:textColor="?android:attr/textColorPrimary"/> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="3"/> + + <TextView + android:id="@+id/error" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:textSize="16sp" + android:gravity="center" + android:textColor="?android:attr/colorError"/> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_marginBottom="20dp" + android:layout_marginLeft="40dp" + android:layout_marginRight="40dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clipChildren="false" + android:clipToPadding="false" + style="@style/LockPatternStyleBiometricPrompt"/> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + +</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 1079206c81ce..d722d618e416 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1009,10 +1009,15 @@ <!-- Biometric Dialog values --> <dimen name="biometric_dialog_biometric_icon_size">64dp</dimen> <dimen name="biometric_dialog_corner_size">4dp</dimen> + <!-- Y translation when showing/dismissing the dialog--> <dimen name="biometric_dialog_animation_translation_offset">350dp</dimen> <dimen name="biometric_dialog_border_padding">4dp</dimen> <dimen name="biometric_dialog_elevation">1dp</dimen> <dimen name="biometric_dialog_icon_padding">16dp</dimen> + <!-- Y translation for biometric contents when transitioning to device credential UI --> + <dimen name="biometric_dialog_medium_to_large_translation_offset">100dp</dimen> + <!-- Y translation for credential contents when animating in --> + <dimen name="biometric_dialog_credential_translation_offset">60dp</dimen> <!-- Wireless Charging Animation values --> <dimen name="wireless_charging_dots_radius_start">0dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c5547800360d..8335c116c95f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -311,6 +311,21 @@ <!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] --> <string name="biometric_dialog_authenticated">Authenticated</string> + <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pin) [CHAR LIMIT=30] --> + <string name="biometric_dialog_use_pin">Use PIN</string> + <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pattern) [CHAR LIMIT=30] --> + <string name="biometric_dialog_use_pattern">Use pattern</string> + <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pass) [CHAR LIMIT=30] --> + <string name="biometric_dialog_use_password">Use password</string> + <!-- Error string shown when the user enters an incorrect PIN [CHAR LIMIT=40]--> + <string name="biometric_dialog_wrong_pin">Wrong PIN</string> + <!-- Error string shown when the user enters an incorrect pattern [CHAR LIMIT=40]--> + <string name="biometric_dialog_wrong_pattern">Wrong pattern</string> + <!-- Error string shown when the user enters an incorrect password [CHAR LIMIT=40]--> + <string name="biometric_dialog_wrong_password">Wrong password</string> + <!-- Error string shown when the user enters too many incorrect attempts [CHAR LIMIT=120]--> + <string name="biometric_dialog_credential_too_many_attempts">Too many incorrect attempts.\nTry again in <xliff:g id="number">%d</xliff:g> seconds.</string> + <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication --> <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string> <!-- Content description of the fingerprint icon when the system-provided fingerprint dialog is showing, for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 6374191c4d7b..96fbcbba6e8b 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -314,6 +314,12 @@ <item name="*android:errorColor">?android:attr/colorError</item> </style> + <style name="LockPatternStyleBiometricPrompt"> + <item name="*android:regularColor">?android:attr/colorForeground</item> + <item name="*android:successColor">?android:attr/colorForeground</item> + <item name="*android:errorColor">?android:attr/colorError</item> + </style> + <style name="qs_theme" parent="@*android:style/Theme.DeviceDefault.QuickSettings"> <item name="lightIconTheme">@style/QSIconTheme</item> <item name="darkIconTheme">@style/QSIconTheme</item> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index 73bbce9c5b35..d20cd72f0712 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -16,8 +16,6 @@ package com.android.systemui.biometrics; -import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -25,6 +23,7 @@ import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.admin.DevicePolicyManager; import android.content.Context; import android.hardware.biometrics.BiometricPrompt; import android.os.Bundle; @@ -34,7 +33,7 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.View; -import android.view.accessibility.AccessibilityEvent; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.widget.Button; import android.widget.ImageView; @@ -42,10 +41,13 @@ import android.widget.LinearLayout; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.LockPatternUtils; import com.android.systemui.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; /** * Contains the Biometric views (title, subtitle, icon, buttons, etc) and its controllers. @@ -97,6 +99,7 @@ public abstract class AuthBiometricView extends LinearLayout { int ACTION_BUTTON_NEGATIVE = 3; int ACTION_BUTTON_TRY_AGAIN = 4; int ACTION_ERROR = 5; + int ACTION_USE_DEVICE_CREDENTIAL = 6; /** * When an action has occurred. The caller will only invoke this when the callback should @@ -145,6 +148,14 @@ public abstract class AuthBiometricView extends LinearLayout { public int getDelayAfterError() { return BiometricPrompt.HIDE_DIALOG_DELAY; } + + public int getMediumToLargeAnimationDurationMs() { + return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS; + } + + public int getAnimateCredentialStartDelayMs() { + return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS; + } } private final Injector mInjector; @@ -154,8 +165,9 @@ public abstract class AuthBiometricView extends LinearLayout { private final int mTextColorHint; private AuthPanelController mPanelController; - private Bundle mBundle; + private Bundle mBiometricPromptBundle; private boolean mRequireConfirmation; + private int mUserId; @AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN; private TextView mTitleView; @@ -212,6 +224,9 @@ public abstract class AuthBiometricView extends LinearLayout { } else if (mSize == AuthDialog.SIZE_SMALL) { Log.w(TAG, "Ignoring background click during small dialog"); return; + } else if (mSize == AuthDialog.SIZE_LARGE) { + Log.w(TAG, "Ignoring background click during large dialog"); + return; } mCallback.onAction(Callback.ACTION_USER_CANCELED); }; @@ -256,7 +271,7 @@ public abstract class AuthBiometricView extends LinearLayout { } public void setBiometricPromptBundle(Bundle bundle) { - mBundle = bundle; + mBiometricPromptBundle = bundle; } public void setCallback(Callback callback) { @@ -267,6 +282,10 @@ public abstract class AuthBiometricView extends LinearLayout { backgroundView.setOnClickListener(mBackgroundClickListener); } + public void setUserId(int userId) { + mUserId = userId; + } + public void setRequireConfirmation(boolean requireConfirmation) { mRequireConfirmation = requireConfirmation; } @@ -287,7 +306,7 @@ public abstract class AuthBiometricView extends LinearLayout { final int newHeight = mIconView.getHeight() + 2 * (int) iconPadding; mPanelController.updateForContentDimensions(mMediumWidth, newHeight, - false /* animate */); + 0 /* animateDurationMs */); mSize = newSize; } else if (mSize == AuthDialog.SIZE_SMALL && newSize == AuthDialog.SIZE_MEDIUM) { @@ -305,10 +324,8 @@ public abstract class AuthBiometricView extends LinearLayout { // Animate the text final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1); - opacityAnimator.setDuration(AuthDialog.ANIMATE_DURATION_MS); opacityAnimator.addUpdateListener((animation) -> { final float opacity = (float) animation.getAnimatedValue(); - mTitleView.setAlpha(opacity); mIndicatorView.setAlpha(opacity); mNegativeButton.setAlpha(opacity); @@ -324,7 +341,7 @@ public abstract class AuthBiometricView extends LinearLayout { // Choreograph together final AnimatorSet as = new AnimatorSet(); - as.setDuration(AuthDialog.ANIMATE_DURATION_MS); + as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS); as.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { @@ -355,11 +372,73 @@ public abstract class AuthBiometricView extends LinearLayout { as.start(); // Animate the panel mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight, - true /* animate */); + AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS); } else if (newSize == AuthDialog.SIZE_MEDIUM) { mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight, - false /* animate */); + 0 /* animateDurationMs */); mSize = newSize; + } else if (newSize == AuthDialog.SIZE_LARGE) { + final boolean isManagedProfile = Utils.isManagedProfile(mContext, mUserId); + + // If it's a managed profile, animate the contents and panel down, since the credential + // contents will be shown on the same "layer" as the background. If it's not a managed + // profile, animate the contents up and expand the panel to full-screen - the credential + // contents will be shown on the same "layer" as the panel. + final float translationY = isManagedProfile ? + -getResources().getDimension( + R.dimen.biometric_dialog_animation_translation_offset) + : getResources().getDimension( + R.dimen.biometric_dialog_medium_to_large_translation_offset); + final AuthBiometricView biometricView = this; + + // Translate at full duration + final ValueAnimator translationAnimator = ValueAnimator.ofFloat( + biometricView.getY(), biometricView.getY() - translationY); + translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs()); + translationAnimator.addUpdateListener((animation) -> { + final float translation = (float) animation.getAnimatedValue(); + biometricView.setTranslationY(translation); + }); + translationAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (biometricView.getParent() != null) { + ((ViewGroup) biometricView.getParent()).removeView(biometricView); + } + mSize = newSize; + } + }); + + // Opacity to 0 in half duration + final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0); + opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2); + opacityAnimator.addUpdateListener((animation) -> { + final float opacity = (float) animation.getAnimatedValue(); + biometricView.setAlpha(opacity); + }); + + if (!isManagedProfile) { + mPanelController.setUseFullScreen(true); + mPanelController.updateForContentDimensions( + mPanelController.getContainerWidth(), + mPanelController.getContainerHeight(), + mInjector.getMediumToLargeAnimationDurationMs()); + } + + // Start the animations together + AnimatorSet as = new AnimatorSet(); + List<Animator> animators = new ArrayList<>(); + animators.add(translationAnimator); + animators.add(opacityAnimator); + if (isManagedProfile) { + animators.add(mPanelController.getTranslationAnimator(translationY)); + animators.add(mPanelController.getAlphaAnimator(0)); + } + as.playTogether(animators); + as.setDuration(isManagedProfile ? mInjector.getMediumToLargeAnimationDurationMs() + : mInjector.getMediumToLargeAnimationDurationMs() * 2 / 3); + as.start(); } else { Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize); } @@ -528,7 +607,11 @@ public abstract class AuthBiometricView extends LinearLayout { if (mState == STATE_PENDING_CONFIRMATION) { mCallback.onAction(Callback.ACTION_USER_CANCELED); } else { - mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE); + if (isDeviceCredentialAllowed()) { + startTransitionToCredentialUI(); + } else { + mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE); + } } }); @@ -544,6 +627,16 @@ public abstract class AuthBiometricView extends LinearLayout { }); } + /** + * Kicks off the animation process and invokes the callback. + */ + void startTransitionToCredentialUI() { + updateSize(AuthDialog.SIZE_LARGE); + mHandler.postDelayed(() -> { + mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL); + }, mInjector.getAnimateCredentialStartDelayMs()); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -556,11 +649,37 @@ public abstract class AuthBiometricView extends LinearLayout { */ @VisibleForTesting void onAttachedToWindowInternal() { - setText(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE)); - setText(mNegativeButton, mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT)); + setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE)); + + final String negativeText; + if (isDeviceCredentialAllowed()) { + + final @Utils.CredentialType int credentialType = + Utils.getCredentialType(mContext, mUserId); + switch(credentialType) { + case Utils.CREDENTIAL_PIN: + negativeText = getResources().getString(R.string.biometric_dialog_use_pin); + break; + case Utils.CREDENTIAL_PATTERN: + negativeText = getResources().getString(R.string.biometric_dialog_use_pattern); + break; + case Utils.CREDENTIAL_PASSWORD: + negativeText = getResources().getString(R.string.biometric_dialog_use_password); + break; + default: + negativeText = getResources().getString(R.string.biometric_dialog_use_password); + break; + } - setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE)); - setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION)); + } else { + negativeText = mBiometricPromptBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT); + } + setText(mNegativeButton, negativeText); + + setTextOrHide(mSubtitleView, + mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE)); + setTextOrHide(mDescriptionView, + mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION)); if (mSavedState == null) { updateState(STATE_AUTHENTICATING_ANIMATING_IN); @@ -655,4 +774,8 @@ public abstract class AuthBiometricView extends LinearLayout { } } } + + private boolean isDeviceCredentialAllowed() { + 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 6555c75f677a..a9359d4ff0db 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -24,7 +24,9 @@ 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; import android.os.Bundle; import android.os.IBinder; @@ -36,6 +38,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.Interpolator; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; @@ -72,17 +75,20 @@ 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; private final Interpolator mLinearOutSlowIn; @VisibleForTesting final BiometricCallback mBiometricCallback; + private final CredentialCallback mCredentialCallback; - private final ViewGroup mContainerView; - private final AuthBiometricView mBiometricView; + @VisibleForTesting final FrameLayout mFrameLayout; + @VisibleForTesting @Nullable AuthBiometricView mBiometricView; + @VisibleForTesting @Nullable AuthCredentialView mCredentialView; private final ImageView mBackgroundView; - private final ScrollView mScrollView; + @VisibleForTesting final ScrollView mBiometricScrollView; private final View mPanelView; private final float mTranslationY; @@ -145,7 +151,31 @@ public class AuthContainerView extends LinearLayout 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, + boolean isManagedProfile) { + return new AuthPanelController(context, panelView, isManagedProfile); + } + + ImageView getBackgroundView(FrameLayout parent) { + return parent.findViewById(R.id.background); + } + + View getPanelView(FrameLayout parent) { + return parent.findViewById(R.id.panel); } } @@ -155,7 +185,7 @@ public class AuthContainerView extends LinearLayout public void onAction(int action) { switch (action) { case AuthBiometricView.Callback.ACTION_AUTHENTICATED: - animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED); + animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED); break; case AuthBiometricView.Callback.ACTION_USER_CANCELED: animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); @@ -169,17 +199,30 @@ public class AuthContainerView extends LinearLayout case AuthBiometricView.Callback.ACTION_ERROR: animateAway(AuthDialogCallback.DISMISSED_ERROR); break; + case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL: + mConfig.mCallback.onDeviceCredentialPressed(); + addCredentialView(false /* animatePanel */, true /* animateContents */); + break; default: Log.e(TAG, "Unhandled action: " + action); } } } + final class CredentialCallback implements AuthCredentialView.Callback { + @Override + public void onCredentialMatched() { + animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); + } + } + @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); @@ -187,51 +230,48 @@ public class AuthContainerView extends LinearLayout .getDimension(R.dimen.biometric_dialog_animation_translation_offset); mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mBiometricCallback = new BiometricCallback(); + mCredentialCallback = new CredentialCallback(); final LayoutInflater factory = LayoutInflater.from(mContext); - mContainerView = (ViewGroup) factory.inflate( - R.layout.auth_container_view, this, false /* attachToRoot */); - - mPanelView = mContainerView.findViewById(R.id.panel); - mPanelController = new AuthPanelController(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; - mScrollView = null; - return; + mFrameLayout = mInjector.inflateContainerView(factory, this); + + final boolean isManagedProfile = Utils.isManagedProfile(mContext, mConfig.mUserId); + + mPanelView = mInjector.getPanelView(mFrameLayout); + mPanelController = mInjector.getPanelController(mContext, mPanelView, isManagedProfile); + + // 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; + } } - mBackgroundView = mContainerView.findViewById(R.id.background); + mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout); + mBackgroundView = mInjector.getBackgroundView(mFrameLayout); + - UserManager userManager = mContext.getSystemService(UserManager.class); - DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); - if (userManager.isManagedProfile(mConfig.mUserId)) { + if (isManagedProfile) { final Drawable image = getResources().getDrawable(R.drawable.work_challenge_background, mContext.getTheme()); + final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); image.setColorFilter(dpm.getOrganizationColorForUser(mConfig.mUserId), PorterDuff.Mode.DARKEN); mBackgroundView.setScaleType(ImageView.ScaleType.CENTER_CROP); mBackgroundView.setImageDrawable(image); } - mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation); - mBiometricView.setPanelController(mPanelController); - mBiometricView.setBiometricPromptBundle(config.mBiometricPromptBundle); - mBiometricView.setCallback(mBiometricCallback); - mBiometricView.setBackgroundView(mBackgroundView); - - mScrollView = mContainerView.findViewById(R.id.scrollview); - mScrollView.addView(mBiometricView); - addView(mContainerView); + addView(mFrameLayout); setOnKeyListener((v, keyCode, event) -> { if (keyCode != KeyEvent.KEYCODE_BACK) { @@ -248,6 +288,53 @@ public class AuthContainerView extends LinearLayout } @Override + public boolean isAllowDeviceCredentials() { + return Utils.isDeviceCredentialAllowed(mConfig.mBiometricPromptBundle); + } + + private void addBiometricView() { + mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation); + mBiometricView.setPanelController(mPanelController); + mBiometricView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle); + mBiometricView.setCallback(mBiometricCallback); + mBiometricView.setBackgroundView(mBackgroundView); + mBiometricView.setUserId(mConfig.mUserId); + mBiometricScrollView.addView(mBiometricView); + } + + /** + * Adds the credential view. When going from biometric to credential view, the biometric + * view starts the panel expansion animation. If the credential view is being shown first, + * it should own the panel expansion. + * @param animatePanel if the credential view needs to own the panel expansion animation + */ + private void addCredentialView(boolean animatePanel, boolean animateContents) { + final LayoutInflater factory = LayoutInflater.from(mContext); + final int credentialType = Utils.getCredentialType(mContext, mConfig.mUserId); + switch (credentialType) { + case Utils.CREDENTIAL_PATTERN: + mCredentialView = (AuthCredentialView) factory.inflate( + R.layout.auth_credential_pattern_view, null, false); + break; + case Utils.CREDENTIAL_PIN: + case Utils.CREDENTIAL_PASSWORD: + mCredentialView = (AuthCredentialView) factory.inflate( + R.layout.auth_credential_password_view, null, false); + break; + default: + throw new IllegalStateException("Unknown credential type: " + credentialType); + } + + mCredentialView.setContainerView(this); + mCredentialView.setUser(mConfig.mUserId); + mCredentialView.setCallback(mCredentialCallback); + mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle); + mCredentialView.setPanelController(mPanelController, animatePanel); + mCredentialView.setShouldAnimateContents(animateContents); + mFrameLayout.addView(mCredentialView); + } + + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight()); @@ -256,8 +343,22 @@ public class AuthContainerView extends LinearLayout @Override public void onAttachedToWindow() { super.onAttachedToWindow(); + onAttachedToWindowInternal(); + } + + @VisibleForTesting + void onAttachedToWindowInternal() { mWakefulnessLifecycle.addObserver(this); + 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 { @@ -265,7 +366,7 @@ public class AuthContainerView extends LinearLayout // The background panel and content are different views since we need to be able to // animate them separately in other places. mPanelView.setY(mTranslationY); - mScrollView.setY(mTranslationY); + mBiometricScrollView.setY(mTranslationY); setAlpha(0f); postOnAnimation(() -> { @@ -276,12 +377,21 @@ public class AuthContainerView extends LinearLayout .withLayer() .withEndAction(this::onDialogAnimatedIn) .start(); - mScrollView.animate() + mBiometricScrollView.animate() .translationY(0) .setDuration(ANIMATION_DURATION_SHOW_MS) .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) @@ -305,7 +415,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)); } @@ -346,7 +458,15 @@ public class AuthContainerView extends LinearLayout @Override public void onSaveState(@NonNull Bundle outState) { outState.putInt(AuthDialog.KEY_CONTAINER_STATE, mContainerState); - mBiometricView.onSaveState(outState); + // In the case where biometric and credential are both allowed, we can assume that + // biometric isn't showing if credential is showing since biometric is shown first. + outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING, + mBiometricView != null && mCredentialView == null); + outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null); + + if (mBiometricView != null) { + mBiometricView.onSaveState(outState); + } } @Override @@ -354,6 +474,11 @@ public class AuthContainerView extends LinearLayout return mConfig.mOpPackageName; } + @Override + public void animateToCredentialUI() { + mBiometricView.startTransitionToCredentialUI(); + } + @VisibleForTesting void animateAway(int reason) { animateAway(true /* sendReason */, reason); @@ -391,12 +516,20 @@ public class AuthContainerView extends LinearLayout .withLayer() .withEndAction(endActionRunnable) .start(); - mScrollView.animate() + mBiometricScrollView.animate() .translationY(mTranslationY) .setDuration(ANIMATION_DURATION_AWAY_MS) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); + if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { + mCredentialView.animate() + .translationY(mTranslationY) + .setDuration(ANIMATION_DURATION_AWAY_MS) + .setInterpolator(mLinearOutSlowIn) + .withLayer() + .start(); + } animate() .alpha(0f) .setDuration(ANIMATION_DURATION_AWAY_MS) @@ -431,7 +564,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 d10a3fede412..4c2afb0a14ca 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -23,6 +23,8 @@ 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; import android.os.Bundle; @@ -105,6 +107,15 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } @Override + public void onDeviceCredentialPressed() { + try { + mReceiver.onDeviceCredentialPressed(); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException when handling credential button", e); + } + } + + @Override public void onDismissed(@DismissedReason int reason) { switch (reason) { case AuthDialogCallback.DISMISSED_USER_CANCELED: @@ -116,11 +127,12 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, break; case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED); + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED); break; - case AuthDialogCallback.DISMISSED_AUTHENTICATED: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED: + sendResultAndCleanUp( + BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED); break; case AuthDialogCallback.DISMISSED_ERROR: @@ -131,6 +143,10 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED); break; + case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED: + sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED); + break; + default: Log.e(TAG, "Unhandled reason: " + reason); break; @@ -185,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; @@ -204,6 +223,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, Log.w(TAG, "mCurrentDialog: " + mCurrentDialog); skipAnimation = true; } + showDialog(args, skipAnimation, null /* savedState */); } @@ -227,14 +247,21 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } @Override - public void onBiometricError(String error) { - if (DEBUG) Log.d(TAG, "onBiometricError: " + error); - mCurrentDialog.onError(error); + public void onBiometricError(int errorCode, String error) { + if (DEBUG) Log.d(TAG, "onBiometricError: " + errorCode + ", " + error); + + final boolean isLockout = errorCode == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT + || errorCode == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT; + if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) { + mCurrentDialog.animateToCredentialUI(); + } else { + mCurrentDialog.onError(error); + } } @Override - public void hideBiometricDialog() { - if (DEBUG) Log.d(TAG, "hideBiometricDialog"); + public void hideAuthenticationDialog() { + if (DEBUG) Log.d(TAG, "hideAuthenticationDialog"); mCurrentDialog.dismissFromSystemServer(); } @@ -262,7 +289,7 @@ 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 @@ -306,6 +333,15 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, // to send its pending callback immediately. if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE) != 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); + } + showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java new file mode 100644 index 000000000000..8df072e9b99e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java @@ -0,0 +1,118 @@ +/* + * 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.content.Context; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.TextView; + +import com.android.internal.widget.LockPatternChecker; +import com.android.internal.widget.LockPatternUtils; +import com.android.systemui.R; + +/** + * Pin and Password UI + */ +public class AuthCredentialPasswordView extends AuthCredentialView + implements TextView.OnEditorActionListener { + + private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView"; + + private final InputMethodManager mImm; + private EditText mPasswordField; + + public AuthCredentialPasswordView(Context context, + AttributeSet attrs) { + super(context, attrs); + mImm = mContext.getSystemService(InputMethodManager.class); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPasswordField = findViewById(R.id.lockPassword); + mPasswordField.setOnEditorActionListener(this); + mPasswordField.setOnKeyListener((v, keyCode, event) -> { + if (keyCode != KeyEvent.KEYCODE_BACK) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_UP) { + mContainerView.animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + } + return true; + }); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mCredentialType == Utils.CREDENTIAL_PIN) { + mPasswordField.setInputType( + InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + } + + // Wait a bit to focus the field so the focusable flag on the window is already set then. + post(() -> { + mPasswordField.requestFocus(); + mImm.showSoftInput(mPasswordField, InputMethodManager.SHOW_IMPLICIT); + }); + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Check if this was the result of hitting the enter key + final boolean isSoftImeEvent = event == null + && (actionId == EditorInfo.IME_NULL + || actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT); + final boolean isKeyboardEnterKey = event != null + && KeyEvent.isConfirmKey(event.getKeyCode()) + && event.getAction() == KeyEvent.ACTION_DOWN; + if (isSoftImeEvent || isKeyboardEnterKey) { + checkPasswordAndUnlock(); + return true; + } + return false; + } + + private void checkPasswordAndUnlock() { + final byte[] password = LockPatternUtils.charSequenceToByteArray(mPasswordField.getText()); + if (password == null || password.length == 0) { + return; + } + + mPendingLockCheck = LockPatternChecker.checkPassword(mLockPatternUtils, + password, mUserId, this::onCredentialChecked); + } + + @Override + protected void onCredentialChecked(boolean matched, int timeoutMs) { + super.onCredentialChecked(matched, timeoutMs); + + if (matched) { + mImm.hideSoftInputFromWindow(getWindowToken(), 0 /* flags */); + } else { + mPasswordField.setText(""); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java new file mode 100644 index 000000000000..6c36f8263237 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java @@ -0,0 +1,102 @@ +/* + * 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.content.Context; +import android.util.AttributeSet; + +import com.android.internal.widget.LockPatternChecker; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternView; +import com.android.systemui.R; + +import java.util.List; + +/** + * Pattern UI + */ +public class AuthCredentialPatternView extends AuthCredentialView { + + private LockPatternView mLockPatternView; + + private class UnlockPatternListener implements LockPatternView.OnPatternListener { + + @Override + public void onPatternStart() { + + } + + @Override + public void onPatternCleared() { + + } + + @Override + public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { + + } + + @Override + public void onPatternDetected(List<LockPatternView.Cell> pattern) { + if (mPendingLockCheck != null) { + mPendingLockCheck.cancel(false); + } + + mLockPatternView.setEnabled(false); + + if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { + // Pattern size is less than the minimum, do not count it as a failed attempt. + onPatternChecked(false /* matched */, 0 /* timeoutMs */); + return; + } + + mPendingLockCheck = LockPatternChecker.checkPattern( + mLockPatternUtils, + pattern, + mUserId, + this::onPatternChecked); + } + + private void onPatternChecked(boolean matched, int timeoutMs) { + AuthCredentialPatternView.this.onCredentialChecked(matched, timeoutMs); + if (timeoutMs > 0) { + mLockPatternView.setEnabled(false); + } else { + mLockPatternView.setEnabled(true); + } + } + } + + @Override + protected void onErrorTimeoutFinish() { + super.onErrorTimeoutFinish(); + mLockPatternView.setEnabled(true); + } + + public AuthCredentialPatternView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mLockPatternView = findViewById(R.id.lockPattern); + mLockPatternView.setOnPatternListener(new UnlockPatternListener()); + mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(mUserId)); + mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java new file mode 100644 index 000000000000..8c8611e49dfb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -0,0 +1,265 @@ +/* + * 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.content.Context; +import android.hardware.biometrics.BiometricPrompt; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.widget.LockPatternUtils; +import com.android.systemui.Interpolators; +import com.android.systemui.R; + +/** + * Abstract base class for Pin, Pattern, or Password authentication, for + * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)} + */ +public abstract class AuthCredentialView extends LinearLayout { + + private static final String TAG = "BiometricPrompt/AuthCredentialView"; + private static final int ERROR_DURATION_MS = 3000; + + private final AccessibilityManager mAccessibilityManager; + + protected final Handler mHandler; + + private Bundle mBiometricPromptBundle; + private AuthPanelController mPanelController; + private boolean mShouldAnimatePanel; + private boolean mShouldAnimateContents; + + private TextView mTitleView; + private TextView mSubtitleView; + private TextView mDescriptionView; + protected TextView mErrorView; + + protected @Utils.CredentialType int mCredentialType; + protected final LockPatternUtils mLockPatternUtils; + protected AuthContainerView mContainerView; + protected Callback mCallback; + protected AsyncTask<?, ?, ?> mPendingLockCheck; + protected int mUserId; + protected ErrorTimer mErrorTimer; + + interface Callback { + void onCredentialMatched(); + } + + protected static class ErrorTimer extends CountDownTimer { + private final TextView mErrorView; + private final Context mContext; + + /** + * @param millisInFuture The number of millis in the future from the call + * to {@link #start()} until the countdown is done and {@link + * #onFinish()} + * is called. + * @param countDownInterval The interval along the way to receive + * {@link #onTick(long)} callbacks. + */ + public ErrorTimer(Context context, long millisInFuture, long countDownInterval, + TextView errorView) { + super(millisInFuture, countDownInterval); + mErrorView = errorView; + mContext = context; + } + + @Override + public void onTick(long millisUntilFinished) { + final int secondsCountdown = (int) (millisUntilFinished / 1000); + mErrorView.setText(mContext.getString( + R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown)); + } + + @Override + public void onFinish() { + mErrorView.setText(""); + } + } + + protected final Runnable mClearErrorRunnable = new Runnable() { + @Override + public void run() { + mErrorView.setText(""); + } + }; + + public AuthCredentialView(Context context, AttributeSet attrs) { + super(context, attrs); + + mLockPatternUtils = new LockPatternUtils(mContext); + mHandler = new Handler(Looper.getMainLooper()); + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); + } + + protected void showError(String error) { + mHandler.removeCallbacks(mClearErrorRunnable); + mErrorView.setText(error); + mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS); + } + + private void setTextOrHide(TextView view, String string) { + if (TextUtils.isEmpty(string)) { + view.setVisibility(View.GONE); + } else { + view.setText(string); + } + + Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); + } + + private void setText(TextView view, String string) { + view.setText(string); + } + + void setUser(int user) { + mUserId = user; + } + + void setCallback(Callback callback) { + mCallback = callback; + } + + void setBiometricPromptBundle(Bundle bundle) { + mBiometricPromptBundle = bundle; + } + + void setPanelController(AuthPanelController panelController, boolean animatePanel) { + mPanelController = panelController; + mShouldAnimatePanel = animatePanel; + } + + void setShouldAnimateContents(boolean animateContents) { + mShouldAnimateContents = animateContents; + } + + void setContainerView(AuthContainerView containerView) { + mContainerView = containerView; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + mCredentialType = Utils.getCredentialType(mContext, mUserId); + + setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE)); + setTextOrHide(mSubtitleView, + mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE)); + setTextOrHide(mDescriptionView, + mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION)); + + // 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(); + }); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mErrorTimer != null) { + mErrorTimer.cancel(); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTitleView = findViewById(R.id.title); + mSubtitleView = findViewById(R.id.subtitle); + mDescriptionView = findViewById(R.id.description); + mErrorView = findViewById(R.id.error); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mShouldAnimatePanel) { + // Credential view is always full screen. + mPanelController.setUseFullScreen(true); + mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(), + mPanelController.getContainerHeight(), 0 /* animateDurationMs */); + mShouldAnimatePanel = false; + } + } + + protected void onErrorTimeoutFinish() {} + + protected void onCredentialChecked(boolean matched, int timeoutMs) { + if (matched) { + mClearErrorRunnable.run(); + mCallback.onCredentialMatched(); + } else { + if (timeoutMs > 0) { + mHandler.removeCallbacks(mClearErrorRunnable); + long deadline = mLockPatternUtils.setLockoutAttemptDeadline(mUserId, timeoutMs); + mErrorTimer = new ErrorTimer(mContext, + deadline - SystemClock.elapsedRealtime(), + LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS, + mErrorView) { + @Override + public void onFinish() { + onErrorTimeoutFinish(); + mClearErrorRunnable.run(); + } + }; + mErrorTimer.start(); + } else { + final int error; + switch (mCredentialType) { + case Utils.CREDENTIAL_PIN: + error = R.string.biometric_dialog_wrong_pin; + break; + case Utils.CREDENTIAL_PATTERN: + error = R.string.biometric_dialog_wrong_pattern; + break; + case Utils.CREDENTIAL_PASSWORD: + error = R.string.biometric_dialog_wrong_password; + break; + default: + error = R.string.biometric_dialog_wrong_password; + break; + } + showError(getResources().getString(error)); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java index edb29538874c..ca95f9d736fc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java @@ -31,6 +31,8 @@ import java.lang.annotation.RetentionPolicy; public interface AuthDialog { String KEY_CONTAINER_STATE = "container_state"; + String KEY_BIOMETRIC_SHOWING = "biometric_showing"; + String KEY_CREDENTIAL_SHOWING = "credential_showing"; String KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY = "try_agian_visibility"; String KEY_BIOMETRIC_STATE = "state"; @@ -40,17 +42,38 @@ public interface AuthDialog { String KEY_BIOMETRIC_DIALOG_SIZE = "size"; int SIZE_UNKNOWN = 0; + /** + * Minimal UI, showing only biometric icon. + */ int SIZE_SMALL = 1; + /** + * Normal-sized biometric UI, showing title, icon, buttons, etc. + */ int SIZE_MEDIUM = 2; + /** + * Full-screen credential UI. + */ int SIZE_LARGE = 3; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_UNKNOWN, SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE}) @interface DialogSize {} /** - * Animation duration, e.g. small to medium dialog, icon translation, etc. + * Animation duration, from small to medium dialog, including back panel, icon translation, etc + */ + int ANIMATE_SMALL_TO_MEDIUM_DURATION_MS = 150; + /** + * Animation duration from medium to large dialog, including biometric fade out, back panel, etc + */ + int ANIMATE_MEDIUM_TO_LARGE_DURATION_MS = 450; + /** + * Delay before notifying {@link AuthCredentialView} to start animating in. + */ + int ANIMATE_CREDENTIAL_START_DELAY_MS = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS * 2 / 3; + /** + * Animation duration when sliding in credential UI */ - int ANIMATE_DURATION_MS = 150; + int ANIMATE_CREDENTIAL_INITIAL_DURATION_MS = 150; /** * Show the dialog. @@ -101,4 +124,14 @@ public interface AuthDialog { * Get the client's package name */ String getOpPackageName(); + + /** + * Animate to credential UI. Typically called after biometric is locked out. + */ + void animateToCredentialUI(); + + /** + * @return true if device credential is allowed. + */ + boolean isAllowDeviceCredentials(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index 70752f5f860e..12bb1228a53b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -27,17 +27,18 @@ public interface AuthDialogCallback { int DISMISSED_USER_CANCELED = 1; int DISMISSED_BUTTON_NEGATIVE = 2; int DISMISSED_BUTTON_POSITIVE = 3; - - int DISMISSED_AUTHENTICATED = 4; + int DISMISSED_BIOMETRIC_AUTHENTICATED = 4; int DISMISSED_ERROR = 5; int DISMISSED_BY_SYSTEM_SERVER = 6; + int DISMISSED_CREDENTIAL_AUTHENTICATED = 7; @IntDef({DISMISSED_USER_CANCELED, DISMISSED_BUTTON_NEGATIVE, DISMISSED_BUTTON_POSITIVE, - DISMISSED_AUTHENTICATED, + DISMISSED_BIOMETRIC_AUTHENTICATED, DISMISSED_ERROR, - DISMISSED_BY_SYSTEM_SERVER}) + DISMISSED_BY_SYSTEM_SERVER, + DISMISSED_CREDENTIAL_AUTHENTICATED}) @interface DismissedReason {} /** @@ -50,4 +51,9 @@ public interface AuthDialogCallback { * Invoked when the "try again" button is clicked */ void onTryAgainPressed(); + + /** + * Invoked when the "use password" button is clicked + */ + void onDeviceCredentialPressed(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java index 55ba0491dc1e..2b8b586961ff 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java @@ -16,15 +16,17 @@ package com.android.systemui.biometrics; +import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Color; import android.graphics.Outline; import android.util.Log; import android.view.View; import android.view.ViewOutlineProvider; +import android.view.animation.AccelerateDecelerateInterpolator; import com.android.systemui.R; -import com.android.systemui.biometrics.AuthDialog; /** * Controls the back panel and its animations for the BiometricPrompt UI. @@ -36,8 +38,9 @@ public class AuthPanelController extends ViewOutlineProvider { private final Context mContext; private final View mPanelView; - private final float mCornerRadius; - private final int mBiometricMargin; + private final boolean mIsManagedProfile; + + private boolean mUseFullScreen; private int mContainerWidth; private int mContainerHeight; @@ -45,14 +48,23 @@ public class AuthPanelController extends ViewOutlineProvider { private int mContentWidth; private int mContentHeight; + private float mCornerRadius; + private int mMargin; + @Override public void getOutline(View view, Outline outline) { final int left = (mContainerWidth - mContentWidth) / 2; final int right = mContainerWidth - left; + + // If the content fits within the container, shrink the height to wrap the content. + // Otherwise, set the outline to be the display size minus the margin - the content within + // is scrollable. final int top = mContentHeight < mContainerHeight - ? mContainerHeight - mContentHeight - mBiometricMargin - : mBiometricMargin; - final int bottom = mContainerHeight - mBiometricMargin; + ? mContainerHeight - mContentHeight - mMargin + : mMargin; + + // TODO(b/139954942) Likely don't need to "+1" after we resolve the navbar styling. + final int bottom = mContainerHeight - mMargin + 1; outline.setRoundRect(left, top, right, bottom, mCornerRadius); } @@ -64,11 +76,34 @@ public class AuthPanelController extends ViewOutlineProvider { mContainerHeight = containerHeight; } - public void updateForContentDimensions(int contentWidth, int contentHeight, boolean animate) { + public void setUseFullScreen(boolean fullScreen) { + mUseFullScreen = fullScreen; + } + + public ValueAnimator getTranslationAnimator(float relativeTranslationY) { + final ValueAnimator animator = ValueAnimator.ofFloat( + mPanelView.getY(), mPanelView.getY() - relativeTranslationY); + animator.addUpdateListener(animation -> { + final float translation = (float) animation.getAnimatedValue(); + mPanelView.setTranslationY(translation); + }); + return animator; + } + + public ValueAnimator getAlphaAnimator(float alpha) { + final ValueAnimator animator = ValueAnimator.ofFloat(mPanelView.getAlpha(), alpha); + animator.addUpdateListener(animation -> { + mPanelView.setAlpha((float) animation.getAnimatedValue()); + }); + return animator; + } + + public void updateForContentDimensions(int contentWidth, int contentHeight, + int animateDurationMs) { if (DEBUG) { Log.v(TAG, "Content Width: " + contentWidth + " Height: " + contentHeight - + " Animate: " + animate); + + " Animate: " + animateDurationMs); } if (mContainerWidth == 0 || mContainerHeight == 0) { @@ -76,27 +111,86 @@ public class AuthPanelController extends ViewOutlineProvider { return; } - if (animate) { + final int margin = mUseFullScreen ? 0 : (int) mContext.getResources() + .getDimension(R.dimen.biometric_dialog_border_padding); + final float cornerRadius = mUseFullScreen ? 0 : mContext.getResources() + .getDimension(R.dimen.biometric_dialog_corner_size); + + // When going to full-screen for managed profiles, fade away so the managed profile + // background behind this view becomes visible. + final boolean shouldFadeAway = mUseFullScreen && mIsManagedProfile; + final int alpha = shouldFadeAway ? 0 : 255; + final float elevation = shouldFadeAway ? 0 : + mContext.getResources().getDimension(R.dimen.biometric_dialog_elevation); + + if (animateDurationMs > 0) { + // Animate margin + ValueAnimator marginAnimator = ValueAnimator.ofInt(mMargin, margin); + marginAnimator.addUpdateListener((animation) -> { + mMargin = (int) animation.getAnimatedValue(); + }); + + // Animate corners + ValueAnimator cornerAnimator = ValueAnimator.ofFloat(mCornerRadius, cornerRadius); + cornerAnimator.addUpdateListener((animation) -> { + mCornerRadius = (float) animation.getAnimatedValue(); + }); + + // Animate height ValueAnimator heightAnimator = ValueAnimator.ofInt(mContentHeight, contentHeight); - heightAnimator.setDuration(AuthDialog.ANIMATE_DURATION_MS); heightAnimator.addUpdateListener((animation) -> { mContentHeight = (int) animation.getAnimatedValue(); mPanelView.invalidateOutline(); }); heightAnimator.start(); + + // Animate width + ValueAnimator widthAnimator = ValueAnimator.ofInt(mContentWidth, contentWidth); + widthAnimator.addUpdateListener((animation) -> { + mContentWidth = (int) animation.getAnimatedValue(); + }); + + // Animate background + ValueAnimator alphaAnimator = ValueAnimator.ofInt( + mPanelView.getBackground().getAlpha(), alpha); + alphaAnimator.addUpdateListener((animation) -> { + if (shouldFadeAway) { + mPanelView.getBackground().setAlpha((int) animation.getAnimatedValue()); + } + }); + + // Play together + AnimatorSet as = new AnimatorSet(); + as.setDuration(animateDurationMs); + as.setInterpolator(new AccelerateDecelerateInterpolator()); + as.playTogether(cornerAnimator, widthAnimator, marginAnimator, alphaAnimator); + as.start(); + } else { + mMargin = margin; + mCornerRadius = cornerRadius; mContentWidth = contentWidth; mContentHeight = contentHeight; + mPanelView.getBackground().setAlpha(alpha); mPanelView.invalidateOutline(); } } - AuthPanelController(Context context, View panelView) { + int getContainerWidth() { + return mContainerWidth; + } + + int getContainerHeight() { + return mContainerHeight; + } + + AuthPanelController(Context context, View panelView, boolean isManagedProfile) { mContext = context; mPanelView = panelView; + mIsManagedProfile = isManagedProfile; mCornerRadius = context.getResources() .getDimension(R.dimen.biometric_dialog_corner_size); - mBiometricMargin = (int) context.getResources() + mMargin = (int) context.getResources() .getDimension(R.dimen.biometric_dialog_border_padding); mPanelView.setOutlineProvider(this); mPanelView.setClipToOutline(true); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java index e00cf6abafaa..d6f830dd2e7a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java @@ -18,14 +18,36 @@ package com.android.systemui.biometrics; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; +import android.annotation.IntDef; +import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.hardware.biometrics.Authenticator; +import android.hardware.biometrics.BiometricPrompt; +import android.os.Bundle; +import android.os.UserManager; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import com.android.internal.widget.LockPatternUtils; +import com.android.systemui.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + public class Utils { + + public static final int CREDENTIAL_PIN = 1; + public static final int CREDENTIAL_PATTERN = 2; + public static final int CREDENTIAL_PASSWORD = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD}) + @interface CredentialType {} + + static float dpToPixels(Context context, float dp) { return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); @@ -46,4 +68,41 @@ 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); + } + + static @CredentialType int getCredentialType(Context context, int userId) { + final LockPatternUtils lpu = new LockPatternUtils(context); + switch (lpu.getKeyguardStoredPasswordQuality(userId)) { + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: + return CREDENTIAL_PATTERN; + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: + return CREDENTIAL_PIN; + case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: + case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: + case DevicePolicyManager.PASSWORD_QUALITY_MANAGED: + return CREDENTIAL_PASSWORD; + default: + return CREDENTIAL_PASSWORD; + } + } + + static boolean isManagedProfile(Context context, int userId) { + final UserManager userManager = context.getSystemService(UserManager.class); + return userManager.isManagedProfile(userId); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 134d4b87a159..36e04fe42ced 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(String error) { } - default void hideBiometricDialog() { } + default void onBiometricError(int errorCode, String error) { } + 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; @@ -773,14 +774,14 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } @Override - public void onBiometricError(String error) { + public void onBiometricError(int errorCode, String error) { synchronized (mLock) { - mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget(); + mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, errorCode, 0, error).sendToTarget(); } } @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 */); @@ -1060,12 +1061,12 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< break; case MSG_BIOMETRIC_ERROR: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).onBiometricError((String) msg.obj); + mCallbacks.get(i).onBiometricError(msg.arg1, (String) msg.obj); } 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 7a09137b1ff8..2c85424bac79 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; @@ -70,7 +71,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() { - initDialog(mContext, mCallback, new MockInjector()); + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); // The onAuthenticated runnable is posted when authentication succeeds. mBiometricView.onAuthenticationSucceeded(); @@ -81,7 +82,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() { - initDialog(mContext, mCallback, new MockInjector()); + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); mBiometricView.setRequireConfirmation(true); mBiometricView.onAuthenticationSucceeded(); @@ -97,7 +98,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testPositiveButton_sendsActionAuthenticated() { Button button = new Button(mContext); - initDialog(mContext, mCallback, new MockInjector() { + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { @Override public Button getPositiveButton() { return button; @@ -114,7 +115,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() { Button button = new Button(mContext); - initDialog(mContext, mCallback, new MockInjector() { + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { @Override public Button getNegativeButton() { return button; @@ -131,7 +132,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testNegativeButton_whenPendingConfirmation_sendsActionUserCanceled() { Button button = new Button(mContext); - initDialog(mContext, mCallback, new MockInjector() { + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { @Override public Button getNegativeButton() { return button; @@ -149,7 +150,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testTryAgainButton_sendsActionTryAgain() { Button button = new Button(mContext); - initDialog(mContext, mCallback, new MockInjector() { + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { @Override public Button getTryAgainButton() { return button; @@ -165,7 +166,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testError_sendsActionError() { - initDialog(mContext, mCallback, new MockInjector()); + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); final String testError = "testError"; mBiometricView.onError(testError); waitForIdleSync(); @@ -176,7 +177,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testBackgroundClicked_sendsActionUserCanceled() { - initDialog(mContext, mCallback, new MockInjector()); + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); View view = new View(mContext); mBiometricView.setBackgroundView(view); @@ -186,7 +187,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() { - initDialog(mContext, mCallback, new MockInjector()); + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); View view = new View(mContext); mBiometricView.setBackgroundView(view); @@ -197,8 +198,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() { - initDialog(mContext, mCallback, new MockInjector()); - mBiometricView.setPanelController(mPanelController); + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); mBiometricView.updateSize(AuthDialog.SIZE_SMALL); View view = new View(mContext); @@ -213,7 +213,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { Button tryAgainButton = new Button(mContext); TextView indicatorView = new TextView(mContext); - initDialog(mContext, mCallback, new MockInjector() { + initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { @Override public Button getTryAgainButton() { return tryAgainButton; @@ -249,16 +249,18 @@ public class AuthBiometricViewTest extends SysuiTestCase { // Create new dialog and restore the previous state into it Button tryAgainButton2 = new Button(mContext); TextView indicatorView2 = new TextView(mContext); - initDialog(mContext, mCallback, state, new MockInjector() { - @Override - public Button getTryAgainButton() { - return tryAgainButton2; - } - @Override - public TextView getIndicatorView() { - return indicatorView2; - } - }); + initDialog(mContext, false /* allowDeviceCredential */, mCallback, state, + new MockInjector() { + @Override + public Button getTryAgainButton() { + return tryAgainButton2; + } + + @Override + public TextView getIndicatorView() { + return indicatorView2; + } + }); mBiometricView.setRequireConfirmation(requireConfirmation); waitForIdleSync(); @@ -271,26 +273,51 @@ public class AuthBiometricViewTest extends SysuiTestCase { // dialog size is known. } - private Bundle buildBiometricPromptBundle() { + @Test + public void testNegativeButton_whenDeviceCredentialAllowed() throws InterruptedException { + Button negativeButton = new Button(mContext); + initDialog(mContext, true /* allowDeviceCredential */, mCallback, new MockInjector() { + @Override + public Button getNegativeButton() { + return negativeButton; + } + }); + + negativeButton.performClick(); + waitForIdleSync(); + + verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL); + } + + private Bundle buildBiometricPromptBundle(boolean allowDeviceCredential) { Bundle bundle = new Bundle(); bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title"); - bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative"); + int authenticators = Authenticator.TYPE_BIOMETRIC; + if (allowDeviceCredential) { + authenticators |= Authenticator.TYPE_CREDENTIAL; + } else { + bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative"); + } + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators); return bundle; } - private void initDialog(Context context, AuthBiometricView.Callback callback, + private void initDialog(Context context, boolean allowDeviceCredential, + AuthBiometricView.Callback callback, Bundle savedState, MockInjector injector) { mBiometricView = new TestableBiometricView(context, null, injector); - mBiometricView.setBiometricPromptBundle(buildBiometricPromptBundle()); + mBiometricView.setBiometricPromptBundle(buildBiometricPromptBundle(allowDeviceCredential)); mBiometricView.setCallback(callback); mBiometricView.restoreState(savedState); mBiometricView.onFinishInflateInternal(); mBiometricView.onAttachedToWindowInternal(); + + mBiometricView.setPanelController(mPanelController); } - private void initDialog(Context context, AuthBiometricView.Callback callback, - MockInjector injector) { - initDialog(context, callback, null /* savedState */, injector); + private void initDialog(Context context, boolean allowDeviceCredential, + AuthBiometricView.Callback callback, MockInjector injector) { + initDialog(context, allowDeviceCredential, callback, null /* savedState */, injector); } private class MockInjector extends AuthBiometricView.Injector { @@ -338,6 +365,16 @@ public class AuthBiometricViewTest extends SysuiTestCase { public int getDelayAfterError() { return 0; // Keep this at 0 for tests to invoke callback immediately. } + + @Override + public int getMediumToLargeAnimationDurationMs() { + return 0; + } + + @Override + public int getAnimateCredentialStartDelayMs() { + return 0; + } } private class TestableBiometricView extends AuthBiometricView { 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 d4fc3f842e9d..990f74ae33c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java @@ -16,12 +16,30 @@ 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; @@ -43,22 +61,21 @@ public class AuthContainerViewTest extends SysuiTestCase { @Before public void setup() { MockitoAnnotations.initMocks(this); - - AuthContainerView.Config config = new AuthContainerView.Config(); - config.mContext = mContext; - config.mCallback = mCallback; - 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_AUTHENTICATED)); + verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED)); } @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)); @@ -66,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)); @@ -73,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(); @@ -80,14 +101,77 @@ 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); } + @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(); + 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 @@ -95,4 +179,32 @@ 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, + boolean isManagedProfile) { + 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 eb7be4fa6332..dcdb5c3f8e9e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -22,8 +22,11 @@ import static junit.framework.TestCase.assertNotNull; 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; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,6 +36,8 @@ 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; import android.os.Bundle; @@ -69,7 +74,7 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private AuthDialog mDialog2; - private TestableBiometricDialogImpl mBiometricDialogImpl; + private TestableAuthController mAuthController; @Before @@ -90,78 +95,90 @@ 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); - verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED); + 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_AUTHENTICATED); - verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + 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 testSendsReasonDismissedBySystemServer_whenDismissedByServer() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER); + public void testSendsReasonServerRequested_whenDismissedByServer() throws Exception { + 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(Authenticator.TYPE_BIOMETRIC, BiometricPrompt.TYPE_FACE); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); + verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED); + } + // Statusbar tests @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<String> captor = ArgumentCaptor.forClass(String.class); verify(mDialog1).onAuthenticationFailed(captor.capture()); @@ -170,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<String> captor = ArgumentCaptor.forClass(String.class); verify(mDialog1).onHelp(captor.capture()); @@ -182,10 +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(errMessage); + mAuthController.onBiometricError(error, errMessage); ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); verify(mDialog1).onError(captor.capture()); @@ -194,30 +212,82 @@ public class AuthControllerTest extends SysuiTestCase { } @Test - public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception { - showDialog(BiometricPrompt.TYPE_FACE); - mBiometricDialogImpl.hideBiometricDialog(); + 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); + + mAuthController.onBiometricError(error, errorString); + verify(mDialog1, never()).onError(anyString()); + verify(mDialog1).animateToCredentialUI(); + } + + @Test + 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); + + mAuthController.onBiometricError(error, errorString); + verify(mDialog1, never()).onError(anyString()); + verify(mDialog1).animateToCredentialUI(); + } + + @Test + 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); + + mAuthController.onBiometricError(error, errorString); + verify(mDialog1).onError(eq(errorString)); + verify(mDialog1, never()).animateToCredentialUI(); + } + + @Test + 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); + + mAuthController.onBiometricError(error, errorString); + verify(mDialog1).onError(eq(errorString)); + verify(mDialog1, never()).animateToCredentialUI(); + } + + @Test + 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 */); @@ -227,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<Bundle> captor = ArgumentCaptor.forClass(Bundle.class); verify(mDialog1).onSaveState(captor.capture()); @@ -248,37 +327,63 @@ public class AuthControllerTest extends SysuiTestCase { } @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<Bundle> 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<ActivityManager.RunningTaskInfo> 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"); @@ -290,13 +395,16 @@ 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); } @@ -304,6 +412,9 @@ public class AuthControllerTest extends SysuiTestCase { protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, int userId, int type, String opPackageName, 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 b252a0d95f94..1bd01e166ddb 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)); } @@ -394,16 +395,17 @@ public class CommandQueueTest extends SysuiTestCase { @Test public void testOnBiometricError() { + final int errorCode = 1; String errorMessage = "test_error_message"; - mCommandQueue.onBiometricError(errorMessage); + mCommandQueue.onBiometricError(errorCode, errorMessage); waitForIdleSync(); - verify(mCallbacks).onBiometricError(eq(errorMessage)); + verify(mCallbacks).onBiometricError(eq(errorCode), eq(errorMessage)); } @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 3d341ef67d6f..4f1db3c96faf 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -26,19 +26,17 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; import android.app.ActivityManager; import android.app.IActivityManager; -import android.app.KeyguardManager; import android.app.UserSwitchObserver; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; 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; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.BiometricsProtoEnums; -import android.hardware.biometrics.IBiometricConfirmDeviceCredentialCallback; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceReceiver; @@ -95,11 +93,8 @@ public class BiometricService extends SystemService { private static final int MSG_ON_READY_FOR_AUTHENTICATION = 8; private static final int MSG_AUTHENTICATE = 9; private static final int MSG_CANCEL_AUTHENTICATION = 10; - 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 MSG_ON_AUTHENTICATION_TIMED_OUT = 11; + private static final int MSG_ON_DEVICE_CREDENTIAL_PRESSED = 12; private static final int[] FEATURE_ID = { TYPE_FINGERPRINT, TYPE_IRIS, @@ -132,19 +127,19 @@ public class BiometricService extends SystemService { */ static final int STATE_AUTH_PENDING_CONFIRM = 5; /** - * Biometric authentication was canceled, but the device is now showing ConfirmDeviceCredential - */ - 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; + static final int STATE_AUTHENTICATED_PENDING_SYSUI = 6; /** * Biometric error, waiting for SysUI to finish animation */ - static final int STATE_ERROR_PENDING_SYSUI = 8; + static final int STATE_ERROR_PENDING_SYSUI = 7; + /** + * Device credential in AuthController is showing + */ + static final int STATE_SHOWING_DEVICE_CREDENTIAL = 8; - final class AuthSession implements IBinder.DeathRecipient { + final class AuthSession { // Map of Authenticator/Cookie pairs. We expect to receive the cookies back from // <Biometric>Services before we can start authenticating. Pairs that have been returned // are moved to mModalitiesMatched. @@ -184,14 +179,10 @@ public class BiometricService extends SystemService { // Timestamp when hardware authentication occurred private long mAuthenticatedTimeMs; - // TODO(b/123378871): Remove when moved. - private IBiometricConfirmDeviceCredentialCallback mConfirmDeviceCredentialCallback; - AuthSession(HashMap<Integer, Integer> modalities, IBinder token, long sessionId, int userId, IBiometricServiceReceiver receiver, String opPackageName, Bundle bundle, int callingUid, int callingPid, int callingUserId, - int modality, boolean requireConfirmation, - IBiometricConfirmDeviceCredentialCallback callback) { + int modality, boolean requireConfirmation) { mModalitiesWaiting = modalities; mToken = token; mSessionId = sessionId; @@ -204,25 +195,12 @@ public class BiometricService extends SystemService { mCallingUserId = callingUserId; mModality = modality; mRequireConfirmation = requireConfirmation; - mConfirmDeviceCredentialCallback = callback; - - if (isFromConfirmDeviceCredential()) { - try { - token.linkToDeath(this, 0 /* flags */); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to link to death", e); - } - } } boolean isCrypto() { return mSessionId != 0; } - boolean isFromConfirmDeviceCredential() { - return mBundle.getBoolean(BiometricPrompt.KEY_FROM_CONFIRM_DEVICE_CREDENTIAL, false); - } - boolean containsCookie(int cookie) { if (mModalitiesWaiting != null && mModalitiesWaiting.containsValue(cookie)) { return true; @@ -233,23 +211,8 @@ public class BiometricService extends SystemService { return false; } - // TODO(b/123378871): Remove when moved. - @Override - public void binderDied() { - mHandler.post(() -> { - Slog.e(TAG, "Binder died, killing ConfirmDeviceCredential"); - if (mConfirmDeviceCredentialCallback == null) { - Slog.e(TAG, "Callback is null"); - return; - } - - try { - mConfirmDeviceCredentialCallback.cancel(); - mConfirmDeviceCredentialCallback = null; - } catch (RemoteException e) { - Slog.e(TAG, "Unable to send cancel", e); - } - }); + boolean isAllowDeviceCredential() { + return Utils.isDeviceCredentialAllowed(mBundle); } } @@ -275,7 +238,7 @@ public class BiometricService extends SystemService { // Get and cache the available authenticator (manager) classes. Used since aidl doesn't support // polymorphism :/ - final ArrayList<Authenticator> mAuthenticators = new ArrayList<>(); + final ArrayList<AuthenticatorWrapper> 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. @@ -284,14 +247,6 @@ public class BiometricService extends SystemService { @VisibleForTesting AuthSession mPendingAuthSession; - // TODO(b/123378871): Remove when moved. - // When BiometricPrompt#setAllowDeviceCredentials is set to true, we need to store the - // client (app) receiver. BiometricService internally launches CDCA which invokes - // BiometricService to start authentication (normal path). When auth is success/rejected, - // CDCA will use an aidl method to poke BiometricService - the result will then be forwarded - // to this receiver. - private IBiometricServiceReceiver mConfirmDeviceCredentialReceiver; - @VisibleForTesting final Handler mHandler = new Handler(Looper.getMainLooper()) { @Override @@ -361,8 +316,7 @@ public class BiometricService extends SystemService { (Bundle) args.arg5 /* bundle */, args.argi2 /* callingUid */, args.argi3 /* callingPid */, - args.argi4 /* callingUserId */, - (IBiometricConfirmDeviceCredentialCallback) args.arg6 /* callback */); + args.argi4 /* callingUserId */); args.recycle(); break; } @@ -376,28 +330,13 @@ public class BiometricService extends SystemService { break; } - case MSG_ON_CONFIRM_DEVICE_CREDENTIAL_SUCCESS: { - handleOnConfirmDeviceCredentialSuccess(); - break; - } - - case MSG_ON_CONFIRM_DEVICE_CREDENTIAL_ERROR: { - SomeArgs args = (SomeArgs) msg.obj; - handleOnConfirmDeviceCredentialError( - args.argi1 /* error */, - (String) args.arg1 /* errorMsg */); - args.recycle(); + case MSG_ON_AUTHENTICATION_TIMED_OUT: { + handleAuthenticationTimedOut((String) msg.obj /* errorMessage */); break; } - case MSG_REGISTER_CANCELLATION_CALLBACK: { - handleRegisterCancellationCallback( - (IBiometricConfirmDeviceCredentialCallback) msg.obj /* callback */); - break; - } - - case MSG_ON_AUTHENTICATION_TIMED_OUT: { - handleAuthenticationTimedOut((String) msg.obj /* errorMessage */); + case MSG_ON_DEVICE_CREDENTIAL_PRESSED: { + handleOnDeviceCredentialPressed(); break; } @@ -408,11 +347,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; } @@ -620,6 +559,11 @@ public class BiometricService extends SystemService { public void onTryAgainPressed() { mHandler.sendEmptyMessage(MSG_ON_TRY_AGAIN_PRESSED); } + + @Override + public void onDeviceCredentialPressed() { + mHandler.sendEmptyMessage(MSG_ON_DEVICE_CREDENTIAL_PRESSED); + } }; @@ -642,18 +586,12 @@ public class BiometricService extends SystemService { @Override // Binder call public void authenticate(IBinder token, long sessionId, int userId, - IBiometricServiceReceiver receiver, String opPackageName, Bundle bundle, - IBiometricConfirmDeviceCredentialCallback callback) + IBiometricServiceReceiver receiver, String opPackageName, Bundle bundle) throws RemoteException { final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); final int callingUserId = UserHandle.getCallingUserId(); - // TODO(b/123378871): Remove when moved. - if (callback != null) { - checkInternalPermission(); - } - // In the BiometricServiceBase, check do the AppOps and foreground check. if (userId == callingUserId) { // Check the USE_BIOMETRIC permission here. @@ -670,12 +608,12 @@ public class BiometricService extends SystemService { return; } - final boolean isFromConfirmDeviceCredential = - bundle.getBoolean(BiometricPrompt.KEY_FROM_CONFIRM_DEVICE_CREDENTIAL, false); - if (isFromConfirmDeviceCredential) { + if (bundle.get(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED) != null) { checkInternalPermission(); } + Utils.combineAuthenticatorBundles(bundle); + // Check the usage of this in system server. Need to remove this check if it becomes // a public API. final boolean useDefaultTitle = @@ -689,39 +627,6 @@ public class BiometricService extends SystemService { } } - // Launch CDC instead if necessary. CDC will return results through an AIDL call, since - // we can't get activity results. Store the receiver somewhere so we can forward the - // result back to the client. - // TODO(b/123378871): Remove when moved. - if (bundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL)) { - mHandler.post(() -> { - final KeyguardManager kgm = getContext().getSystemService( - KeyguardManager.class); - if (!kgm.isDeviceSecure()) { - try { - receiver.onError( - BiometricConstants.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL, - getContext().getString( - R.string.biometric_error_device_not_secured)); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception", e); - } - return; - } - mConfirmDeviceCredentialReceiver = receiver; - // Use this so we don't need to duplicate logic.. - final Intent intent = kgm.createConfirmDeviceCredentialIntent(null /* title */, - null /* description */, userId); - // Then give it the bundle to do magic behavior.. - intent.putExtra(KeyguardManager.EXTRA_BIOMETRIC_PROMPT_BUNDLE, bundle); - // Create a new task with this activity located at the root. - intent.setFlags( - Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - getContext().startActivityAsUser(intent, UserHandle.CURRENT); - }); - return; - } - SomeArgs args = SomeArgs.obtain(); args.arg1 = token; args.arg2 = sessionId; @@ -732,41 +637,11 @@ public class BiometricService extends SystemService { args.argi2 = callingUid; args.argi3 = callingPid; args.argi4 = callingUserId; - args.arg6 = callback; mHandler.obtainMessage(MSG_AUTHENTICATE, args).sendToTarget(); } @Override // Binder call - public void onConfirmDeviceCredentialSuccess() { - checkInternalPermission(); - - mHandler.sendEmptyMessage(MSG_ON_CONFIRM_DEVICE_CREDENTIAL_SUCCESS); - } - - @Override // Binder call - public void onConfirmDeviceCredentialError(int error, String message) { - checkInternalPermission(); - - SomeArgs args = SomeArgs.obtain(); - args.argi1 = error; - args.arg1 = message; - mHandler.obtainMessage(MSG_ON_CONFIRM_DEVICE_CREDENTIAL_ERROR, args).sendToTarget(); - } - - @Override // Binder call - public void registerCancellationCallback( - IBiometricConfirmDeviceCredentialCallback callback) { - // TODO(b/123378871): Remove when moved. - // This callback replaces the one stored in the current session. If the session is null - // we can ignore this, since it means ConfirmDeviceCredential was launched by something - // else (not BiometricPrompt) - checkInternalPermission(); - - mHandler.obtainMessage(MSG_REGISTER_CANCELLATION_CALLBACK, callback).sendToTarget(); - } - - @Override // Binder call public void cancelAuthentication(IBinder token, String opPackageName) throws RemoteException { checkPermission(); @@ -972,8 +847,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); } } @@ -1011,7 +886,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()) { @@ -1108,7 +983,7 @@ public class BiometricService extends SystemService { } private void logDialogDismissed(int reason) { - if (reason == BiometricPrompt.DISMISSED_REASON_CONFIRMED) { + if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) { // Explicit auth, authentication confirmed. // Latency in this case is authenticated -> confirmed. <Biometric>Service // should have the first half (first acquired -> authenticated). @@ -1254,49 +1129,6 @@ public class BiometricService extends SystemService { } } - private void handleOnConfirmDeviceCredentialSuccess() { - if (mConfirmDeviceCredentialReceiver == null) { - Slog.w(TAG, "handleOnConfirmDeviceCredentialSuccess null!"); - return; - } - try { - mConfirmDeviceCredentialReceiver.onAuthenticationSucceeded(); - if (mCurrentAuthSession != null) { - mCurrentAuthSession = null; - } - } catch (RemoteException e) { - Slog.e(TAG, "RemoteException", e); - } - mConfirmDeviceCredentialReceiver = null; - } - - private void handleOnConfirmDeviceCredentialError(int error, String message) { - if (mConfirmDeviceCredentialReceiver == null) { - Slog.w(TAG, "handleOnConfirmDeviceCredentialError null! Error: " - + error + " " + message); - return; - } - try { - mConfirmDeviceCredentialReceiver.onError(error, message); - if (mCurrentAuthSession != null) { - mCurrentAuthSession = null; - } - } catch (RemoteException e) { - Slog.e(TAG, "RemoteException", e); - } - mConfirmDeviceCredentialReceiver = null; - } - - private void handleRegisterCancellationCallback( - IBiometricConfirmDeviceCredentialCallback callback) { - if (mCurrentAuthSession == null) { - Slog.d(TAG, "Current auth session null"); - return; - } - Slog.d(TAG, "Updating cancel callback"); - mCurrentAuthSession.mConfirmDeviceCredentialCallback = callback; - } - private void handleOnError(int cookie, int error, String message) { Slog.d(TAG, "handleOnError: " + error + " cookie: " + cookie); // Errors can either be from the current auth session or the pending auth session. @@ -1307,34 +1139,34 @@ public class BiometricService extends SystemService { // of their intended receivers. 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 - if (DEBUG) Slog.d(TAG, "From CDC, transition to CANCELED_SHOWING_CDC state"); - - // Send the error to ConfirmDeviceCredential so that it goes to Pin/Pattern/Pass - // screen - mCurrentAuthSession.mClientReceiver.onError(error, message); - mCurrentAuthSession.mState = STATE_BIOMETRIC_AUTH_CANCELED_SHOWING_CDC; - mStatusBarService.hideBiometricDialog(); - } else if (mCurrentAuthSession.mState == STATE_AUTH_STARTED) { - mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI; - if (error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) { - mStatusBarService.hideBiometricDialog(); + if (mCurrentAuthSession.mState == STATE_AUTH_STARTED) { + final boolean errorLockout = error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT + || error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT; + if (mCurrentAuthSession.isAllowDeviceCredential() && errorLockout) { + // SystemUI handles transition from biometric to device credential. + mCurrentAuthSession.mState = STATE_SHOWING_DEVICE_CREDENTIAL; + mStatusBarService.onBiometricError(error, message); } else { - mStatusBarService.onBiometricError(message); + mCurrentAuthSession.mState = STATE_ERROR_PENDING_SYSUI; + if (error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) { + mStatusBarService.hideAuthenticationDialog(); + } else { + mStatusBarService.onBiometricError(error, message); + } } } else if (mCurrentAuthSession.mState == STATE_AUTH_PAUSED) { // In the "try again" state, we should forward canceled errors to // the client and and clean up. The 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: " + + mCurrentAuthSession.mState); } else { Slog.e(TAG, "Impossible session error state: " + mCurrentAuthSession.mState); @@ -1342,12 +1174,38 @@ 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); } + } else { + Slog.e(TAG, "Unknown cookie: " + cookie); } } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); @@ -1385,9 +1243,12 @@ public class BiometricService extends SystemService { try { switch (reason) { - case BiometricPrompt.DISMISSED_REASON_CONFIRMED: - case BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED: - mKeyStore.addAuthToken(mCurrentAuthSession.mTokenEscrow); + case BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED: + case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED: + case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED: + if (mCurrentAuthSession.mTokenEscrow != null) { + mKeyStore.addAuthToken(mCurrentAuthSession.mTokenEscrow); + } mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded(); break; @@ -1439,12 +1300,37 @@ public class BiometricService extends SystemService { mCurrentAuthSession.mCallingUid, mCurrentAuthSession.mCallingPid, mCurrentAuthSession.mCallingUserId, - mCurrentAuthSession.mModality, - mCurrentAuthSession.mConfirmDeviceCredentialCallback); + mCurrentAuthSession.mModality); } + private void handleOnDeviceCredentialPressed() { + Slog.d(TAG, "onDeviceCredentialPressed"); + if (mCurrentAuthSession == null) { + Slog.e(TAG, "Auth session null"); + return; + } + + // 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 */); + + 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<Integer, Integer> pair = (Map.Entry) it.next(); @@ -1486,7 +1372,7 @@ public class BiometricService extends SystemService { } if (!continuing) { - mStatusBarService.showBiometricDialog(mCurrentAuthSession.mBundle, + mStatusBarService.showAuthenticationDialog(mCurrentAuthSession.mBundle, mInternalReceiver, modality, requireConfirmation, userId, mCurrentAuthSession.mOpPackageName); } @@ -1498,16 +1384,21 @@ public class BiometricService extends SystemService { private void handleAuthenticate(IBinder token, long sessionId, int userId, IBiometricServiceReceiver receiver, String opPackageName, Bundle bundle, - int callingUid, int callingPid, int callingUserId, - IBiometricConfirmDeviceCredentialCallback callback) { + int callingUid, int callingPid, int callingUserId) { mHandler.post(() -> { final Pair<Integer, Integer> result = checkAndGetBiometricModality(userId); final int modality = result.first; final int error = result.second; - // Check for errors, notify callback, and return - if (error != BiometricConstants.BIOMETRIC_SUCCESS) { + final boolean credentialAllowed = Utils.isDeviceCredentialAllowed(bundle); + + if (error != BiometricConstants.BIOMETRIC_SUCCESS && credentialAllowed) { + // If there's a problem but device credential is allowed, only show credential UI. + bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, + Authenticator.TYPE_CREDENTIAL); + } else if (error != BiometricConstants.BIOMETRIC_SUCCESS) { + // Check for errors, notify callback, and return try { final String hardwareUnavailable = getContext().getString(R.string.biometric_error_hw_unavailable); @@ -1535,7 +1426,7 @@ public class BiometricService extends SystemService { // Start preparing for authentication. Authentication starts when // all modalities requested have invoked onReadyForAuthentication. authenticateInternal(token, sessionId, userId, receiver, opPackageName, bundle, - callingUid, callingPid, callingUserId, modality, callback); + callingUid, callingPid, callingUserId, modality); }); } @@ -1550,8 +1441,7 @@ public class BiometricService extends SystemService { */ private void authenticateInternal(IBinder token, long sessionId, int userId, IBiometricServiceReceiver receiver, String opPackageName, Bundle bundle, - int callingUid, int callingPid, int callingUserId, int modality, - IBiometricConfirmDeviceCredentialCallback callback) { + int callingUid, int callingPid, int callingUserId, int modality) { try { boolean requireConfirmation = bundle.getBoolean( BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true /* default */); @@ -1565,27 +1455,49 @@ public class BiometricService extends SystemService { // with the cookie. Once all cookies are received, we can show the prompt // and let the services start authenticating. The cookie should be non-zero. final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1; + final int authenticators = bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED); Slog.d(TAG, "Creating auth session. Modality: " + modality - + ", cookie: " + cookie); - final HashMap<Integer, Integer> authenticators = new HashMap<>(); - authenticators.put(modality, cookie); - mPendingAuthSession = new AuthSession(authenticators, token, sessionId, userId, - receiver, opPackageName, bundle, callingUid, callingPid, callingUserId, - modality, requireConfirmation, callback); - mPendingAuthSession.mState = STATE_AUTH_CALLED; - // No polymorphism :( - if ((modality & TYPE_FINGERPRINT) != 0) { - mFingerprintService.prepareForAuthentication(token, sessionId, userId, - mInternalReceiver, opPackageName, cookie, - callingUid, callingPid, callingUserId); - } - if ((modality & TYPE_IRIS) != 0) { - Slog.w(TAG, "Iris unsupported"); + + ", cookie: " + cookie + + ", authenticators: " + authenticators); + final HashMap<Integer, Integer> modalities = new HashMap<>(); + + // If it's only device credential, we don't need to wait - LockSettingsService is + // always ready to check credential (SystemUI invokes that path). + if ((authenticators & ~Authenticator.TYPE_CREDENTIAL) != 0) { + modalities.put(modality, cookie); } - if ((modality & TYPE_FACE) != 0) { - mFaceService.prepareForAuthentication(requireConfirmation, - token, sessionId, userId, mInternalReceiver, opPackageName, - cookie, callingUid, callingPid, callingUserId); + mPendingAuthSession = new AuthSession(modalities, token, sessionId, userId, + receiver, opPackageName, bundle, callingUid, callingPid, callingUserId, + modality, requireConfirmation); + + if (authenticators == Authenticator.TYPE_CREDENTIAL) { + mPendingAuthSession.mState = STATE_SHOWING_DEVICE_CREDENTIAL; + mCurrentAuthSession = mPendingAuthSession; + mPendingAuthSession = null; + + mStatusBarService.showAuthenticationDialog( + mCurrentAuthSession.mBundle, + mInternalReceiver, + 0 /* biometricModality */, + false /* requireConfirmation */, + mCurrentAuthSession.mUserId, + mCurrentAuthSession.mOpPackageName); + } else { + mPendingAuthSession.mState = STATE_AUTH_CALLED; + // No polymorphism :( + if ((modality & TYPE_FINGERPRINT) != 0) { + mFingerprintService.prepareForAuthentication(token, sessionId, userId, + mInternalReceiver, opPackageName, cookie, + callingUid, callingPid, callingUserId); + } + if ((modality & TYPE_IRIS) != 0) { + Slog.w(TAG, "Iris unsupported"); + } + if ((modality & TYPE_FACE) != 0) { + mFaceService.prepareForAuthentication(requireConfirmation, + token, sessionId, userId, mInternalReceiver, opPackageName, + cookie, callingUid, callingPid, callingUserId); + } } } catch (RemoteException e) { Slog.e(TAG, "Unable to start authentication", e); @@ -1598,20 +1510,7 @@ public class BiometricService extends SystemService { return; } - if (mCurrentAuthSession != null - && mCurrentAuthSession.mState == STATE_BIOMETRIC_AUTH_CANCELED_SHOWING_CDC) { - if (DEBUG) Slog.d(TAG, "Cancel received while ConfirmDeviceCredential showing"); - try { - mCurrentAuthSession.mConfirmDeviceCredentialCallback.cancel(); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to cancel ConfirmDeviceCredential", e); - } - - // TODO(b/123378871): Remove when moved. Piggy back on this for now to clean up. - handleOnConfirmDeviceCredentialError(BiometricConstants.BIOMETRIC_ERROR_CANCELED, - getContext().getString(R.string.biometric_error_canceled)); - } else if (mCurrentAuthSession != null - && mCurrentAuthSession.mState != STATE_AUTH_STARTED) { + if (mCurrentAuthSession != null && mCurrentAuthSession.mState != STATE_AUTH_STARTED) { // We need to check the current authenticators state. If we're pending confirm // or idle, we need to dismiss the dialog and send an ERROR_CANCELED to the client, // since we won't be getting an onError from the driver. @@ -1624,24 +1523,12 @@ public class BiometricService extends SystemService { ); mCurrentAuthSession = null; - mStatusBarService.hideBiometricDialog(); + mStatusBarService.hideAuthenticationDialog(); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } } else { - boolean fromCDC = false; - if (mCurrentAuthSession != null) { - fromCDC = mCurrentAuthSession.mBundle.getBoolean( - BiometricPrompt.KEY_FROM_CONFIRM_DEVICE_CREDENTIAL, false); - } - - if (fromCDC) { - if (DEBUG) Slog.d(TAG, "Cancelling from CDC"); - cancelInternal(token, opPackageName, false /* fromClient */); - } else { - cancelInternal(token, opPackageName, true /* fromClient */); - } - + cancelInternal(token, opPackageName, true /* fromClient */); } } diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index 4fa29ac541f9..ed5f9de01bcf 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -17,10 +17,15 @@ package com.android.server.biometrics; import android.content.Context; +import android.hardware.biometrics.Authenticator; +import android.hardware.biometrics.BiometricPrompt; import android.os.Build; +import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; +import com.android.internal.annotations.VisibleForTesting; + public class Utils { public static boolean isDebugEnabled(Context context, int targetUserId) { if (targetUserId == UserHandle.USER_NULL) { @@ -38,4 +43,43 @@ public class Utils { } return true; } + + /** + * Combine {@link BiometricPrompt#KEY_ALLOW_DEVICE_CREDENTIAL} with + * {@link BiometricPrompt#KEY_AUTHENTICATORS_ALLOWED}, as the former is not flexible + * enough. + */ + public static void combineAuthenticatorBundles(Bundle bundle) { + boolean biometricEnabled = true; // enabled by default + boolean credentialEnabled = bundle.getBoolean( + BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, false); + 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); + } + + /** + * @param bundle should be first processed by {@link #combineAuthenticatorBundles(Bundle)} + * @return true if device credential allowed. + */ + public static boolean isDeviceCredentialAllowed(Bundle bundle) { + final int authenticators = bundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED); + return (authenticators & Authenticator.TYPE_CREDENTIAL) != 0; + } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 8897eca85d7a..3439d3841973 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) { } } @@ -644,22 +644,22 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onBiometricError(String error) { + public void onBiometricError(int errorCode, String error) { enforceBiometricDialog(); if (mBar != null) { try { - mBar.onBiometricError(error); + mBar.onBiometricError(errorCode, error); } catch (RemoteException ex) { } } } @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 ccf3a908364a..4aeeb0af1bf8 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; @@ -55,6 +56,9 @@ import android.os.Handler; import android.os.IBinder; import android.security.KeyStore; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + import com.android.internal.R; import com.android.internal.statusbar.IStatusBarService; @@ -66,9 +70,6 @@ import org.mockito.MockitoAnnotations; import java.util.List; -import androidx.test.InstrumentationRegistry; -import androidx.test.filters.SmallTest; - @SmallTest public class BiometricServiceTest { @@ -82,6 +83,7 @@ public class BiometricServiceTest { 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 ERROR_LOCKOUT = "error_lockout"; private static final String FINGERPRINT_ACQUIRED_SENSOR_DIRTY = "sensor_dirty"; @@ -179,7 +181,8 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, new MockInjector()); mBiometricService.onStart(); - invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_HW_NOT_PRESENT), eq(ERROR_HW_UNAVAILABLE)); @@ -193,7 +196,8 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, new MockInjector()); mBiometricService.onStart(); - invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS), any()); @@ -208,7 +212,8 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, new MockInjector()); mBiometricService.onStart(); - invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE)); @@ -226,7 +231,8 @@ public class BiometricServiceTest { // Disabled in user settings receives onError when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(false); - invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); verify(mReceiver1).onError( eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(ERROR_HW_UNAVAILABLE)); @@ -236,7 +242,8 @@ public class BiometricServiceTest { when(mBiometricService.mSettingObserver.getFaceEnabledForApps(anyInt())).thenReturn(true); when(mBiometricService.mSettingObserver.getFaceAlwaysRequireConfirmation(anyInt())) .thenReturn(true); - invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); verify(mReceiver1, never()).onError(anyInt(), any(String.class)); verify(mBiometricService.mFaceService).prepareForAuthentication( @@ -255,7 +262,8 @@ public class BiometricServiceTest { resetReceiver(); when(mBiometricService.mSettingObserver.getFaceAlwaysRequireConfirmation(anyInt())) .thenReturn(false); - invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); verify(mBiometricService.mFaceService).prepareForAuthentication( eq(false) /* requireConfirmation */, @@ -277,7 +285,8 @@ public class BiometricServiceTest { mBiometricService.onStart(); // Start testing the happy path - invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); // Creates a pending auth session with the correct initial states @@ -311,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), @@ -333,7 +342,7 @@ public class BiometricServiceTest { // SystemUI sends callback with dismissed reason mBiometricService.mInternalReceiver.onDialogDismissed( - BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED); + BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED); waitForIdle(); // HAT sent to keystore verify(mBiometricService.mKeyStore).addAuthToken(any(byte[].class)); @@ -344,10 +353,32 @@ public class BiometricServiceTest { } @Test + public void testAuthenticate_noBiometrics_credentialAllowed() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, + true /* requireConfirmation */, true /* allowDeviceCredential */); + waitForIdle(); + + 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 testAuthenticate_happyPathWithConfirmation() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - true /* requireConfirmation */); + true /* requireConfirmation */, false /* allowDeviceCredential */); // Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not // sent to KeyStore yet @@ -362,7 +393,7 @@ public class BiometricServiceTest { // SystemUI sends confirm, HAT is sent to keystore and client is notified. mBiometricService.mInternalReceiver.onDialogDismissed( - BiometricPrompt.DISMISSED_REASON_CONFIRMED); + BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED); waitForIdle(); verify(mBiometricService.mKeyStore).addAuthToken(any(byte[].class)); verify(mReceiver1).onAuthenticationSucceeded(); @@ -373,7 +404,7 @@ public class BiometricServiceTest { throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onAuthenticationFailed(); waitForIdle(); @@ -390,7 +421,7 @@ public class BiometricServiceTest { throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onAuthenticationFailed(); waitForIdle(); @@ -406,13 +437,14 @@ public class BiometricServiceTest { public void testErrorCanceled_whenAuthenticating_notifiesSystemUIAndClient() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); // 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 */); + invokeAuthenticate(mBiometricService.mImpl, mReceiver2, false /* requireConfirmation */, + false /* allowDeviceCredential */); waitForIdle(); assertEquals(mBiometricService.mCurrentAuthSession.mState, @@ -430,7 +462,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 @@ -446,7 +478,7 @@ public class BiometricServiceTest { public void testErrorHalTimeout_whenAuthenticating_entersPausedState() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onError( getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), @@ -479,7 +511,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(), @@ -492,7 +524,7 @@ public class BiometricServiceTest { public void testErrorFromHal_whenPaused_notifiesSystemUIAndClient() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireCOnfirmation */); + false /* requireCOnfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onError( getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), @@ -509,7 +541,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); } @@ -522,7 +554,7 @@ public class BiometricServiceTest { // session is done. setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onError( getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), @@ -533,9 +565,10 @@ 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) - .onBiometricError(eq(ERROR_UNABLE_TO_PROCESS)); - verify(mBiometricService.mStatusBarService, never()).hideBiometricDialog(); + verify(mBiometricService.mStatusBarService).onBiometricError( + eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), + eq(ERROR_UNABLE_TO_PROCESS)); + verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog(); verify(mReceiver1, never()).onError(anyInt(), anyString()); // SystemUI animation completed, client is notified, auth session is over @@ -549,11 +582,177 @@ public class BiometricServiceTest { } @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); + Utils.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); + Utils.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); + Utils.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 { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, true /* allowDeviceCredential */); + + mBiometricService.mInternalReceiver.onDeviceCredentialPressed(); + waitForIdle(); + + assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL, + mBiometricService.mCurrentAuthSession.mState); + verify(mReceiver1, never()).onError(anyInt(), anyString()); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_CANCELED, + ERROR_CANCELED); + waitForIdle(); + + assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL, + mBiometricService.mCurrentAuthSession.mState); + verify(mReceiver1, never()).onError(anyInt(), anyString()); + } + + @Test + public void testLockout_whileAuthenticating_credentialAllowed() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, true /* allowDeviceCredential */); + + assertEquals(BiometricService.STATE_AUTH_STARTED, + mBiometricService.mCurrentAuthSession.mState); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, + ERROR_LOCKOUT); + waitForIdle(); + + assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL, + mBiometricService.mCurrentAuthSession.mState); + verify(mBiometricService.mStatusBarService).onBiometricError( + eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT), + eq(ERROR_LOCKOUT)); + } + + @Test + public void testLockout_whenAuthenticating_credentialNotAllowed() throws Exception { + setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, false /* allowDeviceCredential */); + + assertEquals(BiometricService.STATE_AUTH_STARTED, + mBiometricService.mCurrentAuthSession.mState); + + mBiometricService.mInternalReceiver.onError( + getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS, + ERROR_UNABLE_TO_PROCESS); + waitForIdle(); + + assertEquals(BiometricService.STATE_ERROR_PENDING_SYSUI, + mBiometricService.mCurrentAuthSession.mState); + verify(mBiometricService.mStatusBarService).onBiometricError( + eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), + eq(ERROR_UNABLE_TO_PROCESS)); + } + + @Test public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); @@ -575,7 +774,7 @@ public class BiometricServiceTest { public void testDismissedReasonNegative_whilePaused_doesntInvokeHalCancel() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onError( getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), @@ -598,7 +797,7 @@ public class BiometricServiceTest { public void testDismissedReasonUserCancel_whilePaused_doesntInvokeHalCancel() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onError( getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), @@ -621,7 +820,7 @@ public class BiometricServiceTest { public void testDismissedReasonUserCancel_whenPendingConfirmation() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - true /* requireConfirmation */); + true /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onAuthenticationSucceeded( true /* requireConfirmation */, @@ -648,7 +847,7 @@ public class BiometricServiceTest { public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, - false /* requireConfirmation */); + false /* requireConfirmation */, false /* allowDeviceCredential */); mBiometricService.mInternalReceiver.onAcquired( FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, @@ -698,9 +897,10 @@ public class BiometricServiceTest { } private void invokeAuthenticateAndStart(IBiometricService.Stub service, - IBiometricServiceReceiver receiver, boolean requireConfirmation) throws Exception { + IBiometricServiceReceiver receiver, boolean requireConfirmation, + boolean allowDeviceCredential) throws Exception { // Request auth, creates a pending session - invokeAuthenticate(service, receiver, requireConfirmation); + invokeAuthenticate(service, receiver, requireConfirmation, allowDeviceCredential); waitForIdle(); startPendingAuthSession(mBiometricService); @@ -720,20 +920,25 @@ public class BiometricServiceTest { } private static void invokeAuthenticate(IBiometricService.Stub service, - IBiometricServiceReceiver receiver, boolean requireConfirmation) throws Exception { + IBiometricServiceReceiver receiver, boolean requireConfirmation, + boolean allowDeviceCredential) throws Exception { service.authenticate( new Binder() /* token */, 0 /* sessionId */, 0 /* userId */, receiver, TEST_PACKAGE_NAME /* packageName */, - createTestBiometricPromptBundle(requireConfirmation), - null /* IBiometricConfirmDeviceCredentialCallback */); + createTestBiometricPromptBundle(requireConfirmation, allowDeviceCredential)); } - private static Bundle createTestBiometricPromptBundle(boolean requireConfirmation) { + private static Bundle createTestBiometricPromptBundle(boolean requireConfirmation, + boolean allowDeviceCredential) { final Bundle bundle = new Bundle(); bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, requireConfirmation); + + if (allowDeviceCredential) { + bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true); + } return bundle; } @@ -742,6 +947,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(); } |