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