Merge changes from topic "biometric-ui-credentials"
* changes: Animate panel to transparent if profile is managed 17/n: Show credential UI if setDeviceCredentialAllowed(true) and no biometrics 16/n: Add PIN/Password 15/n: Allow Auth UI to start in credential UI 14/n: Animate to device credential UI when lockout occurs 13/n: persist device credential across configuration changes 12/n: Add LockPatternView for setDeviceCredentialAllowed(true) 11/n: Animate panel to full-screen when "Use Password" is pressed Removing old confirm device credential logic
This commit is contained in:
@@ -86,12 +86,6 @@ public class KeyguardManager {
|
||||
public static final String ACTION_CONFIRM_FRP_CREDENTIAL =
|
||||
"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}.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
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;
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,10 +66,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
|
||||
* @hide
|
||||
*/
|
||||
public static final String KEY_DESCRIPTION = "description";
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public static final String KEY_POSITIVE_TEXT = "positive_text";
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@@ -79,14 +75,15 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
|
||||
*/
|
||||
public static final String KEY_REQUIRE_CONFIRMATION = "require_confirmation";
|
||||
/**
|
||||
* This is deprecated. Internally we should use {@link #KEY_AUTHENTICATORS_ALLOWED}
|
||||
* @hide
|
||||
*/
|
||||
public static final String KEY_ALLOW_DEVICE_CREDENTIAL = "allow_device_credential";
|
||||
/**
|
||||
* If this key is set, we will ignore {@link #KEY_ALLOW_DEVICE_CREDENTIAL}
|
||||
* @hide
|
||||
*/
|
||||
public static final String KEY_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;
|
||||
@@ -202,30 +204,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional: Set the text for the positive button. If not set, the positive button
|
||||
* will not show.
|
||||
* @param text
|
||||
* @return
|
||||
* @hide
|
||||
*/
|
||||
@NonNull public Builder setPositiveButton(@NonNull CharSequence text,
|
||||
@NonNull @CallbackExecutor Executor executor,
|
||||
@NonNull DialogInterface.OnClickListener listener) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
throw new IllegalArgumentException("Text must be set and non-empty");
|
||||
}
|
||||
if (executor == null) {
|
||||
throw new IllegalArgumentException("Executor must not be null");
|
||||
}
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("Listener must not be null");
|
||||
}
|
||||
mBundle.putCharSequence(KEY_POSITIVE_TEXT, text);
|
||||
mPositiveButtonInfo = new ButtonInfo(executor, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Required: Set the text for the negative button. This would typically be used as a
|
||||
* "Cancel" button, but may be also used to show an alternative method for authentication,
|
||||
@@ -297,17 +275,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
27
packages/SystemUI/res/drawable/auth_dialog_lock.xml
Normal file
27
packages/SystemUI/res/drawable/auth_dialog_lock.xml
Normal file
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
101
packages/SystemUI/res/layout/auth_credential_password_view.xml
Normal file
101
packages/SystemUI/res/layout/auth_credential_password_view.xml
Normal file
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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] -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE));
|
||||
setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
|
||||
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;
|
||||
}
|
||||
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */);
|
||||
mFrameLayout = mInjector.inflateContainerView(factory, this);
|
||||
|
||||
mPanelView = mContainerView.findViewById(R.id.panel);
|
||||
mPanelController = new AuthPanelController(mContext, mPanelView);
|
||||
final boolean isManagedProfile = Utils.isManagedProfile(mContext, mConfig.mUserId);
|
||||
|
||||
// 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;
|
||||
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) {
|
||||
@@ -247,6 +287,53 @@ public class AuthContainerView extends LinearLayout
|
||||
requestFocus();
|
||||
}
|
||||
|
||||
@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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -104,6 +106,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) {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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_DURATION_MS = 150;
|
||||
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_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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
@@ -247,38 +326,64 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
assertEquals(captor.getValue(), captor2.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigurationPersists_whenBiometricFallbackToCredential() {
|
||||
showDialog(Authenticator.TYPE_CREDENTIAL | Authenticator.TYPE_BIOMETRIC,
|
||||
BiometricPrompt.TYPE_FACE);
|
||||
verify(mDialog1).show(any(), any());
|
||||
|
||||
// Pretend that the UI is now showing device credential UI.
|
||||
doAnswer(invocation -> {
|
||||
Object[] args = invocation.getArguments();
|
||||
Bundle savedState = (Bundle) args[0];
|
||||
savedState.putInt(
|
||||
AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING);
|
||||
savedState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, true);
|
||||
return null; // onSaveState returns void
|
||||
}).when(mDialog1).onSaveState(any());
|
||||
|
||||
mAuthController.onConfigurationChanged(new Configuration());
|
||||
|
||||
// Check that the new dialog was initialized to the credential UI.
|
||||
ArgumentCaptor<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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -131,20 +126,20 @@ public class BiometricService extends SystemService {
|
||||
* Authentication is successful, but we're waiting for the user to press "confirm" button.
|
||||
*/
|
||||
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,31 +330,16 @@ 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();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_REGISTER_CANCELLATION_CALLBACK: {
|
||||
handleRegisterCancellationCallback(
|
||||
(IBiometricConfirmDeviceCredentialCallback) msg.obj /* callback */);
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_AUTHENTICATION_TIMED_OUT: {
|
||||
handleAuthenticationTimedOut((String) msg.obj /* errorMessage */);
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_DEVICE_CREDENTIAL_PRESSED: {
|
||||
handleOnDeviceCredentialPressed();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Slog.e(TAG, "Unknown message: " + msg);
|
||||
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,40 +637,10 @@ 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 {
|
||||
@@ -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,
|
||||
+ ", 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);
|
||||
}
|
||||
mPendingAuthSession = new AuthSession(modalities, 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");
|
||||
}
|
||||
if ((modality & TYPE_FACE) != 0) {
|
||||
mFaceService.prepareForAuthentication(requireConfirmation,
|
||||
token, sessionId, userId, mInternalReceiver, opPackageName,
|
||||
cookie, 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 */);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
@@ -343,11 +352,33 @@ public class BiometricServiceTest {
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -548,12 +581,178 @@ public class BiometricServiceTest {
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, true /* allowDeviceCredential */);
|
||||
waitForIdle();
|
||||
|
||||
mBiometricService.mInternalReceiver.onError(
|
||||
getCookieForPendingSession(mBiometricService.mPendingAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
ERROR_LOCKOUT);
|
||||
waitForIdle();
|
||||
|
||||
// Pending auth session becomes current auth session, since device credential should
|
||||
// be shown now.
|
||||
assertNull(mBiometricService.mPendingAuthSession);
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mState);
|
||||
assertEquals(Authenticator.TYPE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mBundle.getInt(
|
||||
BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED));
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mBundle),
|
||||
any(IBiometricServiceReceiverInternal.class),
|
||||
eq(0 /* biometricModality */),
|
||||
anyBoolean() /* requireConfirmation */,
|
||||
anyInt() /* userId */,
|
||||
eq(TEST_PACKAGE_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, false /* allowDeviceCredential */);
|
||||
waitForIdle();
|
||||
|
||||
mBiometricService.mInternalReceiver.onError(
|
||||
getCookieForPendingSession(mBiometricService.mPendingAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
ERROR_LOCKOUT);
|
||||
waitForIdle();
|
||||
|
||||
// Error is sent to client
|
||||
assertNull(mBiometricService.mPendingAuthSession);
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCombineAuthenticatorBundle_keyAllowDeviceCredentialAlwaysRemoved() {
|
||||
Bundle bundle;
|
||||
int authenticators;
|
||||
|
||||
// In:
|
||||
// KEY_ALLOW_DEVICE_CREDENTIAL = true
|
||||
// KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL
|
||||
// Out:
|
||||
// KEY_ALLOW_DEVICE_CREDENTIAL = null
|
||||
// KEY_AUTHENTICATORS_ALLOWED = TYPE_BIOMETRIC | TYPE_CREDENTIAL
|
||||
bundle = new Bundle();
|
||||
bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true);
|
||||
authenticators = Authenticator.TYPE_CREDENTIAL | Authenticator.TYPE_BIOMETRIC;
|
||||
bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, authenticators);
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user