Merge changes from topic "coex-again-2" into tm-dev

* changes:
  Temporarily disable some biometric prompt tests.
  Add Face and Fingerprint (Co-Ex) support to biometric prompt.
  Remove face to fingerprint multi-sensor behavior.
  Remove duplicated UDFPS view into single fingerprint view.
This commit is contained in:
Joe Bolinger
2022-03-01 23:25:55 +00:00
committed by Android (Google) Code Review
55 changed files with 2190 additions and 3757 deletions

View File

@@ -104,16 +104,16 @@ public class BiometricManager {
public static final int BIOMETRIC_MULTI_SENSOR_DEFAULT = 0;
/**
* Prefer the face sensor and fall back to fingerprint when needed.
* Use face and fingerprint sensors together.
* @hide
*/
public static final int BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT = 1;
public static final int BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE = 1;
/**
* @hide
*/
@IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT,
BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT})
BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE})
@Retention(RetentionPolicy.SOURCE)
public @interface BiometricMultiSensorMode {}

View File

@@ -63,7 +63,7 @@ interface IBiometricService {
// Notify BiometricService when <Biometric>Service is ready to start the prepared client.
// Client lifecycle is still managed in <Biometric>Service.
void onReadyForAuthentication(int cookie);
void onReadyForAuthentication(long requestId, int cookie);
// Requests all BIOMETRIC_STRONG sensors to have their authenticatorId invalidated for the
// specified user. This happens when enrollments have been added on devices with multiple

View File

@@ -30,6 +30,4 @@ oneway interface IBiometricSysuiReceiver {
void onSystemEvent(int event);
// Notifies that the dialog has finished animating.
void onDialogAnimatedIn();
// For multi-sensor devices, notifies that the fingerprint should start now.
void onStartFingerprintNow();
}

View File

@@ -159,7 +159,7 @@ oneway interface IStatusBar
/**
* Used to notify the authentication dialog that a biometric has been authenticated.
*/
void onBiometricAuthenticated();
void onBiometricAuthenticated(int modality);
/**
* Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc.
*/

View File

@@ -125,7 +125,7 @@ interface IStatusBarService
int multiSensorConfig);
// Used to notify the authentication dialog that a biometric has been authenticated
void onBiometricAuthenticated();
void onBiometricAuthenticated(int modality);
// Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
void onBiometricHelp(int modality, String message);
// Used to show an error - the dialog will dismiss after a certain amount of time

View File

@@ -91,23 +91,9 @@ message BiometricServiceStateProto {
STATE_CLIENT_DIED_CANCELLING = 10;
}
enum MultiSensorState {
// Initializing or not yet started.
MULTI_SENSOR_STATE_UNKNOWN = 0;
// Sensors are in the process of being transitioned and there is no active sensor.
MULTI_SENSOR_STATE_SWITCHING = 1;
// Face sensor is being used as the primary input.
MULTI_SENSOR_STATE_FACE_SCANNING = 2;
// Fingerprint sensor is being used as the primary input.
MULTI_SENSOR_STATE_FP_SCANNING = 3;
}
repeated SensorServiceStateProto sensor_service_states = 1;
optional AuthSessionState auth_session_state = 2;
// Additional session state information, when the device has multiple sensors.
optional MultiSensorState auth_session_multi_sensor_state = 3;
}
// Overall state for an instance of a <Biometric>Service, for example FingerprintService or

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -49,8 +49,8 @@
<ImageView
android:id="@+id/biometric_icon"
android:layout_width="@dimen/biometric_dialog_biometric_icon_size"
android:layout_height="@dimen/biometric_dialog_biometric_icon_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@null"
android:scaleType="fitXY" />

View File

@@ -14,12 +14,13 @@
~ limitations under the License.
-->
<com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView
<com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/auth_biometric_contents"/>
</com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView>
</com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView>

View File

@@ -1,5 +1,5 @@
<!--
~ Copyright (C) 2020 The Android Open Source Project
~ Copyright (C) 2022 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.
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
<com.android.systemui.biometrics.AuthBiometricUdfpsView
<com.android.systemui.biometrics.AuthBiometricView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
android:layout_width="match_parent"
@@ -23,4 +23,4 @@
<include layout="@layout/auth_biometric_contents"/>
</com.android.systemui.biometrics.AuthBiometricUdfpsView>
</com.android.systemui.biometrics.AuthBiometricView>

View File

@@ -876,7 +876,8 @@
<dimen name="remote_input_history_extra_height">60dp</dimen>
<!-- Biometric Dialog values -->
<dimen name="biometric_dialog_biometric_icon_size">64dp</dimen>
<dimen name="biometric_dialog_face_icon_size">64dp</dimen>
<dimen name="biometric_dialog_fingerprint_icon_size">80dp</dimen>
<dimen name="biometric_dialog_button_negative_max_width">160dp</dimen>
<dimen name="biometric_dialog_button_positive_max_width">136dp</dimen>
<dimen name="biometric_dialog_corner_size">4dp</dimen>

View File

@@ -322,6 +322,8 @@
<string name="biometric_dialog_face_icon_description_confirmed">Confirmed</string>
<!-- Message shown when a biometric is authenticated, waiting for the user to confirm authentication [CHAR LIMIT=40]-->
<string name="biometric_dialog_tap_confirm">Tap Confirm to complete</string>
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
<string name="biometric_dialog_tap_confirm_with_face">Unlocked by your face. Press to continue.</string>
<!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] -->
<string name="biometric_dialog_authenticated">Authenticated</string>

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2022 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.graphics.drawable.Drawable
import android.util.Log
import android.widget.ImageView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
private const val TAG = "AuthBiometricFaceIconController"
/** Face only icon animator for BiometricPrompt. */
class AuthBiometricFaceIconController(
context: Context,
iconView: ImageView
) : AuthIconController(context, iconView) {
// false = dark to light, true = light to dark
private var lastPulseLightToDark = false
@BiometricState
private var state = 0
init {
val size = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
iconView.layoutParams.width = size
iconView.layoutParams.height = size
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
}
private fun startPulsing() {
lastPulseLightToDark = false
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true)
}
private fun pulseInNextDirection() {
val iconRes = if (lastPulseLightToDark) {
R.drawable.face_dialog_pulse_dark_to_light
} else {
R.drawable.face_dialog_pulse_light_to_dark
}
animateIcon(iconRes, true /* repeat */)
lastPulseLightToDark = !lastPulseLightToDark
}
override fun handleAnimationEnd(drawable: Drawable) {
if (state == STATE_AUTHENTICATING || state == STATE_HELP) {
pulseInNextDirection()
}
}
override fun updateIcon(@BiometricState oldState: Int, @BiometricState newState: Int) {
val lastStateIsErrorIcon = (oldState == STATE_ERROR || oldState == STATE_HELP)
if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticating
)
} else if (newState == STATE_AUTHENTICATING) {
startPulsing()
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticating
)
} else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_confirmed
)
} else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
animateIconOnce(R.drawable.face_dialog_error_to_idle)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_idle
)
} else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticated
)
} else if (newState == STATE_ERROR && oldState != STATE_ERROR) {
animateIconOnce(R.drawable.face_dialog_dark_to_error)
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticated
)
} else if (newState == STATE_PENDING_CONFIRMATION) {
animateIconOnce(R.drawable.face_dialog_wink_from_dark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticated
)
} else if (newState == STATE_IDLE) {
showStaticDrawable(R.drawable.face_dialog_idle_static)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_idle
)
} else {
Log.w(TAG, "Unhandled state: $newState")
}
state = newState
}
}

View File

@@ -1,245 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
/**
* Manages the layout of an auth dialog for devices with both a face sensor and a fingerprint
* sensor. Face authentication is attempted first, followed by fingerprint if the initial attempt is
* unsuccessful.
*/
public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
private static final String TAG = "BiometricPrompt/AuthBiometricFaceToFingerprintView";
protected static class UdfpsIconController extends IconController {
@BiometricState private int mIconState = STATE_IDLE;
protected UdfpsIconController(
@NonNull Context context, @NonNull ImageView iconView, @NonNull TextView textView) {
super(context, iconView, textView);
}
void updateState(@BiometricState int newState) {
updateState(mIconState, newState);
}
@Override
protected void updateState(int lastState, int newState) {
final boolean lastStateIsErrorIcon =
lastState == STATE_ERROR || lastState == STATE_HELP;
switch (newState) {
case STATE_IDLE:
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
case STATE_PENDING_CONFIRMATION:
case STATE_AUTHENTICATED:
if (lastStateIsErrorIcon) {
animateOnce(R.drawable.fingerprint_dialog_error_to_fp);
} else {
showStaticDrawable(R.drawable.fingerprint_dialog_fp_to_error);
}
mIconView.setContentDescription(mContext.getString(
R.string.accessibility_fingerprint_dialog_fingerprint_icon));
break;
case STATE_ERROR:
case STATE_HELP:
if (!lastStateIsErrorIcon) {
animateOnce(R.drawable.fingerprint_dialog_fp_to_error);
} else {
showStaticDrawable(R.drawable.fingerprint_dialog_error_to_fp);
}
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_try_again));
break;
default:
Log.e(TAG, "Unknown biometric dialog state: " + newState);
break;
}
mState = newState;
mIconState = newState;
}
}
@Modality private int mActiveSensorType = TYPE_FACE;
@Nullable private ModalityListener mModalityListener;
@Nullable private FingerprintSensorPropertiesInternal mFingerprintSensorProps;
@Nullable private UdfpsDialogMeasureAdapter mUdfpsMeasureAdapter;
@Nullable @VisibleForTesting UdfpsIconController mUdfpsIconController;
public AuthBiometricFaceToFingerprintView(Context context) {
super(context);
}
public AuthBiometricFaceToFingerprintView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@VisibleForTesting
AuthBiometricFaceToFingerprintView(Context context, AttributeSet attrs, Injector injector) {
super(context, attrs, injector);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mUdfpsIconController = new UdfpsIconController(mContext, mIconView, mIndicatorView);
}
@Modality
int getActiveSensorType() {
return mActiveSensorType;
}
boolean isFingerprintUdfps() {
return mFingerprintSensorProps.isAnyUdfpsType();
}
void setModalityListener(@NonNull ModalityListener listener) {
mModalityListener = listener;
}
void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) {
mFingerprintSensorProps = sensorProps;
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return mActiveSensorType == TYPE_FINGERPRINT ? 0
: super.getDelayAfterAuthenticatedDurationMs();
}
@Override
protected boolean supportsManualRetry() {
return false;
}
@Override
public void onAuthenticationFailed(
@Modality int modality, @Nullable String failureReason) {
super.onAuthenticationFailed(modality, checkErrorForFallback(failureReason));
}
@Override
public void onError(int modality, String error) {
super.onError(modality, checkErrorForFallback(error));
}
private String checkErrorForFallback(String message) {
if (mActiveSensorType == TYPE_FACE) {
Log.d(TAG, "Falling back to fingerprint: " + message);
// switching from face -> fingerprint mode, suppress root error messages
mCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR);
return mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead);
}
return message;
}
@Override
@BiometricState
protected int getStateForAfterError() {
if (mActiveSensorType == TYPE_FACE) {
return STATE_AUTHENTICATING;
}
return super.getStateForAfterError();
}
@Override
public void updateState(@BiometricState int newState) {
if (mActiveSensorType == TYPE_FACE) {
if (newState == STATE_HELP || newState == STATE_ERROR) {
mActiveSensorType = TYPE_FINGERPRINT;
setRequireConfirmation(false);
mConfirmButton.setEnabled(false);
mConfirmButton.setVisibility(View.GONE);
if (mModalityListener != null) {
mModalityListener.onModalitySwitched(TYPE_FACE, mActiveSensorType);
}
// Deactivate the face icon controller so it stops drawing to the view
mFaceIconController.deactivate();
// Then, activate this icon controller. We need to start in the "idle" state
mUdfpsIconController.updateState(STATE_IDLE);
}
} else { // Fingerprint
mUdfpsIconController.updateState(newState);
}
super.updateState(newState);
}
@Override
@NonNull
AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height);
return isFingerprintUdfps()
? getUdfpsMeasureAdapter().onMeasureInternal(width, height, layoutParams)
: layoutParams;
}
@NonNull
private UdfpsDialogMeasureAdapter getUdfpsMeasureAdapter() {
if (mUdfpsMeasureAdapter == null
|| mUdfpsMeasureAdapter.getSensorProps() != mFingerprintSensorProps) {
mUdfpsMeasureAdapter = new UdfpsDialogMeasureAdapter(this, mFingerprintSensorProps);
}
return mUdfpsMeasureAdapter;
}
@Override
public void onSaveState(@NonNull Bundle outState) {
super.onSaveState(outState);
outState.putInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, mActiveSensorType);
outState.putParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS, mFingerprintSensorProps);
}
@Override
public void restoreState(@Nullable Bundle savedState) {
super.restoreState(savedState);
if (savedState != null) {
mActiveSensorType = savedState.getInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, TYPE_FACE);
mFingerprintSensorProps =
savedState.getParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS);
}
}
}

View File

@@ -1,261 +0,0 @@
/*
* 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.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
public class AuthBiometricFaceView extends AuthBiometricView {
private static final String TAG = "BiometricPrompt/AuthBiometricFaceView";
// Delay before dismissing after being authenticated/confirmed.
private static final int HIDE_DELAY_MS = 500;
protected static class IconController extends Animatable2.AnimationCallback {
protected Context mContext;
protected ImageView mIconView;
protected TextView mTextView;
protected Handler mHandler;
protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
protected @BiometricState int mState;
protected boolean mDeactivated;
protected IconController(Context context, ImageView iconView, TextView textView) {
mContext = context;
mIconView = iconView;
mTextView = textView;
mHandler = new Handler(Looper.getMainLooper());
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
}
protected void animateOnce(int iconRes) {
animateIcon(iconRes, false);
}
protected void showStaticDrawable(int iconRes) {
mIconView.setImageDrawable(mContext.getDrawable(iconRes));
}
protected void animateIcon(int iconRes, boolean repeat) {
Log.d(TAG, "animateIcon, state: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
final AnimatedVectorDrawable icon =
(AnimatedVectorDrawable) mContext.getDrawable(iconRes);
mIconView.setImageDrawable(icon);
icon.forceAnimationOnUI();
if (repeat) {
icon.registerAnimationCallback(this);
}
icon.start();
}
protected void startPulsing() {
mLastPulseLightToDark = false;
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true);
}
protected void pulseInNextDirection() {
int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light
: R.drawable.face_dialog_pulse_light_to_dark;
animateIcon(iconRes, true /* repeat */);
mLastPulseLightToDark = !mLastPulseLightToDark;
}
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
Log.d(TAG, "onAnimationEnd, mState: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
if (mState == STATE_AUTHENTICATING || mState == STATE_HELP) {
pulseInNextDirection();
}
}
protected void deactivate() {
mDeactivated = true;
}
protected void updateState(int lastState, int newState) {
if (mDeactivated) {
Log.w(TAG, "Ignoring updateState when deactivated: " + newState);
return;
}
final boolean lastStateIsErrorIcon =
lastState == STATE_ERROR || lastState == STATE_HELP;
if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (newState == STATE_AUTHENTICATING) {
startPulsing();
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (lastState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_confirmed));
} else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
animateOnce(R.drawable.face_dialog_error_to_idle);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_idle));
} else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_ERROR && lastState != STATE_ERROR) {
animateOnce(R.drawable.face_dialog_dark_to_error);
} else if (lastState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_PENDING_CONFIRMATION) {
animateOnce(R.drawable.face_dialog_wink_from_dark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_IDLE) {
showStaticDrawable(R.drawable.face_dialog_idle_static);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_idle));
} else {
Log.w(TAG, "Unhandled state: " + newState);
}
mState = newState;
}
}
@Nullable @VisibleForTesting IconController mFaceIconController;
@NonNull private final OnAttachStateChangeListener mOnAttachStateChangeListener =
new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
mFaceIconController.deactivate();
}
};
public AuthBiometricFaceView(Context context) {
this(context, null);
}
public AuthBiometricFaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@VisibleForTesting
AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector) {
super(context, attrs, injector);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFaceIconController = new IconController(mContext, mIconView, mIndicatorView);
addOnAttachStateChangeListener(mOnAttachStateChangeListener);
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return HIDE_DELAY_MS;
}
@Override
protected int getStateForAfterError() {
return STATE_IDLE;
}
@Override
protected void handleResetAfterError() {
resetErrorView();
}
@Override
protected void handleResetAfterHelp() {
resetErrorView();
}
@Override
protected boolean supportsSmallDialog() {
return true;
}
@Override
protected boolean supportsManualRetry() {
return true;
}
@Override
public void updateState(@BiometricState int newState) {
mFaceIconController.updateState(mState, newState);
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
(newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) {
resetErrorView();
}
// Do this last since the state variable gets updated.
super.updateState(newState);
}
@Override
public void onAuthenticationFailed(@Modality int modality, @Nullable String failureReason) {
if (getSize() == AuthDialog.SIZE_MEDIUM) {
if (supportsManualRetry()) {
mTryAgainButton.setVisibility(View.VISIBLE);
mConfirmButton.setVisibility(View.GONE);
}
}
// Do this last since we want to know if the button is being animated (in the case of
// small -> medium dialog)
super.onAuthenticationFailed(modality, failureReason);
}
private void resetErrorView() {
mIndicatorView.setTextColor(mTextColorHint);
mIndicatorView.setVisibility(View.INVISIBLE);
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.BiometricAuthenticator.Modality
import android.util.AttributeSet
/** Face only view for BiometricPrompt. */
class AuthBiometricFaceView(
context: Context,
attrs: AttributeSet? = null
) : AuthBiometricView(context, attrs) {
override fun getDelayAfterAuthenticatedDurationMs() = HIDE_DELAY_MS
override fun getStateForAfterError() = STATE_IDLE
override fun handleResetAfterError() = resetErrorView()
override fun handleResetAfterHelp() = resetErrorView()
override fun supportsSmallDialog() = true
override fun supportsManualRetry() = true
override fun supportsRequireConfirmation() = true
override fun createIconController(): AuthIconController =
AuthBiometricFaceIconController(mContext, mIconView)
override fun updateState(@BiometricState newState: Int) {
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
newState == STATE_AUTHENTICATING && size == AuthDialog.SIZE_MEDIUM) {
resetErrorView()
}
// Do this last since the state variable gets updated.
super.updateState(newState)
}
override fun onAuthenticationFailed(
@Modality modality: Int,
failureReason: String?
) {
if (size == AuthDialog.SIZE_MEDIUM) {
if (supportsManualRetry()) {
mTryAgainButton.visibility = VISIBLE
mConfirmButton.visibility = GONE
}
}
// Do this last since we want to know if the button is being animated (in the case of
// small -> medium dialog)
super.onAuthenticationFailed(modality, failureReason)
}
private fun resetErrorView() {
mIndicatorView.setTextColor(mTextColorHint)
mIndicatorView.visibility = INVISIBLE
}
companion object {
/** Delay before dismissing after being authenticated/confirmed. */
const val HIDE_DELAY_MS = 500
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2022 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.graphics.drawable.Drawable
import android.widget.ImageView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
/** Face/Fingerprint combined icon animator for BiometricPrompt. */
class AuthBiometricFingerprintAndFaceIconController(
context: Context,
iconView: ImageView
) : AuthBiometricFingerprintIconController(context, iconView) {
override val actsAsConfirmButton: Boolean = true
override fun shouldAnimateForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
): Boolean = when (newState) {
STATE_PENDING_CONFIRMATION -> true
STATE_AUTHENTICATED -> false
else -> super.shouldAnimateForTransition(oldState, newState)
}
override fun getAnimationForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
): Drawable? = when (newState) {
STATE_PENDING_CONFIRMATION -> {
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
context.getDrawable(R.drawable.fingerprint_dialog_error_to_unlock)
} else {
context.getDrawable(R.drawable.fingerprint_dialog_fp_to_unlock)
}
}
STATE_AUTHENTICATED -> null
else -> super.getAnimationForTransition(oldState, newState)
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2022 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.BiometricAuthenticator.Modality
import android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE
import android.util.AttributeSet
import com.android.systemui.R
/** Face/Fingerprint combined view for BiometricPrompt. */
class AuthBiometricFingerprintAndFaceView(
context: Context,
attrs: AttributeSet?
) : AuthBiometricFingerprintView(context, attrs) {
constructor (context: Context) : this(context, null)
override fun getConfirmationPrompt() = R.string.biometric_dialog_tap_confirm_with_face
override fun forceRequireConfirmation(@Modality modality: Int) = modality == TYPE_FACE
override fun ignoreUnsuccessfulEventsFrom(@Modality modality: Int) = modality == TYPE_FACE
override fun onPointerDown(failedModalities: Set<Int>) = failedModalities.contains(TYPE_FACE)
override fun createIconController(): AuthIconController =
AuthBiometricFingerprintAndFaceIconController(mContext, mIconView)
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2022 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.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.widget.ImageView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
/** Fingerprint only icon animator for BiometricPrompt. */
open class AuthBiometricFingerprintIconController(
context: Context,
iconView: ImageView
) : AuthIconController(context, iconView) {
init {
val size = context.resources.getDimensionPixelSize(
R.dimen.biometric_dialog_fingerprint_icon_size
)
iconView.layoutParams.width = size
iconView.layoutParams.height = size
}
override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) {
val icon = getAnimationForTransition(lastState, newState) ?: return
iconView.setImageDrawable(icon)
val iconContentDescription = getIconContentDescription(newState)
if (iconContentDescription != null) {
iconView.contentDescription = iconContentDescription
}
(icon as? AnimatedVectorDrawable)?.apply {
reset()
if (shouldAnimateForTransition(lastState, newState)) {
forceAnimationOnUI()
start()
}
}
}
private fun getIconContentDescription(@BiometricState newState: Int): CharSequence? {
val id = when (newState) {
STATE_IDLE,
STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING,
STATE_PENDING_CONFIRMATION,
STATE_AUTHENTICATED -> R.string.accessibility_fingerprint_dialog_fingerprint_icon
STATE_ERROR,
STATE_HELP -> R.string.biometric_dialog_try_again
else -> null
}
return if (id != null) context.getString(id) else null
}
protected open fun shouldAnimateForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
) = when (newState) {
STATE_HELP,
STATE_ERROR -> true
STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP
STATE_AUTHENTICATED -> false
else -> false
}
protected open fun getAnimationForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
): Drawable? {
val id = when (newState) {
STATE_HELP,
STATE_ERROR -> R.drawable.fingerprint_dialog_fp_to_error
STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING -> {
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
R.drawable.fingerprint_dialog_error_to_fp
} else {
R.drawable.fingerprint_dialog_fp_to_error
}
}
STATE_AUTHENTICATED -> R.drawable.fingerprint_dialog_fp_to_error
else -> return null
}
return if (id != null) context.getDrawable(id) else null
}
}

View File

@@ -1,174 +0,0 @@
/*
* 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.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.systemui.R;
public class AuthBiometricFingerprintView extends AuthBiometricView {
private static final String TAG = "BiometricPrompt/AuthBiometricFingerprintView";
public AuthBiometricFingerprintView(Context context) {
this(context, null);
}
public AuthBiometricFingerprintView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return 0;
}
@Override
protected int getStateForAfterError() {
return STATE_AUTHENTICATING;
}
@Override
protected void handleResetAfterError() {
showTouchSensorString();
}
@Override
protected void handleResetAfterHelp() {
showTouchSensorString();
}
@Override
protected boolean supportsSmallDialog() {
return false;
}
@Override
public void updateState(@BiometricState int newState) {
updateIcon(mState, newState);
// Do this last since the state variable gets updated.
super.updateState(newState);
}
@Override
void onAttachedToWindowInternal() {
super.onAttachedToWindowInternal();
showTouchSensorString();
}
private void showTouchSensorString() {
mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor);
mIndicatorView.setTextColor(mTextColorHint);
}
private void updateIcon(int lastState, int newState) {
final Drawable icon = getAnimationForTransition(lastState, newState);
if (icon == null) {
Log.e(TAG, "Animation not found, " + lastState + " -> " + newState);
return;
}
final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
? (AnimatedVectorDrawable) icon
: null;
mIconView.setImageDrawable(icon);
final CharSequence iconContentDescription = getIconContentDescription(newState);
if (iconContentDescription != null) {
mIconView.setContentDescription(iconContentDescription);
}
if (animation != null && shouldAnimateForTransition(lastState, newState)) {
animation.forceAnimationOnUI();
animation.start();
}
}
@Nullable
private CharSequence getIconContentDescription(int newState) {
switch (newState) {
case STATE_IDLE:
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
case STATE_PENDING_CONFIRMATION:
case STATE_AUTHENTICATED:
return mContext.getString(
R.string.accessibility_fingerprint_dialog_fingerprint_icon);
case STATE_ERROR:
case STATE_HELP:
return mContext.getString(R.string.biometric_dialog_try_again);
default:
return null;
}
}
private boolean shouldAnimateForTransition(int oldState, int newState) {
switch (newState) {
case STATE_HELP:
case STATE_ERROR:
return true;
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
return true;
} else {
return false;
}
case STATE_AUTHENTICATED:
return false;
default:
return false;
}
}
private Drawable getAnimationForTransition(int oldState, int newState) {
int iconRes;
switch (newState) {
case STATE_HELP:
case STATE_ERROR:
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
break;
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
iconRes = R.drawable.fingerprint_dialog_error_to_fp;
} else {
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
}
break;
case STATE_AUTHENTICATED:
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
break;
default:
return null;
}
return mContext.getDrawable(iconRes);
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.fingerprint.FingerprintSensorPropertiesInternal
import android.util.AttributeSet
import android.util.Log
import android.widget.FrameLayout
import android.widget.TextView
import com.android.systemui.R
private const val TAG = "AuthBiometricFingerprintView"
/** Fingerprint only view for BiometricPrompt. */
open class AuthBiometricFingerprintView(
context: Context,
attrs: AttributeSet? = null
) : AuthBiometricView(context, attrs) {
/** If this view is for a UDFPS sensor. */
var isUdfps = false
private set
private var udfpsAdapter: UdfpsDialogMeasureAdapter? = null
/** Set the [sensorProps] of this sensor so the view can be customized prior to layout. */
fun setSensorProperties(sensorProps: FingerprintSensorPropertiesInternal) {
isUdfps = sensorProps.isAnyUdfpsType
udfpsAdapter = if (isUdfps) UdfpsDialogMeasureAdapter(this, sensorProps) else null
}
override fun onMeasureInternal(width: Int, height: Int): AuthDialog.LayoutParams {
val layoutParams = super.onMeasureInternal(width, height)
return udfpsAdapter?.onMeasureInternal(width, height, layoutParams) ?: layoutParams
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val adapter = udfpsAdapter
if (adapter != null) {
// Move the UDFPS icon and indicator text if necessary. This probably only needs to happen
// for devices where the UDFPS sensor is too low.
// TODO(b/201510778): Update this logic to support cases where the sensor or text overlap
// the button bar area.
val bottomSpacerHeight = adapter.bottomSpacerHeight
Log.w(TAG, "bottomSpacerHeight: $bottomSpacerHeight")
if (bottomSpacerHeight < 0) {
val iconFrame = findViewById<FrameLayout>(R.id.biometric_icon_frame)!!
iconFrame.translationY = -bottomSpacerHeight.toFloat()
val indicator = findViewById<TextView>(R.id.indicator)!!
indicator.translationY = -bottomSpacerHeight.toFloat()
}
}
}
override fun getDelayAfterAuthenticatedDurationMs() = 0
override fun getStateForAfterError() = STATE_AUTHENTICATING
override fun handleResetAfterError() = showTouchSensorString()
override fun handleResetAfterHelp() = showTouchSensorString()
override fun supportsSmallDialog() = false
override fun createIconController(): AuthIconController =
AuthBiometricFingerprintIconController(mContext, mIconView)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
showTouchSensorString()
}
private fun showTouchSensorString() {
mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor)
mIndicatorView.setTextColor(mTextColorHint)
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2022 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.annotation.DrawableRes
import android.content.Context
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.ImageView
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
private const val TAG = "AuthIconController"
/** Controller for animating the BiometricPrompt icon/affordance. */
abstract class AuthIconController(
protected val context: Context,
protected val iconView: ImageView
) : Animatable2.AnimationCallback() {
/** If this controller should ignore events and pause. */
var deactivated: Boolean = false
/** If the icon view should be treated as an alternate "confirm" button. */
open val actsAsConfirmButton: Boolean = false
final override fun onAnimationStart(drawable: Drawable) {
super.onAnimationStart(drawable)
}
final override fun onAnimationEnd(drawable: Drawable) {
super.onAnimationEnd(drawable)
if (!deactivated) {
handleAnimationEnd(drawable)
}
}
/** Set the icon to a static image. */
protected fun showStaticDrawable(@DrawableRes iconRes: Int) {
iconView.setImageDrawable(context.getDrawable(iconRes))
}
/** Animate a resource. */
protected fun animateIconOnce(@DrawableRes iconRes: Int) {
animateIcon(iconRes, false)
}
/** Animate a resource. */
protected fun animateIcon(@DrawableRes iconRes: Int, repeat: Boolean) {
if (!deactivated) {
val icon = context.getDrawable(iconRes) as AnimatedVectorDrawable
iconView.setImageDrawable(icon)
icon.forceAnimationOnUI()
if (repeat) {
icon.registerAnimationCallback(this)
}
icon.start()
}
}
/** Update the icon to reflect the [newState]. */
fun updateState(@BiometricState lastState: Int, @BiometricState newState: Int) {
if (deactivated) {
Log.w(TAG, "Ignoring updateState when deactivated: $newState")
} else {
updateIcon(lastState, newState)
}
}
/** If the icon should act as a "retry" button in the [currentState]. */
fun iconTapSendsRetryWhen(@BiometricState currentState: Int): Boolean = false
/** Call during [updateState] if the controller is not [deactivated]. */
abstract fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int)
/** Called during [onAnimationEnd] if the controller is not [deactivated]. */
open fun handleAnimationEnd(drawable: Drawable) {}
}

View File

@@ -1,80 +0,0 @@
/*
* Copyright (C) 2020 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.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.systemui.R;
/**
* Manages the layout for under-display fingerprint sensors (UDFPS). Ensures that UI elements
* do not overlap with
*/
public class AuthBiometricUdfpsView extends AuthBiometricFingerprintView {
private static final String TAG = "AuthBiometricUdfpsView";
@Nullable private UdfpsDialogMeasureAdapter mMeasureAdapter;
public AuthBiometricUdfpsView(Context context) {
this(context, null /* attrs */);
}
public AuthBiometricUdfpsView(Context context, AttributeSet attrs) {
super(context, attrs);
}
void setSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) {
if (mMeasureAdapter == null || mMeasureAdapter.getSensorProps() != sensorProps) {
mMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps);
}
}
@Override
@NonNull
AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height);
return mMeasureAdapter != null
? mMeasureAdapter.onMeasureInternal(width, height, layoutParams)
: layoutParams;
}
@Override
void onLayoutInternal() {
super.onLayoutInternal();
// Move the UDFPS icon and indicator text if necessary. This probably only needs to happen
// for devices where the UDFPS sensor is too low.
// TODO(b/201510778): Update this logic to support cases where the sensor or text overlap
// the button bar area.
final int bottomSpacerHeight = mMeasureAdapter.getBottomSpacerHeight();
Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight);
if (bottomSpacerHeight < 0) {
FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame);
iconFrame.setTranslationY(-bottomSpacerHeight);
TextView indicator = findViewById(R.id.indicator);
indicator.setTranslationY(-bottomSpacerHeight);
}
}
}

View File

@@ -25,6 +25,7 @@ import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricPrompt;
@@ -44,19 +45,21 @@ 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;
import java.util.Set;
/**
* Contains the Biometric views (title, subtitle, icon, buttons, etc) and its controllers.
* Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers.
*/
public abstract class AuthBiometricView extends LinearLayout {
public class AuthBiometricView extends LinearLayout {
private static final String TAG = "BiometricPrompt/AuthBiometricView";
private static final String TAG = "AuthBiometricView";
/**
* Authentication hardware idle.
@@ -102,13 +105,6 @@ public abstract class AuthBiometricView extends LinearLayout {
int ACTION_BUTTON_TRY_AGAIN = 4;
int ACTION_ERROR = 5;
int ACTION_USE_DEVICE_CREDENTIAL = 6;
/**
* Notify the receiver to start the fingerprint sensor.
*
* This is only applicable to multi-sensor devices that need to delay fingerprint auth
* (i.e face -> fingerprint).
*/
int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7;
/**
* When an action has occurred. The caller will only invoke this when the callback should
@@ -118,66 +114,9 @@ public abstract class AuthBiometricView extends LinearLayout {
void onAction(int action);
}
@VisibleForTesting
static class Injector {
AuthBiometricView mBiometricView;
public Button getNegativeButton() {
return mBiometricView.findViewById(R.id.button_negative);
}
public Button getCancelButton() {
return mBiometricView.findViewById(R.id.button_cancel);
}
public Button getUseCredentialButton() {
return mBiometricView.findViewById(R.id.button_use_credential);
}
public Button getConfirmButton() {
return mBiometricView.findViewById(R.id.button_confirm);
}
public Button getTryAgainButton() {
return mBiometricView.findViewById(R.id.button_try_again);
}
public TextView getTitleView() {
return mBiometricView.findViewById(R.id.title);
}
public TextView getSubtitleView() {
return mBiometricView.findViewById(R.id.subtitle);
}
public TextView getDescriptionView() {
return mBiometricView.findViewById(R.id.description);
}
public TextView getIndicatorView() {
return mBiometricView.findViewById(R.id.indicator);
}
public ImageView getIconView() {
return mBiometricView.findViewById(R.id.biometric_icon);
}
public View getIconHolderView() {
return mBiometricView.findViewById(R.id.biometric_icon_frame);
}
public int getDelayAfterError() {
return BiometricPrompt.HIDE_DIALOG_DELAY;
}
public int getMediumToLargeAnimationDurationMs() {
return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
}
}
private final Injector mInjector;
protected final Handler mHandler;
private final AccessibilityManager mAccessibilityManager;
private final LockPatternUtils mLockPatternUtils;
protected final int mTextColorError;
protected final int mTextColorHint;
@@ -195,6 +134,11 @@ public abstract class AuthBiometricView extends LinearLayout {
protected ImageView mIconView;
protected TextView mIndicatorView;
@VisibleForTesting @NonNull AuthIconController mIconController;
@VisibleForTesting int mAnimationDurationShort = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS;
@VisibleForTesting int mAnimationDurationLong = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
@VisibleForTesting int mAnimationDurationHideDialog = BiometricPrompt.HIDE_DIALOG_DELAY;
// Negative button position, exclusively for the app-specified behavior
@VisibleForTesting Button mNegativeButton;
// Negative button position, exclusively for cancelling auth after passive auth success
@@ -217,30 +161,7 @@ public abstract class AuthBiometricView extends LinearLayout {
protected boolean mDialogSizeAnimating;
protected Bundle mSavedState;
/**
* Delay after authentication is confirmed, before the dialog should be animated away.
*/
protected abstract int getDelayAfterAuthenticatedDurationMs();
/**
* State that the dialog/icon should be in after showing a help message.
*/
protected abstract int getStateForAfterError();
/**
* Invoked when the error message is being cleared.
*/
protected abstract void handleResetAfterError();
/**
* Invoked when the help message is being cleared.
*/
protected abstract void handleResetAfterHelp();
/**
* @return true if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}
*/
protected abstract boolean supportsSmallDialog();
private final Runnable mResetErrorRunnable;
private final Runnable mResetHelpRunnable;
private final OnClickListener mBackgroundClickListener = (view) -> {
@@ -262,11 +183,6 @@ public abstract class AuthBiometricView extends LinearLayout {
}
public AuthBiometricView(Context context, AttributeSet attrs) {
this(context, attrs, new Injector());
}
@VisibleForTesting
AuthBiometricView(Context context, AttributeSet attrs, Injector injector) {
super(context, attrs);
mHandler = new Handler(Looper.getMainLooper());
mTextColorError = getResources().getColor(
@@ -274,10 +190,8 @@ public abstract class AuthBiometricView extends LinearLayout {
mTextColorHint = getResources().getColor(
R.color.biometric_dialog_gray, context.getTheme());
mInjector = injector;
mInjector.mBiometricView = this;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mLockPatternUtils = new LockPatternUtils(context);
mResetErrorRunnable = () -> {
updateState(getStateForAfterError());
@@ -292,36 +206,91 @@ public abstract class AuthBiometricView extends LinearLayout {
};
}
public void setPanelController(AuthPanelController panelController) {
/** Delay after authentication is confirmed, before the dialog should be animated away. */
protected int getDelayAfterAuthenticatedDurationMs() {
return 0;
}
/** State that the dialog/icon should be in after showing a help message. */
protected int getStateForAfterError() {
return STATE_IDLE;
}
/** Invoked when the error message is being cleared. */
protected void handleResetAfterError() {}
/** Invoked when the help message is being cleared. */
protected void handleResetAfterHelp() {}
/** True if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}. */
protected boolean supportsSmallDialog() {
return false;
}
/** The string to show when the user must tap to confirm via the button or icon. */
@StringRes
protected int getConfirmationPrompt() {
return R.string.biometric_dialog_tap_confirm;
}
/** True if require confirmation will be honored when set via the API. */
protected boolean supportsRequireConfirmation() {
return false;
}
/** True if confirmation will be required even if it was not supported/requested. */
protected boolean forceRequireConfirmation(@Modality int modality) {
return false;
}
/** Ignore all events from this (secondary) modality except successful authentication. */
protected boolean ignoreUnsuccessfulEventsFrom(@Modality int modality) {
return false;
}
/**
* Create the controller for managing the icons transitions during the prompt.
*
* Subclass should override.
*/
@NonNull
protected AuthIconController createIconController() {
return new AuthIconController(mContext, mIconView) {
@Override
public void updateIcon(int lastState, int newState) {}
};
}
void setPanelController(AuthPanelController panelController) {
mPanelController = panelController;
}
public void setPromptInfo(PromptInfo promptInfo) {
void setPromptInfo(PromptInfo promptInfo) {
mPromptInfo = promptInfo;
}
public void setCallback(Callback callback) {
void setCallback(Callback callback) {
mCallback = callback;
}
public void setBackgroundView(View backgroundView) {
void setBackgroundView(View backgroundView) {
backgroundView.setOnClickListener(mBackgroundClickListener);
}
public void setUserId(int userId) {
void setUserId(int userId) {
mUserId = userId;
}
public void setEffectiveUserId(int effectiveUserId) {
void setEffectiveUserId(int effectiveUserId) {
mEffectiveUserId = effectiveUserId;
}
public void setRequireConfirmation(boolean requireConfirmation) {
mRequireConfirmation = requireConfirmation;
void setRequireConfirmation(boolean requireConfirmation) {
mRequireConfirmation = requireConfirmation && supportsRequireConfirmation();
}
@VisibleForTesting
void updateSize(@AuthDialog.DialogSize int newSize) {
final void updateSize(@AuthDialog.DialogSize int newSize) {
Log.v(TAG, "Current size: " + mSize + " New size: " + newSize);
if (newSize == AuthDialog.SIZE_SMALL) {
mTitleView.setVisibility(View.GONE);
@@ -376,7 +345,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Choreograph together
final AnimatorSet as = new AnimatorSet();
as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
as.setDuration(mAnimationDurationShort);
as.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -429,7 +398,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Translate at full duration
final ValueAnimator translationAnimator = ValueAnimator.ofFloat(
biometricView.getY(), biometricView.getY() - translationY);
translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs());
translationAnimator.setDuration(mAnimationDurationLong);
translationAnimator.addUpdateListener((animation) -> {
final float translation = (float) animation.getAnimatedValue();
biometricView.setTranslationY(translation);
@@ -438,7 +407,7 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (biometricView.getParent() != null) {
if (biometricView.getParent() instanceof ViewGroup) {
((ViewGroup) biometricView.getParent()).removeView(biometricView);
}
mSize = newSize;
@@ -447,7 +416,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Opacity to 0 in half duration
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2);
opacityAnimator.setDuration(mAnimationDurationLong / 2);
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
biometricView.setAlpha(opacity);
@@ -457,7 +426,7 @@ public abstract class AuthBiometricView extends LinearLayout {
mPanelController.updateForContentDimensions(
mPanelController.getContainerWidth(),
mPanelController.getContainerHeight(),
mInjector.getMediumToLargeAnimationDurationMs());
mAnimationDurationLong);
// Start the animations together
AnimatorSet as = new AnimatorSet();
@@ -466,7 +435,7 @@ public abstract class AuthBiometricView extends LinearLayout {
animators.add(opacityAnimator);
as.playTogether(animators);
as.setDuration(mInjector.getMediumToLargeAnimationDurationMs() * 2 / 3);
as.setDuration(mAnimationDurationLong * 2 / 3);
as.start();
} else {
Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
@@ -481,6 +450,8 @@ public abstract class AuthBiometricView extends LinearLayout {
public void updateState(@BiometricState int newState) {
Log.v(TAG, "newState: " + newState);
mIconController.updateState(mState, newState);
switch (newState) {
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
@@ -510,10 +481,11 @@ public abstract class AuthBiometricView extends LinearLayout {
mNegativeButton.setVisibility(View.GONE);
mCancelButton.setVisibility(View.VISIBLE);
mUseCredentialButton.setVisibility(View.GONE);
mConfirmButton.setEnabled(true);
mConfirmButton.setVisibility(View.VISIBLE);
// forced confirmations (multi-sensor) use the icon view as the confirm button
mConfirmButton.setEnabled(mRequireConfirmation);
mConfirmButton.setVisibility(mRequireConfirmation ? View.VISIBLE : View.GONE);
mIndicatorView.setTextColor(mTextColorHint);
mIndicatorView.setText(R.string.biometric_dialog_tap_confirm);
mIndicatorView.setText(getConfirmationPrompt());
mIndicatorView.setVisibility(View.VISIBLE);
break;
@@ -536,9 +508,9 @@ public abstract class AuthBiometricView extends LinearLayout {
updateState(STATE_AUTHENTICATING);
}
public void onAuthenticationSucceeded() {
public void onAuthenticationSucceeded(@Modality int modality) {
removePendingAnimations();
if (mRequireConfirmation) {
if (mRequireConfirmation || forceRequireConfirmation(modality)) {
updateState(STATE_PENDING_CONFIRMATION);
} else {
updateState(STATE_AUTHENTICATED);
@@ -553,6 +525,10 @@ public abstract class AuthBiometricView extends LinearLayout {
*/
public void onAuthenticationFailed(
@Modality int modality, @Nullable String failureReason) {
if (ignoreUnsuccessfulEventsFrom(modality)) {
return;
}
showTemporaryMessage(failureReason, mResetErrorRunnable);
updateState(STATE_ERROR);
}
@@ -564,12 +540,27 @@ public abstract class AuthBiometricView extends LinearLayout {
* @param error message
*/
public void onError(@Modality int modality, String error) {
if (ignoreUnsuccessfulEventsFrom(modality)) {
return;
}
showTemporaryMessage(error, mResetErrorRunnable);
updateState(STATE_ERROR);
mHandler.postDelayed(() -> {
mCallback.onAction(Callback.ACTION_ERROR);
}, mInjector.getDelayAfterError());
mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_ERROR),
mAnimationDurationHideDialog);
}
/**
* Fingerprint pointer down event. This does nothing by default and will not be called if the
* device does not have an appropriate sensor (UDFPS), but it may be used as an alternative
* to the "retry" button when fingerprint is used with other modalities.
*
* @param failedModalities the set of modalities that have failed
* @return true if a retry was initiated as a result of this event
*/
public boolean onPointerDown(Set<Integer> failedModalities) {
return false;
}
/**
@@ -579,6 +570,9 @@ public abstract class AuthBiometricView extends LinearLayout {
* @param help message
*/
public void onHelp(@Modality int modality, String help) {
if (ignoreUnsuccessfulEventsFrom(modality)) {
return;
}
if (mSize != AuthDialog.SIZE_MEDIUM) {
Log.w(TAG, "Help received in size: " + mSize);
return;
@@ -639,7 +633,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// select to enable marquee unless a screen reader is enabled
mIndicatorView.setSelected(!mAccessibilityManager.isEnabled()
|| !mAccessibilityManager.isTouchExplorationEnabled());
mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError());
mHandler.postDelayed(resetMessageRunnable, mAnimationDurationHideDialog);
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
}
@@ -647,29 +641,22 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
onFinishInflateInternal();
}
/**
* After inflation, but before things like restoreState, onAttachedToWindow, etc.
*/
@VisibleForTesting
void onFinishInflateInternal() {
mTitleView = mInjector.getTitleView();
mSubtitleView = mInjector.getSubtitleView();
mDescriptionView = mInjector.getDescriptionView();
mIconView = mInjector.getIconView();
mIconHolderView = mInjector.getIconHolderView();
mIndicatorView = mInjector.getIndicatorView();
mTitleView = findViewById(R.id.title);
mSubtitleView = findViewById(R.id.subtitle);
mDescriptionView = findViewById(R.id.description);
mIconView = findViewById(R.id.biometric_icon);
mIconHolderView = findViewById(R.id.biometric_icon_frame);
mIndicatorView = findViewById(R.id.indicator);
// Negative-side (left) buttons
mNegativeButton = mInjector.getNegativeButton();
mCancelButton = mInjector.getCancelButton();
mUseCredentialButton = mInjector.getUseCredentialButton();
mNegativeButton = findViewById(R.id.button_negative);
mCancelButton = findViewById(R.id.button_cancel);
mUseCredentialButton = findViewById(R.id.button_use_credential);
// Positive-side (right) buttons
mConfirmButton = mInjector.getConfirmButton();
mTryAgainButton = mInjector.getTryAgainButton();
mConfirmButton = findViewById(R.id.button_confirm);
mTryAgainButton = findViewById(R.id.button_try_again);
mNegativeButton.setOnClickListener((view) -> {
mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
@@ -693,6 +680,15 @@ public abstract class AuthBiometricView extends LinearLayout {
mTryAgainButton.setVisibility(View.GONE);
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
});
mIconController = createIconController();
if (mIconController.getActsAsConfirmButton()) {
mIconView.setOnClickListener((view) -> {
if (mState == STATE_PENDING_CONFIRMATION) {
updateState(STATE_AUTHENTICATED);
}
});
}
}
/**
@@ -706,21 +702,13 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onAttachedToWindowInternal();
}
/**
* Contains all the testable logic that should be invoked when {@link #onAttachedToWindow()} is
* invoked.
*/
@VisibleForTesting
void onAttachedToWindowInternal() {
mTitleView.setText(mPromptInfo.getTitle());
if (isDeviceCredentialAllowed()) {
final CharSequence credentialButtonText;
final @Utils.CredentialType int credentialType =
Utils.getCredentialType(mContext, mEffectiveUserId);
@Utils.CredentialType final int credentialType =
Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId);
switch (credentialType) {
case Utils.CREDENTIAL_PIN:
credentialButtonText =
@@ -731,9 +719,6 @@ public abstract class AuthBiometricView extends LinearLayout {
getResources().getString(R.string.biometric_dialog_use_pattern);
break;
case Utils.CREDENTIAL_PASSWORD:
credentialButtonText =
getResources().getString(R.string.biometric_dialog_use_password);
break;
default:
credentialButtonText =
getResources().getString(R.string.biometric_dialog_use_password);
@@ -749,7 +734,6 @@ public abstract class AuthBiometricView extends LinearLayout {
}
setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle());
setTextOrHide(mDescriptionView, mPromptInfo.getDescription());
if (mSavedState == null) {
@@ -774,6 +758,8 @@ public abstract class AuthBiometricView extends LinearLayout {
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIconController.setDeactivated(true);
// Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once
// the new dialog is restored.
mHandler.removeCallbacksAndMessages(null /* all */);
@@ -856,15 +842,7 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLayoutInternal();
}
/**
* Contains all the testable logic that should be invoked when
* {@link #onLayout(boolean, int, int, int, int)}, is invoked.
*/
@VisibleForTesting
void onLayoutInternal() {
// Start with initial size only once. Subsequent layout changes don't matter since we
// only care about the initial icon position.
if (mIconOriginalY == 0) {

View File

@@ -16,9 +16,10 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -52,13 +53,16 @@ import android.widget.LinearLayout;
import android.widget.ScrollView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Top level container/controller for the BiometricPrompt UI.
@@ -66,54 +70,52 @@ import java.util.List;
public class AuthContainerView extends LinearLayout
implements AuthDialog, WakefulnessLifecycle.Observer {
private static final String TAG = "BiometricPrompt/AuthContainerView";
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms
private static final String TAG = "AuthContainerView";
static final int STATE_UNKNOWN = 0;
static final int STATE_ANIMATING_IN = 1;
static final int STATE_PENDING_DISMISS = 2;
static final int STATE_SHOWING = 3;
static final int STATE_ANIMATING_OUT = 4;
static final int STATE_GONE = 5;
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350;
private static final int STATE_UNKNOWN = 0;
private static final int STATE_ANIMATING_IN = 1;
private static final int STATE_PENDING_DISMISS = 2;
private static final int STATE_SHOWING = 3;
private static final int STATE_ANIMATING_OUT = 4;
private static final int STATE_GONE = 5;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
STATE_ANIMATING_OUT, STATE_GONE})
@interface ContainerState {}
private @interface ContainerState {}
final Config mConfig;
final int mEffectiveUserId;
@Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps;
@Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
private final Config mConfig;
private final int mEffectiveUserId;
private final Handler mHandler;
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;
@VisibleForTesting final FrameLayout mFrameLayout;
@VisibleForTesting @Nullable AuthBiometricView mBiometricView;
@VisibleForTesting @Nullable AuthCredentialView mCredentialView;
@VisibleForTesting final ImageView mBackgroundView;
@VisibleForTesting final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
@VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
@VisibleForTesting final BiometricCallback mBiometricCallback;
@Nullable private AuthBiometricView mBiometricView;
@Nullable private AuthCredentialView mCredentialView;
private final AuthPanelController mPanelController;
private final FrameLayout mFrameLayout;
private final ImageView mBackgroundView;
private final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
@ContainerState private int mContainerState = STATE_UNKNOWN;
private final Set<Integer> mFailedModalities = new HashSet<Integer>();
// Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
@Nullable @AuthDialogCallback.DismissedReason Integer mPendingCallbackReason;
@Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
// HAT received from LockSettingsService when credential is verified.
@Nullable byte[] mCredentialAttestation;
@Nullable private byte[] mCredentialAttestation;
@VisibleForTesting
static class Config {
Context mContext;
AuthDialogCallback mCallback;
@@ -122,11 +124,11 @@ public class AuthContainerView extends LinearLayout
int mUserId;
String mOpPackageName;
int[] mSensorIds;
boolean mCredentialAllowed;
boolean mSkipIntro;
long mOperationId;
long mRequestId;
@BiometricMultiSensorMode int mMultiSensorConfig;
boolean mSkipAnimation = false;
@BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
}
public static class Builder {
@@ -167,7 +169,7 @@ public class AuthContainerView extends LinearLayout
return this;
}
public Builder setOperationId(long operationId) {
public Builder setOperationId(@DurationMillisLong long operationId) {
mConfig.mOperationId = operationId;
return this;
}
@@ -178,55 +180,27 @@ public class AuthContainerView extends LinearLayout
return this;
}
@VisibleForTesting
public Builder setSkipAnimationDuration(boolean skip) {
mConfig.mSkipAnimation = skip;
return this;
}
/** The multi-sensor mode. */
public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
mConfig.mMultiSensorConfig = multiSensorConfig;
return this;
}
public AuthContainerView build(int[] sensorIds, boolean credentialAllowed,
public AuthContainerView build(int[] sensorIds,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
WakefulnessLifecycle wakefulnessLifecycle) {
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils) {
mConfig.mSensorIds = sensorIds;
mConfig.mCredentialAllowed = credentialAllowed;
return new AuthContainerView(
mConfig, new Injector(), fpProps, faceProps, wakefulnessLifecycle);
}
}
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);
}
int getAnimateCredentialStartDelayMs() {
return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS;
}
UserManager getUserManager(Context context) {
return UserManager.get(context);
}
int getCredentialType(Context context, int effectiveUserId) {
return Utils.getCredentialType(context, effectiveUserId);
return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
userManager, lockPatternUtils, new Handler(Looper.getMainLooper()));
}
}
@@ -246,6 +220,7 @@ public class AuthContainerView extends LinearLayout
animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
break;
case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN:
mFailedModalities.clear();
mConfig.mCallback.onTryAgainPressed();
break;
case AuthBiometricView.Callback.ACTION_ERROR:
@@ -255,10 +230,7 @@ public class AuthContainerView extends LinearLayout
mConfig.mCallback.onDeviceCredentialPressed();
mHandler.postDelayed(() -> {
addCredentialView(false /* animatePanel */, true /* animateContents */);
}, mInjector.getAnimateCredentialStartDelayMs());
break;
case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR:
mConfig.mCallback.onStartFingerprintNow();
}, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
break;
default:
Log.e(TAG, "Unhandled action: " + action);
@@ -275,21 +247,19 @@ public class AuthContainerView extends LinearLayout
}
@VisibleForTesting
AuthContainerView(Config config, Injector injector,
AuthContainerView(Config config,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
WakefulnessLifecycle wakefulnessLifecycle) {
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull Handler mainHandler) {
super(config.mContext);
mConfig = config;
mInjector = injector;
mFpProps = fpProps;
mFaceProps = faceProps;
mEffectiveUserId = mInjector.getUserManager(mContext)
.getCredentialOwnerProfile(mConfig.mUserId);
mHandler = new Handler(Looper.getMainLooper());
mLockPatternUtils = lockPatternUtils;
mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
mHandler = mainHandler;
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -299,100 +269,42 @@ public class AuthContainerView extends LinearLayout
mBiometricCallback = new BiometricCallback();
mCredentialCallback = new CredentialCallback();
final LayoutInflater factory = LayoutInflater.from(mContext);
mFrameLayout = mInjector.inflateContainerView(factory, this);
mPanelView = mInjector.getPanelView(mFrameLayout);
mPanelController = mInjector.getPanelController(mContext, mPanelView);
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
mFrameLayout = (FrameLayout) layoutInflater.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
addView(mFrameLayout);
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
// Inflate biometric view only if necessary.
final int sensorCount = config.mSensorIds.length;
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
if (sensorCount == 1) {
final int singleSensorAuthId = config.mSensorIds[0];
if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) {
FingerprintSensorPropertiesInternal sensorProps = null;
for (FingerprintSensorPropertiesInternal prop : mFpProps) {
if (prop.sensorId == singleSensorAuthId) {
sensorProps = prop;
break;
}
}
final FingerprintSensorPropertiesInternal fpProperties =
Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds);
final FaceSensorPropertiesInternal faceProperties =
Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds);
if (sensorProps.isAnyUdfpsType()) {
AuthBiometricUdfpsView udfpsView = (AuthBiometricUdfpsView) factory
.inflate(R.layout.auth_biometric_udfps_view, null, false);
udfpsView.setSensorProps(sensorProps);
mBiometricView = udfpsView;
} else {
mBiometricView = (AuthBiometricFingerprintView) factory
.inflate(R.layout.auth_biometric_fingerprint_view, null, false);
}
} else if (Utils.containsSensorId(mFaceProps, singleSensorAuthId)) {
mBiometricView = (AuthBiometricFaceView)
factory.inflate(R.layout.auth_biometric_face_view, null, false);
} else {
// Unknown sensorId
Log.e(TAG, "Unknown sensorId: " + singleSensorAuthId);
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
}
} else if (sensorCount == 2) {
final int[] allSensors = findFaceAndFingerprintSensors();
final int faceSensorId = allSensors[0];
final int fingerprintSensorId = allSensors[1];
if (fingerprintSensorId == -1 || faceSensorId == -1) {
Log.e(TAG, "Missing fingerprint or face for dual-sensor config");
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
}
FingerprintSensorPropertiesInternal fingerprintSensorProps = null;
for (FingerprintSensorPropertiesInternal prop : mFpProps) {
if (prop.sensorId == fingerprintSensorId) {
fingerprintSensorProps = prop;
break;
}
}
if (fingerprintSensorProps != null) {
final AuthBiometricFaceToFingerprintView faceToFingerprintView =
(AuthBiometricFaceToFingerprintView) factory.inflate(
R.layout.auth_biometric_face_to_fingerprint_view, null, false);
faceToFingerprintView.setFingerprintSensorProps(fingerprintSensorProps);
faceToFingerprintView.setModalityListener(new ModalityListener() {
@Override
public void onModalitySwitched(int oldModality, int newModality) {
maybeUpdatePositionForUdfps(true /* invalidate */);
}
});
mBiometricView = faceToFingerprintView;
} else {
Log.e(TAG, "Fingerprint props not found for sensor ID: " + fingerprintSensorId);
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
}
if (fpProperties != null && faceProperties != null) {
final AuthBiometricFingerprintAndFaceView fingerprintAndFaceView =
(AuthBiometricFingerprintAndFaceView) layoutInflater.inflate(
R.layout.auth_biometric_fingerprint_and_face_view, null, false);
fingerprintAndFaceView.setSensorProperties(fpProperties);
mBiometricView = fingerprintAndFaceView;
} else if (fpProperties != null) {
final AuthBiometricFingerprintView fpView =
(AuthBiometricFingerprintView) layoutInflater.inflate(
R.layout.auth_biometric_fingerprint_view, null, false);
fpView.setSensorProperties(fpProperties);
mBiometricView = fpView;
} else if (faceProperties != null) {
mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate(
R.layout.auth_biometric_face_view, null, false);
} else {
Log.e(TAG, "Unsupported sensor array, length: " + sensorCount);
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
Log.e(TAG, "No sensors found!");
}
}
mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout);
mBackgroundView = mInjector.getBackgroundView(mFrameLayout);
addView(mFrameLayout);
// init view before showing
if (mBiometricView != null) {
mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
@@ -431,10 +343,6 @@ public class AuthContainerView extends LinearLayout
return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
}
private void addBiometricView() {
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,
@@ -444,8 +352,8 @@ public class AuthContainerView extends LinearLayout
private void addCredentialView(boolean animatePanel, boolean animateContents) {
final LayoutInflater factory = LayoutInflater.from(mContext);
final @Utils.CredentialType int credentialType = mInjector.getCredentialType(
mContext, mEffectiveUserId);
@Utils.CredentialType final int credentialType = Utils.getCredentialType(
mLockPatternUtils, mEffectiveUserId);
switch (credentialType) {
case Utils.CREDENTIAL_PATTERN:
@@ -493,15 +401,11 @@ public class AuthContainerView extends LinearLayout
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
onAttachedToWindowInternal();
}
@VisibleForTesting
void onAttachedToWindowInternal() {
mWakefulnessLifecycle.addObserver(this);
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
addBiometricView();
mBiometricScrollView.addView(mBiometricView);
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */);
} else {
@@ -521,17 +425,18 @@ public class AuthContainerView extends LinearLayout
mBiometricScrollView.setY(mTranslationY);
setAlpha(0f);
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
postOnAnimation(() -> {
mPanelView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.withEndAction(this::onDialogAnimatedIn)
.start();
mBiometricScrollView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
@@ -539,14 +444,14 @@ public class AuthContainerView extends LinearLayout
mCredentialView.setY(mTranslationY);
mCredentialView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
}
animate()
.alpha(1f)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
@@ -555,15 +460,8 @@ public class AuthContainerView extends LinearLayout
}
private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
if (view instanceof AuthBiometricUdfpsView) {
return true;
}
if (view instanceof AuthBiometricFaceToFingerprintView) {
AuthBiometricFaceToFingerprintView faceToFingerprintView =
(AuthBiometricFaceToFingerprintView) view;
return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT
&& faceToFingerprintView.isFingerprintUdfps();
if (view instanceof AuthBiometricFingerprintView) {
return ((AuthBiometricFingerprintView) view).isUdfps();
}
return false;
@@ -652,12 +550,13 @@ public class AuthContainerView extends LinearLayout
}
@Override
public void onAuthenticationSucceeded() {
mBiometricView.onAuthenticationSucceeded();
public void onAuthenticationSucceeded(@Modality int modality) {
mBiometricView.onAuthenticationSucceeded(modality);
}
@Override
public void onAuthenticationFailed(@Modality int modality, String failureReason) {
mFailedModalities.add(modality);
mBiometricView.onAuthenticationFailed(modality, failureReason);
}
@@ -671,9 +570,18 @@ public class AuthContainerView extends LinearLayout
mBiometricView.onError(modality, error);
}
@Override
public void onPointerDown() {
if (mBiometricView.onPointerDown(mFailedModalities)) {
Log.d(TAG, "retrying failed modalities (pointer down)");
mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
}
}
@Override
public void onSaveState(@NonNull Bundle outState) {
outState.putInt(AuthDialog.KEY_CONTAINER_STATE, mContainerState);
outState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY,
mContainerState == STATE_ANIMATING_OUT);
// 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,
@@ -695,8 +603,7 @@ public class AuthContainerView extends LinearLayout
mBiometricView.startTransitionToCredentialUI();
}
@VisibleForTesting
void animateAway(int reason) {
void animateAway(@AuthDialogCallback.DismissedReason int reason) {
animateAway(true /* sendReason */, reason);
}
@@ -724,31 +631,32 @@ public class AuthContainerView extends LinearLayout
removeWindowIfAttached();
};
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
postOnAnimation(() -> {
mPanelView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.withEndAction(endActionRunnable)
.start();
mBiometricScrollView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
mCredentialView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
}
animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
@@ -773,8 +681,7 @@ public class AuthContainerView extends LinearLayout
mWindowManager.removeView(this);
}
@VisibleForTesting
void onDialogAnimatedIn() {
private void onDialogAnimatedIn() {
if (mContainerState == STATE_PENDING_DISMISS) {
Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
@@ -788,8 +695,7 @@ public class AuthContainerView extends LinearLayout
}
@VisibleForTesting
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken,
CharSequence title) {
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
| WindowManager.LayoutParams.FLAG_SECURE;
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
@@ -805,24 +711,4 @@ public class AuthContainerView extends LinearLayout
lp.token = windowToken;
return lp;
}
// returns [face, fingerprint] sensor ids (id is -1 if not present)
private int[] findFaceAndFingerprintSensors() {
int faceSensorId = -1;
int fingerprintSensorId = -1;
for (final int sensorId : mConfig.mSensorIds) {
if (Utils.containsSensorId(mFpProps, sensorId)) {
fingerprintSensorId = sensorId;
} else if (Utils.containsSensorId(mFaceProps, sensorId)) {
faceSensorId = sensorId;
}
if (fingerprintSensorId != -1 && faceSensorId != -1) {
break;
}
}
return new int[] {faceSensorId, fingerprintSensorId};
}
}

View File

@@ -51,6 +51,7 @@ import android.hardware.fingerprint.IUdfpsHbmListener;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserManager;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.MotionEvent;
@@ -59,6 +60,7 @@ import android.view.WindowManager;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.CoreStartable;
import com.android.systemui.assist.ui.DisplayUtils;
import com.android.systemui.dagger.SysUISingleton;
@@ -123,8 +125,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
@Nullable private SidefpsController mSidefpsController;
@Nullable private IBiometricContextListener mBiometricContextListener;
@VisibleForTesting
TaskStackListener mTaskStackListener;
@VisibleForTesting
IBiometricSysuiReceiver mReceiver;
@VisibleForTesting
@NonNull final BiometricDisplayListener mOrientationListener;
@@ -137,13 +137,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private boolean mAllAuthenticatorsRegistered;
@NonNull private final UserManager mUserManager;
@NonNull private final LockPatternUtils mLockPatternUtils;
private class BiometricTaskStackListener extends TaskStackListener {
@VisibleForTesting
final TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override
public void onTaskStackChanged() {
mHandler.post(AuthController.this::handleTaskStackChanged);
}
}
};
private final IFingerprintAuthenticatorsRegisteredCallback
mFingerprintAuthenticatorsRegisteredCallback =
@@ -256,6 +259,17 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null;
if (mUdfpsProps != null) {
mUdfpsController = mUdfpsControllerFactory.get();
mUdfpsController.addCallback(new UdfpsController.Callback() {
@Override
public void onFingerUp() {}
@Override
public void onFingerDown() {
if (mCurrentDialog != null) {
mCurrentDialog.onPointerDown();
}
}
});
}
mSidefpsProps = !sidefpsProps.isEmpty() ? sidefpsProps : null;
if (mSidefpsProps != null) {
@@ -359,20 +373,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
}
}
@Override
public void onStartFingerprintNow() {
if (mReceiver == null) {
Log.e(TAG, "onStartUdfpsNow: Receiver is null");
return;
}
try {
mReceiver.onStartFingerprintNow();
} catch (RemoteException e) {
Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
}
}
@Override
public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) {
switch (reason) {
@@ -503,12 +503,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
Provider<UdfpsController> udfpsControllerFactory,
Provider<SidefpsController> sidefpsControllerFactory,
@NonNull DisplayManager displayManager,
WakefulnessLifecycle wakefulnessLifecycle,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull StatusBarStateController statusBarStateController,
@Main Handler handler) {
super(context);
mExecution = execution;
mWakefulnessLifecycle = wakefulnessLifecycle;
mUserManager = userManager;
mLockPatternUtils = lockPatternUtils;
mHandler = handler;
mCommandQueue = commandQueue;
mActivityTaskManager = activityTaskManager;
@@ -583,7 +587,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
mFingerprintAuthenticatorsRegisteredCallback);
}
mTaskStackListener = new BiometricTaskStackListener();
mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
}
@@ -668,11 +671,11 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
* example, KeyguardUpdateMonitor has its own {@link FingerprintManager.AuthenticationCallback}.
*/
@Override
public void onBiometricAuthenticated() {
public void onBiometricAuthenticated(@Modality int modality) {
if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: ");
if (mCurrentDialog != null) {
mCurrentDialog.onAuthenticationSucceeded();
mCurrentDialog.onAuthenticationSucceeded(modality);
} else {
Log.w(TAG, "onBiometricAuthenticated callback but dialog gone");
}
@@ -827,7 +830,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
final String opPackageName = (String) args.arg6;
final long operationId = args.argl1;
final long requestId = args.argl2;
final @BiometricMultiSensorMode int multiSensorConfig = args.argi2;
@BiometricMultiSensorMode final int multiSensorConfig = args.argi2;
// Create a new dialog but do not replace the current one yet.
final AuthDialog newDialog = buildDialog(
@@ -835,13 +838,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
requireConfirmation,
userId,
sensorIds,
credentialAllowed,
opPackageName,
skipAnimation,
operationId,
requestId,
multiSensorConfig,
mWakefulnessLifecycle);
mWakefulnessLifecycle,
mUserManager,
mLockPatternUtils);
if (newDialog == null) {
Log.e(TAG, "Unsupported type configuration");
@@ -902,8 +906,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
// Only show the dialog if necessary. If it was animating out, the dialog is supposed
// to send its pending callback immediately.
if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE)
!= AuthContainerView.STATE_ANIMATING_OUT) {
if (!savedState.getBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false)) {
final boolean credentialShowing =
savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING);
if (credentialShowing) {
@@ -927,10 +930,12 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
}
protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation,
int userId, int[] sensorIds, boolean credentialAllowed, String opPackageName,
int userId, int[] sensorIds, String opPackageName,
boolean skipIntro, long operationId, long requestId,
@BiometricMultiSensorMode int multiSensorConfig,
WakefulnessLifecycle wakefulnessLifecycle) {
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils) {
return new AuthContainerView.Builder(mContext)
.setCallback(this)
.setPromptInfo(promptInfo)
@@ -941,7 +946,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
.setOperationId(operationId)
.setRequestId(requestId)
.setMultiSensorConfig(multiSensorConfig)
.build(sensorIds, credentialAllowed, mFpProps, mFaceProps, wakefulnessLifecycle);
.build(sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle, userManager,
lockPatternUtils);
}
/**

View File

@@ -31,7 +31,7 @@ import java.lang.annotation.RetentionPolicy;
*/
public interface AuthDialog {
String KEY_CONTAINER_STATE = "container_state";
String KEY_CONTAINER_GOING_AWAY = "container_going_away";
String KEY_BIOMETRIC_SHOWING = "biometric_showing";
String KEY_CREDENTIAL_SHOWING = "credential_showing";
@@ -64,7 +64,7 @@ public interface AuthDialog {
@interface DialogSize {}
/**
* Parameters used when laying out {@link AuthBiometricView}, its sublclasses, and
* Parameters used when laying out {@link AuthBiometricView}, its subclasses, and
* {@link AuthPanelController}.
*/
class LayoutParams {
@@ -113,7 +113,7 @@ public interface AuthDialog {
/**
* Biometric authenticated. May be pending user confirmation, or completed.
*/
void onAuthenticationSucceeded();
void onAuthenticationSucceeded(@Modality int modality);
/**
* Authentication failed (reject, timeout). Dialog stays showing.
@@ -136,6 +136,9 @@ public interface AuthDialog {
*/
void onError(@Modality int modality, String error);
/** UDFPS pointer down event. */
void onPointerDown();
/**
* Save the current state.
* @param outState

View File

@@ -70,9 +70,4 @@ public interface AuthDialogCallback {
* Notifies when the dialog has finished animating.
*/
void onDialogAnimatedIn();
/**
* Notifies that the fingerprint sensor should be started now.
*/
void onStartFingerprintNow();
}

View File

@@ -23,7 +23,7 @@ import android.util.AttributeSet
*
* Currently doesn't draw anything.
*
* Note that [AuthBiometricUdfpsView] also shows UDFPS animations. At some point we should
* Note that [AuthBiometricFingerprintViewController] also shows UDFPS animations. At some point we should
* de-dupe this if necessary.
*/
class UdfpsBpView(context: Context, attrs: AttributeSet?) : UdfpsAnimationView(context, attrs) {

View File

@@ -1,130 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.biometrics.PromptInfo;
import android.hardware.biometrics.SensorPropertiesInternal;
import android.os.UserManager;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.widget.LockPatternUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
public class Utils {
public static final int CREDENTIAL_PIN = 1;
public static final int CREDENTIAL_PATTERN = 2;
public static final int CREDENTIAL_PASSWORD = 3;
/** Base set of layout flags for fingerprint overlay widgets. */
public static final int FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS =
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
@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);
}
static void notifyAccessibilityContentChanged(AccessibilityManager am, ViewGroup view) {
if (!am.isEnabled()) {
return;
}
AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
event.setContentChangeTypes(CONTENT_CHANGE_TYPE_SUBTREE);
view.sendAccessibilityEventUnchecked(event);
view.notifySubtreeAccessibilityStateChanged(view, view, CONTENT_CHANGE_TYPE_SUBTREE);
}
static boolean isDeviceCredentialAllowed(PromptInfo promptInfo) {
@Authenticators.Types final int authenticators = promptInfo.getAuthenticators();
return (authenticators & Authenticators.DEVICE_CREDENTIAL) != 0;
}
static boolean isBiometricAllowed(PromptInfo promptInfo) {
@Authenticators.Types final int authenticators = promptInfo.getAuthenticators();
return (authenticators & Authenticators.BIOMETRIC_WEAK) != 0;
}
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);
}
static boolean containsSensorId(@Nullable List<? extends SensorPropertiesInternal> properties,
int sensorId) {
if (properties == null) {
return false;
}
for (SensorPropertiesInternal prop : properties) {
if (prop.sensorId == sensorId) {
return true;
}
}
return false;
}
static boolean isSystem(@NonNull Context context, @Nullable String clientPackage) {
final boolean hasPermission = context.checkCallingOrSelfPermission(USE_BIOMETRIC_INTERNAL)
== PackageManager.PERMISSION_GRANTED;
return hasPermission && "android".equals(clientPackage);
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.Manifest
import android.annotation.IntDef
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.biometrics.BiometricManager.Authenticators
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorPropertiesInternal
import android.os.UserManager
import android.util.DisplayMetrics
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import com.android.internal.widget.LockPatternUtils
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
object Utils {
const val CREDENTIAL_PIN = 1
const val CREDENTIAL_PATTERN = 2
const val CREDENTIAL_PASSWORD = 3
/** Base set of layout flags for fingerprint overlay widgets. */
const val FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS =
(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
@JvmStatic
fun dpToPixels(context: Context, dp: Float): Float {
val density = context.resources.displayMetrics.densityDpi.toFloat()
return dp * (density / DisplayMetrics.DENSITY_DEFAULT)
}
@JvmStatic
fun notifyAccessibilityContentChanged(am: AccessibilityManager, view: ViewGroup) {
if (!am.isEnabled) {
return
}
val event = AccessibilityEvent.obtain()
event.eventType = AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
event.contentChangeTypes =
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
view.sendAccessibilityEventUnchecked(event)
view.notifySubtreeAccessibilityStateChanged(
view,
view,
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
)
}
@JvmStatic
fun isDeviceCredentialAllowed(promptInfo: PromptInfo): Boolean =
(promptInfo.authenticators and Authenticators.DEVICE_CREDENTIAL) != 0
@JvmStatic
fun isBiometricAllowed(promptInfo: PromptInfo): Boolean =
(promptInfo.authenticators and Authenticators.BIOMETRIC_WEAK) != 0
@JvmStatic
@CredentialType
fun getCredentialType(utils: LockPatternUtils, userId: Int): Int =
when (utils.getKeyguardStoredPasswordQuality(userId)) {
PASSWORD_QUALITY_SOMETHING -> CREDENTIAL_PATTERN
PASSWORD_QUALITY_NUMERIC,
PASSWORD_QUALITY_NUMERIC_COMPLEX -> CREDENTIAL_PIN
PASSWORD_QUALITY_ALPHABETIC,
PASSWORD_QUALITY_ALPHANUMERIC,
PASSWORD_QUALITY_COMPLEX,
PASSWORD_QUALITY_MANAGED -> CREDENTIAL_PASSWORD
else -> CREDENTIAL_PASSWORD
}
@JvmStatic
fun isManagedProfile(context: Context, userId: Int): Boolean =
context.getSystemService(UserManager::class.java)?.isManagedProfile(userId) ?: false
@JvmStatic
fun <T : SensorPropertiesInternal> findFirstSensorProperties(
properties: List<T>?,
sensorIds: IntArray
): T? = properties?.firstOrNull { sensorIds.contains(it.sensorId) }
@JvmStatic
fun isSystem(context: Context, clientPackage: String?): Boolean {
val hasPermission =
(context.checkCallingOrSelfPermission(Manifest.permission.USE_BIOMETRIC_INTERNAL)
== PackageManager.PERMISSION_GRANTED)
return hasPermission && "android" == clientPackage
}
@Retention(RetentionPolicy.SOURCE)
@IntDef(CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD)
internal annotation class CredentialType
}

View File

@@ -310,11 +310,11 @@ public class CommandQueue extends IStatusBar.Stub implements
long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
}
/** @see IStatusBar#onBiometricAuthenticated() */
default void onBiometricAuthenticated() {
/** @see IStatusBar#onBiometricAuthenticated(int) */
default void onBiometricAuthenticated(@Modality int modality) {
}
/** @see IStatusBar#onBiometricHelp(String) */
/** @see IStatusBar#onBiometricHelp(int, String) */
default void onBiometricHelp(@Modality int modality, String message) {
}
@@ -963,9 +963,11 @@ public class CommandQueue extends IStatusBar.Stub implements
}
@Override
public void onBiometricAuthenticated() {
public void onBiometricAuthenticated(@Modality int modality) {
synchronized (mLock) {
mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED).sendToTarget();
SomeArgs args = SomeArgs.obtain();
args.argi1 = modality;
mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget();
}
}
@@ -1465,9 +1467,11 @@ public class CommandQueue extends IStatusBar.Stub implements
break;
}
case MSG_BIOMETRIC_AUTHENTICATED: {
SomeArgs someArgs = (SomeArgs) msg.obj;
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onBiometricAuthenticated();
mCallbacks.get(i).onBiometricAuthenticated(someArgs.argi1 /* modality */);
}
someArgs.recycle();
break;
}
case MSG_BIOMETRIC_HELP: {

View File

@@ -1,344 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.hardware.biometrics.ComponentInfoInternal;
import android.hardware.biometrics.SensorLocationInternal;
import android.hardware.biometrics.SensorProperties;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@SmallTest
public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
@Mock AuthBiometricView.Callback mCallback;
private AuthBiometricFaceToFingerprintView mFaceToFpView;
@Mock private Button mNegativeButton;
@Mock private Button mCancelButton;
@Mock private Button mConfirmButton;
@Mock private Button mUseCredentialButton;
@Mock private Button mTryAgainButton;
@Mock private TextView mTitleView;
@Mock private TextView mSubtitleView;
@Mock private TextView mDescriptionView;
@Mock private TextView mIndicatorView;
@Mock private ImageView mIconView;
@Mock private View mIconHolderView;
@Mock private AuthBiometricFaceView.IconController mFaceIconController;
@Mock private AuthBiometricFaceToFingerprintView.UdfpsIconController mUdfpsIconController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mFaceToFpView = new TestableView(mContext);
mFaceToFpView.mFaceIconController = mFaceIconController;
mFaceToFpView.mUdfpsIconController = mUdfpsIconController;
mFaceToFpView.setCallback(mCallback);
mFaceToFpView.mNegativeButton = mNegativeButton;
mFaceToFpView.mCancelButton = mCancelButton;
mFaceToFpView.mUseCredentialButton = mUseCredentialButton;
mFaceToFpView.mConfirmButton = mConfirmButton;
mFaceToFpView.mTryAgainButton = mTryAgainButton;
mFaceToFpView.mIndicatorView = mIndicatorView;
}
@Test
public void testStateUpdated_whenDialogAnimatedIn() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt());
}
@Test
public void testIconUpdatesState_whenDialogStateUpdated() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt());
mFaceToFpView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED);
verify(mFaceToFpView.mFaceIconController).updateState(
eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING),
eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATED));
verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt());
assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATED, mFaceToFpView.mState);
}
@Test
public void testStateUpdated_whenSwitchToFingerprint() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR);
verify(mFaceToFpView.mFaceIconController).deactivate();
verify(mFaceToFpView.mUdfpsIconController).updateState(
eq(AuthBiometricFaceToFingerprintView.STATE_IDLE));
verify(mConfirmButton).setVisibility(eq(View.GONE));
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING);
verify(mFaceToFpView.mUdfpsIconController).updateState(
eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
}
@Test
public void testStateUpdated_whenSwitchToFingerprint_invokesCallbacks() {
class TestModalityListener implements ModalityListener {
public int switchCount = 0;
@Override
public void onModalitySwitched(int oldModality, int newModality) {
assertEquals(TYPE_FINGERPRINT, newModality);
assertEquals(TYPE_FACE, oldModality);
switchCount++;
}
}
final TestModalityListener modalityListener = new TestModalityListener();
mFaceToFpView.onDialogAnimatedIn();
mFaceToFpView.setModalityListener(modalityListener);
assertEquals(0, modalityListener.switchCount);
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR);
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING);
assertEquals(1, modalityListener.switchCount);
}
@Test
@Ignore("flaky, b/189031816")
public void testModeUpdated_onSoftError_whenSwitchToFingerprint() {
mFaceToFpView.onDialogAnimatedIn();
mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face");
waitForIdleSync();
verify(mIndicatorView).setText(
eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
verify(mCallback).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
// First we enter the error state, since we need to show the error animation/text. The
// error state is later cleared based on a timer, and we enter STATE_AUTHENTICATING.
assertEquals(AuthBiometricFaceToFingerprintView.STATE_ERROR, mFaceToFpView.mState);
}
@Test
@Ignore("flaky, b/189031816")
public void testModeUpdated_onHardError_whenSwitchToFingerprint() {
mFaceToFpView.onDialogAnimatedIn();
mFaceToFpView.onError(TYPE_FACE, "oh no!");
waitForIdleSync();
verify(mIndicatorView).setText(
eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
verify(mCallback).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
// First we enter the error state, since we need to show the error animation/text. The
// error state is later cleared based on a timer, and we enter STATE_AUTHENTICATING.
assertEquals(AuthBiometricFaceToFingerprintView.STATE_ERROR, mFaceToFpView.mState);
}
@Test
public void testFingerprintOnlyStartsOnFirstError() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
mFaceToFpView.onDialogAnimatedIn();
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR);
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING);
reset(mCallback);
mFaceToFpView.onError(TYPE_FACE, "oh no!");
mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face");
verify(mCallback, never()).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
}
@Test
public void testOnSaveState() {
final FingerprintSensorPropertiesInternal sensorProps = createFingerprintSensorProps();
mFaceToFpView.setFingerprintSensorProps(sensorProps);
final Bundle savedState = new Bundle();
mFaceToFpView.onSaveState(savedState);
assertEquals(savedState.getInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE),
mFaceToFpView.getActiveSensorType());
assertEquals(savedState.getParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS), sensorProps);
}
@Test
public void testRestoreState() {
final Bundle savedState = new Bundle();
savedState.putInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, TYPE_FINGERPRINT);
savedState.putParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS,
createFingerprintSensorProps());
mFaceToFpView.restoreState(savedState);
assertEquals(mFaceToFpView.getActiveSensorType(), TYPE_FINGERPRINT);
assertTrue(mFaceToFpView.isFingerprintUdfps());
}
@NonNull
private static FingerprintSensorPropertiesInternal createFingerprintSensorProps() {
final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
componentInfo.add(new ComponentInfoInternal("componentId", "hardwareVersion",
"firmwareVersion", "serialNumber", "softwareVersion"));
return new FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
componentInfo,
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
true /* resetLockoutRequiresHardwareAuthToken */,
List.of(new SensorLocationInternal("" /* displayId */,
540 /* sensorLocationX */,
1600 /* sensorLocationY */,
100 /* sensorRadius */)));
}
public class TestableView extends AuthBiometricFaceToFingerprintView {
public TestableView(Context context) {
super(context, null, new MockInjector());
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return 0;
}
}
private class MockInjector extends AuthBiometricView.Injector {
@Override
public Button getNegativeButton() {
return mNegativeButton;
}
@Override
public Button getCancelButton() {
return mCancelButton;
}
@Override
public Button getUseCredentialButton() {
return mUseCredentialButton;
}
@Override
public Button getConfirmButton() {
return mConfirmButton;
}
@Override
public Button getTryAgainButton() {
return mTryAgainButton;
}
@Override
public TextView getTitleView() {
return mTitleView;
}
@Override
public TextView getSubtitleView() {
return mSubtitleView;
}
@Override
public TextView getDescriptionView() {
return mDescriptionView;
}
@Override
public TextView getIndicatorView() {
return mIndicatorView;
}
@Override
public ImageView getIconView() {
return mIconView;
}
@Override
public View getIconHolderView() {
return mIconHolderView;
}
@Override
public int getDelayAfterError() {
return 0;
}
@Override
public int getMediumToLargeAnimationDurationMs() {
return 0;
}
}
}

View File

@@ -1,119 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.android.systemui.R;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class AuthBiometricFaceViewTest extends SysuiTestCase {
@Mock
AuthBiometricView.Callback mCallback;
private TestableFaceView mFaceView;
@Mock private Button mNegativeButton;
@Mock private Button mCancelButton;
@Mock private Button mUseCredentialButton;
@Mock private Button mConfirmButton;
@Mock private Button mTryAgainButton;
@Mock private TextView mErrorView;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mFaceView = new TestableFaceView(mContext);
mFaceView.mFaceIconController = mock(TestableFaceView.TestableIconController.class);
mFaceView.setCallback(mCallback);
mFaceView.mNegativeButton = mNegativeButton;
mFaceView.mCancelButton = mCancelButton;
mFaceView.mUseCredentialButton = mUseCredentialButton;
mFaceView.mConfirmButton = mConfirmButton;
mFaceView.mTryAgainButton = mTryAgainButton;
mFaceView.mIndicatorView = mErrorView;
}
@Test
public void testStateUpdated_whenDialogAnimatedIn() {
mFaceView.onDialogAnimatedIn();
verify(mFaceView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING));
}
@Test
public void testIconUpdatesState_whenDialogStateUpdated() {
mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATING);
verify(mFaceView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING));
mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED);
verify(mFaceView.mFaceIconController).updateState(
eq(AuthBiometricFaceView.STATE_AUTHENTICATING),
eq(AuthBiometricFaceView.STATE_AUTHENTICATED));
}
public class TestableFaceView extends AuthBiometricFaceView {
public class TestableIconController extends IconController {
TestableIconController(Context context, ImageView iconView) {
super(context, iconView, mock(TextView.class));
}
public void startPulsing() {
// Stub for testing
}
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return 0; // Keep this at 0 for tests to invoke callback immediately.
}
public TestableFaceView(Context context) {
super(context);
}
}
}

View File

@@ -20,137 +20,109 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import static com.android.systemui.biometrics.AuthBiometricView.Callback.ACTION_AUTHENTICATED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.PromptInfo;
import android.os.Bundle;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.util.AttributeSet;
import android.testing.ViewUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@Ignore
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class AuthBiometricViewTest extends SysuiTestCase {
@Mock private AuthBiometricView.Callback mCallback;
@Mock private AuthPanelController mPanelController;
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private Button mNegativeButton;
@Mock private Button mCancelButton;
@Mock private Button mUseCredentialButton;
@Mock
private AuthBiometricView.Callback mCallback;
@Mock
private AuthPanelController mPanelController;
@Mock private Button mPositiveButton;
@Mock private Button mTryAgainButton;
@Mock private TextView mTitleView;
@Mock private TextView mSubtitleView;
@Mock private TextView mDescriptionView;
@Mock private TextView mIndicatorView;
@Mock private ImageView mIconView;
@Mock private View mIconHolderView;
private TestableBiometricView mBiometricView;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
private AuthBiometricView mBiometricView;
@Test
public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
initDialog(false /* allowDeviceCredential */, mCallback);
// The onAuthenticated runnable is posted when authentication succeeds.
mBiometricView.onAuthenticationSucceeded();
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
waitForIdleSync();
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED);
verify(mCallback).onAction(ACTION_AUTHENTICATED);
}
@Test
public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() {
final Button negativeButton = new Button(mContext);
final Button cancelButton = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return negativeButton;
}
@Override
public Button getCancelButton() {
return cancelButton;
}
});
initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded();
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
waitForIdleSync();
assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState);
verify(mCallback, never()).onAction(anyInt());
assertEquals(View.GONE, negativeButton.getVisibility());
assertEquals(View.VISIBLE, cancelButton.getVisibility());
assertTrue(cancelButton.isEnabled());
// TODO: this should be tested in the subclasses
if (mBiometricView.supportsRequireConfirmation()) {
assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState);
verify(mCallback, never()).onAction(anyInt());
assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
assertEquals(View.VISIBLE, mBiometricView.mCancelButton.getVisibility());
assertTrue(mBiometricView.mCancelButton.isEnabled());
assertTrue(mBiometricView.mConfirmButton.isEnabled());
assertEquals(mContext.getText(R.string.biometric_dialog_tap_confirm),
mBiometricView.mIndicatorView.getText());
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
} else {
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
verify(mCallback).onAction(eq(ACTION_AUTHENTICATED));
}
verify(mBiometricView.mConfirmButton).setEnabled(eq(true));
verify(mIndicatorView).setText(eq(R.string.biometric_dialog_tap_confirm));
verify(mIndicatorView).setVisibility(eq(View.VISIBLE));
}
@Test
public void testPositiveButton_sendsActionAuthenticated() {
Button button = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getConfirmButton() {
return button;
}
});
initDialog(false /* allowDeviceCredential */, mCallback);
button.performClick();
mBiometricView.mConfirmButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED);
verify(mCallback).onAction(ACTION_AUTHENTICATED);
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
}
@Test
public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
Button button = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return button;
}
});
initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.onDialogAnimatedIn();
button.performClick();
mBiometricView.mNegativeButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE);
@@ -158,25 +130,14 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test
public void testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() {
Button cancelButton = new Button(mContext);
Button negativeButton = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return negativeButton;
}
@Override
public Button getCancelButton() {
return cancelButton;
}
});
initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded();
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
assertEquals(View.GONE, negativeButton.getVisibility());
assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
cancelButton.performClick();
mBiometricView.mCancelButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED);
@@ -184,15 +145,9 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test
public void testTryAgainButton_sendsActionTryAgain() {
Button button = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getTryAgainButton() {
return button;
}
});
initDialog(false /* allowDeviceCredential */, mCallback);
button.performClick();
mBiometricView.mTryAgainButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
@@ -202,7 +157,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test
@Ignore("flaky, b/189031816")
public void testError_sendsActionError() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
initDialog(false /* allowDeviceCredential */, mCallback);
final String testError = "testError";
mBiometricView.onError(TYPE_FACE, testError);
waitForIdleSync();
@@ -213,7 +168,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test
public void testBackgroundClicked_sendsActionUserCanceled() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
initDialog(false /* allowDeviceCredential */, mCallback);
View view = new View(mContext);
mBiometricView.setBackgroundView(view);
@@ -223,18 +178,18 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test
public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
initDialog(false /* allowDeviceCredential */, mCallback);
View view = new View(mContext);
mBiometricView.setBackgroundView(view);
mBiometricView.onAuthenticationSucceeded();
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
view.performClick();
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
}
@Test
public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.mLayoutParams = new AuthDialog.LayoutParams(0, 0);
mBiometricView.updateSize(AuthDialog.SIZE_SMALL);
@@ -246,7 +201,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test
public void testIgnoresUselessHelp() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.onDialogAnimatedIn();
waitForIdleSync();
@@ -256,33 +211,16 @@ public class AuthBiometricViewTest extends SysuiTestCase {
mBiometricView.onHelp(TYPE_FINGERPRINT, "");
waitForIdleSync();
verify(mIndicatorView, never()).setText(any());
assertEquals("", mBiometricView.mIndicatorView.getText());
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR));
assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState);
}
@Test
public void testRestoresState() {
final boolean requireConfirmation = true; // set/init from AuthController
final boolean requireConfirmation = true;
Button tryAgainButton = new Button(mContext);
TextView indicatorView = new TextView(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getTryAgainButton() {
return tryAgainButton;
}
@Override
public TextView getIndicatorView() {
return indicatorView;
}
@Override
public int getDelayAfterError() {
// keep a real delay to test saving in the error state
return BiometricPrompt.HIDE_DIALOG_DELAY;
}
});
initDialog(false /* allowDeviceCredential */, mCallback, null, 10000);
final String failureMessage = "testFailureMessage";
mBiometricView.setRequireConfirmation(requireConfirmation);
@@ -292,8 +230,8 @@ public class AuthBiometricViewTest extends SysuiTestCase {
Bundle state = new Bundle();
mBiometricView.onSaveState(state);
assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
assertEquals(View.VISIBLE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility());
assertEquals(View.GONE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
assertEquals(AuthBiometricView.STATE_ERROR, state.getInt(AuthDialog.KEY_BIOMETRIC_STATE));
@@ -307,25 +245,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
// TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle
// Create new dialog and restore the previous state into it
Button tryAgainButton2 = new Button(mContext);
TextView indicatorView2 = new TextView(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, state,
new MockInjector() {
@Override
public Button getTryAgainButton() {
return tryAgainButton2;
}
@Override
public TextView getIndicatorView() {
return indicatorView2;
}
});
initDialog(false /* allowDeviceCredential */, mCallback, state, 10000);
mBiometricView.mAnimationDurationHideDialog = 10000;
mBiometricView.setRequireConfirmation(requireConfirmation);
waitForIdleSync();
// Test restored state
assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility());
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
@@ -334,23 +259,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
}
@Test
public void testCredentialButton_whenDeviceCredentialAllowed() {
final Button negativeButton = new Button(mContext);
final Button useCredentialButton = new Button(mContext);
initDialog(mContext, true /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return negativeButton;
}
public void testCredentialButton_whenDeviceCredentialAllowed() throws InterruptedException {
initDialog(true /* allowDeviceCredential */, mCallback);
@Override
public Button getUseCredentialButton() {
return useCredentialButton;
}
});
assertEquals(View.GONE, negativeButton.getVisibility());
useCredentialButton.performClick();
assertEquals(View.VISIBLE, mBiometricView.mUseCredentialButton.getVisibility());
assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
mBiometricView.mUseCredentialButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL);
@@ -369,120 +283,30 @@ public class AuthBiometricViewTest extends SysuiTestCase {
return promptInfo;
}
private void initDialog(Context context, boolean allowDeviceCredential,
AuthBiometricView.Callback callback,
Bundle savedState, MockInjector injector) {
mBiometricView = new TestableBiometricView(context, null, injector);
private void initDialog(boolean allowDeviceCredential, AuthBiometricView.Callback callback) {
initDialog(allowDeviceCredential, callback,
null /* savedState */, 0 /* hideDelay */);
}
private void initDialog(boolean allowDeviceCredential,
AuthBiometricView.Callback callback, Bundle savedState, int hideDelay) {
final LayoutInflater inflater = LayoutInflater.from(mContext);
mBiometricView = (AuthBiometricView) inflater.inflate(
R.layout.auth_biometric_view, null, false);
mBiometricView.mAnimationDurationLong = 0;
mBiometricView.mAnimationDurationShort = 0;
mBiometricView.mAnimationDurationHideDialog = hideDelay;
mBiometricView.setPromptInfo(buildPromptInfo(allowDeviceCredential));
mBiometricView.setCallback(callback);
mBiometricView.restoreState(savedState);
mBiometricView.onFinishInflateInternal();
mBiometricView.onAttachedToWindowInternal();
ViewUtils.attachView(mBiometricView);
mBiometricView.setPanelController(mPanelController);
waitForIdleSync();
}
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 {
@Override
public Button getNegativeButton() {
return mNegativeButton;
}
@Override
public Button getCancelButton() {
return mCancelButton;
}
@Override
public Button getUseCredentialButton() {
return mUseCredentialButton;
}
@Override
public Button getConfirmButton() {
return mPositiveButton;
}
@Override
public Button getTryAgainButton() {
return mTryAgainButton;
}
@Override
public TextView getTitleView() {
return mTitleView;
}
@Override
public TextView getSubtitleView() {
return mSubtitleView;
}
@Override
public TextView getDescriptionView() {
return mDescriptionView;
}
@Override
public TextView getIndicatorView() {
return mIndicatorView;
}
@Override
public ImageView getIconView() {
return mIconView;
}
@Override
public View getIconHolderView() {
return mIconHolderView;
}
@Override
public int getDelayAfterError() {
return 0; // Keep this at 0 for tests to invoke callback immediately.
}
@Override
public int getMediumToLargeAnimationDurationMs() {
return 0;
}
}
private class TestableBiometricView extends AuthBiometricView {
TestableBiometricView(Context context, AttributeSet attrs,
Injector injector) {
super(context, attrs, injector);
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return 0; // Keep this at 0 for tests to invoke callback immediately.
}
@Override
protected int getStateForAfterError() {
return 0;
}
@Override
protected void handleResetAfterError() {
}
@Override
protected void handleResetAfterHelp() {
}
@Override
protected boolean supportsSmallDialog() {
return false;
}
@Override
protected void waitForIdleSync() {
TestableLooper.get(this).processAllMessages();
super.waitForIdleSync();
}
}

View File

@@ -1,329 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
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.anyInt;
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 static org.mockito.Mockito.when;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.ComponentInfoInternal;
import android.hardware.biometrics.PromptInfo;
import android.hardware.biometrics.SensorProperties;
import android.hardware.face.FaceSensorPropertiesInternal;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.IBinder;
import android.os.UserManager;
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.view.WindowInsets;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ScrollView;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class AuthContainerViewTest extends SysuiTestCase {
private TestableAuthContainer mAuthContainer;
private @Mock AuthDialogCallback mCallback;
private @Mock UserManager mUserManager;
private @Mock WakefulnessLifecycle mWakefulnessLifecycle;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testActionAuthenticated_sendsDismissedAuthenticated() {
initializeContainer(Authenticators.BIOMETRIC_WEAK);
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_AUTHENTICATED);
verify(mCallback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED),
eq(null) /* credentialAttestation */);
}
@Test
public void testActionUserCanceled_sendsDismissedUserCanceled() {
initializeContainer(Authenticators.BIOMETRIC_WEAK);
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USER_CANCELED);
verify(mCallback).onSystemEvent(eq(
BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL));
verify(mCallback).onDismissed(
eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
eq(null) /* credentialAttestation */);
}
@Test
public void testActionButtonNegative_sendsDismissedButtonNegative() {
initializeContainer(Authenticators.BIOMETRIC_WEAK);
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE);
verify(mCallback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE),
eq(null) /* credentialAttestation */);
}
@Test
public void testActionTryAgain_sendsTryAgain() {
initializeContainer(Authenticators.BIOMETRIC_WEAK);
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
verify(mCallback).onTryAgainPressed();
}
@Test
public void testActionError_sendsDismissedError() {
initializeContainer(Authenticators.BIOMETRIC_WEAK);
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_ERROR);
verify(mCallback).onDismissed(
eq(AuthDialogCallback.DISMISSED_ERROR),
eq(null) /* credentialAttestation */);
}
@Test
public void testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
initializeContainer(
Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_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(
Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL);
mAuthContainer.mBiometricView = mock(AuthBiometricView.class);
mAuthContainer.animateToCredentialUI();
verify(mAuthContainer.mBiometricView).startTransitionToCredentialUI();
}
@Test
public void testShowBiometricUI() {
initializeContainer(Authenticators.BIOMETRIC_WEAK);
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(Authenticators.DEVICE_CREDENTIAL);
mAuthContainer.onAttachedToWindowInternal();
assertNull(null, mAuthContainer.mBiometricView);
assertNotNull(mAuthContainer.mCredentialView);
verify(mAuthContainer.mFrameLayout).addView(mAuthContainer.mCredentialView);
}
@Test
public void testCredentialViewUsesEffectiveUserId() {
final int dummyEffectiveUserId = 200;
when(mUserManager.getCredentialOwnerProfile(anyInt())).thenReturn(dummyEffectiveUserId);
initializeContainer(Authenticators.DEVICE_CREDENTIAL);
mAuthContainer.onAttachedToWindowInternal();
assertTrue(mAuthContainer.mCredentialView instanceof AuthCredentialPatternView);
assertEquals(dummyEffectiveUserId, mAuthContainer.mCredentialView.mEffectiveUserId);
assertEquals(Utils.CREDENTIAL_PATTERN, mAuthContainer.mCredentialView.mCredentialType);
}
@Test
public void testCredentialUI_disablesClickingOnBackground() {
// In the credential view, clicking on the background (to cancel authentication) is not
// valid. Thus, the listener should be null, and it should not be in the accessibility
// hierarchy.
initializeContainer(Authenticators.DEVICE_CREDENTIAL);
mAuthContainer.onAttachedToWindowInternal();
verify(mAuthContainer.mBackgroundView).setOnClickListener(eq(null));
verify(mAuthContainer.mBackgroundView).setImportantForAccessibility(
eq(View.IMPORTANT_FOR_ACCESSIBILITY_NO));
}
@Test
public void testOnDialogAnimatedIn_sendsCancelReason_whenPendingDismiss() {
initializeContainer(Authenticators.BIOMETRIC_WEAK);
mAuthContainer.mContainerState = AuthContainerView.STATE_PENDING_DISMISS;
mAuthContainer.onDialogAnimatedIn();
verify(mCallback).onDismissed(
eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
eq(null) /* credentialAttestation */);
}
@Test
public void testLayoutParams_hasSecureWindowFlag() {
final IBinder windowToken = mock(IBinder.class);
final WindowManager.LayoutParams layoutParams =
AuthContainerView.getLayoutParams(windowToken, "");
assertTrue((layoutParams.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0);
}
@Test
public void testLayoutParams_excludesImeInsets() {
final IBinder windowToken = mock(IBinder.class);
final WindowManager.LayoutParams layoutParams =
AuthContainerView.getLayoutParams(windowToken, "");
assertTrue((layoutParams.getFitInsetsTypes() & WindowInsets.Type.ime()) == 0);
}
private void initializeContainer(int authenticators) {
AuthContainerView.Config config = new AuthContainerView.Config();
config.mContext = mContext;
config.mCallback = mCallback;
config.mSensorIds = new int[] {0};
config.mCredentialAllowed = false;
PromptInfo promptInfo = new PromptInfo();
promptInfo.setAuthenticators(authenticators);
config.mPromptInfo = promptInfo;
final List<FingerprintSensorPropertiesInternal> fpProps = new ArrayList<>();
final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */,
"vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
"00000001" /* serialNumber */, "" /* softwareVersion */));
componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */,
"" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
"vendor/version/revision" /* softwareVersion */));
fpProps.add(new FingerprintSensorPropertiesInternal(0,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
componentInfo,
FingerprintSensorProperties.TYPE_REAR,
false /* resetLockoutRequiresHardwareAuthToken */));
mAuthContainer = new TestableAuthContainer(config, fpProps, null /* faceProps */,
mWakefulnessLifecycle);
}
private class TestableAuthContainer extends AuthContainerView {
TestableAuthContainer(AuthContainerView.Config config,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
WakefulnessLifecycle wakefulnessLifecycle) {
super(config, new MockInjector(), fpProps, faceProps, wakefulnessLifecycle);
}
@Override
public void animateAway(int reason) {
// TODO: Credential attestation should be testable/tested
mConfig.mCallback.onDismissed(reason, null /* credentialAttestation */);
}
}
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);
}
@Override
public int getAnimateCredentialStartDelayMs() {
return 0;
}
@Override
public UserManager getUserManager(Context context) {
return mUserManager;
}
@Override
public @Utils.CredentialType int getCredentialType(Context context, int effectiveUserId) {
return Utils.CREDENTIAL_PATTERN;
}
}
}

View File

@@ -0,0 +1,324 @@
/*
* Copyright (C) 2022 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.app.admin.DevicePolicyManager
import android.hardware.biometrics.BiometricConstants
import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorProperties
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.os.Handler
import android.os.IBinder
import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.testing.ViewUtils
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
import android.widget.ScrollView
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.google.common.truth.Truth.assertThat
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.eq
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
@Ignore
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
@SmallTest
class AuthContainerViewTest : SysuiTestCase() {
@JvmField @Rule
var rule = MockitoJUnit.rule()
@Mock
lateinit var callback: AuthDialogCallback
@Mock
lateinit var userManager: UserManager
@Mock
lateinit var lockPatternUtils: LockPatternUtils
@Mock
lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Mock
lateinit var windowToken: IBinder
private lateinit var authContainer: TestAuthContainerView
@Test
fun testActionAuthenticated_sendsDismissedAuthenticated() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_AUTHENTICATED
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionUserCanceled_sendsDismissedUserCanceled() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USER_CANCELED
)
waitForIdleSync()
verify(callback).onSystemEvent(
eq(BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL)
)
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionButtonNegative_sendsDismissedButtonNegative() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionTryAgain_sendsTryAgain() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN
)
waitForIdleSync()
verify(callback).onTryAgainPressed()
}
@Test
fun testActionError_sendsDismissedError() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_ERROR
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_ERROR),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
initializeContainer(
BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL
)
waitForIdleSync()
verify(callback).onDeviceCredentialPressed()
assertThat(authContainer.hasCredentialView()).isTrue()
}
@Test
fun testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() {
initializeContainer(
BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
authContainer.animateToCredentialUI()
waitForIdleSync()
assertThat(authContainer.hasCredentialView()).isTrue()
}
@Test
fun testShowBiometricUI() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
waitForIdleSync()
assertThat(authContainer.hasCredentialView()).isFalse()
assertThat(authContainer.hasBiometricPrompt()).isTrue()
}
@Test
fun testShowCredentialUI() {
initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
waitForIdleSync()
assertThat(authContainer.hasCredentialView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
}
@Test
fun testCredentialViewUsesEffectiveUserId() {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200)
whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200))).thenReturn(
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
)
initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
waitForIdleSync()
assertThat(authContainer.hasCredentialPatternView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
}
@Test
fun testCredentialUI_disablesClickingOnBackground() {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn(
DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
)
// In the credential view, clicking on the background (to cancel authentication) is not
// valid. Thus, the listener should be null, and it should not be in the accessibility
// hierarchy.
initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
waitForIdleSync()
assertThat(authContainer.hasCredentialPasswordView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
assertThat(
authContainer.findViewById<View>(R.id.background)?.isImportantForAccessibility
).isFalse()
authContainer.findViewById<View>(R.id.background)?.performClick()
waitForIdleSync()
assertThat(authContainer.hasCredentialPasswordView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
}
@Test
fun testLayoutParams_hasSecureWindowFlag() {
val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0).isTrue()
}
@Test
fun testLayoutParams_excludesImeInsets() {
val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.ime()) == 0).isTrue()
}
private fun initializeContainer(authenticators: Int) {
val config = AuthContainerView.Config()
config.mContext = mContext
config.mCallback = callback
config.mSensorIds = intArrayOf(0)
config.mSkipAnimation = true
config.mPromptInfo = PromptInfo()
config.mPromptInfo.authenticators = authenticators
val componentInfo = listOf(
ComponentInfoInternal(
"faceSensor" /* componentId */,
"vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
"00000001" /* serialNumber */, "" /* softwareVersion */
),
ComponentInfoInternal(
"matchingAlgorithm" /* componentId */,
"" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
"vendor/version/revision" /* softwareVersion */
)
)
val fpProps = listOf(
FingerprintSensorPropertiesInternal(
0,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
componentInfo,
FingerprintSensorProperties.TYPE_REAR,
false /* resetLockoutRequiresHardwareAuthToken */
)
)
authContainer = TestAuthContainerView(
config,
fpProps,
listOf(),
wakefulnessLifecycle,
userManager,
lockPatternUtils,
Handler(TestableLooper.get(this).looper)
)
ViewUtils.attachView(authContainer)
}
private inner class TestAuthContainerView(
config: Config,
fpProps: List<FingerprintSensorPropertiesInternal>,
faceProps: List<FaceSensorPropertiesInternal>,
wakefulnessLifecycle: WakefulnessLifecycle,
userManager: UserManager,
lockPatternUtils: LockPatternUtils,
mainHandler: Handler
) : AuthContainerView(
config, fpProps, faceProps,
wakefulnessLifecycle, userManager, lockPatternUtils, mainHandler
) {
override fun postOnAnimation(runnable: Runnable) {
runnable.run()
}
}
override fun waitForIdleSync() {
TestableLooper.get(this).processAllMessages()
super.waitForIdleSync()
}
}
private fun AuthContainerView.hasBiometricPrompt() =
(findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0
private fun AuthContainerView.hasCredentialView() =
hasCredentialPatternView() || hasCredentialPasswordView()
private fun AuthContainerView.hasCredentialPatternView() =
findViewById<View>(R.id.lockPattern) != null
private fun AuthContainerView.hasCredentialPasswordView() =
findViewById<View>(R.id.lockPassword) != null

View File

@@ -16,8 +16,9 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
@@ -62,6 +63,7 @@ import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
@@ -71,6 +73,7 @@ import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -79,6 +82,8 @@ import com.android.systemui.util.concurrency.Execution;
import com.android.systemui.util.concurrency.FakeExecution;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.AdditionalMatchers;
@@ -86,7 +91,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.List;
@@ -94,11 +100,15 @@ import java.util.Random;
import javax.inject.Provider;
@Ignore
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class AuthControllerTest extends SysuiTestCase {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
private PackageManager mPackageManager;
@Mock
@@ -128,6 +138,10 @@ public class AuthControllerTest extends SysuiTestCase {
@Mock
private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock
private UserManager mUserManager;
@Mock
private LockPatternUtils mLockPatternUtils;
@Mock
private StatusBarStateController mStatusBarStateController;
@Captor
ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mAuthenticatorsRegisteredCaptor;
@@ -144,8 +158,6 @@ public class AuthControllerTest extends SysuiTestCase {
@Before
public void setup() throws RemoteException {
MockitoAnnotations.initMocks(this);
mContextSpy = spy(mContext);
mExecution = new FakeExecution();
mTestableLooper = TestableLooper.get(this);
@@ -343,8 +355,8 @@ public class AuthControllerTest extends SysuiTestCase {
@Test
public void testOnAuthenticationSucceededInvoked_whenSystemRequested() {
showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
mAuthController.onBiometricAuthenticated();
verify(mDialog1).onAuthenticationSucceeded();
mAuthController.onBiometricAuthenticated(TYPE_FINGERPRINT);
verify(mDialog1).onAuthenticationSucceeded(eq(TYPE_FINGERPRINT));
}
@Test
@@ -528,8 +540,7 @@ public class AuthControllerTest extends SysuiTestCase {
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
Bundle savedState = (Bundle) args[0];
savedState.putInt(
AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING);
savedState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false);
return null; // onSaveState returns void
}).when(mDialog1).onSaveState(any());
@@ -558,8 +569,7 @@ public class AuthControllerTest extends SysuiTestCase {
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
Bundle savedState = (Bundle) args[0];
savedState.putInt(
AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING);
savedState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false);
savedState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, true);
return null; // onSaveState returns void
}).when(mDialog1).onSaveState(any());
@@ -697,7 +707,7 @@ public class AuthControllerTest extends SysuiTestCase {
0 /* operationId */,
"testPackage",
1 /* requestId */,
BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT);
BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE);
}
private PromptInfo createTestPromptInfo() {
@@ -739,15 +749,16 @@ public class AuthControllerTest extends SysuiTestCase {
super(context, execution, commandQueue, activityTaskManager, windowManager,
fingerprintManager, faceManager, udfpsControllerFactory,
sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle,
statusBarStateController, mHandler);
mUserManager, mLockPatternUtils, statusBarStateController, mHandler);
}
@Override
protected AuthDialog buildDialog(PromptInfo promptInfo,
boolean requireConfirmation, int userId, int[] sensorIds, boolean credentialAllowed,
boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
@BiometricManager.BiometricMultiSensorMode int multiSensorConfig,
WakefulnessLifecycle wakefulnessLifecycle) {
WakefulnessLifecycle wakefulnessLifecycle, UserManager userManager,
LockPatternUtils lockPatternUtils) {
mLastBiometricPromptInfo = promptInfo;

View File

@@ -17,10 +17,10 @@
package com.android.systemui.biometrics
import android.animation.Animator
import android.graphics.Insets
import android.app.ActivityManager
import android.app.ActivityTaskManager
import android.content.ComponentName
import android.graphics.Insets
import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -65,8 +65,8 @@ import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset

View File

@@ -57,9 +57,9 @@ import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)

View File

@@ -38,11 +38,11 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.nullable
import org.mockito.Mockito.never
import org.mockito.Mockito.`when` as whenever
import org.mockito.Mockito.nullable
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
private const val DISPLAY_ID = "" // default display id
private const val SENSOR_X = 50

View File

@@ -448,9 +448,10 @@ public class CommandQueueTest extends SysuiTestCase {
@Test
public void testOnBiometricAuthenticated() {
mCommandQueue.onBiometricAuthenticated();
final int id = 12;
mCommandQueue.onBiometricAuthenticated(id);
waitForIdleSync();
verify(mCallbacks).onBiometricAuthenticated();
verify(mCallbacks).onBiometricAuthenticated(eq(id));
}
@Test

View File

@@ -20,12 +20,8 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FACE_SCANNING;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FP_SCANNING;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_SWITCHING;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_UNKNOWN;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE;
@@ -100,14 +96,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
@Retention(RetentionPolicy.SOURCE)
@interface SessionState {}
/** Defined in biometrics.proto */
@IntDef({
MULTI_SENSOR_STATE_UNKNOWN,
MULTI_SENSOR_STATE_FACE_SCANNING,
MULTI_SENSOR_STATE_FP_SCANNING})
@Retention(RetentionPolicy.SOURCE)
@interface MultiSensorState {}
/**
* Notify the holder of the AuthSession that the caller/client's binder has died. The
* holder (BiometricService) should schedule {@link AuthSession#onClientDied()} to be run
@@ -119,7 +107,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
private final Context mContext;
private final IStatusBarService mStatusBarService;
private final IBiometricSysuiReceiver mSysuiReceiver;
@VisibleForTesting final IBiometricSysuiReceiver mSysuiReceiver;
private final KeyStore mKeyStore;
private final Random mRandom;
private final ClientDeathReceiver mClientDeathReceiver;
@@ -133,7 +121,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
private final long mRequestId;
private final long mOperationId;
private final int mUserId;
private final IBiometricSensorReceiver mSensorReceiver;
@VisibleForTesting final IBiometricSensorReceiver mSensorReceiver;
// Original receiver from BiometricPrompt.
private final IBiometricServiceReceiver mClientReceiver;
private final String mOpPackageName;
@@ -143,10 +131,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
// The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE;
private @BiometricMultiSensorMode int mMultiSensorMode;
private @MultiSensorState int mMultiSensorState;
private int[] mSensors;
// TODO(b/197265902): merge into state
private boolean mCancelled;
private int mAuthenticatedSensorId = -1;
// For explicit confirmation, do not send to keystore until the user has confirmed
// the authentication.
private byte[] mTokenEscrow;
@@ -232,8 +220,16 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
}
private void setSensorsToStateWaitingForCookie() throws RemoteException {
private void setSensorsToStateWaitingForCookie(boolean isTryAgain) throws RemoteException {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
@BiometricSensor.SensorState final int state = sensor.getSensorState();
if (isTryAgain
&& state != BiometricSensor.STATE_STOPPED
&& state != BiometricSensor.STATE_CANCELING) {
Slog.d(TAG, "Skip retry because sensor: " + sensor.id + " is: " + state);
continue;
}
final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
final boolean requireConfirmation = isConfirmationRequired(sensor);
@@ -254,7 +250,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mSensors = new int[0];
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mStatusBarService.showAuthenticationDialog(
mPromptInfo,
@@ -269,7 +264,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
mMultiSensorMode);
} else if (!mPreAuthInfo.eligibleSensors.isEmpty()) {
// Some combination of biometric or biometric|credential is requested
setSensorsToStateWaitingForCookie();
setSensorsToStateWaitingForCookie(false /* isTryAgain */);
mState = STATE_AUTH_CALLED;
} else {
// No authenticators requested. This should never happen - an exception should have
@@ -283,6 +278,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
Slog.w(TAG, "Received cookie but already cancelled (ignoring): " + cookie);
return;
}
if (hasAuthenticated()) {
Slog.d(TAG, "onCookieReceived after successful auth");
return;
}
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
sensor.goToStateCookieReturnedIfCookieMatches(cookie);
@@ -307,7 +306,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
mMultiSensorMode = getMultiSensorModeForNewSession(
mPreAuthInfo.eligibleSensors);
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mStatusBarService.showAuthenticationDialog(mPromptInfo,
mSysuiReceiver,
@@ -381,9 +379,8 @@ public final class AuthSession implements IBinder.DeathRecipient {
// sending the final error callback to the application.
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
try {
final boolean shouldCancel = filter.apply(sensor);
Slog.d(TAG, "sensorId: " + sensor.id + ", shouldCancel: " + shouldCancel);
if (shouldCancel) {
if (filter.apply(sensor)) {
Slog.d(TAG, "Cancelling sensorId: " + sensor.id);
sensor.goToStateCancelling(mToken, mOpPackageName, mRequestId);
}
} catch (RemoteException e) {
@@ -412,10 +409,16 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
}
// do not propagate the error and let onAuthenticationSucceeded handle the new state
if (hasAuthenticated()) {
Slog.d(TAG, "onErrorReceived after successful auth (ignoring)");
return false;
}
mErrorEscrow = error;
mVendorCodeEscrow = vendorCode;
final @BiometricAuthenticator.Modality int modality = sensorIdToModality(sensorId);
@Modality final int modality = sensorIdToModality(sensorId);
switch (mState) {
case STATE_AUTH_CALLED: {
@@ -430,7 +433,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mSensors = new int[0];
mStatusBarService.showAuthenticationDialog(
@@ -468,12 +470,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
return true;
} else {
mState = STATE_ERROR_PENDING_SYSUI;
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT
&& mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING) {
// wait for the UI to signal when modality should switch
Slog.d(TAG, "onErrorReceived: waiting for modality switch callback");
mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING;
}
mStatusBarService.onBiometricError(modality, error, vendorCode);
}
break;
@@ -505,6 +501,11 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
if (hasAuthenticated()) {
Slog.d(TAG, "onAcquired after successful auth");
return;
}
final String message = getAcquiredMessageForSensor(sensorId, acquiredInfo, vendorCode);
Slog.d(TAG, "sensorId: " + sensorId + " acquiredInfo: " + acquiredInfo
+ " message: " + message);
@@ -520,6 +521,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
void onSystemEvent(int event) {
if (hasAuthenticated()) {
Slog.d(TAG, "onSystemEvent after successful auth");
return;
}
if (!mPromptInfo.isReceiveSystemEvents()) {
return;
}
@@ -538,53 +543,35 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_AUTH_STARTED_UI_SHOWING;
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
mMultiSensorState = MULTI_SENSOR_STATE_FACE_SCANNING;
} else {
startFingerprintSensorsNow();
}
}
// call anytime after onDialogAnimatedIn() to indicate it's appropriate to start the
// fingerprint sensor (i.e. face auth has failed or is not available)
void onStartFingerprint() {
if (mMultiSensorMode != BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
Slog.e(TAG, "onStartFingerprint, unexpected mode: " + mMultiSensorMode);
return;
}
if (mState != STATE_AUTH_STARTED
&& mState != STATE_AUTH_STARTED_UI_SHOWING
&& mState != STATE_AUTH_PAUSED
&& mState != STATE_ERROR_PENDING_SYSUI) {
Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
}
mMultiSensorState = MULTI_SENSOR_STATE_FP_SCANNING;
startFingerprintSensorsNow();
}
// unguarded helper for the above methods only
private void startFingerprintSensorsNow() {
startAllPreparedFingerprintSensors();
mState = STATE_AUTH_STARTED_UI_SHOWING;
}
void onTryAgainPressed() {
if (hasAuthenticated()) {
Slog.d(TAG, "onTryAgainPressed after successful auth");
return;
}
if (mState != STATE_AUTH_PAUSED) {
Slog.w(TAG, "onTryAgainPressed, state: " + mState);
}
try {
setSensorsToStateWaitingForCookie();
setSensorsToStateWaitingForCookie(true /* isTryAgain */);
mState = STATE_AUTH_PAUSED_RESUMING;
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException: " + e);
}
}
void onAuthenticationSucceeded(int sensorId, boolean strong,
byte[] token) {
void onAuthenticationSucceeded(int sensorId, boolean strong, byte[] token) {
if (hasAuthenticated()) {
Slog.d(TAG, "onAuthenticationSucceeded after successful auth");
return;
}
mAuthenticatedSensorId = sensorId;
if (strong) {
mTokenEscrow = token;
} else {
@@ -596,7 +583,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
try {
// Notify SysUI that the biometric has been authenticated. SysUI already knows
// the implicit/explicit state and will react accordingly.
mStatusBarService.onBiometricAuthenticated();
mStatusBarService.onBiometricAuthenticated(sensorIdToModality(sensorId));
final boolean requireConfirmation = isConfirmationRequiredByAnyEligibleSensor();
@@ -609,20 +596,22 @@ public final class AuthSession implements IBinder.DeathRecipient {
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
cancelAllSensors(sensor -> sensor.id != sensorId);
}
void onAuthenticationRejected() {
try {
mStatusBarService.onBiometricError(TYPE_NONE,
BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */);
void onAuthenticationRejected(int sensorId) {
if (hasAuthenticated()) {
Slog.d(TAG, "onAuthenticationRejected after successful auth");
return;
}
// TODO: This logic will need to be updated if BP is multi-modal
if (hasPausableBiometric()) {
// Pause authentication. onBiometricAuthenticated(false) causes the
// dialog to show a "try again" button for passive modalities.
try {
mStatusBarService.onBiometricError(sensorIdToModality(sensorId),
BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */);
if (pauseSensorIfSupported(sensorId)) {
mState = STATE_AUTH_PAUSED;
}
mClientReceiver.onAuthenticationFailed();
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
@@ -630,15 +619,34 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
void onAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) {
if (hasAuthenticated()) {
Slog.d(TAG, "onAuthenticationTimedOut after successful auth");
return;
}
try {
mStatusBarService.onBiometricError(sensorIdToModality(sensorId), error, vendorCode);
pauseSensorIfSupported(sensorId);
mState = STATE_AUTH_PAUSED;
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
}
private boolean pauseSensorIfSupported(int sensorId) {
if (sensorIdToModality(sensorId) == TYPE_FACE) {
cancelAllSensors(sensor -> sensor.id == sensorId);
return true;
}
return false;
}
void onDeviceCredentialPressed() {
if (hasAuthenticated()) {
Slog.d(TAG, "onDeviceCredentialPressed after successful auth");
return;
}
// Cancel authentication. Skip the token/package check since we are cancelling
// from system server. The interface is permission protected so this is fine.
cancelAllSensors();
@@ -666,6 +674,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
}
private boolean hasAuthenticated() {
return mAuthenticatedSensorId != -1;
}
private void logOnDialogDismissed(@BiometricPrompt.DismissedReason int reason) {
if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) {
// Explicit auth, authentication confirmed.
@@ -794,6 +806,11 @@ public final class AuthSession implements IBinder.DeathRecipient {
* @return true if this AuthSession is finished, e.g. should be set to null
*/
boolean onCancelAuthSession(boolean force) {
if (hasAuthenticated()) {
Slog.d(TAG, "onCancelAuthSession after successful auth");
return true;
}
mCancelled = true;
final boolean authStarted = mState == STATE_AUTH_CALLED
@@ -848,15 +865,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
return remainingCookies == 0;
}
private boolean hasPausableBiometric() {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
if (sensor.modality == TYPE_FACE) {
return true;
}
}
return false;
}
@SessionState int getState() {
return mState;
}
@@ -919,7 +927,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
if (hasFace && hasFingerprint) {
return BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
}
return BIOMETRIC_MULTI_SENSOR_DEFAULT;
}

View File

@@ -131,8 +131,10 @@ public abstract class BiometricSensor {
void goToStateCancelling(IBinder token, String opPackageName, long requestId)
throws RemoteException {
impl.cancelAuthenticationFromService(token, opPackageName, requestId);
mSensorState = STATE_CANCELING;
if (mSensorState != STATE_CANCELING) {
impl.cancelAuthenticationFromService(token, opPackageName, requestId);
mSensorState = STATE_CANCELING;
}
}
void goToStoppedStateIfCookieMatches(int cookie, int error) {

View File

@@ -55,7 +55,6 @@ import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
@@ -84,6 +83,7 @@ import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
/**
* System service that arbitrates the modality for BiometricPrompt to use.
@@ -92,22 +92,6 @@ public class BiometricService extends SystemService {
static final String TAG = "BiometricService";
private static final int MSG_ON_AUTHENTICATION_SUCCEEDED = 2;
private static final int MSG_ON_AUTHENTICATION_REJECTED = 3;
private static final int MSG_ON_ERROR = 4;
private static final int MSG_ON_ACQUIRED = 5;
private static final int MSG_ON_DISMISSED = 6;
private static final int MSG_ON_TRY_AGAIN_PRESSED = 7;
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_AUTHENTICATION_TIMED_OUT = 11;
private static final int MSG_ON_DEVICE_CREDENTIAL_PRESSED = 12;
private static final int MSG_ON_SYSTEM_EVENT = 13;
private static final int MSG_CLIENT_DIED = 14;
private static final int MSG_ON_DIALOG_ANIMATED_IN = 15;
private static final int MSG_ON_START_FINGERPRINT_NOW = 16;
private final Injector mInjector;
private final DevicePolicyManager mDevicePolicyManager;
@VisibleForTesting
@@ -116,7 +100,7 @@ public class BiometricService extends SystemService {
final SettingObserver mSettingObserver;
private final List<EnabledOnKeyguardCallback> mEnabledOnKeyguardCallbacks;
private final Random mRandom = new Random();
@NonNull private final AtomicLong mRequestCounter;
@NonNull private final Supplier<Long> mRequestCounter;
@VisibleForTesting
IStatusBarService mStatusBarService;
@@ -128,133 +112,13 @@ public class BiometricService extends SystemService {
// Get and cache the available biometric authenticators and their associated info.
final ArrayList<BiometricSensor> mSensors = new ArrayList<>();
@VisibleForTesting
BiometricStrengthController mBiometricStrengthController;
// The current authentication session, null if idle/done.
@VisibleForTesting
AuthSession mCurrentAuthSession;
@VisibleForTesting
final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ON_AUTHENTICATION_SUCCEEDED: {
SomeArgs args = (SomeArgs) msg.obj;
handleAuthenticationSucceeded(
args.argi1 /* sensorId */,
(byte[]) args.arg1 /* token */);
args.recycle();
break;
}
case MSG_ON_AUTHENTICATION_REJECTED: {
handleAuthenticationRejected();
break;
}
case MSG_ON_ERROR: {
SomeArgs args = (SomeArgs) msg.obj;
handleOnError(
args.argi1 /* sensorId */,
args.argi2 /* cookie */,
args.argi3 /* error */,
args.argi4 /* vendorCode */);
args.recycle();
break;
}
case MSG_ON_ACQUIRED: {
SomeArgs args = (SomeArgs) msg.obj;
handleOnAcquired(
args.argi1 /* sensorId */,
args.argi2 /* acquiredInfo */,
args.argi3 /* vendorCode */);
args.recycle();
break;
}
case MSG_ON_DISMISSED: {
handleOnDismissed(msg.arg1, (byte[]) msg.obj);
break;
}
case MSG_ON_TRY_AGAIN_PRESSED: {
handleOnTryAgainPressed();
break;
}
case MSG_ON_READY_FOR_AUTHENTICATION: {
SomeArgs args = (SomeArgs) msg.obj;
handleOnReadyForAuthentication(
args.argi1 /* cookie */);
args.recycle();
break;
}
case MSG_AUTHENTICATE: {
SomeArgs args = (SomeArgs) msg.obj;
handleAuthenticate(
(IBinder) args.arg1 /* token */,
(long) args.arg6 /* requestId */,
(long) args.arg2 /* operationId */,
args.argi1 /* userid */,
(IBiometricServiceReceiver) args.arg3 /* receiver */,
(String) args.arg4 /* opPackageName */,
(PromptInfo) args.arg5 /* promptInfo */);
args.recycle();
break;
}
case MSG_CANCEL_AUTHENTICATION: {
SomeArgs args = (SomeArgs) msg.obj;
handleCancelAuthentication((long) args.arg3 /* requestId */);
args.recycle();
break;
}
case MSG_ON_AUTHENTICATION_TIMED_OUT: {
SomeArgs args = (SomeArgs) msg.obj;
handleAuthenticationTimedOut(
args.argi1 /* sensorId */,
args.argi2 /* cookie */,
args.argi3 /* error */,
args.argi4 /* vendorCode */);
args.recycle();
break;
}
case MSG_ON_DEVICE_CREDENTIAL_PRESSED: {
handleOnDeviceCredentialPressed();
break;
}
case MSG_ON_SYSTEM_EVENT: {
handleOnSystemEvent((int) msg.obj);
break;
}
case MSG_CLIENT_DIED: {
handleClientDied();
break;
}
case MSG_ON_DIALOG_ANIMATED_IN: {
handleOnDialogAnimatedIn();
break;
}
case MSG_ON_START_FINGERPRINT_NOW: {
handleOnStartFingerprintNow();
break;
}
default:
Slog.e(TAG, "Unknown message: " + msg);
break;
}
}
};
AuthSession mAuthSession;
private final Handler mHandler = new Handler(Looper.getMainLooper());
/**
* Tracks authenticatorId invalidation. For more details, see
@@ -552,93 +416,74 @@ public class BiometricService extends SystemService {
}
// Receives events from individual biometric sensors.
@VisibleForTesting
final IBiometricSensorReceiver mBiometricSensorReceiver = new IBiometricSensorReceiver.Stub() {
@Override
public void onAuthenticationSucceeded(int sensorId, byte[] token) {
SomeArgs args = SomeArgs.obtain();
args.argi1 = sensorId;
args.arg1 = token;
mHandler.obtainMessage(MSG_ON_AUTHENTICATION_SUCCEEDED, args).sendToTarget();
}
@Override
public void onAuthenticationFailed(int sensorId) {
Slog.v(TAG, "onAuthenticationFailed");
mHandler.obtainMessage(MSG_ON_AUTHENTICATION_REJECTED).sendToTarget();
}
@Override
public void onError(int sensorId, int cookie, @BiometricConstants.Errors int error,
int vendorCode) {
// Determine if error is hard or soft error. Certain errors (such as TIMEOUT) are
// soft errors and we should allow the user to try authenticating again instead of
// dismissing BiometricPrompt.
if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) {
SomeArgs args = SomeArgs.obtain();
args.argi1 = sensorId;
args.argi2 = cookie;
args.argi3 = error;
args.argi4 = vendorCode;
mHandler.obtainMessage(MSG_ON_AUTHENTICATION_TIMED_OUT, args).sendToTarget();
} else {
SomeArgs args = SomeArgs.obtain();
args.argi1 = sensorId;
args.argi2 = cookie;
args.argi3 = error;
args.argi4 = vendorCode;
mHandler.obtainMessage(MSG_ON_ERROR, args).sendToTarget();
private IBiometricSensorReceiver createBiometricSensorReceiver(final long requestId) {
return new IBiometricSensorReceiver.Stub() {
@Override
public void onAuthenticationSucceeded(int sensorId, byte[] token) {
mHandler.post(() -> handleAuthenticationSucceeded(requestId, sensorId, token));
}
}
@Override
public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
SomeArgs args = SomeArgs.obtain();
args.argi1 = sensorId;
args.argi2 = acquiredInfo;
args.argi3 = vendorCode;
mHandler.obtainMessage(MSG_ON_ACQUIRED, args).sendToTarget();
}
};
@Override
public void onAuthenticationFailed(int sensorId) {
Slog.v(TAG, "onAuthenticationFailed");
mHandler.post(() -> handleAuthenticationRejected(requestId, sensorId));
}
final IBiometricSysuiReceiver mSysuiReceiver = new IBiometricSysuiReceiver.Stub() {
@Override
public void onDialogDismissed(@BiometricPrompt.DismissedReason int reason,
@Nullable byte[] credentialAttestation) {
mHandler.obtainMessage(MSG_ON_DISMISSED,
reason,
0 /* arg2 */,
credentialAttestation /* obj */).sendToTarget();
}
@Override
public void onError(int sensorId, int cookie, @BiometricConstants.Errors int error,
int vendorCode) {
// Determine if error is hard or soft error. Certain errors (such as TIMEOUT) are
// soft errors and we should allow the user to try authenticating again instead of
// dismissing BiometricPrompt.
if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) {
mHandler.post(() -> handleAuthenticationTimedOut(
requestId, sensorId, cookie, error, vendorCode));
} else {
mHandler.post(() -> handleOnError(
requestId, sensorId, cookie, error, vendorCode));
}
}
@Override
public void onTryAgainPressed() {
mHandler.sendEmptyMessage(MSG_ON_TRY_AGAIN_PRESSED);
}
@Override
public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
mHandler.post(() -> handleOnAcquired(
requestId, sensorId, acquiredInfo, vendorCode));
}
};
}
@Override
public void onDeviceCredentialPressed() {
mHandler.sendEmptyMessage(MSG_ON_DEVICE_CREDENTIAL_PRESSED);
}
private IBiometricSysuiReceiver createSysuiReceiver(final long requestId) {
return new IBiometricSysuiReceiver.Stub() {
@Override
public void onDialogDismissed(@BiometricPrompt.DismissedReason int reason,
@Nullable byte[] credentialAttestation) {
mHandler.post(() -> handleOnDismissed(requestId, reason, credentialAttestation));
}
@Override
public void onSystemEvent(int event) {
mHandler.obtainMessage(MSG_ON_SYSTEM_EVENT, event).sendToTarget();
}
@Override
public void onTryAgainPressed() {
mHandler.post(() -> handleOnTryAgainPressed(requestId));
}
@Override
public void onDialogAnimatedIn() {
mHandler.obtainMessage(MSG_ON_DIALOG_ANIMATED_IN).sendToTarget();
}
@Override
public void onDeviceCredentialPressed() {
mHandler.post(() -> handleOnDeviceCredentialPressed(requestId));
}
@Override
public void onStartFingerprintNow() {
mHandler.obtainMessage(MSG_ON_START_FINGERPRINT_NOW).sendToTarget();
}
};
@Override
public void onSystemEvent(int event) {
mHandler.post(() -> handleOnSystemEvent(requestId, event));
}
private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> {
mHandler.sendEmptyMessage(MSG_CLIENT_DIED);
@Override
public void onDialogAnimatedIn() {
mHandler.post(() -> handleOnDialogAnimatedIn(requestId));
}
};
}
private AuthSession.ClientDeathReceiver createClientDeathReceiver(final long requestId) {
return () -> mHandler.post(() -> handleClientDied(requestId));
};
/**
@@ -679,12 +524,10 @@ public class BiometricService extends SystemService {
}
@Override // Binder call
public void onReadyForAuthentication(int cookie) {
public void onReadyForAuthentication(long requestId, int cookie) {
checkInternalPermission();
SomeArgs args = SomeArgs.obtain();
args.argi1 = cookie;
mHandler.obtainMessage(MSG_ON_READY_FOR_AUTHENTICATION, args).sendToTarget();
mHandler.post(() -> handleOnReadyForAuthentication(requestId, cookie));
}
@Override // Binder call
@@ -711,18 +554,9 @@ public class BiometricService extends SystemService {
}
}
final long requestId = mRequestCounter.incrementAndGet();
SomeArgs args = SomeArgs.obtain();
args.arg1 = token;
args.arg2 = operationId;
args.argi1 = userId;
args.arg3 = receiver;
args.arg4 = opPackageName;
args.arg5 = promptInfo;
args.arg6 = requestId;
mHandler.obtainMessage(MSG_AUTHENTICATE, args).sendToTarget();
final long requestId = mRequestCounter.get();
mHandler.post(() -> handleAuthenticate(
token, requestId, operationId, userId, receiver, opPackageName, promptInfo));
return requestId;
}
@@ -736,7 +570,7 @@ public class BiometricService extends SystemService {
args.arg2 = opPackageName;
args.arg3 = requestId;
mHandler.obtainMessage(MSG_CANCEL_AUTHENTICATION, args).sendToTarget();
mHandler.post(() -> handleCancelAuthentication(requestId));
}
@Override // Binder call
@@ -1002,8 +836,7 @@ public class BiometricService extends SystemService {
Slog.d(TAG, "ClearSchedulerBuffer: " + clearSchedulerBuffer);
final ProtoOutputStream proto = new ProtoOutputStream(fd);
proto.write(BiometricServiceStateProto.AUTH_SESSION_STATE,
mCurrentAuthSession != null ? mCurrentAuthSession.getState()
: STATE_AUTH_IDLE);
mAuthSession != null ? mAuthSession.getState() : STATE_AUTH_IDLE);
for (BiometricSensor sensor : mSensors) {
byte[] serviceState = sensor.impl
.dumpSensorServiceStateProto(clearSchedulerBuffer);
@@ -1128,8 +961,9 @@ public class BiometricService extends SystemService {
CoexCoordinator.FACE_HAPTIC_DISABLE, 1) != 0;
}
public AtomicLong getRequestGenerator() {
return new AtomicLong(0);
public Supplier<Long> getRequestGenerator() {
final AtomicLong generator = new AtomicLong(0);
return () -> generator.incrementAndGet();
}
}
@@ -1202,172 +1036,184 @@ public class BiometricService extends SystemService {
return false;
}
private void handleAuthenticationSucceeded(int sensorId, byte[] token) {
@Nullable
private AuthSession getAuthSessionIfCurrent(long requestId) {
final AuthSession session = mAuthSession;
if (session != null && session.getRequestId() == requestId) {
return session;
}
return null;
}
private void handleAuthenticationSucceeded(long requestId, int sensorId, byte[] token) {
Slog.v(TAG, "handleAuthenticationSucceeded(), sensorId: " + sensorId);
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
// after user dismissed/canceled dialog).
if (mCurrentAuthSession == null) {
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.e(TAG, "handleAuthenticationSucceeded: AuthSession is null");
return;
}
mCurrentAuthSession.onAuthenticationSucceeded(sensorId, isStrongBiometric(sensorId), token);
session.onAuthenticationSucceeded(sensorId, isStrongBiometric(sensorId), token);
}
private void handleAuthenticationRejected() {
private void handleAuthenticationRejected(long requestId, int sensorId) {
Slog.v(TAG, "handleAuthenticationRejected()");
// Should never happen, log this to catch bad HAL behavior (e.g. auth rejected
// after user dismissed/canceled dialog).
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleAuthenticationRejected: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleAuthenticationRejected: AuthSession is not current");
return;
}
mCurrentAuthSession.onAuthenticationRejected();
session.onAuthenticationRejected(sensorId);
}
private void handleAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) {
private void handleAuthenticationTimedOut(long requestId, int sensorId, int cookie, int error,
int vendorCode) {
Slog.v(TAG, "handleAuthenticationTimedOut(), sensorId: " + sensorId
+ ", cookie: " + cookie
+ ", error: " + error
+ ", vendorCode: " + vendorCode);
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
// after user dismissed/canceled dialog).
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleAuthenticationTimedOut: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleAuthenticationTimedOut: AuthSession is not current");
return;
}
mCurrentAuthSession.onAuthenticationTimedOut(sensorId, cookie, error, vendorCode);
session.onAuthenticationTimedOut(sensorId, cookie, error, vendorCode);
}
private void handleOnError(int sensorId, int cookie, @BiometricConstants.Errors int error,
int vendorCode) {
private void handleOnError(long requestId, int sensorId, int cookie,
@BiometricConstants.Errors int error, int vendorCode) {
Slog.d(TAG, "handleOnError() sensorId: " + sensorId
+ ", cookie: " + cookie
+ ", error: " + error
+ ", vendorCode: " + vendorCode);
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleOnError: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleOnError: AuthSession is not current");
return;
}
try {
final boolean finished = mCurrentAuthSession
.onErrorReceived(sensorId, cookie, error, vendorCode);
final boolean finished = session.onErrorReceived(sensorId, cookie, error, vendorCode);
if (finished) {
Slog.d(TAG, "handleOnError: AuthSession finished");
mCurrentAuthSession = null;
mAuthSession = null;
}
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
}
private void handleOnAcquired(int sensorId, int acquiredInfo, int vendorCode) {
private void handleOnAcquired(long requestId, int sensorId, int acquiredInfo, int vendorCode) {
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
// after user dismissed/canceled dialog).
if (mCurrentAuthSession == null) {
Slog.e(TAG, "onAcquired: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "onAcquired: AuthSession is not current");
return;
}
mCurrentAuthSession.onAcquired(sensorId, acquiredInfo, vendorCode);
session.onAcquired(sensorId, acquiredInfo, vendorCode);
}
private void handleOnDismissed(@BiometricPrompt.DismissedReason int reason,
private void handleOnDismissed(long requestId, @BiometricPrompt.DismissedReason int reason,
@Nullable byte[] credentialAttestation) {
if (mCurrentAuthSession == null) {
Slog.e(TAG, "onDismissed: " + reason + ", AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.e(TAG, "onDismissed: " + reason + ", AuthSession is not current");
return;
}
mCurrentAuthSession.onDialogDismissed(reason, credentialAttestation);
mCurrentAuthSession = null;
session.onDialogDismissed(reason, credentialAttestation);
mAuthSession = null;
}
private void handleOnTryAgainPressed() {
private void handleOnTryAgainPressed(long requestId) {
Slog.d(TAG, "onTryAgainPressed");
// No need to check permission, since it can only be invoked by SystemUI
// (or system server itself).
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleOnTryAgainPressed: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleOnTryAgainPressed: AuthSession is not current");
return;
}
mCurrentAuthSession.onTryAgainPressed();
session.onTryAgainPressed();
}
private void handleOnDeviceCredentialPressed() {
private void handleOnDeviceCredentialPressed(long requestId) {
Slog.d(TAG, "onDeviceCredentialPressed");
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleOnDeviceCredentialPressed: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleOnDeviceCredentialPressed: AuthSession is not current");
return;
}
mCurrentAuthSession.onDeviceCredentialPressed();
session.onDeviceCredentialPressed();
}
private void handleOnSystemEvent(int event) {
private void handleOnSystemEvent(long requestId, int event) {
Slog.d(TAG, "onSystemEvent: " + event);
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleOnSystemEvent: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleOnSystemEvent: AuthSession is not current");
return;
}
mCurrentAuthSession.onSystemEvent(event);
session.onSystemEvent(event);
}
private void handleClientDied() {
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleClientDied: AuthSession is null");
private void handleClientDied(long requestId) {
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleClientDied: AuthSession is not current");
return;
}
Slog.e(TAG, "Session: " + mCurrentAuthSession);
final boolean finished = mCurrentAuthSession.onClientDied();
Slog.e(TAG, "Session: " + session);
final boolean finished = session.onClientDied();
if (finished) {
mCurrentAuthSession = null;
mAuthSession = null;
}
}
private void handleOnDialogAnimatedIn() {
private void handleOnDialogAnimatedIn(long requestId) {
Slog.d(TAG, "handleOnDialogAnimatedIn");
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleOnDialogAnimatedIn: AuthSession is null");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleOnDialogAnimatedIn: AuthSession is not current");
return;
}
mCurrentAuthSession.onDialogAnimatedIn();
}
private void handleOnStartFingerprintNow() {
Slog.d(TAG, "handleOnStartFingerprintNow");
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleOnStartFingerprintNow: AuthSession is null");
return;
}
mCurrentAuthSession.onStartFingerprint();
session.onDialogAnimatedIn();
}
/**
* 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) {
if (mCurrentAuthSession == null) {
private void handleOnReadyForAuthentication(long requestId, int cookie) {
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == 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, "handleOnReadyForAuthentication: AuthSession is null");
Slog.w(TAG, "handleOnReadyForAuthentication: AuthSession is not current");
return;
}
mCurrentAuthSession.onCookieReceived(cookie);
session.onCookieReceived(cookie);
}
private void handleAuthenticate(IBinder token, long requestId, long operationId, int userId,
@@ -1428,47 +1274,41 @@ public class BiometricService extends SystemService {
// No need to dismiss dialog / send error yet if we're continuing authentication, e.g.
// "Try again" is showing due to something like ERROR_TIMEOUT.
if (mCurrentAuthSession != null) {
if (mAuthSession != null) {
// Forcefully cancel authentication. Dismiss the UI, and immediately send
// ERROR_CANCELED to the client. Note that we should/will ignore HAL ERROR_CANCELED.
// Expect to see some harmless "unknown cookie" errors.
Slog.w(TAG, "Existing AuthSession: " + mCurrentAuthSession);
mCurrentAuthSession.onCancelAuthSession(true /* force */);
mCurrentAuthSession = null;
Slog.w(TAG, "Existing AuthSession: " + mAuthSession);
mAuthSession.onCancelAuthSession(true /* force */);
mAuthSession = null;
}
final boolean debugEnabled = mInjector.isDebugEnabled(getContext(), userId);
mCurrentAuthSession = new AuthSession(getContext(), mStatusBarService, mSysuiReceiver,
mKeyStore, mRandom, mClientDeathReceiver, preAuthInfo, token, requestId,
operationId, userId, mBiometricSensorReceiver, receiver, opPackageName, promptInfo,
debugEnabled, mInjector.getFingerprintSensorProperties(getContext()));
mAuthSession = new AuthSession(getContext(), mStatusBarService,
createSysuiReceiver(requestId), mKeyStore, mRandom,
createClientDeathReceiver(requestId), preAuthInfo, token, requestId,
operationId, userId, createBiometricSensorReceiver(requestId), receiver,
opPackageName, promptInfo, debugEnabled,
mInjector.getFingerprintSensorProperties(getContext()));
try {
mCurrentAuthSession.goToInitialState();
mAuthSession.goToInitialState();
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
}
private void handleCancelAuthentication(long requestId) {
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleCancelAuthentication: AuthSession is null");
return;
}
if (mCurrentAuthSession.getRequestId() != requestId) {
// TODO: actually cancel the operation
// This can happen if the operation has been queued, but is cancelled before
// it reaches the head of the scheduler. Consider it a programming error for now
// and ignore it.
Slog.e(TAG, "handleCancelAuthentication: AuthSession mismatch current requestId: "
+ mCurrentAuthSession.getRequestId() + " cancel for: " + requestId
+ " (ignoring cancellation)");
final AuthSession session = getAuthSessionIfCurrent(requestId);
if (session == null) {
Slog.w(TAG, "handleCancelAuthentication: AuthSession is not current");
// TODO: actually cancel the operation?
return;
}
final boolean finished = mCurrentAuthSession.onCancelAuthSession(false /* force */);
final boolean finished = session.onCancelAuthSession(false /* force */);
if (finished) {
Slog.d(TAG, "handleCancelAuthentication: AuthSession finished");
mCurrentAuthSession = null;
mAuthSession = null;
}
}
@@ -1491,7 +1331,7 @@ public class BiometricService extends SystemService {
pw.println(" " + sensor);
}
pw.println();
pw.println("CurrentSession: " + mCurrentAuthSession);
pw.println("CurrentSession: " + mAuthSession);
pw.println();
pw.println("CoexCoordinator: " + CoexCoordinator.getInstance().toString());
pw.println();

View File

@@ -86,6 +86,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
private long mStartTimeMs;
private boolean mAuthAttempted;
private boolean mAuthSuccess = false;
// TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update
// the state. We should think of a way to improve this in the future.
@@ -237,6 +238,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
"Successful background authentication!");
}
mAuthSuccess = true;
markAlreadyDone();
if (mTaskStackListener != null) {
@@ -502,6 +504,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
return mAuthAttempted;
}
/** If an auth attempt completed successfully. */
public boolean wasAuthSuccessful() {
return mAuthSuccess;
}
protected int getShowOverlayReason() {
if (isKeyguard()) {
return BiometricOverlayConstants.REASON_AUTH_KEYGUARD;

View File

@@ -316,7 +316,8 @@ public class BiometricScheduler {
}
} else {
try {
mBiometricService.onReadyForAuthentication(cookie);
mBiometricService.onReadyForAuthentication(
mCurrentOperation.getClientMonitor().getRequestId(), cookie);
} catch (RemoteException e) {
Slog.e(getTag(), "Remote exception when contacting BiometricService", e);
}

View File

@@ -173,18 +173,13 @@ public class CoexCoordinator {
}
// SensorType to AuthenticationClient map
private final Map<Integer, AuthenticationClient<?>> mClientMap;
@VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths;
private final Map<Integer, AuthenticationClient<?>> mClientMap = new HashMap<>();
@VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths = new LinkedList<>();
private boolean mAdvancedLogicEnabled;
private boolean mFaceHapticDisabledWhenNonBypass;
private final Handler mHandler;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private CoexCoordinator() {
// Singleton
mClientMap = new HashMap<>();
mSuccessfulAuths = new LinkedList<>();
mHandler = new Handler(Looper.getMainLooper());
}
private CoexCoordinator() {}
public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType,
@NonNull AuthenticationClient<?> client) {
@@ -221,8 +216,14 @@ public class CoexCoordinator {
public void onAuthenticationSucceeded(long currentTimeMillis,
@NonNull AuthenticationClient<?> client,
@NonNull Callback callback) {
final boolean isUsingSingleModality = isSingleAuthOnly(client);
if (client.isBiometricPrompt()) {
callback.sendHapticFeedback();
if (!isUsingSingleModality && hasMultipleSuccessfulAuthentications()) {
// only send feedback on the first one
} else {
callback.sendHapticFeedback();
}
// For BP, BiometricService will add the authToken to Keystore.
callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
@@ -234,7 +235,7 @@ public class CoexCoordinator {
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
callback.handleLifecycleAfterAuth();
} else if (mAdvancedLogicEnabled && client.isKeyguard()) {
if (isSingleAuthOnly(client)) {
if (isUsingSingleModality) {
// Single sensor authentication
callback.sendHapticFeedback();
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
@@ -295,10 +296,10 @@ public class CoexCoordinator {
@NonNull AuthenticationClient<?> client,
@LockoutTracker.LockoutMode int lockoutMode,
@NonNull Callback callback) {
final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
final boolean isUsingSingleModality = isSingleAuthOnly(client);
if (keyguardAdvancedLogic) {
if (isSingleAuthOnly(client)) {
if (mAdvancedLogicEnabled && client.isKeyguard()) {
if (isUsingSingleModality) {
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
} else {
@@ -319,8 +320,7 @@ public class CoexCoordinator {
// also done now.
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
}
else {
} else {
// UDFPS auth has never been attempted.
if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) {
Slog.w(TAG, "Skipping face reject haptic");
@@ -360,6 +360,11 @@ public class CoexCoordinator {
callback.handleLifecycleAfterAuth();
}
}
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
if (!isCurrentFaceAuth(client)) {
callback.sendHapticFeedback();
}
callback.handleLifecycleAfterAuth();
} else {
callback.sendHapticFeedback();
callback.handleLifecycleAfterAuth();
@@ -380,6 +385,8 @@ public class CoexCoordinator {
*/
public void onAuthenticationError(@NonNull AuthenticationClient<?> client,
@BiometricConstants.Errors int error, @NonNull ErrorCallback callback) {
final boolean isUsingSingleModality = isSingleAuthOnly(client);
// Figure out non-coex state
final boolean shouldUsuallyVibrate;
if (isCurrentFaceAuth(client)) {
@@ -401,25 +408,26 @@ public class CoexCoordinator {
}
// Figure out coex state
final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
final boolean hapticSuppressedByCoex;
if (keyguardAdvancedLogic) {
if (isSingleAuthOnly(client)) {
if (mAdvancedLogicEnabled && client.isKeyguard()) {
if (isUsingSingleModality) {
hapticSuppressedByCoex = false;
} else {
hapticSuppressedByCoex = isCurrentFaceAuth(client)
&& !client.isKeyguardBypassEnabled();
}
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
hapticSuppressedByCoex = isCurrentFaceAuth(client);
} else {
hapticSuppressedByCoex = false;
}
// Combine and send feedback if appropriate
Slog.d(TAG, "shouldUsuallyVibrate: " + shouldUsuallyVibrate
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
if (shouldUsuallyVibrate && !hapticSuppressedByCoex) {
callback.sendHapticFeedback();
} else {
Slog.v(TAG, "no haptic shouldUsuallyVibrate: " + shouldUsuallyVibrate
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
}
}
@@ -504,6 +512,19 @@ public class CoexCoordinator {
return true;
}
private boolean hasMultipleSuccessfulAuthentications() {
int count = 0;
for (AuthenticationClient<?> c : mClientMap.values()) {
if (c.wasAuthSuccessful()) {
count++;
}
if (count > 1) {
return true;
}
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();

View File

@@ -856,11 +856,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
}
@Override
public void onBiometricAuthenticated() {
public void onBiometricAuthenticated(@Modality int modality) {
enforceBiometricDialog();
if (mBar != null) {
try {
mBar.onBiometricAuthenticated();
mBar.onBiometricAuthenticated(modality);
} catch (RemoteException ex) {
}
}

View File

@@ -219,7 +219,7 @@ public class AuthSessionTest {
public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes()
throws Exception {
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
testMultiAuth_fingerprintSensorStartsAfter(false /* fingerprintStartsAfterDelay */);
testMultiAuth_fingerprintSensorStartsAfterUINotifies();
}
@Test
@@ -227,10 +227,10 @@ public class AuthSessionTest {
throws Exception {
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
testMultiAuth_fingerprintSensorStartsAfter(true /* fingerprintStartsAfterDelay */);
testMultiAuth_fingerprintSensorStartsAfterUINotifies();
}
public void testMultiAuth_fingerprintSensorStartsAfter(boolean fingerprintStartsAfterDelay)
public void testMultiAuth_fingerprintSensorStartsAfterUINotifies()
throws Exception {
final long operationId = 123;
final int userId = 10;
@@ -274,12 +274,6 @@ public class AuthSessionTest {
// Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started.
session.onDialogAnimatedIn();
if (fingerprintStartsAfterDelay) {
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
assertEquals(BiometricSensor.STATE_COOKIE_RETURNED,
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
session.onStartFingerprint();
}
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
assertEquals(BiometricSensor.STATE_AUTHENTICATING,
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());

View File

@@ -16,6 +16,7 @@
package com.android.server.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
@@ -85,14 +86,11 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
@Presubmit
@SmallTest
public class BiometricServiceTest {
private static final String TAG = "BiometricServiceTest";
private static final String TEST_PACKAGE_NAME = "test_package";
private static final long TEST_REQUEST_ID = 44;
@@ -153,7 +151,7 @@ public class BiometricServiceTest {
.thenReturn(mock(BiometricStrengthController.class));
when(mInjector.getTrustManager()).thenReturn(mTrustManager);
when(mInjector.getDevicePolicyManager(any())).thenReturn(mDevicePolicyManager);
when(mInjector.getRequestGenerator()).thenReturn(new AtomicLong(TEST_REQUEST_ID - 1));
when(mInjector.getRequestGenerator()).thenReturn(() -> TEST_REQUEST_ID);
when(mResources.getString(R.string.biometric_error_hw_unavailable))
.thenReturn(ERROR_HW_UNAVAILABLE);
@@ -178,22 +176,22 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
true /* requireConfirmation */, null /* authenticators */);
waitForIdle();
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession),
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
anyInt());
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */);
waitForIdle();
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
mBiometricService.mCurrentAuthSession.binderDied();
mBiometricService.mAuthSession.binderDied();
waitForIdle();
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
}
@@ -205,31 +203,31 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
true /* requireConfirmation */, null /* authenticators */);
waitForIdle();
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession),
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
anyInt());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mCurrentAuthSession.binderDied();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
mBiometricService.mAuthSession.binderDied();
waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession);
assertNotNull(mBiometricService.mAuthSession);
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
assertEquals(STATE_CLIENT_DIED_CANCELLING,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
verify(mBiometricService.mCurrentAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl)
verify(mBiometricService.mAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl)
.cancelAuthenticationFromService(any(), any(), anyLong());
// Simulate ERROR_CANCELED received from HAL
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */);
waitForIdle();
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
}
@Test
@@ -265,12 +263,12 @@ public class BiometricServiceTest {
Authenticators.DEVICE_CREDENTIAL);
waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession);
assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
// StatusBar showBiometricDialog invoked
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */,
@@ -304,21 +302,21 @@ public class BiometricServiceTest {
mBiometricService = new BiometricService(mContext, mInjector);
mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
null /* authenticators */);
waitForIdle();
verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS),
eq(0 /* vendorCode */));
}
@Test
public void testAuthenticate_notStrongEnough_returnsHardwareNotPresent() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
Authenticators.BIOMETRIC_STRONG);
@@ -335,7 +333,7 @@ public class BiometricServiceTest {
// is able to proceed.
final int[] modalities = new int[] {
BiometricAuthenticator.TYPE_FINGERPRINT,
TYPE_FINGERPRINT,
BiometricAuthenticator.TYPE_FACE,
};
@@ -356,7 +354,7 @@ public class BiometricServiceTest {
// StatusBar showBiometricDialog invoked with face, which was set up to be STRONG
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[] {SENSOR_ID_FACE}),
eq(false) /* credentialAllowed */,
@@ -377,14 +375,14 @@ public class BiometricServiceTest {
mBiometricService = new BiometricService(mContext, mInjector);
mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
null /* authenticators */);
waitForIdle();
verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE),
eq(0 /* vendorCode */));
}
@@ -415,13 +413,13 @@ public class BiometricServiceTest {
waitForIdle();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
final byte[] HAT = generateRandomHAT();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE,
HAT);
waitForIdle();
// Confirmation is required
assertEquals(STATE_AUTH_PENDING_CONFIRM,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
// Enrolled, not disabled in settings, user doesn't require confirmation in settings
resetReceivers();
@@ -431,25 +429,25 @@ public class BiometricServiceTest {
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
null /* authenticators */);
waitForIdle();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE,
HAT);
waitForIdle();
// Confirmation not required, waiting for dialog to dismiss
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
}
@Test
public void testAuthenticate_happyPathWithoutConfirmation_strongBiometric() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
testAuthenticate_happyPathWithoutConfirmation(true /* isStrongBiometric */);
}
@Test
public void testAuthenticate_happyPathWithoutConfirmation_weakBiometric() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
testAuthenticate_happyPathWithoutConfirmation(false /* isStrongBiometric */);
}
@@ -461,7 +459,7 @@ public class BiometricServiceTest {
waitForIdle();
// Creates a pending auth session with the correct initial states
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
// Invokes <Modality>Service#prepareForAuthentication
ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -477,19 +475,19 @@ public class BiometricServiceTest {
cookieCaptor.capture() /* cookie */,
anyBoolean() /* allowBackgroundAuthentication */);
// onReadyForAuthentication, mCurrentAuthSession state OK
mBiometricService.mImpl.onReadyForAuthentication(cookieCaptor.getValue());
// onReadyForAuthentication, mAuthSession state OK
mBiometricService.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookieCaptor.getValue());
waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
// startPreparedClient invoked
mBiometricService.mCurrentAuthSession.onDialogAnimatedIn();
mBiometricService.mAuthSession.onDialogAnimatedIn();
verify(mBiometricService.mSensors.get(0).impl)
.startPreparedClient(cookieCaptor.getValue());
// StatusBar showBiometricDialog invoked
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
any(),
eq(false) /* credentialAllowed */,
@@ -502,18 +500,18 @@ public class BiometricServiceTest {
// Hardware authenticated
final byte[] HAT = generateRandomHAT();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FINGERPRINT,
HAT);
waitForIdle();
// Waiting for SystemUI to send dismissed callback
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
// Notify SystemUI hardware authenticated
verify(mBiometricService.mStatusBarService).onBiometricAuthenticated();
verify(mBiometricService.mStatusBarService).onBiometricAuthenticated(TYPE_FINGERPRINT);
// SystemUI sends callback with dismissed reason
mBiometricService.mSysuiReceiver.onDialogDismissed(
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED,
null /* credentialAttestation */);
waitForIdle();
@@ -527,7 +525,7 @@ public class BiometricServiceTest {
verify(mReceiver1).onAuthenticationSucceeded(
BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC);
// Current session becomes null
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
}
@Test
@@ -542,11 +540,11 @@ public class BiometricServiceTest {
waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
assertEquals(Authenticators.DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */,
@@ -578,16 +576,16 @@ public class BiometricServiceTest {
// Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not
// sent to KeyStore yet
final byte[] HAT = generateRandomHAT();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE,
HAT);
waitForIdle();
// Waiting for SystemUI to send confirmation callback
assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mAuthSession.getState());
verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
// SystemUI sends confirm, HAT is sent to keystore and client is notified.
mBiometricService.mSysuiReceiver.onDialogDismissed(
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED,
null /* credentialAttestation */);
waitForIdle();
@@ -624,33 +622,34 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE);
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE);
waitForIdle();
verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_NONE),
eq(BiometricAuthenticator.TYPE_FACE),
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
eq(0 /* vendorCode */));
verify(mReceiver1).onAuthenticationFailed();
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
}
@Test
public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FINGERPRINT);
mBiometricService.mAuthSession.mSensorReceiver
.onAuthenticationFailed(SENSOR_ID_FINGERPRINT);
waitForIdle();
verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_NONE),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
eq(0 /* vendorCode */));
verify(mReceiver1).onAuthenticationFailed();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
}
@Test
@@ -678,14 +677,14 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */);
waitForIdle();
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FACE),
eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT),
@@ -694,15 +693,15 @@ public class BiometricServiceTest {
verify(mReceiver1, never()).onAuthenticationFailed();
// No auth session. Pressing try again will create one.
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
// Pressing "Try again" on SystemUI
mBiometricService.mSysuiReceiver.onTryAgainPressed();
mBiometricService.mAuthSession.mSysuiReceiver.onTryAgainPressed();
waitForIdle();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
// AuthSession is now resuming
assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mAuthSession.getState());
// Test resuming when hardware becomes ready. SystemUI should not be requested to
// show another dialog since it's already showing.
@@ -728,14 +727,14 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */);
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */);
waitForIdle();
@@ -748,7 +747,7 @@ public class BiometricServiceTest {
// Dialog is hidden immediately
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
// Auth session is over
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
}
@Test
@@ -757,61 +756,61 @@ public class BiometricServiceTest {
// For errors that show in SystemUI, BiometricService stays in STATE_ERROR_PENDING_SYSUI
// until SystemUI notifies us that the dialog is dismissed at which point the current
// session is done.
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
0 /* vendorCode */);
waitForIdle();
// Sends error to SystemUI and does not notify client yet
assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mAuthSession.getState());
verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
eq(0 /* vendorCode */));
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
// SystemUI animation completed, client is notified, auth session is over
mBiometricService.mSysuiReceiver.onDialogDismissed(
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_ERROR, null /* credentialAttestation */);
waitForIdle();
verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
eq(0 /* vendorCode */));
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
}
@Test
public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */,
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
waitForIdle();
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mBiometricSensorReceiver.onError(
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT,
getCookieForPendingSession(mBiometricService.mCurrentAuthSession),
getCookieForPendingSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
0 /* vendorCode */);
waitForIdle();
// We should be showing device credential now
assertNotNull(mBiometricService.mCurrentAuthSession);
assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
assertEquals(Authenticators.DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */,
@@ -826,23 +825,23 @@ public class BiometricServiceTest {
@Test
public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
waitForIdle();
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT,
getCookieForPendingSession(mBiometricService.mCurrentAuthSession),
getCookieForPendingSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
0 /* vendorCode */);
waitForIdle();
// Error is sent to client
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
eq(0) /* vendorCode */);
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
}
@Test
@@ -861,7 +860,7 @@ public class BiometricServiceTest {
private void testBiometricAuth_whenLockout(@LockoutTracker.LockoutMode int lockoutMode,
int biometricPromptError) throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
.thenReturn(lockoutMode);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
@@ -869,16 +868,15 @@ public class BiometricServiceTest {
waitForIdle();
// Modality and error are sent
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
eq(biometricPromptError), eq(0) /* vendorCode */);
}
@Test
public void testBiometricOrCredentialAuth_whenBiometricLockout_showsCredential()
throws Exception {
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
.thenReturn(true);
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
.thenReturn(LockoutTracker.LOCKOUT_PERMANENT);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
@@ -887,13 +885,13 @@ public class BiometricServiceTest {
waitForIdle();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
assertNotNull(mBiometricService.mCurrentAuthSession);
assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
assertEquals(Authenticators.DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */,
@@ -959,73 +957,73 @@ public class BiometricServiceTest {
@Test
public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */,
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
mBiometricService.mSysuiReceiver.onDeviceCredentialPressed();
mBiometricService.mAuthSession.mSysuiReceiver.onDeviceCredentialPressed();
waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */);
waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
}
@Test
public void testLockout_whileAuthenticating_credentialAllowed() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */,
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
0 /* vendorCode */);
waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
eq(0 /* vendorCode */));
}
@Test
public void testLockout_whenAuthenticating_credentialNotAllowed() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
0 /* vendorCode */);
waitForIdle();
assertEquals(STATE_ERROR_PENDING_SYSUI,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
eq(0 /* vendorCode */));
}
@@ -1033,20 +1031,20 @@ public class BiometricServiceTest {
@Test
public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mSysuiReceiver.onDialogDismissed(
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle();
verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
eq(0 /* vendorCode */));
verify(mBiometricService.mSensors.get(0).impl).cancelAuthenticationFromService(
any(), any(), anyLong());
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
}
@Test
@@ -1055,12 +1053,12 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */);
mBiometricService.mSysuiReceiver.onDialogDismissed(
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */);
waitForIdle();
@@ -1069,18 +1067,17 @@ public class BiometricServiceTest {
}
@Test
public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws
Exception {
public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */);
mBiometricService.mSysuiReceiver.onDialogDismissed(
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle();
@@ -1094,10 +1091,10 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
true /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE,
new byte[69] /* HAT */);
mBiometricService.mSysuiReceiver.onDialogDismissed(
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle();
@@ -1108,19 +1105,19 @@ public class BiometricServiceTest {
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
eq(0 /* vendorCode */));
verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
assertNull(mBiometricService.mCurrentAuthSession);
assertNull(mBiometricService.mAuthSession);
}
@Test
public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception {
when(mContext.getResources().getString(anyInt())).thenReturn("test string");
final int modality = BiometricAuthenticator.TYPE_FINGERPRINT;
final int modality = TYPE_FINGERPRINT;
setupAuthForOnly(modality, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAcquired(
mBiometricService.mAuthSession.mSensorReceiver.onAcquired(
SENSOR_ID_FINGERPRINT,
FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
0 /* vendorCode */);
@@ -1130,29 +1127,29 @@ public class BiometricServiceTest {
// string is retrieved for now, but it's also very unlikely to break anyway.
verify(mBiometricService.mStatusBarService)
.onBiometricHelp(eq(modality), anyString());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
}
@Test
public void testCancel_whenAuthenticating() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mImpl.cancelAuthentication(mBiometricService.mCurrentAuthSession.mToken,
mBiometricService.mImpl.cancelAuthentication(mBiometricService.mAuthSession.mToken,
TEST_PACKAGE_NAME, TEST_REQUEST_ID);
waitForIdle();
// Pretend that the HAL has responded to cancel with ERROR_CANCELED
mBiometricService.mBiometricSensorReceiver.onError(
mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */);
waitForIdle();
// Hides system dialog and invokes the onError callback
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
eq(0 /* vendorCode */));
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
@@ -1161,7 +1158,7 @@ public class BiometricServiceTest {
@Test
public void testCanAuthenticate_whenDeviceHasRequestedBiometricStrength() throws Exception {
// When only biometric is requested, and sensor is strong enough
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG));
@@ -1170,7 +1167,7 @@ public class BiometricServiceTest {
@Test
public void testCanAuthenticate_whenDeviceDoesNotHaveRequestedBiometricStrength()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
// When only biometric is requested, and sensor is not strong enough
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
@@ -1208,9 +1205,8 @@ public class BiometricServiceTest {
@Test
public void testCanAuthenticate_whenNoBiometricsEnrolled() throws Exception {
// With credential set up, test the following.
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
.thenReturn(true);
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
false /* enrolled */);
// When only biometric is requested
@@ -1277,7 +1273,7 @@ public class BiometricServiceTest {
private void testCanAuthenticate_whenLockedOut(@LockoutTracker.LockoutMode int lockoutMode)
throws Exception {
// When only biometric is requested, and sensor is strong enough
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
.thenReturn(lockoutMode);
@@ -1311,7 +1307,7 @@ public class BiometricServiceTest {
for (int i = 0; i < testCases.length; i++) {
final BiometricSensor sensor =
new BiometricSensor(mContext, 0 /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT,
TYPE_FINGERPRINT,
testCases[i][0],
mock(IBiometricAuthenticator.class)) {
@Override
@@ -1341,7 +1337,7 @@ public class BiometricServiceTest {
.thenReturn(true);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
mBiometricService.mImpl.registerAuthenticator(0 /* testId */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator);
verify(mBiometricService.mBiometricStrengthController).updateStrengths();
@@ -1360,7 +1356,7 @@ public class BiometricServiceTest {
.thenReturn(true);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
mBiometricService.mImpl.registerAuthenticator(testId /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator);
// Downgrade the authenticator
@@ -1378,7 +1374,7 @@ public class BiometricServiceTest {
false /* requireConfirmation */, authenticators);
waitForIdle();
verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
eq(TYPE_FINGERPRINT),
eq(BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED),
eq(0) /* vendorCode */);
@@ -1392,7 +1388,7 @@ public class BiometricServiceTest {
authenticators);
waitForIdle();
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[] {testId}),
eq(false) /* credentialAllowed */,
@@ -1414,9 +1410,9 @@ public class BiometricServiceTest {
false /* requireConfirmation */,
authenticators);
waitForIdle();
assertTrue(Utils.isCredentialRequested(mBiometricService.mCurrentAuthSession.mPromptInfo));
assertTrue(Utils.isCredentialRequested(mBiometricService.mAuthSession.mPromptInfo));
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */,
@@ -1442,7 +1438,7 @@ public class BiometricServiceTest {
false /* requireConfirmation */, authenticators);
waitForIdle();
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[] {testId}) /* sensorIds */,
eq(false) /* credentialAllowed */,
@@ -1495,29 +1491,29 @@ public class BiometricServiceTest {
@Test
public void testWorkAuthentication_fingerprintWorksIfNotDisabledByDevicePolicyManager()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mDevicePolicyManager
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
.thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
Authenticators.BIOMETRIC_STRONG);
waitForIdle();
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
startPendingAuthSession(mBiometricService);
waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
}
@Test
public void testAuthentication_normalAppIgnoresDevicePolicy() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mDevicePolicyManager
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, Authenticators.BIOMETRIC_STRONG);
waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
}
@Test
@@ -1530,18 +1526,17 @@ public class BiometricServiceTest {
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
Authenticators.BIOMETRIC_STRONG);
waitForIdle();
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
startPendingAuthSession(mBiometricService);
waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
}
@Test
public void testWorkAuthentication_fingerprintFailsIfDisabledByDevicePolicyManager()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
.thenReturn(true);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
when(mDevicePolicyManager
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
@@ -1555,9 +1550,9 @@ public class BiometricServiceTest {
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver2,
Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL);
waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession);
assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState());
mBiometricService.mAuthSession.getState());
verify(mReceiver2, never()).onError(anyInt(), anyInt(), anyInt());
}
@@ -1580,7 +1575,7 @@ public class BiometricServiceTest {
when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) {
if ((modality & TYPE_FINGERPRINT) != 0) {
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
.thenReturn(enrolled);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
@@ -1614,7 +1609,7 @@ public class BiometricServiceTest {
final int modality = modalities[i];
final int strength = strengths[i];
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) {
if ((modality & TYPE_FINGERPRINT) != 0) {
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
.thenReturn(true);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
@@ -1654,8 +1649,9 @@ public class BiometricServiceTest {
startPendingAuthSession(mBiometricService);
waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession);
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
assertNotNull(mBiometricService.mAuthSession);
assertEquals(TEST_REQUEST_ID, mBiometricService.mAuthSession.getRequestId());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
return requestId;
}
@@ -1663,14 +1659,14 @@ public class BiometricServiceTest {
private static void startPendingAuthSession(BiometricService service) throws Exception {
// Get the cookie so we can pretend the hardware is ready to authenticate
// Currently we only support single modality per auth
final PreAuthInfo preAuthInfo = service.mCurrentAuthSession.mPreAuthInfo;
final PreAuthInfo preAuthInfo = service.mAuthSession.mPreAuthInfo;
assertEquals(preAuthInfo.eligibleSensors.size(), 1);
assertEquals(preAuthInfo.numSensorsWaitingForCookie(), 1);
final int cookie = preAuthInfo.eligibleSensors.get(0).getCookie();
assertNotEquals(cookie, 0);
service.mImpl.onReadyForAuthentication(cookie);
service.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookie);
}
private static long invokeAuthenticate(IBiometricService.Stub service,

View File

@@ -29,12 +29,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
@@ -43,9 +39,11 @@ import androidx.test.filters.SmallTest;
import com.android.server.biometrics.sensors.fingerprint.Udfps;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.LinkedList;
@@ -53,39 +51,38 @@ import java.util.LinkedList;
@SmallTest
public class CoexCoordinatorTest {
private static final String TAG = "CoexCoordinatorTest";
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
private CoexCoordinator mCoexCoordinator;
private Handler mHandler;
@Mock
private Context mContext;
@Mock
private CoexCoordinator.Callback mCallback;
@Mock
private CoexCoordinator.ErrorCallback mErrorCallback;
@Mock
private AuthenticationClient mFaceClient;
@Mock
private AuthenticationClient mFingerprintClient;
@Mock(extraInterfaces = {Udfps.class})
private AuthenticationClient mUdfpsClient;
private CoexCoordinator mCoexCoordinator;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mHandler = new Handler(Looper.getMainLooper());
mCoexCoordinator = CoexCoordinator.getInstance();
mCoexCoordinator.setAdvancedLogicEnabled(true);
mCoexCoordinator.setFaceHapticDisabledWhenNonBypass(true);
mCoexCoordinator.reset();
}
@Test
public void testBiometricPrompt_authSuccess() {
mCoexCoordinator.reset();
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
@@ -93,15 +90,12 @@ public class CoexCoordinatorTest {
@Test
public void testBiometricPrompt_authReject_whenNotLockedOut() {
mCoexCoordinator.reset();
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
client, LockoutTracker.LOCKOUT_NONE, mCallback);
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
@@ -109,30 +103,97 @@ public class CoexCoordinatorTest {
@Test
public void testBiometricPrompt_authReject_whenLockedOut() {
mCoexCoordinator.reset();
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
client, LockoutTracker.LOCKOUT_TIMED, mCallback);
mFaceClient, LockoutTracker.LOCKOUT_TIMED, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testBiometricPrompt_coex_success() {
testBiometricPrompt_coex_success(false /* twice */);
}
@Test
public void testBiometricPrompt_coex_successWithoutDouble() {
testBiometricPrompt_coex_success(true /* twice */);
}
private void testBiometricPrompt_coex_success(boolean twice) {
initFaceAndFingerprintForBiometricPrompt();
when(mFaceClient.wasAuthSuccessful()).thenReturn(true);
when(mUdfpsClient.wasAuthSuccessful()).thenReturn(twice, true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mUdfpsClient, mCallback);
if (twice) {
verify(mCallback, never()).sendHapticFeedback();
} else {
verify(mCallback).sendHapticFeedback();
}
}
@Test
public void testBiometricPrompt_coex_reject() {
initFaceAndFingerprintForBiometricPrompt();
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback, never()).sendHapticFeedback();
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
}
@Test
public void testBiometricPrompt_coex_errorNoHaptics() {
initFaceAndFingerprintForBiometricPrompt();
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
mCoexCoordinator.onAuthenticationError(mUdfpsClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
verify(mErrorCallback, never()).sendHapticFeedback();
}
private void initFaceAndFingerprintForBiometricPrompt() {
when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(false);
when(mUdfpsClient.isBiometricPrompt()).thenReturn(true);
when(mUdfpsClient.wasAuthAttempted()).thenReturn(true);
}
@Test
public void testKeyguard_faceAuthOnly_success() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
@@ -140,21 +201,16 @@ public class CoexCoordinatorTest {
@Test
public void testKeyguard_faceAuth_udfpsNotTouching_faceSuccess() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
// Haptics tested in #testKeyguard_bypass_haptics. Let's leave this commented out (instead
// of removed) to keep this context.
// verify(mCallback).sendHapticFeedback();
@@ -192,25 +248,19 @@ public class CoexCoordinatorTest {
private void testKeyguard_bypass_haptics(boolean bypassEnabled, boolean faceAccepted,
boolean shouldReceiveHaptics) {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
if (faceAccepted) {
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
mCallback);
} else {
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
}
@@ -244,24 +294,18 @@ public class CoexCoordinatorTest {
private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted,
long udfpsRejectedAfterMs) {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
when (udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
// For easier reading
final CoexCoordinator.Callback faceCallback = mCallback;
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
faceCallback);
verify(faceCallback, never()).sendHapticFeedback();
verify(faceCallback, never()).sendAuthenticationResult(anyBoolean());
@@ -272,9 +316,9 @@ public class CoexCoordinatorTest {
// Reset the mock
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size());
assertEquals(faceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
assertEquals(mFaceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
if (thenUdfpsAccepted) {
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
udfpsCallback);
verify(udfpsCallback).sendHapticFeedback();
verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */);
@@ -284,7 +328,7 @@ public class CoexCoordinatorTest {
assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
} else {
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, udfpsClient,
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, mUdfpsClient,
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) {
verify(udfpsCallback, never()).sendHapticFeedback();
@@ -310,56 +354,44 @@ public class CoexCoordinatorTest {
@Test
public void testKeyguard_udfpsAuthSuccess_whileFaceScanning() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true));
verify(faceClient).cancel();
verify(mFaceClient).cancel();
verify(mCallback).handleLifecycleAfterAuth();
}
@Test
public void testKeyguard_faceRejectedWhenUdfpsTouching_thenUdfpsRejected() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback, never()).sendHapticFeedback();
verify(mCallback).handleLifecycleAfterAuth();
// BiometricScheduler removes the face authentication client after rejection
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
// Then UDFPS rejected
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, udfpsClient,
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mUdfpsClient,
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
verify(udfpsCallback).sendHapticFeedback();
verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
@@ -368,26 +400,20 @@ public class CoexCoordinatorTest {
@Test
public void testKeyguard_udfpsRejected_thenFaceRejected_noKeyguardBypass() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(faceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, udfpsClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
// Auth was attempted
when(udfpsClient.getState())
when(mUdfpsClient.getState())
.thenReturn(AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED);
verify(mCallback, never()).sendHapticFeedback();
verify(mCallback).handleLifecycleAfterAuth();
@@ -395,7 +421,7 @@ public class CoexCoordinatorTest {
// Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since
// unlike face, its lifecycle becomes "paused" instead of "finished".
CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class);
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, faceClient,
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, faceCallback);
verify(faceCallback).sendHapticFeedback();
verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
@@ -404,20 +430,16 @@ public class CoexCoordinatorTest {
@Test
public void testKeyguard_capacitiveAccepted_whenFaceScanning() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.isKeyguard()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class);
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(fpClient.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, fpClient, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFingerprintClient, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
@@ -425,21 +447,16 @@ public class CoexCoordinatorTest {
@Test
public void testKeyguard_capacitiveRejected_whenFaceScanning() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(mFingerprintClient.isKeyguard()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class);
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(fpClient.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, fpClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
mFingerprintClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth();
@@ -447,14 +464,11 @@ public class CoexCoordinatorTest {
@Test
public void testNonKeyguard_rejectAndNotLockedOut() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(false);
when(faceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback();
@@ -464,14 +478,11 @@ public class CoexCoordinatorTest {
@Test
public void testNonKeyguard_rejectLockedOut() {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(false);
when(faceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_TIMED, mCallback);
verify(mCallback).sendHapticFeedback();
@@ -496,16 +507,13 @@ public class CoexCoordinatorTest {
@Test
public void testBiometricPrompt_FaceError() {
mCoexCoordinator.reset();
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isBiometricPrompt()).thenReturn(true);
when(client.wasAuthAttempted()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
mErrorCallback);
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
verify(mErrorCallback).sendHapticFeedback();
}
@@ -520,18 +528,15 @@ public class CoexCoordinatorTest {
}
private void testKeyguard_faceAuthOnly(boolean bypassEnabled) {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
when(mFaceClient.wasUserDetected()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class);
when(client.isKeyguard()).thenReturn(true);
when(client.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(client.wasAuthAttempted()).thenReturn(true);
when(client.wasUserDetected()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
mErrorCallback);
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
verify(mErrorCallback).sendHapticFeedback();
}
@@ -546,23 +551,17 @@ public class CoexCoordinatorTest {
}
private void testKeyguard_coex_faceError(boolean bypassEnabled) {
mCoexCoordinator.reset();
when(mFaceClient.isKeyguard()).thenReturn(true);
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
when(mFaceClient.wasUserDetected()).thenReturn(true);
when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
when(faceClient.isKeyguard()).thenReturn(true);
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(faceClient.wasAuthAttempted()).thenReturn(true);
when(faceClient.wasUserDetected()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
withSettings().extraInterfaces(Udfps.class));
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
mCoexCoordinator.onAuthenticationError(faceClient,
mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
if (bypassEnabled) {