Merge changes from topic "coex-again-2" into tm-dev am: 532df4fc03

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17040854

Change-Id: I9a50e837b0d781fdfbaa910912dd577653f96d49
This commit is contained in:
Joe Bolinger
2022-03-01 23:41:52 +00:00
committed by Automerger Merge Worker
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; 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 * @hide
*/ */
public static final int BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT = 1; public static final int BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE = 1;
/** /**
* @hide * @hide
*/ */
@IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT, @IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT,
BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT}) BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface BiometricMultiSensorMode {} public @interface BiometricMultiSensorMode {}

View File

@@ -63,7 +63,7 @@ interface IBiometricService {
// Notify BiometricService when <Biometric>Service is ready to start the prepared client. // Notify BiometricService when <Biometric>Service is ready to start the prepared client.
// Client lifecycle is still managed in <Biometric>Service. // 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 // 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 // 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); void onSystemEvent(int event);
// Notifies that the dialog has finished animating. // Notifies that the dialog has finished animating.
void onDialogAnimatedIn(); 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. * 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. * 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); int multiSensorConfig);
// Used to notify the authentication dialog that a biometric has been authenticated // 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 // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
void onBiometricHelp(int modality, String message); void onBiometricHelp(int modality, String message);
// Used to show an error - the dialog will dismiss after a certain amount of time // 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; 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; repeated SensorServiceStateProto sensor_service_states = 1;
optional AuthSessionState auth_session_state = 2; 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 // 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 <ImageView
android:id="@+id/biometric_icon" android:id="@+id/biometric_icon"
android:layout_width="@dimen/biometric_dialog_biometric_icon_size" android:layout_width="wrap_content"
android:layout_height="@dimen/biometric_dialog_biometric_icon_size" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:contentDescription="@null" android:contentDescription="@null"
android:scaleType="fitXY" /> android:scaleType="fitXY" />

View File

@@ -14,12 +14,13 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView <com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/auth_biometric_contents"/> <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"); ~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License. ~ you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<com.android.systemui.biometrics.AuthBiometricUdfpsView <com.android.systemui.biometrics.AuthBiometricView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents" android:id="@+id/contents"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -23,4 +23,4 @@
<include layout="@layout/auth_biometric_contents"/> <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> <dimen name="remote_input_history_extra_height">60dp</dimen>
<!-- Biometric Dialog values --> <!-- 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_negative_max_width">160dp</dimen>
<dimen name="biometric_dialog_button_positive_max_width">136dp</dimen> <dimen name="biometric_dialog_button_positive_max_width">136dp</dimen>
<dimen name="biometric_dialog_corner_size">4dp</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> <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]--> <!-- 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> <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] --> <!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] -->
<string name="biometric_dialog_authenticated">Authenticated</string> <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.IntDef;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.annotation.StringRes;
import android.content.Context; import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricPrompt;
@@ -44,19 +45,21 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R; import com.android.systemui.R;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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. * Authentication hardware idle.
@@ -102,13 +105,6 @@ public abstract class AuthBiometricView extends LinearLayout {
int ACTION_BUTTON_TRY_AGAIN = 4; int ACTION_BUTTON_TRY_AGAIN = 4;
int ACTION_ERROR = 5; int ACTION_ERROR = 5;
int ACTION_USE_DEVICE_CREDENTIAL = 6; 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 * 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); 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; protected final Handler mHandler;
private final AccessibilityManager mAccessibilityManager; private final AccessibilityManager mAccessibilityManager;
private final LockPatternUtils mLockPatternUtils;
protected final int mTextColorError; protected final int mTextColorError;
protected final int mTextColorHint; protected final int mTextColorHint;
@@ -195,6 +134,11 @@ public abstract class AuthBiometricView extends LinearLayout {
protected ImageView mIconView; protected ImageView mIconView;
protected TextView mIndicatorView; 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 // Negative button position, exclusively for the app-specified behavior
@VisibleForTesting Button mNegativeButton; @VisibleForTesting Button mNegativeButton;
// Negative button position, exclusively for cancelling auth after passive auth success // 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 boolean mDialogSizeAnimating;
protected Bundle mSavedState; 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 mResetErrorRunnable;
private final Runnable mResetHelpRunnable; private final Runnable mResetHelpRunnable;
private final OnClickListener mBackgroundClickListener = (view) -> { private final OnClickListener mBackgroundClickListener = (view) -> {
@@ -262,11 +183,6 @@ public abstract class AuthBiometricView extends LinearLayout {
} }
public AuthBiometricView(Context context, AttributeSet attrs) { public AuthBiometricView(Context context, AttributeSet attrs) {
this(context, attrs, new Injector());
}
@VisibleForTesting
AuthBiometricView(Context context, AttributeSet attrs, Injector injector) {
super(context, attrs); super(context, attrs);
mHandler = new Handler(Looper.getMainLooper()); mHandler = new Handler(Looper.getMainLooper());
mTextColorError = getResources().getColor( mTextColorError = getResources().getColor(
@@ -274,10 +190,8 @@ public abstract class AuthBiometricView extends LinearLayout {
mTextColorHint = getResources().getColor( mTextColorHint = getResources().getColor(
R.color.biometric_dialog_gray, context.getTheme()); R.color.biometric_dialog_gray, context.getTheme());
mInjector = injector;
mInjector.mBiometricView = this;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mLockPatternUtils = new LockPatternUtils(context);
mResetErrorRunnable = () -> { mResetErrorRunnable = () -> {
updateState(getStateForAfterError()); 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; mPanelController = panelController;
} }
public void setPromptInfo(PromptInfo promptInfo) { void setPromptInfo(PromptInfo promptInfo) {
mPromptInfo = promptInfo; mPromptInfo = promptInfo;
} }
public void setCallback(Callback callback) { void setCallback(Callback callback) {
mCallback = callback; mCallback = callback;
} }
public void setBackgroundView(View backgroundView) { void setBackgroundView(View backgroundView) {
backgroundView.setOnClickListener(mBackgroundClickListener); backgroundView.setOnClickListener(mBackgroundClickListener);
} }
public void setUserId(int userId) { void setUserId(int userId) {
mUserId = userId; mUserId = userId;
} }
public void setEffectiveUserId(int effectiveUserId) { void setEffectiveUserId(int effectiveUserId) {
mEffectiveUserId = effectiveUserId; mEffectiveUserId = effectiveUserId;
} }
public void setRequireConfirmation(boolean requireConfirmation) { void setRequireConfirmation(boolean requireConfirmation) {
mRequireConfirmation = requireConfirmation; mRequireConfirmation = requireConfirmation && supportsRequireConfirmation();
} }
@VisibleForTesting @VisibleForTesting
void updateSize(@AuthDialog.DialogSize int newSize) { final void updateSize(@AuthDialog.DialogSize int newSize) {
Log.v(TAG, "Current size: " + mSize + " New size: " + newSize); Log.v(TAG, "Current size: " + mSize + " New size: " + newSize);
if (newSize == AuthDialog.SIZE_SMALL) { if (newSize == AuthDialog.SIZE_SMALL) {
mTitleView.setVisibility(View.GONE); mTitleView.setVisibility(View.GONE);
@@ -376,7 +345,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Choreograph together // Choreograph together
final AnimatorSet as = new AnimatorSet(); final AnimatorSet as = new AnimatorSet();
as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS); as.setDuration(mAnimationDurationShort);
as.addListener(new AnimatorListenerAdapter() { as.addListener(new AnimatorListenerAdapter() {
@Override @Override
public void onAnimationStart(Animator animation) { public void onAnimationStart(Animator animation) {
@@ -429,7 +398,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Translate at full duration // Translate at full duration
final ValueAnimator translationAnimator = ValueAnimator.ofFloat( final ValueAnimator translationAnimator = ValueAnimator.ofFloat(
biometricView.getY(), biometricView.getY() - translationY); biometricView.getY(), biometricView.getY() - translationY);
translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs()); translationAnimator.setDuration(mAnimationDurationLong);
translationAnimator.addUpdateListener((animation) -> { translationAnimator.addUpdateListener((animation) -> {
final float translation = (float) animation.getAnimatedValue(); final float translation = (float) animation.getAnimatedValue();
biometricView.setTranslationY(translation); biometricView.setTranslationY(translation);
@@ -438,7 +407,7 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation); super.onAnimationEnd(animation);
if (biometricView.getParent() != null) { if (biometricView.getParent() instanceof ViewGroup) {
((ViewGroup) biometricView.getParent()).removeView(biometricView); ((ViewGroup) biometricView.getParent()).removeView(biometricView);
} }
mSize = newSize; mSize = newSize;
@@ -447,7 +416,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Opacity to 0 in half duration // Opacity to 0 in half duration
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0); final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2); opacityAnimator.setDuration(mAnimationDurationLong / 2);
opacityAnimator.addUpdateListener((animation) -> { opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue(); final float opacity = (float) animation.getAnimatedValue();
biometricView.setAlpha(opacity); biometricView.setAlpha(opacity);
@@ -457,7 +426,7 @@ public abstract class AuthBiometricView extends LinearLayout {
mPanelController.updateForContentDimensions( mPanelController.updateForContentDimensions(
mPanelController.getContainerWidth(), mPanelController.getContainerWidth(),
mPanelController.getContainerHeight(), mPanelController.getContainerHeight(),
mInjector.getMediumToLargeAnimationDurationMs()); mAnimationDurationLong);
// Start the animations together // Start the animations together
AnimatorSet as = new AnimatorSet(); AnimatorSet as = new AnimatorSet();
@@ -466,7 +435,7 @@ public abstract class AuthBiometricView extends LinearLayout {
animators.add(opacityAnimator); animators.add(opacityAnimator);
as.playTogether(animators); as.playTogether(animators);
as.setDuration(mInjector.getMediumToLargeAnimationDurationMs() * 2 / 3); as.setDuration(mAnimationDurationLong * 2 / 3);
as.start(); as.start();
} else { } else {
Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize); 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) { public void updateState(@BiometricState int newState) {
Log.v(TAG, "newState: " + newState); Log.v(TAG, "newState: " + newState);
mIconController.updateState(mState, newState);
switch (newState) { switch (newState) {
case STATE_AUTHENTICATING_ANIMATING_IN: case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING: case STATE_AUTHENTICATING:
@@ -510,10 +481,11 @@ public abstract class AuthBiometricView extends LinearLayout {
mNegativeButton.setVisibility(View.GONE); mNegativeButton.setVisibility(View.GONE);
mCancelButton.setVisibility(View.VISIBLE); mCancelButton.setVisibility(View.VISIBLE);
mUseCredentialButton.setVisibility(View.GONE); mUseCredentialButton.setVisibility(View.GONE);
mConfirmButton.setEnabled(true); // forced confirmations (multi-sensor) use the icon view as the confirm button
mConfirmButton.setVisibility(View.VISIBLE); mConfirmButton.setEnabled(mRequireConfirmation);
mConfirmButton.setVisibility(mRequireConfirmation ? View.VISIBLE : View.GONE);
mIndicatorView.setTextColor(mTextColorHint); mIndicatorView.setTextColor(mTextColorHint);
mIndicatorView.setText(R.string.biometric_dialog_tap_confirm); mIndicatorView.setText(getConfirmationPrompt());
mIndicatorView.setVisibility(View.VISIBLE); mIndicatorView.setVisibility(View.VISIBLE);
break; break;
@@ -536,9 +508,9 @@ public abstract class AuthBiometricView extends LinearLayout {
updateState(STATE_AUTHENTICATING); updateState(STATE_AUTHENTICATING);
} }
public void onAuthenticationSucceeded() { public void onAuthenticationSucceeded(@Modality int modality) {
removePendingAnimations(); removePendingAnimations();
if (mRequireConfirmation) { if (mRequireConfirmation || forceRequireConfirmation(modality)) {
updateState(STATE_PENDING_CONFIRMATION); updateState(STATE_PENDING_CONFIRMATION);
} else { } else {
updateState(STATE_AUTHENTICATED); updateState(STATE_AUTHENTICATED);
@@ -553,6 +525,10 @@ public abstract class AuthBiometricView extends LinearLayout {
*/ */
public void onAuthenticationFailed( public void onAuthenticationFailed(
@Modality int modality, @Nullable String failureReason) { @Modality int modality, @Nullable String failureReason) {
if (ignoreUnsuccessfulEventsFrom(modality)) {
return;
}
showTemporaryMessage(failureReason, mResetErrorRunnable); showTemporaryMessage(failureReason, mResetErrorRunnable);
updateState(STATE_ERROR); updateState(STATE_ERROR);
} }
@@ -564,12 +540,27 @@ public abstract class AuthBiometricView extends LinearLayout {
* @param error message * @param error message
*/ */
public void onError(@Modality int modality, String error) { public void onError(@Modality int modality, String error) {
if (ignoreUnsuccessfulEventsFrom(modality)) {
return;
}
showTemporaryMessage(error, mResetErrorRunnable); showTemporaryMessage(error, mResetErrorRunnable);
updateState(STATE_ERROR); updateState(STATE_ERROR);
mHandler.postDelayed(() -> { mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_ERROR),
mCallback.onAction(Callback.ACTION_ERROR); mAnimationDurationHideDialog);
}, mInjector.getDelayAfterError()); }
/**
* 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 * @param help message
*/ */
public void onHelp(@Modality int modality, String help) { public void onHelp(@Modality int modality, String help) {
if (ignoreUnsuccessfulEventsFrom(modality)) {
return;
}
if (mSize != AuthDialog.SIZE_MEDIUM) { if (mSize != AuthDialog.SIZE_MEDIUM) {
Log.w(TAG, "Help received in size: " + mSize); Log.w(TAG, "Help received in size: " + mSize);
return; return;
@@ -639,7 +633,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// select to enable marquee unless a screen reader is enabled // select to enable marquee unless a screen reader is enabled
mIndicatorView.setSelected(!mAccessibilityManager.isEnabled() mIndicatorView.setSelected(!mAccessibilityManager.isEnabled()
|| !mAccessibilityManager.isTouchExplorationEnabled()); || !mAccessibilityManager.isTouchExplorationEnabled());
mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError()); mHandler.postDelayed(resetMessageRunnable, mAnimationDurationHideDialog);
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
} }
@@ -647,29 +641,22 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override @Override
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
onFinishInflateInternal();
}
/** mTitleView = findViewById(R.id.title);
* After inflation, but before things like restoreState, onAttachedToWindow, etc. mSubtitleView = findViewById(R.id.subtitle);
*/ mDescriptionView = findViewById(R.id.description);
@VisibleForTesting mIconView = findViewById(R.id.biometric_icon);
void onFinishInflateInternal() { mIconHolderView = findViewById(R.id.biometric_icon_frame);
mTitleView = mInjector.getTitleView(); mIndicatorView = findViewById(R.id.indicator);
mSubtitleView = mInjector.getSubtitleView();
mDescriptionView = mInjector.getDescriptionView();
mIconView = mInjector.getIconView();
mIconHolderView = mInjector.getIconHolderView();
mIndicatorView = mInjector.getIndicatorView();
// Negative-side (left) buttons // Negative-side (left) buttons
mNegativeButton = mInjector.getNegativeButton(); mNegativeButton = findViewById(R.id.button_negative);
mCancelButton = mInjector.getCancelButton(); mCancelButton = findViewById(R.id.button_cancel);
mUseCredentialButton = mInjector.getUseCredentialButton(); mUseCredentialButton = findViewById(R.id.button_use_credential);
// Positive-side (right) buttons // Positive-side (right) buttons
mConfirmButton = mInjector.getConfirmButton(); mConfirmButton = findViewById(R.id.button_confirm);
mTryAgainButton = mInjector.getTryAgainButton(); mTryAgainButton = findViewById(R.id.button_try_again);
mNegativeButton.setOnClickListener((view) -> { mNegativeButton.setOnClickListener((view) -> {
mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE); mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
@@ -693,6 +680,15 @@ public abstract class AuthBiometricView extends LinearLayout {
mTryAgainButton.setVisibility(View.GONE); mTryAgainButton.setVisibility(View.GONE);
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 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 @Override
protected void onAttachedToWindow() { protected void onAttachedToWindow() {
super.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()); mTitleView.setText(mPromptInfo.getTitle());
if (isDeviceCredentialAllowed()) { if (isDeviceCredentialAllowed()) {
final CharSequence credentialButtonText; final CharSequence credentialButtonText;
final @Utils.CredentialType int credentialType = @Utils.CredentialType final int credentialType =
Utils.getCredentialType(mContext, mEffectiveUserId); Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId);
switch (credentialType) { switch (credentialType) {
case Utils.CREDENTIAL_PIN: case Utils.CREDENTIAL_PIN:
credentialButtonText = credentialButtonText =
@@ -731,9 +719,6 @@ public abstract class AuthBiometricView extends LinearLayout {
getResources().getString(R.string.biometric_dialog_use_pattern); getResources().getString(R.string.biometric_dialog_use_pattern);
break; break;
case Utils.CREDENTIAL_PASSWORD: case Utils.CREDENTIAL_PASSWORD:
credentialButtonText =
getResources().getString(R.string.biometric_dialog_use_password);
break;
default: default:
credentialButtonText = credentialButtonText =
getResources().getString(R.string.biometric_dialog_use_password); getResources().getString(R.string.biometric_dialog_use_password);
@@ -749,7 +734,6 @@ public abstract class AuthBiometricView extends LinearLayout {
} }
setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle()); setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle());
setTextOrHide(mDescriptionView, mPromptInfo.getDescription()); setTextOrHide(mDescriptionView, mPromptInfo.getDescription());
if (mSavedState == null) { if (mSavedState == null) {
@@ -774,6 +758,8 @@ public abstract class AuthBiometricView extends LinearLayout {
protected void onDetachedFromWindow() { protected void onDetachedFromWindow() {
super.onDetachedFromWindow(); super.onDetachedFromWindow();
mIconController.setDeactivated(true);
// Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once // Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once
// the new dialog is restored. // the new dialog is restored.
mHandler.removeCallbacksAndMessages(null /* all */); mHandler.removeCallbacksAndMessages(null /* all */);
@@ -856,15 +842,7 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override @Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) { public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, 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 // Start with initial size only once. Subsequent layout changes don't matter since we
// only care about the initial icon position. // only care about the initial icon position.
if (mIconOriginalY == 0) { if (mIconOriginalY == 0) {

View File

@@ -16,9 +16,10 @@
package com.android.systemui.biometrics; 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 static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.annotation.DurationMillisLong;
import android.annotation.IntDef; import android.annotation.IntDef;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
@@ -52,13 +53,16 @@ import android.widget.LinearLayout;
import android.widget.ScrollView; import android.widget.ScrollView;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.Interpolators;
import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Top level container/controller for the BiometricPrompt UI. * Top level container/controller for the BiometricPrompt UI.
@@ -66,54 +70,52 @@ import java.util.List;
public class AuthContainerView extends LinearLayout public class AuthContainerView extends LinearLayout
implements AuthDialog, WakefulnessLifecycle.Observer { implements AuthDialog, WakefulnessLifecycle.Observer {
private static final String TAG = "BiometricPrompt/AuthContainerView"; private static final String TAG = "AuthContainerView";
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms
static final int STATE_UNKNOWN = 0; private static final int ANIMATION_DURATION_SHOW_MS = 250;
static final int STATE_ANIMATING_IN = 1; private static final int ANIMATION_DURATION_AWAY_MS = 350;
static final int STATE_PENDING_DISMISS = 2;
static final int STATE_SHOWING = 3; private static final int STATE_UNKNOWN = 0;
static final int STATE_ANIMATING_OUT = 4; private static final int STATE_ANIMATING_IN = 1;
static final int STATE_GONE = 5; 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) @Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING, @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
STATE_ANIMATING_OUT, STATE_GONE}) STATE_ANIMATING_OUT, STATE_GONE})
@interface ContainerState {} private @interface ContainerState {}
final Config mConfig; private final Config mConfig;
final int mEffectiveUserId; private final int mEffectiveUserId;
@Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps;
@Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
private final Handler mHandler; private final Handler mHandler;
private final Injector mInjector;
private final IBinder mWindowToken = new Binder(); private final IBinder mWindowToken = new Binder();
private final WindowManager mWindowManager; private final WindowManager mWindowManager;
private final AuthPanelController mPanelController;
private final Interpolator mLinearOutSlowIn; private final Interpolator mLinearOutSlowIn;
@VisibleForTesting final BiometricCallback mBiometricCallback;
private final CredentialCallback mCredentialCallback; private final CredentialCallback mCredentialCallback;
private final LockPatternUtils mLockPatternUtils;
@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 WakefulnessLifecycle mWakefulnessLifecycle; 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. // 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. // HAT received from LockSettingsService when credential is verified.
@Nullable byte[] mCredentialAttestation; @Nullable private byte[] mCredentialAttestation;
@VisibleForTesting
static class Config { static class Config {
Context mContext; Context mContext;
AuthDialogCallback mCallback; AuthDialogCallback mCallback;
@@ -122,11 +124,11 @@ public class AuthContainerView extends LinearLayout
int mUserId; int mUserId;
String mOpPackageName; String mOpPackageName;
int[] mSensorIds; int[] mSensorIds;
boolean mCredentialAllowed;
boolean mSkipIntro; boolean mSkipIntro;
long mOperationId; long mOperationId;
long mRequestId; long mRequestId;
@BiometricMultiSensorMode int mMultiSensorConfig; boolean mSkipAnimation = false;
@BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
} }
public static class Builder { public static class Builder {
@@ -167,7 +169,7 @@ public class AuthContainerView extends LinearLayout
return this; return this;
} }
public Builder setOperationId(long operationId) { public Builder setOperationId(@DurationMillisLong long operationId) {
mConfig.mOperationId = operationId; mConfig.mOperationId = operationId;
return this; return this;
} }
@@ -178,55 +180,27 @@ public class AuthContainerView extends LinearLayout
return this; return this;
} }
@VisibleForTesting
public Builder setSkipAnimationDuration(boolean skip) {
mConfig.mSkipAnimation = skip;
return this;
}
/** The multi-sensor mode. */ /** The multi-sensor mode. */
public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) { public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
mConfig.mMultiSensorConfig = multiSensorConfig; mConfig.mMultiSensorConfig = multiSensorConfig;
return this; return this;
} }
public AuthContainerView build(int[] sensorIds, boolean credentialAllowed, public AuthContainerView build(int[] sensorIds,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps, @Nullable List<FaceSensorPropertiesInternal> faceProps,
WakefulnessLifecycle wakefulnessLifecycle) { @NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils) {
mConfig.mSensorIds = sensorIds; mConfig.mSensorIds = sensorIds;
mConfig.mCredentialAllowed = credentialAllowed; return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
return new AuthContainerView( userManager, lockPatternUtils, new Handler(Looper.getMainLooper()));
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);
} }
} }
@@ -246,6 +220,7 @@ public class AuthContainerView extends LinearLayout
animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE); animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
break; break;
case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN: case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN:
mFailedModalities.clear();
mConfig.mCallback.onTryAgainPressed(); mConfig.mCallback.onTryAgainPressed();
break; break;
case AuthBiometricView.Callback.ACTION_ERROR: case AuthBiometricView.Callback.ACTION_ERROR:
@@ -255,10 +230,7 @@ public class AuthContainerView extends LinearLayout
mConfig.mCallback.onDeviceCredentialPressed(); mConfig.mCallback.onDeviceCredentialPressed();
mHandler.postDelayed(() -> { mHandler.postDelayed(() -> {
addCredentialView(false /* animatePanel */, true /* animateContents */); addCredentialView(false /* animatePanel */, true /* animateContents */);
}, mInjector.getAnimateCredentialStartDelayMs()); }, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
break;
case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR:
mConfig.mCallback.onStartFingerprintNow();
break; break;
default: default:
Log.e(TAG, "Unhandled action: " + action); Log.e(TAG, "Unhandled action: " + action);
@@ -275,21 +247,19 @@ public class AuthContainerView extends LinearLayout
} }
@VisibleForTesting @VisibleForTesting
AuthContainerView(Config config, Injector injector, AuthContainerView(Config config,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps, @Nullable List<FaceSensorPropertiesInternal> faceProps,
WakefulnessLifecycle wakefulnessLifecycle) { @NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull Handler mainHandler) {
super(config.mContext); super(config.mContext);
mConfig = config; mConfig = config;
mInjector = injector; mLockPatternUtils = lockPatternUtils;
mFpProps = fpProps; mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
mFaceProps = faceProps; mHandler = mainHandler;
mEffectiveUserId = mInjector.getUserManager(mContext)
.getCredentialOwnerProfile(mConfig.mUserId);
mHandler = new Handler(Looper.getMainLooper());
mWindowManager = mContext.getSystemService(WindowManager.class); mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle; mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -299,100 +269,42 @@ public class AuthContainerView extends LinearLayout
mBiometricCallback = new BiometricCallback(); mBiometricCallback = new BiometricCallback();
mCredentialCallback = new CredentialCallback(); mCredentialCallback = new CredentialCallback();
final LayoutInflater factory = LayoutInflater.from(mContext); final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
mFrameLayout = mInjector.inflateContainerView(factory, this); mFrameLayout = (FrameLayout) layoutInflater.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
mPanelView = mInjector.getPanelView(mFrameLayout); addView(mFrameLayout);
mPanelController = mInjector.getPanelController(mContext, mPanelView); 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. // Inflate biometric view only if necessary.
final int sensorCount = config.mSensorIds.length;
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
if (sensorCount == 1) { final FingerprintSensorPropertiesInternal fpProperties =
final int singleSensorAuthId = config.mSensorIds[0]; Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds);
if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) { final FaceSensorPropertiesInternal faceProperties =
FingerprintSensorPropertiesInternal sensorProps = null; Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds);
for (FingerprintSensorPropertiesInternal prop : mFpProps) {
if (prop.sensorId == singleSensorAuthId) {
sensorProps = prop;
break;
}
}
if (sensorProps.isAnyUdfpsType()) { if (fpProperties != null && faceProperties != null) {
AuthBiometricUdfpsView udfpsView = (AuthBiometricUdfpsView) factory final AuthBiometricFingerprintAndFaceView fingerprintAndFaceView =
.inflate(R.layout.auth_biometric_udfps_view, null, false); (AuthBiometricFingerprintAndFaceView) layoutInflater.inflate(
udfpsView.setSensorProps(sensorProps); R.layout.auth_biometric_fingerprint_and_face_view, null, false);
mBiometricView = udfpsView; fingerprintAndFaceView.setSensorProperties(fpProperties);
} else { mBiometricView = fingerprintAndFaceView;
mBiometricView = (AuthBiometricFingerprintView) factory } else if (fpProperties != null) {
.inflate(R.layout.auth_biometric_fingerprint_view, null, false); final AuthBiometricFingerprintView fpView =
} (AuthBiometricFingerprintView) layoutInflater.inflate(
} else if (Utils.containsSensorId(mFaceProps, singleSensorAuthId)) { R.layout.auth_biometric_fingerprint_view, null, false);
mBiometricView = (AuthBiometricFaceView) fpView.setSensorProperties(fpProperties);
factory.inflate(R.layout.auth_biometric_face_view, null, false); mBiometricView = fpView;
} else { } else if (faceProperties != null) {
// Unknown sensorId mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate(
Log.e(TAG, "Unknown sensorId: " + singleSensorAuthId); R.layout.auth_biometric_face_view, null, false);
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;
}
} else { } else {
Log.e(TAG, "Unsupported sensor array, length: " + sensorCount); Log.e(TAG, "No sensors found!");
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
} }
} }
mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout);
mBackgroundView = mInjector.getBackgroundView(mFrameLayout);
addView(mFrameLayout);
// init view before showing // init view before showing
if (mBiometricView != null) { if (mBiometricView != null) {
mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation); mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
@@ -431,10 +343,6 @@ public class AuthContainerView extends LinearLayout
return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo); return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
} }
private void addBiometricView() {
mBiometricScrollView.addView(mBiometricView);
}
/** /**
* Adds the credential view. When going from biometric to credential view, the biometric * 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, * 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) { private void addCredentialView(boolean animatePanel, boolean animateContents) {
final LayoutInflater factory = LayoutInflater.from(mContext); final LayoutInflater factory = LayoutInflater.from(mContext);
final @Utils.CredentialType int credentialType = mInjector.getCredentialType( @Utils.CredentialType final int credentialType = Utils.getCredentialType(
mContext, mEffectiveUserId); mLockPatternUtils, mEffectiveUserId);
switch (credentialType) { switch (credentialType) {
case Utils.CREDENTIAL_PATTERN: case Utils.CREDENTIAL_PATTERN:
@@ -493,15 +401,11 @@ public class AuthContainerView extends LinearLayout
@Override @Override
public void onAttachedToWindow() { public void onAttachedToWindow() {
super.onAttachedToWindow(); super.onAttachedToWindow();
onAttachedToWindowInternal();
}
@VisibleForTesting
void onAttachedToWindowInternal() {
mWakefulnessLifecycle.addObserver(this); mWakefulnessLifecycle.addObserver(this);
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
addBiometricView(); mBiometricScrollView.addView(mBiometricView);
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) { } else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */); addCredentialView(true /* animatePanel */, false /* animateContents */);
} else { } else {
@@ -521,17 +425,18 @@ public class AuthContainerView extends LinearLayout
mBiometricScrollView.setY(mTranslationY); mBiometricScrollView.setY(mTranslationY);
setAlpha(0f); setAlpha(0f);
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
postOnAnimation(() -> { postOnAnimation(() -> {
mPanelView.animate() mPanelView.animate()
.translationY(0) .translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.withEndAction(this::onDialogAnimatedIn) .withEndAction(this::onDialogAnimatedIn)
.start(); .start();
mBiometricScrollView.animate() mBiometricScrollView.animate()
.translationY(0) .translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.start(); .start();
@@ -539,14 +444,14 @@ public class AuthContainerView extends LinearLayout
mCredentialView.setY(mTranslationY); mCredentialView.setY(mTranslationY);
mCredentialView.animate() mCredentialView.animate()
.translationY(0) .translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.start(); .start();
} }
animate() animate()
.alpha(1f) .alpha(1f)
.setDuration(ANIMATION_DURATION_SHOW_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.start(); .start();
@@ -555,15 +460,8 @@ public class AuthContainerView extends LinearLayout
} }
private static boolean shouldUpdatePositionForUdfps(@NonNull View view) { private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
if (view instanceof AuthBiometricUdfpsView) { if (view instanceof AuthBiometricFingerprintView) {
return true; return ((AuthBiometricFingerprintView) view).isUdfps();
}
if (view instanceof AuthBiometricFaceToFingerprintView) {
AuthBiometricFaceToFingerprintView faceToFingerprintView =
(AuthBiometricFaceToFingerprintView) view;
return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT
&& faceToFingerprintView.isFingerprintUdfps();
} }
return false; return false;
@@ -652,12 +550,13 @@ public class AuthContainerView extends LinearLayout
} }
@Override @Override
public void onAuthenticationSucceeded() { public void onAuthenticationSucceeded(@Modality int modality) {
mBiometricView.onAuthenticationSucceeded(); mBiometricView.onAuthenticationSucceeded(modality);
} }
@Override @Override
public void onAuthenticationFailed(@Modality int modality, String failureReason) { public void onAuthenticationFailed(@Modality int modality, String failureReason) {
mFailedModalities.add(modality);
mBiometricView.onAuthenticationFailed(modality, failureReason); mBiometricView.onAuthenticationFailed(modality, failureReason);
} }
@@ -671,9 +570,18 @@ public class AuthContainerView extends LinearLayout
mBiometricView.onError(modality, error); 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 @Override
public void onSaveState(@NonNull Bundle outState) { 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 // 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. // biometric isn't showing if credential is showing since biometric is shown first.
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING, outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING,
@@ -695,8 +603,7 @@ public class AuthContainerView extends LinearLayout
mBiometricView.startTransitionToCredentialUI(); mBiometricView.startTransitionToCredentialUI();
} }
@VisibleForTesting void animateAway(@AuthDialogCallback.DismissedReason int reason) {
void animateAway(int reason) {
animateAway(true /* sendReason */, reason); animateAway(true /* sendReason */, reason);
} }
@@ -724,31 +631,32 @@ public class AuthContainerView extends LinearLayout
removeWindowIfAttached(); removeWindowIfAttached();
}; };
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
postOnAnimation(() -> { postOnAnimation(() -> {
mPanelView.animate() mPanelView.animate()
.translationY(mTranslationY) .translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.withEndAction(endActionRunnable) .withEndAction(endActionRunnable)
.start(); .start();
mBiometricScrollView.animate() mBiometricScrollView.animate()
.translationY(mTranslationY) .translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.start(); .start();
if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
mCredentialView.animate() mCredentialView.animate()
.translationY(mTranslationY) .translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.start(); .start();
} }
animate() animate()
.alpha(0f) .alpha(0f)
.setDuration(ANIMATION_DURATION_AWAY_MS) .setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn) .setInterpolator(mLinearOutSlowIn)
.withLayer() .withLayer()
.start(); .start();
@@ -773,8 +681,7 @@ public class AuthContainerView extends LinearLayout
mWindowManager.removeView(this); mWindowManager.removeView(this);
} }
@VisibleForTesting private void onDialogAnimatedIn() {
void onDialogAnimatedIn() {
if (mContainerState == STATE_PENDING_DISMISS) { if (mContainerState == STATE_PENDING_DISMISS) {
Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
@@ -788,8 +695,7 @@ public class AuthContainerView extends LinearLayout
} }
@VisibleForTesting @VisibleForTesting
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
CharSequence title) {
final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
| WindowManager.LayoutParams.FLAG_SECURE; | WindowManager.LayoutParams.FLAG_SECURE;
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
@@ -805,24 +711,4 @@ public class AuthContainerView extends LinearLayout
lp.token = windowToken; lp.token = windowToken;
return lp; 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.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.UserManager;
import android.util.Log; import android.util.Log;
import android.util.SparseBooleanArray; import android.util.SparseBooleanArray;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -59,6 +60,7 @@ import android.view.WindowManager;
import com.android.internal.R; import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs; import com.android.internal.os.SomeArgs;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.CoreStartable; import com.android.systemui.CoreStartable;
import com.android.systemui.assist.ui.DisplayUtils; import com.android.systemui.assist.ui.DisplayUtils;
import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.SysUISingleton;
@@ -123,8 +125,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
@Nullable private SidefpsController mSidefpsController; @Nullable private SidefpsController mSidefpsController;
@Nullable private IBiometricContextListener mBiometricContextListener; @Nullable private IBiometricContextListener mBiometricContextListener;
@VisibleForTesting @VisibleForTesting
TaskStackListener mTaskStackListener;
@VisibleForTesting
IBiometricSysuiReceiver mReceiver; IBiometricSysuiReceiver mReceiver;
@VisibleForTesting @VisibleForTesting
@NonNull final BiometricDisplayListener mOrientationListener; @NonNull final BiometricDisplayListener mOrientationListener;
@@ -137,13 +137,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
@NonNull private final SensorPrivacyManager mSensorPrivacyManager; @NonNull private final SensorPrivacyManager mSensorPrivacyManager;
private final WakefulnessLifecycle mWakefulnessLifecycle; private final WakefulnessLifecycle mWakefulnessLifecycle;
private boolean mAllAuthenticatorsRegistered; 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 @Override
public void onTaskStackChanged() { public void onTaskStackChanged() {
mHandler.post(AuthController.this::handleTaskStackChanged); mHandler.post(AuthController.this::handleTaskStackChanged);
} }
} };
private final IFingerprintAuthenticatorsRegisteredCallback private final IFingerprintAuthenticatorsRegisteredCallback
mFingerprintAuthenticatorsRegisteredCallback = mFingerprintAuthenticatorsRegisteredCallback =
@@ -256,6 +259,17 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null; mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null;
if (mUdfpsProps != null) { if (mUdfpsProps != null) {
mUdfpsController = mUdfpsControllerFactory.get(); 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; mSidefpsProps = !sidefpsProps.isEmpty() ? sidefpsProps : null;
if (mSidefpsProps != 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 @Override
public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) { public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) {
switch (reason) { switch (reason) {
@@ -503,12 +503,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
Provider<UdfpsController> udfpsControllerFactory, Provider<UdfpsController> udfpsControllerFactory,
Provider<SidefpsController> sidefpsControllerFactory, Provider<SidefpsController> sidefpsControllerFactory,
@NonNull DisplayManager displayManager, @NonNull DisplayManager displayManager,
WakefulnessLifecycle wakefulnessLifecycle, @NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull StatusBarStateController statusBarStateController, @NonNull StatusBarStateController statusBarStateController,
@Main Handler handler) { @Main Handler handler) {
super(context); super(context);
mExecution = execution; mExecution = execution;
mWakefulnessLifecycle = wakefulnessLifecycle; mWakefulnessLifecycle = wakefulnessLifecycle;
mUserManager = userManager;
mLockPatternUtils = lockPatternUtils;
mHandler = handler; mHandler = handler;
mCommandQueue = commandQueue; mCommandQueue = commandQueue;
mActivityTaskManager = activityTaskManager; mActivityTaskManager = activityTaskManager;
@@ -583,7 +587,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
mFingerprintAuthenticatorsRegisteredCallback); mFingerprintAuthenticatorsRegisteredCallback);
} }
mTaskStackListener = new BiometricTaskStackListener();
mActivityTaskManager.registerTaskStackListener(mTaskStackListener); mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
} }
@@ -668,11 +671,11 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
* example, KeyguardUpdateMonitor has its own {@link FingerprintManager.AuthenticationCallback}. * example, KeyguardUpdateMonitor has its own {@link FingerprintManager.AuthenticationCallback}.
*/ */
@Override @Override
public void onBiometricAuthenticated() { public void onBiometricAuthenticated(@Modality int modality) {
if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: "); if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: ");
if (mCurrentDialog != null) { if (mCurrentDialog != null) {
mCurrentDialog.onAuthenticationSucceeded(); mCurrentDialog.onAuthenticationSucceeded(modality);
} else { } else {
Log.w(TAG, "onBiometricAuthenticated callback but dialog gone"); 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 String opPackageName = (String) args.arg6;
final long operationId = args.argl1; final long operationId = args.argl1;
final long requestId = args.argl2; 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. // Create a new dialog but do not replace the current one yet.
final AuthDialog newDialog = buildDialog( final AuthDialog newDialog = buildDialog(
@@ -835,13 +838,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
requireConfirmation, requireConfirmation,
userId, userId,
sensorIds, sensorIds,
credentialAllowed,
opPackageName, opPackageName,
skipAnimation, skipAnimation,
operationId, operationId,
requestId, requestId,
multiSensorConfig, multiSensorConfig,
mWakefulnessLifecycle); mWakefulnessLifecycle,
mUserManager,
mLockPatternUtils);
if (newDialog == null) { if (newDialog == null) {
Log.e(TAG, "Unsupported type configuration"); 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 // Only show the dialog if necessary. If it was animating out, the dialog is supposed
// to send its pending callback immediately. // to send its pending callback immediately.
if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE) if (!savedState.getBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false)) {
!= AuthContainerView.STATE_ANIMATING_OUT) {
final boolean credentialShowing = final boolean credentialShowing =
savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING); savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING);
if (credentialShowing) { if (credentialShowing) {
@@ -927,10 +930,12 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
} }
protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation, 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, boolean skipIntro, long operationId, long requestId,
@BiometricMultiSensorMode int multiSensorConfig, @BiometricMultiSensorMode int multiSensorConfig,
WakefulnessLifecycle wakefulnessLifecycle) { @NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils) {
return new AuthContainerView.Builder(mContext) return new AuthContainerView.Builder(mContext)
.setCallback(this) .setCallback(this)
.setPromptInfo(promptInfo) .setPromptInfo(promptInfo)
@@ -941,7 +946,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
.setOperationId(operationId) .setOperationId(operationId)
.setRequestId(requestId) .setRequestId(requestId)
.setMultiSensorConfig(multiSensorConfig) .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 { 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_BIOMETRIC_SHOWING = "biometric_showing";
String KEY_CREDENTIAL_SHOWING = "credential_showing"; String KEY_CREDENTIAL_SHOWING = "credential_showing";
@@ -64,7 +64,7 @@ public interface AuthDialog {
@interface DialogSize {} @interface DialogSize {}
/** /**
* Parameters used when laying out {@link AuthBiometricView}, its sublclasses, and * Parameters used when laying out {@link AuthBiometricView}, its subclasses, and
* {@link AuthPanelController}. * {@link AuthPanelController}.
*/ */
class LayoutParams { class LayoutParams {
@@ -113,7 +113,7 @@ public interface AuthDialog {
/** /**
* Biometric authenticated. May be pending user confirmation, or completed. * Biometric authenticated. May be pending user confirmation, or completed.
*/ */
void onAuthenticationSucceeded(); void onAuthenticationSucceeded(@Modality int modality);
/** /**
* Authentication failed (reject, timeout). Dialog stays showing. * Authentication failed (reject, timeout). Dialog stays showing.
@@ -136,6 +136,9 @@ public interface AuthDialog {
*/ */
void onError(@Modality int modality, String error); void onError(@Modality int modality, String error);
/** UDFPS pointer down event. */
void onPointerDown();
/** /**
* Save the current state. * Save the current state.
* @param outState * @param outState

View File

@@ -70,9 +70,4 @@ public interface AuthDialogCallback {
* Notifies when the dialog has finished animating. * Notifies when the dialog has finished animating.
*/ */
void onDialogAnimatedIn(); 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. * 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. * de-dupe this if necessary.
*/ */
class UdfpsBpView(context: Context, attrs: AttributeSet?) : UdfpsAnimationView(context, attrs) { 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) { long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
} }
/** @see IStatusBar#onBiometricAuthenticated() */ /** @see IStatusBar#onBiometricAuthenticated(int) */
default void onBiometricAuthenticated() { default void onBiometricAuthenticated(@Modality int modality) {
} }
/** @see IStatusBar#onBiometricHelp(String) */ /** @see IStatusBar#onBiometricHelp(int, String) */
default void onBiometricHelp(@Modality int modality, String message) { default void onBiometricHelp(@Modality int modality, String message) {
} }
@@ -963,9 +963,11 @@ public class CommandQueue extends IStatusBar.Stub implements
} }
@Override @Override
public void onBiometricAuthenticated() { public void onBiometricAuthenticated(@Modality int modality) {
synchronized (mLock) { 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; break;
} }
case MSG_BIOMETRIC_AUTHENTICATED: { case MSG_BIOMETRIC_AUTHENTICATED: {
SomeArgs someArgs = (SomeArgs) msg.obj;
for (int i = 0; i < mCallbacks.size(); i++) { for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onBiometricAuthenticated(); mCallbacks.get(i).onBiometricAuthenticated(someArgs.argi1 /* modality */);
} }
someArgs.recycle();
break; break;
} }
case MSG_BIOMETRIC_HELP: { 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.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators; 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.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.PromptInfo;
import android.os.Bundle; import android.os.Bundle;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableLooper.RunWithLooper;
import android.util.AttributeSet; import android.testing.ViewUtils;
import android.view.LayoutInflater;
import android.view.View; 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.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@Ignore
@RunWith(AndroidTestingRunner.class) @RunWith(AndroidTestingRunner.class)
@RunWithLooper @RunWithLooper
@SmallTest @SmallTest
public class AuthBiometricViewTest extends SysuiTestCase { public class AuthBiometricViewTest extends SysuiTestCase {
@Mock private AuthBiometricView.Callback mCallback; @Rule
@Mock private AuthPanelController mPanelController; public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private Button mNegativeButton; @Mock
@Mock private Button mCancelButton; private AuthBiometricView.Callback mCallback;
@Mock private Button mUseCredentialButton; @Mock
private AuthPanelController mPanelController;
@Mock private Button mPositiveButton; private AuthBiometricView mBiometricView;
@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);
}
@Test @Test
public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() { public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); initDialog(false /* allowDeviceCredential */, mCallback);
// The onAuthenticated runnable is posted when authentication succeeds. // The onAuthenticated runnable is posted when authentication succeeds.
mBiometricView.onAuthenticationSucceeded(); mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
waitForIdleSync(); waitForIdleSync();
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED); verify(mCallback).onAction(ACTION_AUTHENTICATED);
} }
@Test @Test
public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() { public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() {
final Button negativeButton = new Button(mContext); initDialog(false /* allowDeviceCredential */, mCallback);
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;
}
});
mBiometricView.setRequireConfirmation(true); mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded(); mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
waitForIdleSync(); waitForIdleSync();
assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState);
verify(mCallback, never()).onAction(anyInt());
assertEquals(View.GONE, negativeButton.getVisibility()); // TODO: this should be tested in the subclasses
assertEquals(View.VISIBLE, cancelButton.getVisibility()); if (mBiometricView.supportsRequireConfirmation()) {
assertTrue(cancelButton.isEnabled()); 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 @Test
public void testPositiveButton_sendsActionAuthenticated() { public void testPositiveButton_sendsActionAuthenticated() {
Button button = new Button(mContext); initDialog(false /* allowDeviceCredential */, mCallback);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getConfirmButton() {
return button;
}
});
button.performClick(); mBiometricView.mConfirmButton.performClick();
waitForIdleSync(); waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED); verify(mCallback).onAction(ACTION_AUTHENTICATED);
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
} }
@Test @Test
public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() { public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
Button button = new Button(mContext); initDialog(false /* allowDeviceCredential */, mCallback);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return button;
}
});
mBiometricView.onDialogAnimatedIn(); mBiometricView.onDialogAnimatedIn();
button.performClick(); mBiometricView.mNegativeButton.performClick();
waitForIdleSync(); waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE);
@@ -158,25 +130,14 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test @Test
public void testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() { public void testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() {
Button cancelButton = new Button(mContext); initDialog(false /* allowDeviceCredential */, mCallback);
Button negativeButton = new Button(mContext);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return negativeButton;
}
@Override
public Button getCancelButton() {
return cancelButton;
}
});
mBiometricView.setRequireConfirmation(true); 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(); waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED);
@@ -184,15 +145,9 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test @Test
public void testTryAgainButton_sendsActionTryAgain() { public void testTryAgainButton_sendsActionTryAgain() {
Button button = new Button(mContext); initDialog(false /* allowDeviceCredential */, mCallback);
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getTryAgainButton() {
return button;
}
});
button.performClick(); mBiometricView.mTryAgainButton.performClick();
waitForIdleSync(); waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
@@ -202,7 +157,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test @Test
@Ignore("flaky, b/189031816") @Ignore("flaky, b/189031816")
public void testError_sendsActionError() { public void testError_sendsActionError() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); initDialog(false /* allowDeviceCredential */, mCallback);
final String testError = "testError"; final String testError = "testError";
mBiometricView.onError(TYPE_FACE, testError); mBiometricView.onError(TYPE_FACE, testError);
waitForIdleSync(); waitForIdleSync();
@@ -213,7 +168,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test @Test
public void testBackgroundClicked_sendsActionUserCanceled() { public void testBackgroundClicked_sendsActionUserCanceled() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); initDialog(false /* allowDeviceCredential */, mCallback);
View view = new View(mContext); View view = new View(mContext);
mBiometricView.setBackgroundView(view); mBiometricView.setBackgroundView(view);
@@ -223,18 +178,18 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test @Test
public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() { public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); initDialog(false /* allowDeviceCredential */, mCallback);
View view = new View(mContext); View view = new View(mContext);
mBiometricView.setBackgroundView(view); mBiometricView.setBackgroundView(view);
mBiometricView.onAuthenticationSucceeded(); mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
view.performClick(); view.performClick();
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)); verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
} }
@Test @Test
public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() { public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.mLayoutParams = new AuthDialog.LayoutParams(0, 0); mBiometricView.mLayoutParams = new AuthDialog.LayoutParams(0, 0);
mBiometricView.updateSize(AuthDialog.SIZE_SMALL); mBiometricView.updateSize(AuthDialog.SIZE_SMALL);
@@ -246,7 +201,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Test @Test
public void testIgnoresUselessHelp() { public void testIgnoresUselessHelp() {
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.onDialogAnimatedIn(); mBiometricView.onDialogAnimatedIn();
waitForIdleSync(); waitForIdleSync();
@@ -256,33 +211,16 @@ public class AuthBiometricViewTest extends SysuiTestCase {
mBiometricView.onHelp(TYPE_FINGERPRINT, ""); mBiometricView.onHelp(TYPE_FINGERPRINT, "");
waitForIdleSync(); waitForIdleSync();
verify(mIndicatorView, never()).setText(any()); assertEquals("", mBiometricView.mIndicatorView.getText());
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)); verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR));
assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState); assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState);
} }
@Test @Test
public void testRestoresState() { public void testRestoresState() {
final boolean requireConfirmation = true; // set/init from AuthController final boolean requireConfirmation = true;
Button tryAgainButton = new Button(mContext); initDialog(false /* allowDeviceCredential */, mCallback, null, 10000);
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;
}
});
final String failureMessage = "testFailureMessage"; final String failureMessage = "testFailureMessage";
mBiometricView.setRequireConfirmation(requireConfirmation); mBiometricView.setRequireConfirmation(requireConfirmation);
@@ -292,8 +230,8 @@ public class AuthBiometricViewTest extends SysuiTestCase {
Bundle state = new Bundle(); Bundle state = new Bundle();
mBiometricView.onSaveState(state); mBiometricView.onSaveState(state);
assertEquals(View.VISIBLE, tryAgainButton.getVisibility()); assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility());
assertEquals(View.VISIBLE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); assertEquals(View.GONE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState); assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
assertEquals(AuthBiometricView.STATE_ERROR, state.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); 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 // TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle
// Create new dialog and restore the previous state into it // Create new dialog and restore the previous state into it
Button tryAgainButton2 = new Button(mContext); initDialog(false /* allowDeviceCredential */, mCallback, state, 10000);
TextView indicatorView2 = new TextView(mContext); mBiometricView.mAnimationDurationHideDialog = 10000;
initDialog(mContext, false /* allowDeviceCredential */, mCallback, state,
new MockInjector() {
@Override
public Button getTryAgainButton() {
return tryAgainButton2;
}
@Override
public TextView getIndicatorView() {
return indicatorView2;
}
});
mBiometricView.setRequireConfirmation(requireConfirmation); mBiometricView.setRequireConfirmation(requireConfirmation);
waitForIdleSync(); waitForIdleSync();
// Test restored state assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility());
assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState); assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility()); assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
@@ -334,23 +259,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
} }
@Test @Test
public void testCredentialButton_whenDeviceCredentialAllowed() { public void testCredentialButton_whenDeviceCredentialAllowed() throws InterruptedException {
final Button negativeButton = new Button(mContext); initDialog(true /* allowDeviceCredential */, mCallback);
final Button useCredentialButton = new Button(mContext);
initDialog(mContext, true /* allowDeviceCredential */, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return negativeButton;
}
@Override assertEquals(View.VISIBLE, mBiometricView.mUseCredentialButton.getVisibility());
public Button getUseCredentialButton() { assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
return useCredentialButton; mBiometricView.mUseCredentialButton.performClick();
}
});
assertEquals(View.GONE, negativeButton.getVisibility());
useCredentialButton.performClick();
waitForIdleSync(); waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL);
@@ -369,120 +283,30 @@ public class AuthBiometricViewTest extends SysuiTestCase {
return promptInfo; return promptInfo;
} }
private void initDialog(Context context, boolean allowDeviceCredential, private void initDialog(boolean allowDeviceCredential, AuthBiometricView.Callback callback) {
AuthBiometricView.Callback callback, initDialog(allowDeviceCredential, callback,
Bundle savedState, MockInjector injector) { null /* savedState */, 0 /* hideDelay */);
mBiometricView = new TestableBiometricView(context, null, injector); }
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.setPromptInfo(buildPromptInfo(allowDeviceCredential));
mBiometricView.setCallback(callback); mBiometricView.setCallback(callback);
mBiometricView.restoreState(savedState); mBiometricView.restoreState(savedState);
mBiometricView.onFinishInflateInternal(); ViewUtils.attachView(mBiometricView);
mBiometricView.onAttachedToWindowInternal();
mBiometricView.setPanelController(mPanelController); mBiometricView.setPanelController(mPanelController);
waitForIdleSync();
} }
private void initDialog(Context context, boolean allowDeviceCredential, @Override
AuthBiometricView.Callback callback, MockInjector injector) { protected void waitForIdleSync() {
initDialog(context, allowDeviceCredential, callback, null /* savedState */, injector); TestableLooper.get(this).processAllMessages();
} super.waitForIdleSync();
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;
}
} }
} }

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

View File

@@ -17,10 +17,10 @@
package com.android.systemui.biometrics package com.android.systemui.biometrics
import android.animation.Animator import android.animation.Animator
import android.graphics.Insets
import android.app.ActivityManager import android.app.ActivityManager
import android.app.ActivityTaskManager import android.app.ActivityTaskManager
import android.content.ComponentName import android.content.ComponentName
import android.graphics.Insets
import android.graphics.Rect import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS 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.`when`
import org.mockito.Mockito.any import org.mockito.Mockito.any
import org.mockito.Mockito.anyFloat import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset

View File

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

View File

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

View File

@@ -448,9 +448,10 @@ public class CommandQueueTest extends SysuiTestCase {
@Test @Test
public void testOnBiometricAuthenticated() { public void testOnBiometricAuthenticated() {
mCommandQueue.onBiometricAuthenticated(); final int id = 12;
mCommandQueue.onBiometricAuthenticated(id);
waitForIdleSync(); waitForIdleSync();
verify(mCallbacks).onBiometricAuthenticated(); verify(mCallbacks).onBiometricAuthenticated(eq(id));
} }
@Test @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_FINGERPRINT;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; 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_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_AUTHENTICATED_PENDING_SYSUI;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE;
@@ -100,14 +96,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@interface SessionState {} @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 * 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 * 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 Context mContext;
private final IStatusBarService mStatusBarService; private final IStatusBarService mStatusBarService;
private final IBiometricSysuiReceiver mSysuiReceiver; @VisibleForTesting final IBiometricSysuiReceiver mSysuiReceiver;
private final KeyStore mKeyStore; private final KeyStore mKeyStore;
private final Random mRandom; private final Random mRandom;
private final ClientDeathReceiver mClientDeathReceiver; private final ClientDeathReceiver mClientDeathReceiver;
@@ -133,7 +121,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
private final long mRequestId; private final long mRequestId;
private final long mOperationId; private final long mOperationId;
private final int mUserId; private final int mUserId;
private final IBiometricSensorReceiver mSensorReceiver; @VisibleForTesting final IBiometricSensorReceiver mSensorReceiver;
// Original receiver from BiometricPrompt. // Original receiver from BiometricPrompt.
private final IBiometricServiceReceiver mClientReceiver; private final IBiometricServiceReceiver mClientReceiver;
private final String mOpPackageName; 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 // The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE; private @SessionState int mState = STATE_AUTH_IDLE;
private @BiometricMultiSensorMode int mMultiSensorMode; private @BiometricMultiSensorMode int mMultiSensorMode;
private @MultiSensorState int mMultiSensorState;
private int[] mSensors; private int[] mSensors;
// TODO(b/197265902): merge into state // TODO(b/197265902): merge into state
private boolean mCancelled; private boolean mCancelled;
private int mAuthenticatedSensorId = -1;
// For explicit confirmation, do not send to keystore until the user has confirmed // For explicit confirmation, do not send to keystore until the user has confirmed
// the authentication. // the authentication.
private byte[] mTokenEscrow; 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) { 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 int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
final boolean requireConfirmation = isConfirmationRequired(sensor); final boolean requireConfirmation = isConfirmationRequired(sensor);
@@ -254,7 +250,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_SHOWING_DEVICE_CREDENTIAL; mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mSensors = new int[0]; mSensors = new int[0];
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT; mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mStatusBarService.showAuthenticationDialog( mStatusBarService.showAuthenticationDialog(
mPromptInfo, mPromptInfo,
@@ -269,7 +264,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
mMultiSensorMode); mMultiSensorMode);
} else if (!mPreAuthInfo.eligibleSensors.isEmpty()) { } else if (!mPreAuthInfo.eligibleSensors.isEmpty()) {
// Some combination of biometric or biometric|credential is requested // Some combination of biometric or biometric|credential is requested
setSensorsToStateWaitingForCookie(); setSensorsToStateWaitingForCookie(false /* isTryAgain */);
mState = STATE_AUTH_CALLED; mState = STATE_AUTH_CALLED;
} else { } else {
// No authenticators requested. This should never happen - an exception should have // 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); Slog.w(TAG, "Received cookie but already cancelled (ignoring): " + cookie);
return; return;
} }
if (hasAuthenticated()) {
Slog.d(TAG, "onCookieReceived after successful auth");
return;
}
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
sensor.goToStateCookieReturnedIfCookieMatches(cookie); sensor.goToStateCookieReturnedIfCookieMatches(cookie);
@@ -307,7 +306,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
mMultiSensorMode = getMultiSensorModeForNewSession( mMultiSensorMode = getMultiSensorModeForNewSession(
mPreAuthInfo.eligibleSensors); mPreAuthInfo.eligibleSensors);
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mStatusBarService.showAuthenticationDialog(mPromptInfo, mStatusBarService.showAuthenticationDialog(mPromptInfo,
mSysuiReceiver, mSysuiReceiver,
@@ -381,9 +379,8 @@ public final class AuthSession implements IBinder.DeathRecipient {
// sending the final error callback to the application. // sending the final error callback to the application.
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
try { try {
final boolean shouldCancel = filter.apply(sensor); if (filter.apply(sensor)) {
Slog.d(TAG, "sensorId: " + sensor.id + ", shouldCancel: " + shouldCancel); Slog.d(TAG, "Cancelling sensorId: " + sensor.id);
if (shouldCancel) {
sensor.goToStateCancelling(mToken, mOpPackageName, mRequestId); sensor.goToStateCancelling(mToken, mOpPackageName, mRequestId);
} }
} catch (RemoteException e) { } 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; mErrorEscrow = error;
mVendorCodeEscrow = vendorCode; mVendorCodeEscrow = vendorCode;
final @BiometricAuthenticator.Modality int modality = sensorIdToModality(sensorId); @Modality final int modality = sensorIdToModality(sensorId);
switch (mState) { switch (mState) {
case STATE_AUTH_CALLED: { case STATE_AUTH_CALLED: {
@@ -430,7 +433,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_SHOWING_DEVICE_CREDENTIAL; mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT; mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mSensors = new int[0]; mSensors = new int[0];
mStatusBarService.showAuthenticationDialog( mStatusBarService.showAuthenticationDialog(
@@ -468,12 +470,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
return true; return true;
} else { } else {
mState = STATE_ERROR_PENDING_SYSUI; 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); mStatusBarService.onBiometricError(modality, error, vendorCode);
} }
break; break;
@@ -505,6 +501,11 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
void onAcquired(int sensorId, int acquiredInfo, int vendorCode) { 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); final String message = getAcquiredMessageForSensor(sensorId, acquiredInfo, vendorCode);
Slog.d(TAG, "sensorId: " + sensorId + " acquiredInfo: " + acquiredInfo Slog.d(TAG, "sensorId: " + sensorId + " acquiredInfo: " + acquiredInfo
+ " message: " + message); + " message: " + message);
@@ -520,6 +521,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
void onSystemEvent(int event) { void onSystemEvent(int event) {
if (hasAuthenticated()) {
Slog.d(TAG, "onSystemEvent after successful auth");
return;
}
if (!mPromptInfo.isReceiveSystemEvents()) { if (!mPromptInfo.isReceiveSystemEvents()) {
return; return;
} }
@@ -538,53 +543,35 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_AUTH_STARTED_UI_SHOWING; 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(); startAllPreparedFingerprintSensors();
mState = STATE_AUTH_STARTED_UI_SHOWING; mState = STATE_AUTH_STARTED_UI_SHOWING;
} }
void onTryAgainPressed() { void onTryAgainPressed() {
if (hasAuthenticated()) {
Slog.d(TAG, "onTryAgainPressed after successful auth");
return;
}
if (mState != STATE_AUTH_PAUSED) { if (mState != STATE_AUTH_PAUSED) {
Slog.w(TAG, "onTryAgainPressed, state: " + mState); Slog.w(TAG, "onTryAgainPressed, state: " + mState);
} }
try { try {
setSensorsToStateWaitingForCookie(); setSensorsToStateWaitingForCookie(true /* isTryAgain */);
mState = STATE_AUTH_PAUSED_RESUMING; mState = STATE_AUTH_PAUSED_RESUMING;
} catch (RemoteException e) { } catch (RemoteException e) {
Slog.e(TAG, "RemoteException: " + e); Slog.e(TAG, "RemoteException: " + e);
} }
} }
void onAuthenticationSucceeded(int sensorId, boolean strong, void onAuthenticationSucceeded(int sensorId, boolean strong, byte[] token) {
byte[] token) { if (hasAuthenticated()) {
Slog.d(TAG, "onAuthenticationSucceeded after successful auth");
return;
}
mAuthenticatedSensorId = sensorId;
if (strong) { if (strong) {
mTokenEscrow = token; mTokenEscrow = token;
} else { } else {
@@ -596,7 +583,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
try { try {
// Notify SysUI that the biometric has been authenticated. SysUI already knows // Notify SysUI that the biometric has been authenticated. SysUI already knows
// the implicit/explicit state and will react accordingly. // the implicit/explicit state and will react accordingly.
mStatusBarService.onBiometricAuthenticated(); mStatusBarService.onBiometricAuthenticated(sensorIdToModality(sensorId));
final boolean requireConfirmation = isConfirmationRequiredByAnyEligibleSensor(); final boolean requireConfirmation = isConfirmationRequiredByAnyEligibleSensor();
@@ -609,20 +596,22 @@ public final class AuthSession implements IBinder.DeathRecipient {
} catch (RemoteException e) { } catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e); Slog.e(TAG, "RemoteException", e);
} }
cancelAllSensors(sensor -> sensor.id != sensorId);
} }
void onAuthenticationRejected() { void onAuthenticationRejected(int sensorId) {
try { if (hasAuthenticated()) {
mStatusBarService.onBiometricError(TYPE_NONE, Slog.d(TAG, "onAuthenticationRejected after successful auth");
BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */); return;
}
// TODO: This logic will need to be updated if BP is multi-modal try {
if (hasPausableBiometric()) { mStatusBarService.onBiometricError(sensorIdToModality(sensorId),
// Pause authentication. onBiometricAuthenticated(false) causes the BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */);
// dialog to show a "try again" button for passive modalities. if (pauseSensorIfSupported(sensorId)) {
mState = STATE_AUTH_PAUSED; mState = STATE_AUTH_PAUSED;
} }
mClientReceiver.onAuthenticationFailed(); mClientReceiver.onAuthenticationFailed();
} catch (RemoteException e) { } catch (RemoteException e) {
Slog.e(TAG, "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) { void onAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) {
if (hasAuthenticated()) {
Slog.d(TAG, "onAuthenticationTimedOut after successful auth");
return;
}
try { try {
mStatusBarService.onBiometricError(sensorIdToModality(sensorId), error, vendorCode); mStatusBarService.onBiometricError(sensorIdToModality(sensorId), error, vendorCode);
pauseSensorIfSupported(sensorId);
mState = STATE_AUTH_PAUSED; mState = STATE_AUTH_PAUSED;
} catch (RemoteException e) { } catch (RemoteException e) {
Slog.e(TAG, "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() { void onDeviceCredentialPressed() {
if (hasAuthenticated()) {
Slog.d(TAG, "onDeviceCredentialPressed after successful auth");
return;
}
// Cancel authentication. Skip the token/package check since we are cancelling // Cancel authentication. Skip the token/package check since we are cancelling
// from system server. The interface is permission protected so this is fine. // from system server. The interface is permission protected so this is fine.
cancelAllSensors(); 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) { private void logOnDialogDismissed(@BiometricPrompt.DismissedReason int reason) {
if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) { if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) {
// Explicit auth, authentication 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 * @return true if this AuthSession is finished, e.g. should be set to null
*/ */
boolean onCancelAuthSession(boolean force) { boolean onCancelAuthSession(boolean force) {
if (hasAuthenticated()) {
Slog.d(TAG, "onCancelAuthSession after successful auth");
return true;
}
mCancelled = true; mCancelled = true;
final boolean authStarted = mState == STATE_AUTH_CALLED final boolean authStarted = mState == STATE_AUTH_CALLED
@@ -848,15 +865,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
return remainingCookies == 0; return remainingCookies == 0;
} }
private boolean hasPausableBiometric() {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
if (sensor.modality == TYPE_FACE) {
return true;
}
}
return false;
}
@SessionState int getState() { @SessionState int getState() {
return mState; return mState;
} }
@@ -919,7 +927,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
} }
if (hasFace && hasFingerprint) { if (hasFace && hasFingerprint) {
return BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT; return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
} }
return BIOMETRIC_MULTI_SENSOR_DEFAULT; return BIOMETRIC_MULTI_SENSOR_DEFAULT;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
package com.android.server.biometrics; 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.Authenticators;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
@@ -85,14 +86,11 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.util.Random; import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
@Presubmit @Presubmit
@SmallTest @SmallTest
public class BiometricServiceTest { public class BiometricServiceTest {
private static final String TAG = "BiometricServiceTest";
private static final String TEST_PACKAGE_NAME = "test_package"; private static final String TEST_PACKAGE_NAME = "test_package";
private static final long TEST_REQUEST_ID = 44; private static final long TEST_REQUEST_ID = 44;
@@ -153,7 +151,7 @@ public class BiometricServiceTest {
.thenReturn(mock(BiometricStrengthController.class)); .thenReturn(mock(BiometricStrengthController.class));
when(mInjector.getTrustManager()).thenReturn(mTrustManager); when(mInjector.getTrustManager()).thenReturn(mTrustManager);
when(mInjector.getDevicePolicyManager(any())).thenReturn(mDevicePolicyManager); 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)) when(mResources.getString(R.string.biometric_error_hw_unavailable))
.thenReturn(ERROR_HW_UNAVAILABLE); .thenReturn(ERROR_HW_UNAVAILABLE);
@@ -178,22 +176,22 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
true /* requireConfirmation */, null /* authenticators */); true /* requireConfirmation */, null /* authenticators */);
waitForIdle(); waitForIdle();
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession), verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
anyInt()); anyInt());
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE, SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
mBiometricService.mCurrentAuthSession.binderDied(); mBiometricService.mAuthSession.binderDied();
waitForIdle(); waitForIdle();
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
} }
@@ -205,31 +203,31 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
true /* requireConfirmation */, null /* authenticators */); true /* requireConfirmation */, null /* authenticators */);
waitForIdle(); waitForIdle();
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession), verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
anyInt()); anyInt());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
mBiometricService.mCurrentAuthSession.binderDied(); mBiometricService.mAuthSession.binderDied();
waitForIdle(); waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession); assertNotNull(mBiometricService.mAuthSession);
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog(); verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
assertEquals(STATE_CLIENT_DIED_CANCELLING, 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()); .cancelAuthenticationFromService(any(), any(), anyLong());
// Simulate ERROR_CANCELED received from HAL // Simulate ERROR_CANCELED received from HAL
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE, SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED, BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
} }
@Test @Test
@@ -265,12 +263,12 @@ public class BiometricServiceTest {
Authenticators.DEVICE_CREDENTIAL); Authenticators.DEVICE_CREDENTIAL);
waitForIdle(); waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession); assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
// StatusBar showBiometricDialog invoked // StatusBar showBiometricDialog invoked
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */, eq(true) /* credentialAllowed */,
@@ -304,21 +302,21 @@ public class BiometricServiceTest {
mBiometricService = new BiometricService(mContext, mInjector); mBiometricService = new BiometricService(mContext, mInjector);
mBiometricService.onStart(); mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(0 /* id */, mBiometricService.mImpl.registerAuthenticator(0 /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator); mFingerprintAuthenticator);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
null /* authenticators */); null /* authenticators */);
waitForIdle(); waitForIdle();
verify(mReceiver1).onError( verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS), eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
} }
@Test @Test
public void testAuthenticate_notStrongEnough_returnsHardwareNotPresent() throws Exception { 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 */, invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
Authenticators.BIOMETRIC_STRONG); Authenticators.BIOMETRIC_STRONG);
@@ -335,7 +333,7 @@ public class BiometricServiceTest {
// is able to proceed. // is able to proceed.
final int[] modalities = new int[] { final int[] modalities = new int[] {
BiometricAuthenticator.TYPE_FINGERPRINT, TYPE_FINGERPRINT,
BiometricAuthenticator.TYPE_FACE, BiometricAuthenticator.TYPE_FACE,
}; };
@@ -356,7 +354,7 @@ public class BiometricServiceTest {
// StatusBar showBiometricDialog invoked with face, which was set up to be STRONG // StatusBar showBiometricDialog invoked with face, which was set up to be STRONG
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[] {SENSOR_ID_FACE}), AdditionalMatchers.aryEq(new int[] {SENSOR_ID_FACE}),
eq(false) /* credentialAllowed */, eq(false) /* credentialAllowed */,
@@ -377,14 +375,14 @@ public class BiometricServiceTest {
mBiometricService = new BiometricService(mContext, mInjector); mBiometricService = new BiometricService(mContext, mInjector);
mBiometricService.onStart(); mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(0 /* id */, mBiometricService.mImpl.registerAuthenticator(0 /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator); mFingerprintAuthenticator);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
null /* authenticators */); null /* authenticators */);
waitForIdle(); waitForIdle();
verify(mReceiver1).onError( verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
} }
@@ -415,13 +413,13 @@ public class BiometricServiceTest {
waitForIdle(); waitForIdle();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
final byte[] HAT = generateRandomHAT(); final byte[] HAT = generateRandomHAT();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE, SENSOR_ID_FACE,
HAT); HAT);
waitForIdle(); waitForIdle();
// Confirmation is required // Confirmation is required
assertEquals(STATE_AUTH_PENDING_CONFIRM, assertEquals(STATE_AUTH_PENDING_CONFIRM,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
// Enrolled, not disabled in settings, user doesn't require confirmation in settings // Enrolled, not disabled in settings, user doesn't require confirmation in settings
resetReceivers(); resetReceivers();
@@ -431,25 +429,25 @@ public class BiometricServiceTest {
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
null /* authenticators */); null /* authenticators */);
waitForIdle(); waitForIdle();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE, SENSOR_ID_FACE,
HAT); HAT);
waitForIdle(); waitForIdle();
// Confirmation not required, waiting for dialog to dismiss // Confirmation not required, waiting for dialog to dismiss
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI, assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
} }
@Test @Test
public void testAuthenticate_happyPathWithoutConfirmation_strongBiometric() throws Exception { public void testAuthenticate_happyPathWithoutConfirmation_strongBiometric() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
testAuthenticate_happyPathWithoutConfirmation(true /* isStrongBiometric */); testAuthenticate_happyPathWithoutConfirmation(true /* isStrongBiometric */);
} }
@Test @Test
public void testAuthenticate_happyPathWithoutConfirmation_weakBiometric() throws Exception { public void testAuthenticate_happyPathWithoutConfirmation_weakBiometric() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
testAuthenticate_happyPathWithoutConfirmation(false /* isStrongBiometric */); testAuthenticate_happyPathWithoutConfirmation(false /* isStrongBiometric */);
} }
@@ -461,7 +459,7 @@ public class BiometricServiceTest {
waitForIdle(); waitForIdle();
// Creates a pending auth session with the correct initial states // 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 // Invokes <Modality>Service#prepareForAuthentication
ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -477,19 +475,19 @@ public class BiometricServiceTest {
cookieCaptor.capture() /* cookie */, cookieCaptor.capture() /* cookie */,
anyBoolean() /* allowBackgroundAuthentication */); anyBoolean() /* allowBackgroundAuthentication */);
// onReadyForAuthentication, mCurrentAuthSession state OK // onReadyForAuthentication, mAuthSession state OK
mBiometricService.mImpl.onReadyForAuthentication(cookieCaptor.getValue()); mBiometricService.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookieCaptor.getValue());
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
// startPreparedClient invoked // startPreparedClient invoked
mBiometricService.mCurrentAuthSession.onDialogAnimatedIn(); mBiometricService.mAuthSession.onDialogAnimatedIn();
verify(mBiometricService.mSensors.get(0).impl) verify(mBiometricService.mSensors.get(0).impl)
.startPreparedClient(cookieCaptor.getValue()); .startPreparedClient(cookieCaptor.getValue());
// StatusBar showBiometricDialog invoked // StatusBar showBiometricDialog invoked
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
any(), any(),
eq(false) /* credentialAllowed */, eq(false) /* credentialAllowed */,
@@ -502,18 +500,18 @@ public class BiometricServiceTest {
// Hardware authenticated // Hardware authenticated
final byte[] HAT = generateRandomHAT(); final byte[] HAT = generateRandomHAT();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FINGERPRINT, SENSOR_ID_FINGERPRINT,
HAT); HAT);
waitForIdle(); waitForIdle();
// Waiting for SystemUI to send dismissed callback // Waiting for SystemUI to send dismissed callback
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI, assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
// Notify SystemUI hardware authenticated // Notify SystemUI hardware authenticated
verify(mBiometricService.mStatusBarService).onBiometricAuthenticated(); verify(mBiometricService.mStatusBarService).onBiometricAuthenticated(TYPE_FINGERPRINT);
// SystemUI sends callback with dismissed reason // SystemUI sends callback with dismissed reason
mBiometricService.mSysuiReceiver.onDialogDismissed( mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED,
null /* credentialAttestation */); null /* credentialAttestation */);
waitForIdle(); waitForIdle();
@@ -527,7 +525,7 @@ public class BiometricServiceTest {
verify(mReceiver1).onAuthenticationSucceeded( verify(mReceiver1).onAuthenticationSucceeded(
BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC); BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC);
// Current session becomes null // Current session becomes null
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
} }
@Test @Test
@@ -542,11 +540,11 @@ public class BiometricServiceTest {
waitForIdle(); waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
assertEquals(Authenticators.DEVICE_CREDENTIAL, assertEquals(Authenticators.DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators()); mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */, eq(true) /* credentialAllowed */,
@@ -578,16 +576,16 @@ public class BiometricServiceTest {
// Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not // Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not
// sent to KeyStore yet // sent to KeyStore yet
final byte[] HAT = generateRandomHAT(); final byte[] HAT = generateRandomHAT();
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE, SENSOR_ID_FACE,
HAT); HAT);
waitForIdle(); waitForIdle();
// Waiting for SystemUI to send confirmation callback // 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)); verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
// SystemUI sends confirm, HAT is sent to keystore and client is notified. // SystemUI sends confirm, HAT is sent to keystore and client is notified.
mBiometricService.mSysuiReceiver.onDialogDismissed( mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED,
null /* credentialAttestation */); null /* credentialAttestation */);
waitForIdle(); waitForIdle();
@@ -624,33 +622,34 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE); mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE);
waitForIdle(); waitForIdle();
verify(mBiometricService.mStatusBarService).onBiometricError( verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_NONE), eq(BiometricAuthenticator.TYPE_FACE),
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED), eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
verify(mReceiver1).onAuthenticationFailed(); verify(mReceiver1).onAuthenticationFailed();
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
} }
@Test @Test
public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating() public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating()
throws Exception { throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FINGERPRINT); mBiometricService.mAuthSession.mSensorReceiver
.onAuthenticationFailed(SENSOR_ID_FINGERPRINT);
waitForIdle(); waitForIdle();
verify(mBiometricService.mStatusBarService).onBiometricError( verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_NONE), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED), eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
verify(mReceiver1).onAuthenticationFailed(); verify(mReceiver1).onAuthenticationFailed();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
} }
@Test @Test
@@ -678,14 +677,14 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE, SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
verify(mBiometricService.mStatusBarService).onBiometricError( verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FACE), eq(BiometricAuthenticator.TYPE_FACE),
eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT), eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT),
@@ -694,15 +693,15 @@ public class BiometricServiceTest {
verify(mReceiver1, never()).onAuthenticationFailed(); verify(mReceiver1, never()).onAuthenticationFailed();
// No auth session. Pressing try again will create one. // 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 // Pressing "Try again" on SystemUI
mBiometricService.mSysuiReceiver.onTryAgainPressed(); mBiometricService.mAuthSession.mSysuiReceiver.onTryAgainPressed();
waitForIdle(); waitForIdle();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
// AuthSession is now resuming // 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 // Test resuming when hardware becomes ready. SystemUI should not be requested to
// show another dialog since it's already showing. // show another dialog since it's already showing.
@@ -728,14 +727,14 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE, SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */); 0 /* vendorCode */);
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE, SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED, BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
@@ -748,7 +747,7 @@ public class BiometricServiceTest {
// Dialog is hidden immediately // Dialog is hidden immediately
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
// Auth session is over // Auth session is over
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
} }
@Test @Test
@@ -757,61 +756,61 @@ public class BiometricServiceTest {
// For errors that show in SystemUI, BiometricService stays in STATE_ERROR_PENDING_SYSUI // 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 // until SystemUI notifies us that the dialog is dismissed at which point the current
// session is done. // session is done.
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT, SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS, BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
// Sends error to SystemUI and does not notify client yet // 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( verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog(); verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
// SystemUI animation completed, client is notified, auth session is over // SystemUI animation completed, client is notified, auth session is over
mBiometricService.mSysuiReceiver.onDialogDismissed( mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_ERROR, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_ERROR, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
verify(mReceiver1).onError( verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
} }
@Test @Test
public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception { public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, false /* requireConfirmation */,
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK); Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT, SENSOR_ID_FINGERPRINT,
getCookieForPendingSession(mBiometricService.mCurrentAuthSession), getCookieForPendingSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
// We should be showing device credential now // We should be showing device credential now
assertNotNull(mBiometricService.mCurrentAuthSession); assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
assertEquals(Authenticators.DEVICE_CREDENTIAL, assertEquals(Authenticators.DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators()); mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */, eq(true) /* credentialAllowed */,
@@ -826,23 +825,23 @@ public class BiometricServiceTest {
@Test @Test
public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed() public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed()
throws Exception { throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
waitForIdle(); waitForIdle();
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT, SENSOR_ID_FINGERPRINT,
getCookieForPendingSession(mBiometricService.mCurrentAuthSession), getCookieForPendingSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
// Error is sent to client // Error is sent to client
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT), verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT), eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
eq(0) /* vendorCode */); eq(0) /* vendorCode */);
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
} }
@Test @Test
@@ -861,7 +860,7 @@ public class BiometricServiceTest {
private void testBiometricAuth_whenLockout(@LockoutTracker.LockoutMode int lockoutMode, private void testBiometricAuth_whenLockout(@LockoutTracker.LockoutMode int lockoutMode,
int biometricPromptError) throws Exception { int biometricPromptError) throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
.thenReturn(lockoutMode); .thenReturn(lockoutMode);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
@@ -869,16 +868,15 @@ public class BiometricServiceTest {
waitForIdle(); waitForIdle();
// Modality and error are sent // Modality and error are sent
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT), verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
eq(biometricPromptError), eq(0) /* vendorCode */); eq(biometricPromptError), eq(0) /* vendorCode */);
} }
@Test @Test
public void testBiometricOrCredentialAuth_whenBiometricLockout_showsCredential() public void testBiometricOrCredentialAuth_whenBiometricLockout_showsCredential()
throws Exception { throws Exception {
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
.thenReturn(true); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
.thenReturn(LockoutTracker.LOCKOUT_PERMANENT); .thenReturn(LockoutTracker.LOCKOUT_PERMANENT);
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
@@ -887,13 +885,13 @@ public class BiometricServiceTest {
waitForIdle(); waitForIdle();
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
assertNotNull(mBiometricService.mCurrentAuthSession); assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
assertEquals(Authenticators.DEVICE_CREDENTIAL, assertEquals(Authenticators.DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators()); mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */, eq(true) /* credentialAllowed */,
@@ -959,73 +957,73 @@ public class BiometricServiceTest {
@Test @Test
public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI() public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI()
throws Exception { throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, false /* requireConfirmation */,
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK); Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
mBiometricService.mSysuiReceiver.onDeviceCredentialPressed(); mBiometricService.mAuthSession.mSysuiReceiver.onDeviceCredentialPressed();
waitForIdle(); waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT, SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED, BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
} }
@Test @Test
public void testLockout_whileAuthenticating_credentialAllowed() throws Exception { public void testLockout_whileAuthenticating_credentialAllowed() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, false /* requireConfirmation */,
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK); 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, SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
verify(mBiometricService.mStatusBarService).onBiometricError( verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT), eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
} }
@Test @Test
public void testLockout_whenAuthenticating_credentialNotAllowed() throws Exception { public void testLockout_whenAuthenticating_credentialNotAllowed() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); 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, SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS, BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
assertEquals(STATE_ERROR_PENDING_SYSUI, assertEquals(STATE_ERROR_PENDING_SYSUI,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
verify(mBiometricService.mStatusBarService).onBiometricError( verify(mBiometricService.mStatusBarService).onBiometricError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
} }
@@ -1033,20 +1031,20 @@ public class BiometricServiceTest {
@Test @Test
public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication() public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication()
throws Exception { throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mSysuiReceiver.onDialogDismissed( mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
verify(mReceiver1).onError( verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
verify(mBiometricService.mSensors.get(0).impl).cancelAuthenticationFromService( verify(mBiometricService.mSensors.get(0).impl).cancelAuthenticationFromService(
any(), any(), anyLong()); any(), any(), anyLong());
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
} }
@Test @Test
@@ -1055,12 +1053,12 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE, SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */); 0 /* vendorCode */);
mBiometricService.mSysuiReceiver.onDialogDismissed( mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
@@ -1069,18 +1067,17 @@ public class BiometricServiceTest {
} }
@Test @Test
public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws Exception {
Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FACE, SENSOR_ID_FACE,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
0 /* vendorCode */); 0 /* vendorCode */);
mBiometricService.mSysuiReceiver.onDialogDismissed( mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
@@ -1094,10 +1091,10 @@ public class BiometricServiceTest {
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
true /* requireConfirmation */, null /* authenticators */); true /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
SENSOR_ID_FACE, SENSOR_ID_FACE,
new byte[69] /* HAT */); new byte[69] /* HAT */);
mBiometricService.mSysuiReceiver.onDialogDismissed( mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
waitForIdle(); waitForIdle();
@@ -1108,19 +1105,19 @@ public class BiometricServiceTest {
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
assertNull(mBiometricService.mCurrentAuthSession); assertNull(mBiometricService.mAuthSession);
} }
@Test @Test
public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception { public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception {
when(mContext.getResources().getString(anyInt())).thenReturn("test string"); when(mContext.getResources().getString(anyInt())).thenReturn("test string");
final int modality = BiometricAuthenticator.TYPE_FINGERPRINT; final int modality = TYPE_FINGERPRINT;
setupAuthForOnly(modality, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(modality, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mBiometricSensorReceiver.onAcquired( mBiometricService.mAuthSession.mSensorReceiver.onAcquired(
SENSOR_ID_FINGERPRINT, SENSOR_ID_FINGERPRINT,
FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
0 /* vendorCode */); 0 /* vendorCode */);
@@ -1130,29 +1127,29 @@ public class BiometricServiceTest {
// string is retrieved for now, but it's also very unlikely to break anyway. // string is retrieved for now, but it's also very unlikely to break anyway.
verify(mBiometricService.mStatusBarService) verify(mBiometricService.mStatusBarService)
.onBiometricHelp(eq(modality), anyString()); .onBiometricHelp(eq(modality), anyString());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
} }
@Test @Test
public void testCancel_whenAuthenticating() throws Exception { public void testCancel_whenAuthenticating() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, null /* authenticators */); false /* requireConfirmation */, null /* authenticators */);
mBiometricService.mImpl.cancelAuthentication(mBiometricService.mCurrentAuthSession.mToken, mBiometricService.mImpl.cancelAuthentication(mBiometricService.mAuthSession.mToken,
TEST_PACKAGE_NAME, TEST_REQUEST_ID); TEST_PACKAGE_NAME, TEST_REQUEST_ID);
waitForIdle(); waitForIdle();
// Pretend that the HAL has responded to cancel with ERROR_CANCELED // Pretend that the HAL has responded to cancel with ERROR_CANCELED
mBiometricService.mBiometricSensorReceiver.onError( mBiometricService.mAuthSession.mSensorReceiver.onError(
SENSOR_ID_FINGERPRINT, SENSOR_ID_FINGERPRINT,
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), getCookieForCurrentSession(mBiometricService.mAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED, BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */); 0 /* vendorCode */);
waitForIdle(); waitForIdle();
// Hides system dialog and invokes the onError callback // 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(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
eq(0 /* vendorCode */)); eq(0 /* vendorCode */));
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
@@ -1161,7 +1158,7 @@ public class BiometricServiceTest {
@Test @Test
public void testCanAuthenticate_whenDeviceHasRequestedBiometricStrength() throws Exception { public void testCanAuthenticate_whenDeviceHasRequestedBiometricStrength() throws Exception {
// When only biometric is requested, and sensor is strong enough // 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, assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG)); invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG));
@@ -1170,7 +1167,7 @@ public class BiometricServiceTest {
@Test @Test
public void testCanAuthenticate_whenDeviceDoesNotHaveRequestedBiometricStrength() public void testCanAuthenticate_whenDeviceDoesNotHaveRequestedBiometricStrength()
throws Exception { 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 only biometric is requested, and sensor is not strong enough
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
@@ -1208,9 +1205,8 @@ public class BiometricServiceTest {
@Test @Test
public void testCanAuthenticate_whenNoBiometricsEnrolled() throws Exception { public void testCanAuthenticate_whenNoBiometricsEnrolled() throws Exception {
// With credential set up, test the following. // With credential set up, test the following.
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
.thenReturn(true); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
false /* enrolled */); false /* enrolled */);
// When only biometric is requested // When only biometric is requested
@@ -1277,7 +1273,7 @@ public class BiometricServiceTest {
private void testCanAuthenticate_whenLockedOut(@LockoutTracker.LockoutMode int lockoutMode) private void testCanAuthenticate_whenLockedOut(@LockoutTracker.LockoutMode int lockoutMode)
throws Exception { throws Exception {
// When only biometric is requested, and sensor is strong enough // 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())) when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
.thenReturn(lockoutMode); .thenReturn(lockoutMode);
@@ -1311,7 +1307,7 @@ public class BiometricServiceTest {
for (int i = 0; i < testCases.length; i++) { for (int i = 0; i < testCases.length; i++) {
final BiometricSensor sensor = final BiometricSensor sensor =
new BiometricSensor(mContext, 0 /* id */, new BiometricSensor(mContext, 0 /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT, TYPE_FINGERPRINT,
testCases[i][0], testCases[i][0],
mock(IBiometricAuthenticator.class)) { mock(IBiometricAuthenticator.class)) {
@Override @Override
@@ -1341,7 +1337,7 @@ public class BiometricServiceTest {
.thenReturn(true); .thenReturn(true);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
mBiometricService.mImpl.registerAuthenticator(0 /* testId */, mBiometricService.mImpl.registerAuthenticator(0 /* testId */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator); mFingerprintAuthenticator);
verify(mBiometricService.mBiometricStrengthController).updateStrengths(); verify(mBiometricService.mBiometricStrengthController).updateStrengths();
@@ -1360,7 +1356,7 @@ public class BiometricServiceTest {
.thenReturn(true); .thenReturn(true);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
mBiometricService.mImpl.registerAuthenticator(testId /* id */, mBiometricService.mImpl.registerAuthenticator(testId /* id */,
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
mFingerprintAuthenticator); mFingerprintAuthenticator);
// Downgrade the authenticator // Downgrade the authenticator
@@ -1378,7 +1374,7 @@ public class BiometricServiceTest {
false /* requireConfirmation */, authenticators); false /* requireConfirmation */, authenticators);
waitForIdle(); waitForIdle();
verify(mReceiver1).onError( verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_FINGERPRINT), eq(TYPE_FINGERPRINT),
eq(BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED), eq(BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED),
eq(0) /* vendorCode */); eq(0) /* vendorCode */);
@@ -1392,7 +1388,7 @@ public class BiometricServiceTest {
authenticators); authenticators);
waitForIdle(); waitForIdle();
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[] {testId}), AdditionalMatchers.aryEq(new int[] {testId}),
eq(false) /* credentialAllowed */, eq(false) /* credentialAllowed */,
@@ -1414,9 +1410,9 @@ public class BiometricServiceTest {
false /* requireConfirmation */, false /* requireConfirmation */,
authenticators); authenticators);
waitForIdle(); waitForIdle();
assertTrue(Utils.isCredentialRequested(mBiometricService.mCurrentAuthSession.mPromptInfo)); assertTrue(Utils.isCredentialRequested(mBiometricService.mAuthSession.mPromptInfo));
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
eq(true) /* credentialAllowed */, eq(true) /* credentialAllowed */,
@@ -1442,7 +1438,7 @@ public class BiometricServiceTest {
false /* requireConfirmation */, authenticators); false /* requireConfirmation */, authenticators);
waitForIdle(); waitForIdle();
verify(mBiometricService.mStatusBarService).showAuthenticationDialog( verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
eq(mBiometricService.mCurrentAuthSession.mPromptInfo), eq(mBiometricService.mAuthSession.mPromptInfo),
any(IBiometricSysuiReceiver.class), any(IBiometricSysuiReceiver.class),
AdditionalMatchers.aryEq(new int[] {testId}) /* sensorIds */, AdditionalMatchers.aryEq(new int[] {testId}) /* sensorIds */,
eq(false) /* credentialAllowed */, eq(false) /* credentialAllowed */,
@@ -1495,29 +1491,29 @@ public class BiometricServiceTest {
@Test @Test
public void testWorkAuthentication_fingerprintWorksIfNotDisabledByDevicePolicyManager() public void testWorkAuthentication_fingerprintWorksIfNotDisabledByDevicePolicyManager()
throws Exception { throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mDevicePolicyManager when(mDevicePolicyManager
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */)) .getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
.thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); .thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1, invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
Authenticators.BIOMETRIC_STRONG); Authenticators.BIOMETRIC_STRONG);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
startPendingAuthSession(mBiometricService); startPendingAuthSession(mBiometricService);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
} }
@Test @Test
public void testAuthentication_normalAppIgnoresDevicePolicy() throws Exception { public void testAuthentication_normalAppIgnoresDevicePolicy() throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mDevicePolicyManager when(mDevicePolicyManager
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */)) .getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */, Authenticators.BIOMETRIC_STRONG); false /* requireConfirmation */, Authenticators.BIOMETRIC_STRONG);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
} }
@Test @Test
@@ -1530,18 +1526,17 @@ public class BiometricServiceTest {
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1, invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
Authenticators.BIOMETRIC_STRONG); Authenticators.BIOMETRIC_STRONG);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
startPendingAuthSession(mBiometricService); startPendingAuthSession(mBiometricService);
waitForIdle(); waitForIdle();
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
} }
@Test @Test
public void testWorkAuthentication_fingerprintFailsIfDisabledByDevicePolicyManager() public void testWorkAuthentication_fingerprintFailsIfDisabledByDevicePolicyManager()
throws Exception { throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
.thenReturn(true);
when(mDevicePolicyManager when(mDevicePolicyManager
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */)) .getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
@@ -1555,9 +1550,9 @@ public class BiometricServiceTest {
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver2, invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver2,
Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL); Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL);
waitForIdle(); waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession); assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.getState()); mBiometricService.mAuthSession.getState());
verify(mReceiver2, never()).onError(anyInt(), anyInt(), anyInt()); verify(mReceiver2, never()).onError(anyInt(), anyInt(), anyInt());
} }
@@ -1580,7 +1575,7 @@ public class BiometricServiceTest {
when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) { if ((modality & TYPE_FINGERPRINT) != 0) {
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
.thenReturn(enrolled); .thenReturn(enrolled);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
@@ -1614,7 +1609,7 @@ public class BiometricServiceTest {
final int modality = modalities[i]; final int modality = modalities[i];
final int strength = strengths[i]; final int strength = strengths[i];
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) { if ((modality & TYPE_FINGERPRINT) != 0) {
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
.thenReturn(true); .thenReturn(true);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
@@ -1654,8 +1649,9 @@ public class BiometricServiceTest {
startPendingAuthSession(mBiometricService); startPendingAuthSession(mBiometricService);
waitForIdle(); waitForIdle();
assertNotNull(mBiometricService.mCurrentAuthSession); assertNotNull(mBiometricService.mAuthSession);
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); assertEquals(TEST_REQUEST_ID, mBiometricService.mAuthSession.getRequestId());
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
return requestId; return requestId;
} }
@@ -1663,14 +1659,14 @@ public class BiometricServiceTest {
private static void startPendingAuthSession(BiometricService service) throws Exception { private static void startPendingAuthSession(BiometricService service) throws Exception {
// Get the cookie so we can pretend the hardware is ready to authenticate // Get the cookie so we can pretend the hardware is ready to authenticate
// Currently we only support single modality per auth // 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.eligibleSensors.size(), 1);
assertEquals(preAuthInfo.numSensorsWaitingForCookie(), 1); assertEquals(preAuthInfo.numSensorsWaitingForCookie(), 1);
final int cookie = preAuthInfo.eligibleSensors.get(0).getCookie(); final int cookie = preAuthInfo.eligibleSensors.get(0).getCookie();
assertNotEquals(cookie, 0); assertNotEquals(cookie, 0);
service.mImpl.onReadyForAuthentication(cookie); service.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookie);
} }
private static long invokeAuthenticate(IBiometricService.Stub service, 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.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricConstants;
import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry; import androidx.test.InstrumentationRegistry;
@@ -43,9 +39,11 @@ import androidx.test.filters.SmallTest;
import com.android.server.biometrics.sensors.fingerprint.Udfps; import com.android.server.biometrics.sensors.fingerprint.Udfps;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.LinkedList; import java.util.LinkedList;
@@ -53,39 +51,38 @@ import java.util.LinkedList;
@SmallTest @SmallTest
public class CoexCoordinatorTest { 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 @Mock
private CoexCoordinator.Callback mCallback; private CoexCoordinator.Callback mCallback;
@Mock @Mock
private CoexCoordinator.ErrorCallback mErrorCallback; private CoexCoordinator.ErrorCallback mErrorCallback;
@Mock
private AuthenticationClient mFaceClient;
@Mock
private AuthenticationClient mFingerprintClient;
@Mock(extraInterfaces = {Udfps.class})
private AuthenticationClient mUdfpsClient;
private CoexCoordinator mCoexCoordinator;
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this);
mHandler = new Handler(Looper.getMainLooper());
mCoexCoordinator = CoexCoordinator.getInstance(); mCoexCoordinator = CoexCoordinator.getInstance();
mCoexCoordinator.setAdvancedLogicEnabled(true); mCoexCoordinator.setAdvancedLogicEnabled(true);
mCoexCoordinator.setFaceHapticDisabledWhenNonBypass(true); mCoexCoordinator.setFaceHapticDisabledWhenNonBypass(true);
mCoexCoordinator.reset();
} }
@Test @Test
public void testBiometricPrompt_authSuccess() { public void testBiometricPrompt_authSuccess() {
mCoexCoordinator.reset(); when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(client.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
@@ -93,15 +90,12 @@ public class CoexCoordinatorTest {
@Test @Test
public void testBiometricPrompt_authReject_whenNotLockedOut() { public void testBiometricPrompt_authReject_whenNotLockedOut() {
mCoexCoordinator.reset(); when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(client.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
client, LockoutTracker.LOCKOUT_NONE, mCallback); mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
@@ -109,30 +103,97 @@ public class CoexCoordinatorTest {
@Test @Test
public void testBiometricPrompt_authReject_whenLockedOut() { public void testBiometricPrompt_authReject_whenLockedOut() {
mCoexCoordinator.reset(); when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(client.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
client, LockoutTracker.LOCKOUT_TIMED, mCallback); mFaceClient, LockoutTracker.LOCKOUT_TIMED, mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
verify(mCallback, never()).sendAuthenticationResult(anyBoolean()); verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
verify(mCallback).handleLifecycleAfterAuth(); 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 @Test
public void testKeyguard_faceAuthOnly_success() { public void testKeyguard_faceAuthOnly_success() {
mCoexCoordinator.reset(); when(mFaceClient.isKeyguard()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(client.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mFaceClient, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */); verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
@@ -140,21 +201,16 @@ public class CoexCoordinatorTest {
@Test @Test
public void testKeyguard_faceAuth_udfpsNotTouching_faceSuccess() { public void testKeyguard_faceAuth_udfpsNotTouching_faceSuccess() {
mCoexCoordinator.reset(); when(mFaceClient.isKeyguard()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); when(mUdfpsClient.isKeyguard()).thenReturn(true);
when(faceClient.isKeyguard()).thenReturn(true); when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
withSettings().extraInterfaces(Udfps.class)); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
when(udfpsClient.isKeyguard()).thenReturn(true);
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); mFaceClient, mCallback);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
mCallback);
// Haptics tested in #testKeyguard_bypass_haptics. Let's leave this commented out (instead // Haptics tested in #testKeyguard_bypass_haptics. Let's leave this commented out (instead
// of removed) to keep this context. // of removed) to keep this context.
// verify(mCallback).sendHapticFeedback(); // verify(mCallback).sendHapticFeedback();
@@ -192,25 +248,19 @@ public class CoexCoordinatorTest {
private void testKeyguard_bypass_haptics(boolean bypassEnabled, boolean faceAccepted, private void testKeyguard_bypass_haptics(boolean bypassEnabled, boolean faceAccepted,
boolean shouldReceiveHaptics) { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
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);
if (faceAccepted) { if (faceAccepted) {
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient, mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
mCallback); mCallback);
} else { } else {
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient, mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, mCallback); LockoutTracker.LOCKOUT_NONE, mCallback);
} }
@@ -244,24 +294,18 @@ public class CoexCoordinatorTest {
private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted, private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted,
long udfpsRejectedAfterMs) { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); 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);
when (udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
// For easier reading // For easier reading
final CoexCoordinator.Callback faceCallback = mCallback; final CoexCoordinator.Callback faceCallback = mCallback;
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient, mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
faceCallback); faceCallback);
verify(faceCallback, never()).sendHapticFeedback(); verify(faceCallback, never()).sendHapticFeedback();
verify(faceCallback, never()).sendAuthenticationResult(anyBoolean()); verify(faceCallback, never()).sendAuthenticationResult(anyBoolean());
@@ -272,9 +316,9 @@ public class CoexCoordinatorTest {
// Reset the mock // Reset the mock
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class); CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size()); assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size());
assertEquals(faceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient); assertEquals(mFaceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
if (thenUdfpsAccepted) { if (thenUdfpsAccepted) {
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient, mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
udfpsCallback); udfpsCallback);
verify(udfpsCallback).sendHapticFeedback(); verify(udfpsCallback).sendHapticFeedback();
verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */); verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */);
@@ -284,7 +328,7 @@ public class CoexCoordinatorTest {
assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty()); assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
} else { } else {
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, udfpsClient, mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, mUdfpsClient,
LockoutTracker.LOCKOUT_NONE, udfpsCallback); LockoutTracker.LOCKOUT_NONE, udfpsCallback);
if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) { if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) {
verify(udfpsCallback, never()).sendHapticFeedback(); verify(udfpsCallback, never()).sendHapticFeedback();
@@ -310,56 +354,44 @@ public class CoexCoordinatorTest {
@Test @Test
public void testKeyguard_udfpsAuthSuccess_whileFaceScanning() { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
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,
mCallback); mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true)); verify(mCallback).sendAuthenticationResult(eq(true));
verify(faceClient).cancel(); verify(mFaceClient).cancel();
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
} }
@Test @Test
public void testKeyguard_faceRejectedWhenUdfpsTouching_thenUdfpsRejected() { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
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,
LockoutTracker.LOCKOUT_NONE, mCallback); LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback, never()).sendHapticFeedback(); verify(mCallback, never()).sendHapticFeedback();
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
// BiometricScheduler removes the face authentication client after rejection // BiometricScheduler removes the face authentication client after rejection
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, faceClient); mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
// Then UDFPS rejected // Then UDFPS rejected
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class); CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, udfpsClient, mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mUdfpsClient,
LockoutTracker.LOCKOUT_NONE, udfpsCallback); LockoutTracker.LOCKOUT_NONE, udfpsCallback);
verify(udfpsCallback).sendHapticFeedback(); verify(udfpsCallback).sendHapticFeedback();
verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
@@ -368,26 +400,20 @@ public class CoexCoordinatorTest {
@Test @Test
public void testKeyguard_udfpsRejected_thenFaceRejected_noKeyguardBypass() { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
when(faceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
withSettings().extraInterfaces(Udfps.class)); mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
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);
// Auth was attempted // Auth was attempted
when(udfpsClient.getState()) when(mUdfpsClient.getState())
.thenReturn(AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED); .thenReturn(AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED);
verify(mCallback, never()).sendHapticFeedback(); verify(mCallback, never()).sendHapticFeedback();
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
@@ -395,7 +421,7 @@ public class CoexCoordinatorTest {
// Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since // Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since
// unlike face, its lifecycle becomes "paused" instead of "finished". // unlike face, its lifecycle becomes "paused" instead of "finished".
CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class); CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class);
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, faceClient, mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mFaceClient,
LockoutTracker.LOCKOUT_NONE, faceCallback); LockoutTracker.LOCKOUT_NONE, faceCallback);
verify(faceCallback).sendHapticFeedback(); verify(faceCallback).sendHapticFeedback();
verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
@@ -404,20 +430,16 @@ public class CoexCoordinatorTest {
@Test @Test
public void testKeyguard_capacitiveAccepted_whenFaceScanning() { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class); mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); mFingerprintClient, mCallback);
when(fpClient.isKeyguard()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient);
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, fpClient, mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */); verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
@@ -425,21 +447,16 @@ public class CoexCoordinatorTest {
@Test @Test
public void testKeyguard_capacitiveRejected_whenFaceScanning() { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class); mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); mFingerprintClient, LockoutTracker.LOCKOUT_NONE, mCallback);
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);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
verify(mCallback).handleLifecycleAfterAuth(); verify(mCallback).handleLifecycleAfterAuth();
@@ -447,14 +464,11 @@ public class CoexCoordinatorTest {
@Test @Test
public void testNonKeyguard_rejectAndNotLockedOut() { public void testNonKeyguard_rejectAndNotLockedOut() {
mCoexCoordinator.reset(); when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(false); mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
when(faceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
LockoutTracker.LOCKOUT_NONE, mCallback); LockoutTracker.LOCKOUT_NONE, mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
@@ -464,14 +478,11 @@ public class CoexCoordinatorTest {
@Test @Test
public void testNonKeyguard_rejectLockedOut() { public void testNonKeyguard_rejectLockedOut() {
mCoexCoordinator.reset(); when(mFaceClient.isKeyguard()).thenReturn(false);
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(false); mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
when(faceClient.isBiometricPrompt()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
LockoutTracker.LOCKOUT_TIMED, mCallback); LockoutTracker.LOCKOUT_TIMED, mCallback);
verify(mCallback).sendHapticFeedback(); verify(mCallback).sendHapticFeedback();
@@ -496,16 +507,13 @@ public class CoexCoordinatorTest {
@Test @Test
public void testBiometricPrompt_FaceError() { public void testBiometricPrompt_FaceError() {
mCoexCoordinator.reset(); when(mFaceClient.isBiometricPrompt()).thenReturn(true);
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
AuthenticationClient<?> client = mock(AuthenticationClient.class); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(client.isBiometricPrompt()).thenReturn(true);
when(client.wasAuthAttempted()).thenReturn(true);
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
mErrorCallback);
verify(mErrorCallback).sendHapticFeedback(); verify(mErrorCallback).sendHapticFeedback();
} }
@@ -520,18 +528,15 @@ public class CoexCoordinatorTest {
} }
private void testKeyguard_faceAuthOnly(boolean bypassEnabled) { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
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, client); mCoexCoordinator.onAuthenticationError(mFaceClient,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
mErrorCallback);
verify(mErrorCallback).sendHapticFeedback(); verify(mErrorCallback).sendHapticFeedback();
} }
@@ -546,23 +551,17 @@ public class CoexCoordinatorTest {
} }
private void testKeyguard_coex_faceError(boolean bypassEnabled) { 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); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
when(faceClient.isKeyguard()).thenReturn(true); mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
when(faceClient.wasAuthAttempted()).thenReturn(true);
when(faceClient.wasUserDetected()).thenReturn(true);
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, mCoexCoordinator.onAuthenticationError(mFaceClient,
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,
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback); BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
if (bypassEnabled) { if (bypassEnabled) {