Merge changes from topic "coex-again-2" into tm-dev
* changes: Temporarily disable some biometric prompt tests. Add Face and Fingerprint (Co-Ex) support to biometric prompt. Remove face to fingerprint multi-sensor behavior. Remove duplicated UDFPS view into single fingerprint view.
This commit is contained in:
@@ -104,16 +104,16 @@ public class BiometricManager {
|
||||
public static final int BIOMETRIC_MULTI_SENSOR_DEFAULT = 0;
|
||||
|
||||
/**
|
||||
* Prefer the face sensor and fall back to fingerprint when needed.
|
||||
* Use face and fingerprint sensors together.
|
||||
* @hide
|
||||
*/
|
||||
public static final int BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT = 1;
|
||||
public static final int BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE = 1;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT,
|
||||
BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT})
|
||||
BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface BiometricMultiSensorMode {}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ interface IBiometricService {
|
||||
|
||||
// Notify BiometricService when <Biometric>Service is ready to start the prepared client.
|
||||
// Client lifecycle is still managed in <Biometric>Service.
|
||||
void onReadyForAuthentication(int cookie);
|
||||
void onReadyForAuthentication(long requestId, int cookie);
|
||||
|
||||
// Requests all BIOMETRIC_STRONG sensors to have their authenticatorId invalidated for the
|
||||
// specified user. This happens when enrollments have been added on devices with multiple
|
||||
|
||||
@@ -30,6 +30,4 @@ oneway interface IBiometricSysuiReceiver {
|
||||
void onSystemEvent(int event);
|
||||
// Notifies that the dialog has finished animating.
|
||||
void onDialogAnimatedIn();
|
||||
// For multi-sensor devices, notifies that the fingerprint should start now.
|
||||
void onStartFingerprintNow();
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ oneway interface IStatusBar
|
||||
/**
|
||||
* Used to notify the authentication dialog that a biometric has been authenticated.
|
||||
*/
|
||||
void onBiometricAuthenticated();
|
||||
void onBiometricAuthenticated(int modality);
|
||||
/**
|
||||
* Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc.
|
||||
*/
|
||||
|
||||
@@ -125,7 +125,7 @@ interface IStatusBarService
|
||||
int multiSensorConfig);
|
||||
|
||||
// Used to notify the authentication dialog that a biometric has been authenticated
|
||||
void onBiometricAuthenticated();
|
||||
void onBiometricAuthenticated(int modality);
|
||||
// Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
|
||||
void onBiometricHelp(int modality, String message);
|
||||
// Used to show an error - the dialog will dismiss after a certain amount of time
|
||||
|
||||
@@ -91,23 +91,9 @@ message BiometricServiceStateProto {
|
||||
STATE_CLIENT_DIED_CANCELLING = 10;
|
||||
}
|
||||
|
||||
enum MultiSensorState {
|
||||
// Initializing or not yet started.
|
||||
MULTI_SENSOR_STATE_UNKNOWN = 0;
|
||||
// Sensors are in the process of being transitioned and there is no active sensor.
|
||||
MULTI_SENSOR_STATE_SWITCHING = 1;
|
||||
// Face sensor is being used as the primary input.
|
||||
MULTI_SENSOR_STATE_FACE_SCANNING = 2;
|
||||
// Fingerprint sensor is being used as the primary input.
|
||||
MULTI_SENSOR_STATE_FP_SCANNING = 3;
|
||||
}
|
||||
|
||||
repeated SensorServiceStateProto sensor_service_states = 1;
|
||||
|
||||
optional AuthSessionState auth_session_state = 2;
|
||||
|
||||
// Additional session state information, when the device has multiple sensors.
|
||||
optional MultiSensorState auth_session_multi_sensor_state = 3;
|
||||
}
|
||||
|
||||
// Overall state for an instance of a <Biometric>Service, for example FingerprintService or
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -49,8 +49,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/biometric_icon"
|
||||
android:layout_width="@dimen/biometric_dialog_biometric_icon_size"
|
||||
android:layout_height="@dimen/biometric_dialog_biometric_icon_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView
|
||||
<com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/contents"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/auth_biometric_contents"/>
|
||||
|
||||
</com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView>
|
||||
</com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~ Copyright (C) 2022 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<com.android.systemui.biometrics.AuthBiometricUdfpsView
|
||||
<com.android.systemui.biometrics.AuthBiometricView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/contents"
|
||||
android:layout_width="match_parent"
|
||||
@@ -23,4 +23,4 @@
|
||||
|
||||
<include layout="@layout/auth_biometric_contents"/>
|
||||
|
||||
</com.android.systemui.biometrics.AuthBiometricUdfpsView>
|
||||
</com.android.systemui.biometrics.AuthBiometricView>
|
||||
@@ -876,7 +876,8 @@
|
||||
<dimen name="remote_input_history_extra_height">60dp</dimen>
|
||||
|
||||
<!-- Biometric Dialog values -->
|
||||
<dimen name="biometric_dialog_biometric_icon_size">64dp</dimen>
|
||||
<dimen name="biometric_dialog_face_icon_size">64dp</dimen>
|
||||
<dimen name="biometric_dialog_fingerprint_icon_size">80dp</dimen>
|
||||
<dimen name="biometric_dialog_button_negative_max_width">160dp</dimen>
|
||||
<dimen name="biometric_dialog_button_positive_max_width">136dp</dimen>
|
||||
<dimen name="biometric_dialog_corner_size">4dp</dimen>
|
||||
|
||||
@@ -322,6 +322,8 @@
|
||||
<string name="biometric_dialog_face_icon_description_confirmed">Confirmed</string>
|
||||
<!-- Message shown when a biometric is authenticated, waiting for the user to confirm authentication [CHAR LIMIT=40]-->
|
||||
<string name="biometric_dialog_tap_confirm">Tap Confirm to complete</string>
|
||||
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
|
||||
<string name="biometric_dialog_tap_confirm_with_face">Unlocked by your face. Press to continue.</string>
|
||||
<!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] -->
|
||||
<string name="biometric_dialog_authenticated">Authenticated</string>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import android.animation.ValueAnimator;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.StringRes;
|
||||
import android.content.Context;
|
||||
import android.hardware.biometrics.BiometricAuthenticator.Modality;
|
||||
import android.hardware.biometrics.BiometricPrompt;
|
||||
@@ -44,19 +45,21 @@ import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
import com.android.systemui.R;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Contains the Biometric views (title, subtitle, icon, buttons, etc) and its controllers.
|
||||
* Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers.
|
||||
*/
|
||||
public abstract class AuthBiometricView extends LinearLayout {
|
||||
public class AuthBiometricView extends LinearLayout {
|
||||
|
||||
private static final String TAG = "BiometricPrompt/AuthBiometricView";
|
||||
private static final String TAG = "AuthBiometricView";
|
||||
|
||||
/**
|
||||
* Authentication hardware idle.
|
||||
@@ -102,13 +105,6 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
int ACTION_BUTTON_TRY_AGAIN = 4;
|
||||
int ACTION_ERROR = 5;
|
||||
int ACTION_USE_DEVICE_CREDENTIAL = 6;
|
||||
/**
|
||||
* Notify the receiver to start the fingerprint sensor.
|
||||
*
|
||||
* This is only applicable to multi-sensor devices that need to delay fingerprint auth
|
||||
* (i.e face -> fingerprint).
|
||||
*/
|
||||
int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7;
|
||||
|
||||
/**
|
||||
* When an action has occurred. The caller will only invoke this when the callback should
|
||||
@@ -118,66 +114,9 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
void onAction(int action);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class Injector {
|
||||
AuthBiometricView mBiometricView;
|
||||
|
||||
public Button getNegativeButton() {
|
||||
return mBiometricView.findViewById(R.id.button_negative);
|
||||
}
|
||||
|
||||
public Button getCancelButton() {
|
||||
return mBiometricView.findViewById(R.id.button_cancel);
|
||||
}
|
||||
|
||||
public Button getUseCredentialButton() {
|
||||
return mBiometricView.findViewById(R.id.button_use_credential);
|
||||
}
|
||||
|
||||
public Button getConfirmButton() {
|
||||
return mBiometricView.findViewById(R.id.button_confirm);
|
||||
}
|
||||
|
||||
public Button getTryAgainButton() {
|
||||
return mBiometricView.findViewById(R.id.button_try_again);
|
||||
}
|
||||
|
||||
public TextView getTitleView() {
|
||||
return mBiometricView.findViewById(R.id.title);
|
||||
}
|
||||
|
||||
public TextView getSubtitleView() {
|
||||
return mBiometricView.findViewById(R.id.subtitle);
|
||||
}
|
||||
|
||||
public TextView getDescriptionView() {
|
||||
return mBiometricView.findViewById(R.id.description);
|
||||
}
|
||||
|
||||
public TextView getIndicatorView() {
|
||||
return mBiometricView.findViewById(R.id.indicator);
|
||||
}
|
||||
|
||||
public ImageView getIconView() {
|
||||
return mBiometricView.findViewById(R.id.biometric_icon);
|
||||
}
|
||||
|
||||
public View getIconHolderView() {
|
||||
return mBiometricView.findViewById(R.id.biometric_icon_frame);
|
||||
}
|
||||
|
||||
public int getDelayAfterError() {
|
||||
return BiometricPrompt.HIDE_DIALOG_DELAY;
|
||||
}
|
||||
|
||||
public int getMediumToLargeAnimationDurationMs() {
|
||||
return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
|
||||
}
|
||||
}
|
||||
|
||||
private final Injector mInjector;
|
||||
protected final Handler mHandler;
|
||||
private final AccessibilityManager mAccessibilityManager;
|
||||
private final LockPatternUtils mLockPatternUtils;
|
||||
protected final int mTextColorError;
|
||||
protected final int mTextColorHint;
|
||||
|
||||
@@ -195,6 +134,11 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
protected ImageView mIconView;
|
||||
protected TextView mIndicatorView;
|
||||
|
||||
@VisibleForTesting @NonNull AuthIconController mIconController;
|
||||
@VisibleForTesting int mAnimationDurationShort = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS;
|
||||
@VisibleForTesting int mAnimationDurationLong = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
|
||||
@VisibleForTesting int mAnimationDurationHideDialog = BiometricPrompt.HIDE_DIALOG_DELAY;
|
||||
|
||||
// Negative button position, exclusively for the app-specified behavior
|
||||
@VisibleForTesting Button mNegativeButton;
|
||||
// Negative button position, exclusively for cancelling auth after passive auth success
|
||||
@@ -217,30 +161,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
protected boolean mDialogSizeAnimating;
|
||||
protected Bundle mSavedState;
|
||||
|
||||
/**
|
||||
* Delay after authentication is confirmed, before the dialog should be animated away.
|
||||
*/
|
||||
protected abstract int getDelayAfterAuthenticatedDurationMs();
|
||||
/**
|
||||
* State that the dialog/icon should be in after showing a help message.
|
||||
*/
|
||||
protected abstract int getStateForAfterError();
|
||||
/**
|
||||
* Invoked when the error message is being cleared.
|
||||
*/
|
||||
protected abstract void handleResetAfterError();
|
||||
/**
|
||||
* Invoked when the help message is being cleared.
|
||||
*/
|
||||
protected abstract void handleResetAfterHelp();
|
||||
|
||||
/**
|
||||
* @return true if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}
|
||||
*/
|
||||
protected abstract boolean supportsSmallDialog();
|
||||
|
||||
private final Runnable mResetErrorRunnable;
|
||||
|
||||
private final Runnable mResetHelpRunnable;
|
||||
|
||||
private final OnClickListener mBackgroundClickListener = (view) -> {
|
||||
@@ -262,11 +183,6 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
}
|
||||
|
||||
public AuthBiometricView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, new Injector());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
AuthBiometricView(Context context, AttributeSet attrs, Injector injector) {
|
||||
super(context, attrs);
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mTextColorError = getResources().getColor(
|
||||
@@ -274,10 +190,8 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
mTextColorHint = getResources().getColor(
|
||||
R.color.biometric_dialog_gray, context.getTheme());
|
||||
|
||||
mInjector = injector;
|
||||
mInjector.mBiometricView = this;
|
||||
|
||||
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
|
||||
mLockPatternUtils = new LockPatternUtils(context);
|
||||
|
||||
mResetErrorRunnable = () -> {
|
||||
updateState(getStateForAfterError());
|
||||
@@ -292,36 +206,91 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
};
|
||||
}
|
||||
|
||||
public void setPanelController(AuthPanelController panelController) {
|
||||
/** Delay after authentication is confirmed, before the dialog should be animated away. */
|
||||
protected int getDelayAfterAuthenticatedDurationMs() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** State that the dialog/icon should be in after showing a help message. */
|
||||
protected int getStateForAfterError() {
|
||||
return STATE_IDLE;
|
||||
}
|
||||
|
||||
/** Invoked when the error message is being cleared. */
|
||||
protected void handleResetAfterError() {}
|
||||
|
||||
/** Invoked when the help message is being cleared. */
|
||||
protected void handleResetAfterHelp() {}
|
||||
|
||||
/** True if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}. */
|
||||
protected boolean supportsSmallDialog() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The string to show when the user must tap to confirm via the button or icon. */
|
||||
@StringRes
|
||||
protected int getConfirmationPrompt() {
|
||||
return R.string.biometric_dialog_tap_confirm;
|
||||
}
|
||||
|
||||
/** True if require confirmation will be honored when set via the API. */
|
||||
protected boolean supportsRequireConfirmation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True if confirmation will be required even if it was not supported/requested. */
|
||||
protected boolean forceRequireConfirmation(@Modality int modality) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Ignore all events from this (secondary) modality except successful authentication. */
|
||||
protected boolean ignoreUnsuccessfulEventsFrom(@Modality int modality) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the controller for managing the icons transitions during the prompt.
|
||||
*
|
||||
* Subclass should override.
|
||||
*/
|
||||
@NonNull
|
||||
protected AuthIconController createIconController() {
|
||||
return new AuthIconController(mContext, mIconView) {
|
||||
@Override
|
||||
public void updateIcon(int lastState, int newState) {}
|
||||
};
|
||||
}
|
||||
|
||||
void setPanelController(AuthPanelController panelController) {
|
||||
mPanelController = panelController;
|
||||
}
|
||||
|
||||
public void setPromptInfo(PromptInfo promptInfo) {
|
||||
void setPromptInfo(PromptInfo promptInfo) {
|
||||
mPromptInfo = promptInfo;
|
||||
}
|
||||
|
||||
public void setCallback(Callback callback) {
|
||||
void setCallback(Callback callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
public void setBackgroundView(View backgroundView) {
|
||||
void setBackgroundView(View backgroundView) {
|
||||
backgroundView.setOnClickListener(mBackgroundClickListener);
|
||||
}
|
||||
|
||||
public void setUserId(int userId) {
|
||||
void setUserId(int userId) {
|
||||
mUserId = userId;
|
||||
}
|
||||
|
||||
public void setEffectiveUserId(int effectiveUserId) {
|
||||
void setEffectiveUserId(int effectiveUserId) {
|
||||
mEffectiveUserId = effectiveUserId;
|
||||
}
|
||||
|
||||
public void setRequireConfirmation(boolean requireConfirmation) {
|
||||
mRequireConfirmation = requireConfirmation;
|
||||
void setRequireConfirmation(boolean requireConfirmation) {
|
||||
mRequireConfirmation = requireConfirmation && supportsRequireConfirmation();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateSize(@AuthDialog.DialogSize int newSize) {
|
||||
final void updateSize(@AuthDialog.DialogSize int newSize) {
|
||||
Log.v(TAG, "Current size: " + mSize + " New size: " + newSize);
|
||||
if (newSize == AuthDialog.SIZE_SMALL) {
|
||||
mTitleView.setVisibility(View.GONE);
|
||||
@@ -376,7 +345,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
|
||||
// Choreograph together
|
||||
final AnimatorSet as = new AnimatorSet();
|
||||
as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
|
||||
as.setDuration(mAnimationDurationShort);
|
||||
as.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
@@ -429,7 +398,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
// Translate at full duration
|
||||
final ValueAnimator translationAnimator = ValueAnimator.ofFloat(
|
||||
biometricView.getY(), biometricView.getY() - translationY);
|
||||
translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs());
|
||||
translationAnimator.setDuration(mAnimationDurationLong);
|
||||
translationAnimator.addUpdateListener((animation) -> {
|
||||
final float translation = (float) animation.getAnimatedValue();
|
||||
biometricView.setTranslationY(translation);
|
||||
@@ -438,7 +407,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
if (biometricView.getParent() != null) {
|
||||
if (biometricView.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) biometricView.getParent()).removeView(biometricView);
|
||||
}
|
||||
mSize = newSize;
|
||||
@@ -447,7 +416,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
|
||||
// Opacity to 0 in half duration
|
||||
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
|
||||
opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2);
|
||||
opacityAnimator.setDuration(mAnimationDurationLong / 2);
|
||||
opacityAnimator.addUpdateListener((animation) -> {
|
||||
final float opacity = (float) animation.getAnimatedValue();
|
||||
biometricView.setAlpha(opacity);
|
||||
@@ -457,7 +426,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
mPanelController.updateForContentDimensions(
|
||||
mPanelController.getContainerWidth(),
|
||||
mPanelController.getContainerHeight(),
|
||||
mInjector.getMediumToLargeAnimationDurationMs());
|
||||
mAnimationDurationLong);
|
||||
|
||||
// Start the animations together
|
||||
AnimatorSet as = new AnimatorSet();
|
||||
@@ -466,7 +435,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
animators.add(opacityAnimator);
|
||||
|
||||
as.playTogether(animators);
|
||||
as.setDuration(mInjector.getMediumToLargeAnimationDurationMs() * 2 / 3);
|
||||
as.setDuration(mAnimationDurationLong * 2 / 3);
|
||||
as.start();
|
||||
} else {
|
||||
Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
|
||||
@@ -481,6 +450,8 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
public void updateState(@BiometricState int newState) {
|
||||
Log.v(TAG, "newState: " + newState);
|
||||
|
||||
mIconController.updateState(mState, newState);
|
||||
|
||||
switch (newState) {
|
||||
case STATE_AUTHENTICATING_ANIMATING_IN:
|
||||
case STATE_AUTHENTICATING:
|
||||
@@ -510,10 +481,11 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
mNegativeButton.setVisibility(View.GONE);
|
||||
mCancelButton.setVisibility(View.VISIBLE);
|
||||
mUseCredentialButton.setVisibility(View.GONE);
|
||||
mConfirmButton.setEnabled(true);
|
||||
mConfirmButton.setVisibility(View.VISIBLE);
|
||||
// forced confirmations (multi-sensor) use the icon view as the confirm button
|
||||
mConfirmButton.setEnabled(mRequireConfirmation);
|
||||
mConfirmButton.setVisibility(mRequireConfirmation ? View.VISIBLE : View.GONE);
|
||||
mIndicatorView.setTextColor(mTextColorHint);
|
||||
mIndicatorView.setText(R.string.biometric_dialog_tap_confirm);
|
||||
mIndicatorView.setText(getConfirmationPrompt());
|
||||
mIndicatorView.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
@@ -536,9 +508,9 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
updateState(STATE_AUTHENTICATING);
|
||||
}
|
||||
|
||||
public void onAuthenticationSucceeded() {
|
||||
public void onAuthenticationSucceeded(@Modality int modality) {
|
||||
removePendingAnimations();
|
||||
if (mRequireConfirmation) {
|
||||
if (mRequireConfirmation || forceRequireConfirmation(modality)) {
|
||||
updateState(STATE_PENDING_CONFIRMATION);
|
||||
} else {
|
||||
updateState(STATE_AUTHENTICATED);
|
||||
@@ -553,6 +525,10 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
*/
|
||||
public void onAuthenticationFailed(
|
||||
@Modality int modality, @Nullable String failureReason) {
|
||||
if (ignoreUnsuccessfulEventsFrom(modality)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showTemporaryMessage(failureReason, mResetErrorRunnable);
|
||||
updateState(STATE_ERROR);
|
||||
}
|
||||
@@ -564,12 +540,27 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
* @param error message
|
||||
*/
|
||||
public void onError(@Modality int modality, String error) {
|
||||
if (ignoreUnsuccessfulEventsFrom(modality)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showTemporaryMessage(error, mResetErrorRunnable);
|
||||
updateState(STATE_ERROR);
|
||||
|
||||
mHandler.postDelayed(() -> {
|
||||
mCallback.onAction(Callback.ACTION_ERROR);
|
||||
}, mInjector.getDelayAfterError());
|
||||
mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_ERROR),
|
||||
mAnimationDurationHideDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fingerprint pointer down event. This does nothing by default and will not be called if the
|
||||
* device does not have an appropriate sensor (UDFPS), but it may be used as an alternative
|
||||
* to the "retry" button when fingerprint is used with other modalities.
|
||||
*
|
||||
* @param failedModalities the set of modalities that have failed
|
||||
* @return true if a retry was initiated as a result of this event
|
||||
*/
|
||||
public boolean onPointerDown(Set<Integer> failedModalities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -579,6 +570,9 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
* @param help message
|
||||
*/
|
||||
public void onHelp(@Modality int modality, String help) {
|
||||
if (ignoreUnsuccessfulEventsFrom(modality)) {
|
||||
return;
|
||||
}
|
||||
if (mSize != AuthDialog.SIZE_MEDIUM) {
|
||||
Log.w(TAG, "Help received in size: " + mSize);
|
||||
return;
|
||||
@@ -639,7 +633,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
// select to enable marquee unless a screen reader is enabled
|
||||
mIndicatorView.setSelected(!mAccessibilityManager.isEnabled()
|
||||
|| !mAccessibilityManager.isTouchExplorationEnabled());
|
||||
mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError());
|
||||
mHandler.postDelayed(resetMessageRunnable, mAnimationDurationHideDialog);
|
||||
|
||||
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
|
||||
}
|
||||
@@ -647,29 +641,22 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
onFinishInflateInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* After inflation, but before things like restoreState, onAttachedToWindow, etc.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void onFinishInflateInternal() {
|
||||
mTitleView = mInjector.getTitleView();
|
||||
mSubtitleView = mInjector.getSubtitleView();
|
||||
mDescriptionView = mInjector.getDescriptionView();
|
||||
mIconView = mInjector.getIconView();
|
||||
mIconHolderView = mInjector.getIconHolderView();
|
||||
mIndicatorView = mInjector.getIndicatorView();
|
||||
mTitleView = findViewById(R.id.title);
|
||||
mSubtitleView = findViewById(R.id.subtitle);
|
||||
mDescriptionView = findViewById(R.id.description);
|
||||
mIconView = findViewById(R.id.biometric_icon);
|
||||
mIconHolderView = findViewById(R.id.biometric_icon_frame);
|
||||
mIndicatorView = findViewById(R.id.indicator);
|
||||
|
||||
// Negative-side (left) buttons
|
||||
mNegativeButton = mInjector.getNegativeButton();
|
||||
mCancelButton = mInjector.getCancelButton();
|
||||
mUseCredentialButton = mInjector.getUseCredentialButton();
|
||||
mNegativeButton = findViewById(R.id.button_negative);
|
||||
mCancelButton = findViewById(R.id.button_cancel);
|
||||
mUseCredentialButton = findViewById(R.id.button_use_credential);
|
||||
|
||||
// Positive-side (right) buttons
|
||||
mConfirmButton = mInjector.getConfirmButton();
|
||||
mTryAgainButton = mInjector.getTryAgainButton();
|
||||
mConfirmButton = findViewById(R.id.button_confirm);
|
||||
mTryAgainButton = findViewById(R.id.button_try_again);
|
||||
|
||||
mNegativeButton.setOnClickListener((view) -> {
|
||||
mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
|
||||
@@ -693,6 +680,15 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
mTryAgainButton.setVisibility(View.GONE);
|
||||
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
|
||||
});
|
||||
|
||||
mIconController = createIconController();
|
||||
if (mIconController.getActsAsConfirmButton()) {
|
||||
mIconView.setOnClickListener((view) -> {
|
||||
if (mState == STATE_PENDING_CONFIRMATION) {
|
||||
updateState(STATE_AUTHENTICATED);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -706,21 +702,13 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
onAttachedToWindowInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains all the testable logic that should be invoked when {@link #onAttachedToWindow()} is
|
||||
* invoked.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void onAttachedToWindowInternal() {
|
||||
mTitleView.setText(mPromptInfo.getTitle());
|
||||
|
||||
if (isDeviceCredentialAllowed()) {
|
||||
final CharSequence credentialButtonText;
|
||||
final @Utils.CredentialType int credentialType =
|
||||
Utils.getCredentialType(mContext, mEffectiveUserId);
|
||||
@Utils.CredentialType final int credentialType =
|
||||
Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId);
|
||||
switch (credentialType) {
|
||||
case Utils.CREDENTIAL_PIN:
|
||||
credentialButtonText =
|
||||
@@ -731,9 +719,6 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
getResources().getString(R.string.biometric_dialog_use_pattern);
|
||||
break;
|
||||
case Utils.CREDENTIAL_PASSWORD:
|
||||
credentialButtonText =
|
||||
getResources().getString(R.string.biometric_dialog_use_password);
|
||||
break;
|
||||
default:
|
||||
credentialButtonText =
|
||||
getResources().getString(R.string.biometric_dialog_use_password);
|
||||
@@ -749,7 +734,6 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
}
|
||||
|
||||
setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle());
|
||||
|
||||
setTextOrHide(mDescriptionView, mPromptInfo.getDescription());
|
||||
|
||||
if (mSavedState == null) {
|
||||
@@ -774,6 +758,8 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
mIconController.setDeactivated(true);
|
||||
|
||||
// Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once
|
||||
// the new dialog is restored.
|
||||
mHandler.removeCallbacksAndMessages(null /* all */);
|
||||
@@ -856,15 +842,7 @@ public abstract class AuthBiometricView extends LinearLayout {
|
||||
@Override
|
||||
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLayoutInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains all the testable logic that should be invoked when
|
||||
* {@link #onLayout(boolean, int, int, int, int)}, is invoked.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void onLayoutInternal() {
|
||||
// Start with initial size only once. Subsequent layout changes don't matter since we
|
||||
// only care about the initial icon position.
|
||||
if (mIconOriginalY == 0) {
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
|
||||
|
||||
import android.annotation.DurationMillisLong;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
@@ -52,13 +53,16 @@ import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.Interpolators;
|
||||
import com.android.systemui.keyguard.WakefulnessLifecycle;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Top level container/controller for the BiometricPrompt UI.
|
||||
@@ -66,54 +70,52 @@ import java.util.List;
|
||||
public class AuthContainerView extends LinearLayout
|
||||
implements AuthDialog, WakefulnessLifecycle.Observer {
|
||||
|
||||
private static final String TAG = "BiometricPrompt/AuthContainerView";
|
||||
private static final int ANIMATION_DURATION_SHOW_MS = 250;
|
||||
private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms
|
||||
private static final String TAG = "AuthContainerView";
|
||||
|
||||
static final int STATE_UNKNOWN = 0;
|
||||
static final int STATE_ANIMATING_IN = 1;
|
||||
static final int STATE_PENDING_DISMISS = 2;
|
||||
static final int STATE_SHOWING = 3;
|
||||
static final int STATE_ANIMATING_OUT = 4;
|
||||
static final int STATE_GONE = 5;
|
||||
private static final int ANIMATION_DURATION_SHOW_MS = 250;
|
||||
private static final int ANIMATION_DURATION_AWAY_MS = 350;
|
||||
|
||||
private static final int STATE_UNKNOWN = 0;
|
||||
private static final int STATE_ANIMATING_IN = 1;
|
||||
private static final int STATE_PENDING_DISMISS = 2;
|
||||
private static final int STATE_SHOWING = 3;
|
||||
private static final int STATE_ANIMATING_OUT = 4;
|
||||
private static final int STATE_GONE = 5;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
|
||||
STATE_ANIMATING_OUT, STATE_GONE})
|
||||
@interface ContainerState {}
|
||||
private @interface ContainerState {}
|
||||
|
||||
final Config mConfig;
|
||||
final int mEffectiveUserId;
|
||||
@Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps;
|
||||
@Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
|
||||
private final Config mConfig;
|
||||
private final int mEffectiveUserId;
|
||||
private final Handler mHandler;
|
||||
private final Injector mInjector;
|
||||
private final IBinder mWindowToken = new Binder();
|
||||
private final WindowManager mWindowManager;
|
||||
private final AuthPanelController mPanelController;
|
||||
private final Interpolator mLinearOutSlowIn;
|
||||
@VisibleForTesting final BiometricCallback mBiometricCallback;
|
||||
private final CredentialCallback mCredentialCallback;
|
||||
|
||||
@VisibleForTesting final FrameLayout mFrameLayout;
|
||||
@VisibleForTesting @Nullable AuthBiometricView mBiometricView;
|
||||
@VisibleForTesting @Nullable AuthCredentialView mCredentialView;
|
||||
|
||||
@VisibleForTesting final ImageView mBackgroundView;
|
||||
@VisibleForTesting final ScrollView mBiometricScrollView;
|
||||
private final View mPanelView;
|
||||
|
||||
private final float mTranslationY;
|
||||
|
||||
private final LockPatternUtils mLockPatternUtils;
|
||||
private final WakefulnessLifecycle mWakefulnessLifecycle;
|
||||
|
||||
@VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
|
||||
@VisibleForTesting final BiometricCallback mBiometricCallback;
|
||||
|
||||
@Nullable private AuthBiometricView mBiometricView;
|
||||
@Nullable private AuthCredentialView mCredentialView;
|
||||
private final AuthPanelController mPanelController;
|
||||
private final FrameLayout mFrameLayout;
|
||||
private final ImageView mBackgroundView;
|
||||
private final ScrollView mBiometricScrollView;
|
||||
private final View mPanelView;
|
||||
private final float mTranslationY;
|
||||
@ContainerState private int mContainerState = STATE_UNKNOWN;
|
||||
private final Set<Integer> mFailedModalities = new HashSet<Integer>();
|
||||
|
||||
// Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
|
||||
@Nullable @AuthDialogCallback.DismissedReason Integer mPendingCallbackReason;
|
||||
@Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
|
||||
// HAT received from LockSettingsService when credential is verified.
|
||||
@Nullable byte[] mCredentialAttestation;
|
||||
@Nullable private byte[] mCredentialAttestation;
|
||||
|
||||
@VisibleForTesting
|
||||
static class Config {
|
||||
Context mContext;
|
||||
AuthDialogCallback mCallback;
|
||||
@@ -122,11 +124,11 @@ public class AuthContainerView extends LinearLayout
|
||||
int mUserId;
|
||||
String mOpPackageName;
|
||||
int[] mSensorIds;
|
||||
boolean mCredentialAllowed;
|
||||
boolean mSkipIntro;
|
||||
long mOperationId;
|
||||
long mRequestId;
|
||||
@BiometricMultiSensorMode int mMultiSensorConfig;
|
||||
boolean mSkipAnimation = false;
|
||||
@BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
@@ -167,7 +169,7 @@ public class AuthContainerView extends LinearLayout
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setOperationId(long operationId) {
|
||||
public Builder setOperationId(@DurationMillisLong long operationId) {
|
||||
mConfig.mOperationId = operationId;
|
||||
return this;
|
||||
}
|
||||
@@ -178,55 +180,27 @@ public class AuthContainerView extends LinearLayout
|
||||
return this;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Builder setSkipAnimationDuration(boolean skip) {
|
||||
mConfig.mSkipAnimation = skip;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** The multi-sensor mode. */
|
||||
public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
|
||||
mConfig.mMultiSensorConfig = multiSensorConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthContainerView build(int[] sensorIds, boolean credentialAllowed,
|
||||
public AuthContainerView build(int[] sensorIds,
|
||||
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
|
||||
@Nullable List<FaceSensorPropertiesInternal> faceProps,
|
||||
WakefulnessLifecycle wakefulnessLifecycle) {
|
||||
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
|
||||
@NonNull UserManager userManager,
|
||||
@NonNull LockPatternUtils lockPatternUtils) {
|
||||
mConfig.mSensorIds = sensorIds;
|
||||
mConfig.mCredentialAllowed = credentialAllowed;
|
||||
return new AuthContainerView(
|
||||
mConfig, new Injector(), fpProps, faceProps, wakefulnessLifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Injector {
|
||||
ScrollView getBiometricScrollView(FrameLayout parent) {
|
||||
return parent.findViewById(R.id.biometric_scrollview);
|
||||
}
|
||||
|
||||
FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) {
|
||||
return (FrameLayout) factory.inflate(
|
||||
R.layout.auth_container_view, root, false /* attachToRoot */);
|
||||
}
|
||||
|
||||
AuthPanelController getPanelController(Context context, View panelView) {
|
||||
return new AuthPanelController(context, panelView);
|
||||
}
|
||||
|
||||
ImageView getBackgroundView(FrameLayout parent) {
|
||||
return parent.findViewById(R.id.background);
|
||||
}
|
||||
|
||||
View getPanelView(FrameLayout parent) {
|
||||
return parent.findViewById(R.id.panel);
|
||||
}
|
||||
|
||||
int getAnimateCredentialStartDelayMs() {
|
||||
return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS;
|
||||
}
|
||||
|
||||
UserManager getUserManager(Context context) {
|
||||
return UserManager.get(context);
|
||||
}
|
||||
|
||||
int getCredentialType(Context context, int effectiveUserId) {
|
||||
return Utils.getCredentialType(context, effectiveUserId);
|
||||
return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
|
||||
userManager, lockPatternUtils, new Handler(Looper.getMainLooper()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +220,7 @@ public class AuthContainerView extends LinearLayout
|
||||
animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
|
||||
break;
|
||||
case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN:
|
||||
mFailedModalities.clear();
|
||||
mConfig.mCallback.onTryAgainPressed();
|
||||
break;
|
||||
case AuthBiometricView.Callback.ACTION_ERROR:
|
||||
@@ -255,10 +230,7 @@ public class AuthContainerView extends LinearLayout
|
||||
mConfig.mCallback.onDeviceCredentialPressed();
|
||||
mHandler.postDelayed(() -> {
|
||||
addCredentialView(false /* animatePanel */, true /* animateContents */);
|
||||
}, mInjector.getAnimateCredentialStartDelayMs());
|
||||
break;
|
||||
case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR:
|
||||
mConfig.mCallback.onStartFingerprintNow();
|
||||
}, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unhandled action: " + action);
|
||||
@@ -275,21 +247,19 @@ public class AuthContainerView extends LinearLayout
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
AuthContainerView(Config config, Injector injector,
|
||||
AuthContainerView(Config config,
|
||||
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
|
||||
@Nullable List<FaceSensorPropertiesInternal> faceProps,
|
||||
WakefulnessLifecycle wakefulnessLifecycle) {
|
||||
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
|
||||
@NonNull UserManager userManager,
|
||||
@NonNull LockPatternUtils lockPatternUtils,
|
||||
@NonNull Handler mainHandler) {
|
||||
super(config.mContext);
|
||||
|
||||
mConfig = config;
|
||||
mInjector = injector;
|
||||
mFpProps = fpProps;
|
||||
mFaceProps = faceProps;
|
||||
|
||||
mEffectiveUserId = mInjector.getUserManager(mContext)
|
||||
.getCredentialOwnerProfile(mConfig.mUserId);
|
||||
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mLockPatternUtils = lockPatternUtils;
|
||||
mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
|
||||
mHandler = mainHandler;
|
||||
mWindowManager = mContext.getSystemService(WindowManager.class);
|
||||
mWakefulnessLifecycle = wakefulnessLifecycle;
|
||||
|
||||
@@ -299,100 +269,42 @@ public class AuthContainerView extends LinearLayout
|
||||
mBiometricCallback = new BiometricCallback();
|
||||
mCredentialCallback = new CredentialCallback();
|
||||
|
||||
final LayoutInflater factory = LayoutInflater.from(mContext);
|
||||
mFrameLayout = mInjector.inflateContainerView(factory, this);
|
||||
|
||||
mPanelView = mInjector.getPanelView(mFrameLayout);
|
||||
mPanelController = mInjector.getPanelController(mContext, mPanelView);
|
||||
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
|
||||
mFrameLayout = (FrameLayout) layoutInflater.inflate(
|
||||
R.layout.auth_container_view, this, false /* attachToRoot */);
|
||||
addView(mFrameLayout);
|
||||
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
|
||||
mBackgroundView = mFrameLayout.findViewById(R.id.background);
|
||||
mPanelView = mFrameLayout.findViewById(R.id.panel);
|
||||
mPanelController = new AuthPanelController(mContext, mPanelView);
|
||||
|
||||
// Inflate biometric view only if necessary.
|
||||
final int sensorCount = config.mSensorIds.length;
|
||||
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
|
||||
if (sensorCount == 1) {
|
||||
final int singleSensorAuthId = config.mSensorIds[0];
|
||||
if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) {
|
||||
FingerprintSensorPropertiesInternal sensorProps = null;
|
||||
for (FingerprintSensorPropertiesInternal prop : mFpProps) {
|
||||
if (prop.sensorId == singleSensorAuthId) {
|
||||
sensorProps = prop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final FingerprintSensorPropertiesInternal fpProperties =
|
||||
Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds);
|
||||
final FaceSensorPropertiesInternal faceProperties =
|
||||
Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds);
|
||||
|
||||
if (sensorProps.isAnyUdfpsType()) {
|
||||
AuthBiometricUdfpsView udfpsView = (AuthBiometricUdfpsView) factory
|
||||
.inflate(R.layout.auth_biometric_udfps_view, null, false);
|
||||
udfpsView.setSensorProps(sensorProps);
|
||||
mBiometricView = udfpsView;
|
||||
} else {
|
||||
mBiometricView = (AuthBiometricFingerprintView) factory
|
||||
.inflate(R.layout.auth_biometric_fingerprint_view, null, false);
|
||||
}
|
||||
} else if (Utils.containsSensorId(mFaceProps, singleSensorAuthId)) {
|
||||
mBiometricView = (AuthBiometricFaceView)
|
||||
factory.inflate(R.layout.auth_biometric_face_view, null, false);
|
||||
} else {
|
||||
// Unknown sensorId
|
||||
Log.e(TAG, "Unknown sensorId: " + singleSensorAuthId);
|
||||
mBiometricView = null;
|
||||
mBackgroundView = null;
|
||||
mBiometricScrollView = null;
|
||||
return;
|
||||
}
|
||||
} else if (sensorCount == 2) {
|
||||
final int[] allSensors = findFaceAndFingerprintSensors();
|
||||
final int faceSensorId = allSensors[0];
|
||||
final int fingerprintSensorId = allSensors[1];
|
||||
|
||||
if (fingerprintSensorId == -1 || faceSensorId == -1) {
|
||||
Log.e(TAG, "Missing fingerprint or face for dual-sensor config");
|
||||
mBiometricView = null;
|
||||
mBackgroundView = null;
|
||||
mBiometricScrollView = null;
|
||||
return;
|
||||
}
|
||||
|
||||
FingerprintSensorPropertiesInternal fingerprintSensorProps = null;
|
||||
for (FingerprintSensorPropertiesInternal prop : mFpProps) {
|
||||
if (prop.sensorId == fingerprintSensorId) {
|
||||
fingerprintSensorProps = prop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fingerprintSensorProps != null) {
|
||||
final AuthBiometricFaceToFingerprintView faceToFingerprintView =
|
||||
(AuthBiometricFaceToFingerprintView) factory.inflate(
|
||||
R.layout.auth_biometric_face_to_fingerprint_view, null, false);
|
||||
faceToFingerprintView.setFingerprintSensorProps(fingerprintSensorProps);
|
||||
faceToFingerprintView.setModalityListener(new ModalityListener() {
|
||||
@Override
|
||||
public void onModalitySwitched(int oldModality, int newModality) {
|
||||
maybeUpdatePositionForUdfps(true /* invalidate */);
|
||||
}
|
||||
});
|
||||
mBiometricView = faceToFingerprintView;
|
||||
} else {
|
||||
Log.e(TAG, "Fingerprint props not found for sensor ID: " + fingerprintSensorId);
|
||||
mBiometricView = null;
|
||||
mBackgroundView = null;
|
||||
mBiometricScrollView = null;
|
||||
return;
|
||||
}
|
||||
if (fpProperties != null && faceProperties != null) {
|
||||
final AuthBiometricFingerprintAndFaceView fingerprintAndFaceView =
|
||||
(AuthBiometricFingerprintAndFaceView) layoutInflater.inflate(
|
||||
R.layout.auth_biometric_fingerprint_and_face_view, null, false);
|
||||
fingerprintAndFaceView.setSensorProperties(fpProperties);
|
||||
mBiometricView = fingerprintAndFaceView;
|
||||
} else if (fpProperties != null) {
|
||||
final AuthBiometricFingerprintView fpView =
|
||||
(AuthBiometricFingerprintView) layoutInflater.inflate(
|
||||
R.layout.auth_biometric_fingerprint_view, null, false);
|
||||
fpView.setSensorProperties(fpProperties);
|
||||
mBiometricView = fpView;
|
||||
} else if (faceProperties != null) {
|
||||
mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate(
|
||||
R.layout.auth_biometric_face_view, null, false);
|
||||
} else {
|
||||
Log.e(TAG, "Unsupported sensor array, length: " + sensorCount);
|
||||
mBiometricView = null;
|
||||
mBackgroundView = null;
|
||||
mBiometricScrollView = null;
|
||||
return;
|
||||
Log.e(TAG, "No sensors found!");
|
||||
}
|
||||
}
|
||||
|
||||
mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout);
|
||||
mBackgroundView = mInjector.getBackgroundView(mFrameLayout);
|
||||
|
||||
addView(mFrameLayout);
|
||||
|
||||
// init view before showing
|
||||
if (mBiometricView != null) {
|
||||
mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
|
||||
@@ -431,10 +343,6 @@ public class AuthContainerView extends LinearLayout
|
||||
return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
|
||||
}
|
||||
|
||||
private void addBiometricView() {
|
||||
mBiometricScrollView.addView(mBiometricView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the credential view. When going from biometric to credential view, the biometric
|
||||
* view starts the panel expansion animation. If the credential view is being shown first,
|
||||
@@ -444,8 +352,8 @@ public class AuthContainerView extends LinearLayout
|
||||
private void addCredentialView(boolean animatePanel, boolean animateContents) {
|
||||
final LayoutInflater factory = LayoutInflater.from(mContext);
|
||||
|
||||
final @Utils.CredentialType int credentialType = mInjector.getCredentialType(
|
||||
mContext, mEffectiveUserId);
|
||||
@Utils.CredentialType final int credentialType = Utils.getCredentialType(
|
||||
mLockPatternUtils, mEffectiveUserId);
|
||||
|
||||
switch (credentialType) {
|
||||
case Utils.CREDENTIAL_PATTERN:
|
||||
@@ -493,15 +401,11 @@ public class AuthContainerView extends LinearLayout
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
onAttachedToWindowInternal();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void onAttachedToWindowInternal() {
|
||||
mWakefulnessLifecycle.addObserver(this);
|
||||
|
||||
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
|
||||
addBiometricView();
|
||||
mBiometricScrollView.addView(mBiometricView);
|
||||
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
|
||||
addCredentialView(true /* animatePanel */, false /* animateContents */);
|
||||
} else {
|
||||
@@ -521,17 +425,18 @@ public class AuthContainerView extends LinearLayout
|
||||
mBiometricScrollView.setY(mTranslationY);
|
||||
|
||||
setAlpha(0f);
|
||||
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
|
||||
postOnAnimation(() -> {
|
||||
mPanelView.animate()
|
||||
.translationY(0)
|
||||
.setDuration(ANIMATION_DURATION_SHOW_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.withEndAction(this::onDialogAnimatedIn)
|
||||
.start();
|
||||
mBiometricScrollView.animate()
|
||||
.translationY(0)
|
||||
.setDuration(ANIMATION_DURATION_SHOW_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
@@ -539,14 +444,14 @@ public class AuthContainerView extends LinearLayout
|
||||
mCredentialView.setY(mTranslationY);
|
||||
mCredentialView.animate()
|
||||
.translationY(0)
|
||||
.setDuration(ANIMATION_DURATION_SHOW_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
}
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.setDuration(ANIMATION_DURATION_SHOW_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
@@ -555,15 +460,8 @@ public class AuthContainerView extends LinearLayout
|
||||
}
|
||||
|
||||
private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
|
||||
if (view instanceof AuthBiometricUdfpsView) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (view instanceof AuthBiometricFaceToFingerprintView) {
|
||||
AuthBiometricFaceToFingerprintView faceToFingerprintView =
|
||||
(AuthBiometricFaceToFingerprintView) view;
|
||||
return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT
|
||||
&& faceToFingerprintView.isFingerprintUdfps();
|
||||
if (view instanceof AuthBiometricFingerprintView) {
|
||||
return ((AuthBiometricFingerprintView) view).isUdfps();
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -652,12 +550,13 @@ public class AuthContainerView extends LinearLayout
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded() {
|
||||
mBiometricView.onAuthenticationSucceeded();
|
||||
public void onAuthenticationSucceeded(@Modality int modality) {
|
||||
mBiometricView.onAuthenticationSucceeded(modality);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed(@Modality int modality, String failureReason) {
|
||||
mFailedModalities.add(modality);
|
||||
mBiometricView.onAuthenticationFailed(modality, failureReason);
|
||||
}
|
||||
|
||||
@@ -671,9 +570,18 @@ public class AuthContainerView extends LinearLayout
|
||||
mBiometricView.onError(modality, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPointerDown() {
|
||||
if (mBiometricView.onPointerDown(mFailedModalities)) {
|
||||
Log.d(TAG, "retrying failed modalities (pointer down)");
|
||||
mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveState(@NonNull Bundle outState) {
|
||||
outState.putInt(AuthDialog.KEY_CONTAINER_STATE, mContainerState);
|
||||
outState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY,
|
||||
mContainerState == STATE_ANIMATING_OUT);
|
||||
// In the case where biometric and credential are both allowed, we can assume that
|
||||
// biometric isn't showing if credential is showing since biometric is shown first.
|
||||
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING,
|
||||
@@ -695,8 +603,7 @@ public class AuthContainerView extends LinearLayout
|
||||
mBiometricView.startTransitionToCredentialUI();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void animateAway(int reason) {
|
||||
void animateAway(@AuthDialogCallback.DismissedReason int reason) {
|
||||
animateAway(true /* sendReason */, reason);
|
||||
}
|
||||
|
||||
@@ -724,31 +631,32 @@ public class AuthContainerView extends LinearLayout
|
||||
removeWindowIfAttached();
|
||||
};
|
||||
|
||||
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
|
||||
postOnAnimation(() -> {
|
||||
mPanelView.animate()
|
||||
.translationY(mTranslationY)
|
||||
.setDuration(ANIMATION_DURATION_AWAY_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.withEndAction(endActionRunnable)
|
||||
.start();
|
||||
mBiometricScrollView.animate()
|
||||
.translationY(mTranslationY)
|
||||
.setDuration(ANIMATION_DURATION_AWAY_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
|
||||
mCredentialView.animate()
|
||||
.translationY(mTranslationY)
|
||||
.setDuration(ANIMATION_DURATION_AWAY_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
}
|
||||
animate()
|
||||
.alpha(0f)
|
||||
.setDuration(ANIMATION_DURATION_AWAY_MS)
|
||||
.setDuration(animateDuration)
|
||||
.setInterpolator(mLinearOutSlowIn)
|
||||
.withLayer()
|
||||
.start();
|
||||
@@ -773,8 +681,7 @@ public class AuthContainerView extends LinearLayout
|
||||
mWindowManager.removeView(this);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void onDialogAnimatedIn() {
|
||||
private void onDialogAnimatedIn() {
|
||||
if (mContainerState == STATE_PENDING_DISMISS) {
|
||||
Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
|
||||
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
|
||||
@@ -788,8 +695,7 @@ public class AuthContainerView extends LinearLayout
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken,
|
||||
CharSequence title) {
|
||||
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
|
||||
final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
|
||||
| WindowManager.LayoutParams.FLAG_SECURE;
|
||||
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
|
||||
@@ -805,24 +711,4 @@ public class AuthContainerView extends LinearLayout
|
||||
lp.token = windowToken;
|
||||
return lp;
|
||||
}
|
||||
|
||||
// returns [face, fingerprint] sensor ids (id is -1 if not present)
|
||||
private int[] findFaceAndFingerprintSensors() {
|
||||
int faceSensorId = -1;
|
||||
int fingerprintSensorId = -1;
|
||||
|
||||
for (final int sensorId : mConfig.mSensorIds) {
|
||||
if (Utils.containsSensorId(mFpProps, sensorId)) {
|
||||
fingerprintSensorId = sensorId;
|
||||
} else if (Utils.containsSensorId(mFaceProps, sensorId)) {
|
||||
faceSensorId = sensorId;
|
||||
}
|
||||
|
||||
if (fingerprintSensorId != -1 && faceSensorId != -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new int[] {faceSensorId, fingerprintSensorId};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import android.hardware.fingerprint.IUdfpsHbmListener;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.MotionEvent;
|
||||
@@ -59,6 +60,7 @@ import android.view.WindowManager;
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.os.SomeArgs;
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
import com.android.systemui.CoreStartable;
|
||||
import com.android.systemui.assist.ui.DisplayUtils;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
@@ -123,8 +125,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
@Nullable private SidefpsController mSidefpsController;
|
||||
@Nullable private IBiometricContextListener mBiometricContextListener;
|
||||
@VisibleForTesting
|
||||
TaskStackListener mTaskStackListener;
|
||||
@VisibleForTesting
|
||||
IBiometricSysuiReceiver mReceiver;
|
||||
@VisibleForTesting
|
||||
@NonNull final BiometricDisplayListener mOrientationListener;
|
||||
@@ -137,13 +137,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
|
||||
private final WakefulnessLifecycle mWakefulnessLifecycle;
|
||||
private boolean mAllAuthenticatorsRegistered;
|
||||
@NonNull private final UserManager mUserManager;
|
||||
@NonNull private final LockPatternUtils mLockPatternUtils;
|
||||
|
||||
private class BiometricTaskStackListener extends TaskStackListener {
|
||||
@VisibleForTesting
|
||||
final TaskStackListener mTaskStackListener = new TaskStackListener() {
|
||||
@Override
|
||||
public void onTaskStackChanged() {
|
||||
mHandler.post(AuthController.this::handleTaskStackChanged);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final IFingerprintAuthenticatorsRegisteredCallback
|
||||
mFingerprintAuthenticatorsRegisteredCallback =
|
||||
@@ -256,6 +259,17 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null;
|
||||
if (mUdfpsProps != null) {
|
||||
mUdfpsController = mUdfpsControllerFactory.get();
|
||||
mUdfpsController.addCallback(new UdfpsController.Callback() {
|
||||
@Override
|
||||
public void onFingerUp() {}
|
||||
|
||||
@Override
|
||||
public void onFingerDown() {
|
||||
if (mCurrentDialog != null) {
|
||||
mCurrentDialog.onPointerDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
mSidefpsProps = !sidefpsProps.isEmpty() ? sidefpsProps : null;
|
||||
if (mSidefpsProps != null) {
|
||||
@@ -359,20 +373,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartFingerprintNow() {
|
||||
if (mReceiver == null) {
|
||||
Log.e(TAG, "onStartUdfpsNow: Receiver is null");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mReceiver.onStartFingerprintNow();
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) {
|
||||
switch (reason) {
|
||||
@@ -503,12 +503,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
Provider<UdfpsController> udfpsControllerFactory,
|
||||
Provider<SidefpsController> sidefpsControllerFactory,
|
||||
@NonNull DisplayManager displayManager,
|
||||
WakefulnessLifecycle wakefulnessLifecycle,
|
||||
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
|
||||
@NonNull UserManager userManager,
|
||||
@NonNull LockPatternUtils lockPatternUtils,
|
||||
@NonNull StatusBarStateController statusBarStateController,
|
||||
@Main Handler handler) {
|
||||
super(context);
|
||||
mExecution = execution;
|
||||
mWakefulnessLifecycle = wakefulnessLifecycle;
|
||||
mUserManager = userManager;
|
||||
mLockPatternUtils = lockPatternUtils;
|
||||
mHandler = handler;
|
||||
mCommandQueue = commandQueue;
|
||||
mActivityTaskManager = activityTaskManager;
|
||||
@@ -583,7 +587,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
mFingerprintAuthenticatorsRegisteredCallback);
|
||||
}
|
||||
|
||||
mTaskStackListener = new BiometricTaskStackListener();
|
||||
mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
|
||||
}
|
||||
|
||||
@@ -668,11 +671,11 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
* example, KeyguardUpdateMonitor has its own {@link FingerprintManager.AuthenticationCallback}.
|
||||
*/
|
||||
@Override
|
||||
public void onBiometricAuthenticated() {
|
||||
public void onBiometricAuthenticated(@Modality int modality) {
|
||||
if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: ");
|
||||
|
||||
if (mCurrentDialog != null) {
|
||||
mCurrentDialog.onAuthenticationSucceeded();
|
||||
mCurrentDialog.onAuthenticationSucceeded(modality);
|
||||
} else {
|
||||
Log.w(TAG, "onBiometricAuthenticated callback but dialog gone");
|
||||
}
|
||||
@@ -827,7 +830,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
final String opPackageName = (String) args.arg6;
|
||||
final long operationId = args.argl1;
|
||||
final long requestId = args.argl2;
|
||||
final @BiometricMultiSensorMode int multiSensorConfig = args.argi2;
|
||||
@BiometricMultiSensorMode final int multiSensorConfig = args.argi2;
|
||||
|
||||
// Create a new dialog but do not replace the current one yet.
|
||||
final AuthDialog newDialog = buildDialog(
|
||||
@@ -835,13 +838,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
requireConfirmation,
|
||||
userId,
|
||||
sensorIds,
|
||||
credentialAllowed,
|
||||
opPackageName,
|
||||
skipAnimation,
|
||||
operationId,
|
||||
requestId,
|
||||
multiSensorConfig,
|
||||
mWakefulnessLifecycle);
|
||||
mWakefulnessLifecycle,
|
||||
mUserManager,
|
||||
mLockPatternUtils);
|
||||
|
||||
if (newDialog == null) {
|
||||
Log.e(TAG, "Unsupported type configuration");
|
||||
@@ -902,8 +906,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
|
||||
// Only show the dialog if necessary. If it was animating out, the dialog is supposed
|
||||
// to send its pending callback immediately.
|
||||
if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE)
|
||||
!= AuthContainerView.STATE_ANIMATING_OUT) {
|
||||
if (!savedState.getBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false)) {
|
||||
final boolean credentialShowing =
|
||||
savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING);
|
||||
if (credentialShowing) {
|
||||
@@ -927,10 +930,12 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
}
|
||||
|
||||
protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation,
|
||||
int userId, int[] sensorIds, boolean credentialAllowed, String opPackageName,
|
||||
int userId, int[] sensorIds, String opPackageName,
|
||||
boolean skipIntro, long operationId, long requestId,
|
||||
@BiometricMultiSensorMode int multiSensorConfig,
|
||||
WakefulnessLifecycle wakefulnessLifecycle) {
|
||||
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
|
||||
@NonNull UserManager userManager,
|
||||
@NonNull LockPatternUtils lockPatternUtils) {
|
||||
return new AuthContainerView.Builder(mContext)
|
||||
.setCallback(this)
|
||||
.setPromptInfo(promptInfo)
|
||||
@@ -941,7 +946,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
|
||||
.setOperationId(operationId)
|
||||
.setRequestId(requestId)
|
||||
.setMultiSensorConfig(multiSensorConfig)
|
||||
.build(sensorIds, credentialAllowed, mFpProps, mFaceProps, wakefulnessLifecycle);
|
||||
.build(sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle, userManager,
|
||||
lockPatternUtils);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
*/
|
||||
public interface AuthDialog {
|
||||
|
||||
String KEY_CONTAINER_STATE = "container_state";
|
||||
String KEY_CONTAINER_GOING_AWAY = "container_going_away";
|
||||
String KEY_BIOMETRIC_SHOWING = "biometric_showing";
|
||||
String KEY_CREDENTIAL_SHOWING = "credential_showing";
|
||||
|
||||
@@ -64,7 +64,7 @@ public interface AuthDialog {
|
||||
@interface DialogSize {}
|
||||
|
||||
/**
|
||||
* Parameters used when laying out {@link AuthBiometricView}, its sublclasses, and
|
||||
* Parameters used when laying out {@link AuthBiometricView}, its subclasses, and
|
||||
* {@link AuthPanelController}.
|
||||
*/
|
||||
class LayoutParams {
|
||||
@@ -113,7 +113,7 @@ public interface AuthDialog {
|
||||
/**
|
||||
* Biometric authenticated. May be pending user confirmation, or completed.
|
||||
*/
|
||||
void onAuthenticationSucceeded();
|
||||
void onAuthenticationSucceeded(@Modality int modality);
|
||||
|
||||
/**
|
||||
* Authentication failed (reject, timeout). Dialog stays showing.
|
||||
@@ -136,6 +136,9 @@ public interface AuthDialog {
|
||||
*/
|
||||
void onError(@Modality int modality, String error);
|
||||
|
||||
/** UDFPS pointer down event. */
|
||||
void onPointerDown();
|
||||
|
||||
/**
|
||||
* Save the current state.
|
||||
* @param outState
|
||||
|
||||
@@ -70,9 +70,4 @@ public interface AuthDialogCallback {
|
||||
* Notifies when the dialog has finished animating.
|
||||
*/
|
||||
void onDialogAnimatedIn();
|
||||
|
||||
/**
|
||||
* Notifies that the fingerprint sensor should be started now.
|
||||
*/
|
||||
void onStartFingerprintNow();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.util.AttributeSet
|
||||
*
|
||||
* Currently doesn't draw anything.
|
||||
*
|
||||
* Note that [AuthBiometricUdfpsView] also shows UDFPS animations. At some point we should
|
||||
* Note that [AuthBiometricFingerprintViewController] also shows UDFPS animations. At some point we should
|
||||
* de-dupe this if necessary.
|
||||
*/
|
||||
class UdfpsBpView(context: Context, attrs: AttributeSet?) : UdfpsAnimationView(context, attrs) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
120
packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt
Normal file
120
packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt
Normal 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
|
||||
}
|
||||
@@ -310,11 +310,11 @@ public class CommandQueue extends IStatusBar.Stub implements
|
||||
long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
|
||||
}
|
||||
|
||||
/** @see IStatusBar#onBiometricAuthenticated() */
|
||||
default void onBiometricAuthenticated() {
|
||||
/** @see IStatusBar#onBiometricAuthenticated(int) */
|
||||
default void onBiometricAuthenticated(@Modality int modality) {
|
||||
}
|
||||
|
||||
/** @see IStatusBar#onBiometricHelp(String) */
|
||||
/** @see IStatusBar#onBiometricHelp(int, String) */
|
||||
default void onBiometricHelp(@Modality int modality, String message) {
|
||||
}
|
||||
|
||||
@@ -963,9 +963,11 @@ public class CommandQueue extends IStatusBar.Stub implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBiometricAuthenticated() {
|
||||
public void onBiometricAuthenticated(@Modality int modality) {
|
||||
synchronized (mLock) {
|
||||
mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED).sendToTarget();
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = modality;
|
||||
mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1465,9 +1467,11 @@ public class CommandQueue extends IStatusBar.Stub implements
|
||||
break;
|
||||
}
|
||||
case MSG_BIOMETRIC_AUTHENTICATED: {
|
||||
SomeArgs someArgs = (SomeArgs) msg.obj;
|
||||
for (int i = 0; i < mCallbacks.size(); i++) {
|
||||
mCallbacks.get(i).onBiometricAuthenticated();
|
||||
mCallbacks.get(i).onBiometricAuthenticated(someArgs.argi1 /* modality */);
|
||||
}
|
||||
someArgs.recycle();
|
||||
break;
|
||||
}
|
||||
case MSG_BIOMETRIC_HELP: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,137 +20,109 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.Authenticators;
|
||||
|
||||
import static com.android.systemui.biometrics.AuthBiometricView.Callback.ACTION_AUTHENTICATED;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.biometrics.BiometricPrompt;
|
||||
import android.hardware.biometrics.PromptInfo;
|
||||
import android.os.Bundle;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.testing.TestableLooper.RunWithLooper;
|
||||
import android.util.AttributeSet;
|
||||
import android.testing.ViewUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
@Ignore
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@RunWithLooper
|
||||
@SmallTest
|
||||
public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
|
||||
@Mock private AuthBiometricView.Callback mCallback;
|
||||
@Mock private AuthPanelController mPanelController;
|
||||
@Rule
|
||||
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
|
||||
@Mock private Button mNegativeButton;
|
||||
@Mock private Button mCancelButton;
|
||||
@Mock private Button mUseCredentialButton;
|
||||
@Mock
|
||||
private AuthBiometricView.Callback mCallback;
|
||||
@Mock
|
||||
private AuthPanelController mPanelController;
|
||||
|
||||
@Mock private Button mPositiveButton;
|
||||
@Mock private Button mTryAgainButton;
|
||||
|
||||
@Mock private TextView mTitleView;
|
||||
@Mock private TextView mSubtitleView;
|
||||
@Mock private TextView mDescriptionView;
|
||||
@Mock private TextView mIndicatorView;
|
||||
@Mock private ImageView mIconView;
|
||||
@Mock private View mIconHolderView;
|
||||
|
||||
private TestableBiometricView mBiometricView;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
}
|
||||
private AuthBiometricView mBiometricView;
|
||||
|
||||
@Test
|
||||
public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() {
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
// The onAuthenticated runnable is posted when authentication succeeds.
|
||||
mBiometricView.onAuthenticationSucceeded();
|
||||
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
|
||||
waitForIdleSync();
|
||||
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
|
||||
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED);
|
||||
verify(mCallback).onAction(ACTION_AUTHENTICATED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() {
|
||||
final Button negativeButton = new Button(mContext);
|
||||
final Button cancelButton = new Button(mContext);
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
|
||||
@Override
|
||||
public Button getNegativeButton() {
|
||||
return negativeButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Button getCancelButton() {
|
||||
return cancelButton;
|
||||
}
|
||||
});
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
mBiometricView.setRequireConfirmation(true);
|
||||
mBiometricView.onAuthenticationSucceeded();
|
||||
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
|
||||
waitForIdleSync();
|
||||
assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState);
|
||||
verify(mCallback, never()).onAction(anyInt());
|
||||
|
||||
assertEquals(View.GONE, negativeButton.getVisibility());
|
||||
assertEquals(View.VISIBLE, cancelButton.getVisibility());
|
||||
assertTrue(cancelButton.isEnabled());
|
||||
// TODO: this should be tested in the subclasses
|
||||
if (mBiometricView.supportsRequireConfirmation()) {
|
||||
assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState);
|
||||
|
||||
verify(mCallback, never()).onAction(anyInt());
|
||||
|
||||
assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
|
||||
assertEquals(View.VISIBLE, mBiometricView.mCancelButton.getVisibility());
|
||||
assertTrue(mBiometricView.mCancelButton.isEnabled());
|
||||
|
||||
assertTrue(mBiometricView.mConfirmButton.isEnabled());
|
||||
assertEquals(mContext.getText(R.string.biometric_dialog_tap_confirm),
|
||||
mBiometricView.mIndicatorView.getText());
|
||||
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
|
||||
} else {
|
||||
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
|
||||
verify(mCallback).onAction(eq(ACTION_AUTHENTICATED));
|
||||
}
|
||||
|
||||
verify(mBiometricView.mConfirmButton).setEnabled(eq(true));
|
||||
verify(mIndicatorView).setText(eq(R.string.biometric_dialog_tap_confirm));
|
||||
verify(mIndicatorView).setVisibility(eq(View.VISIBLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPositiveButton_sendsActionAuthenticated() {
|
||||
Button button = new Button(mContext);
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
|
||||
@Override
|
||||
public Button getConfirmButton() {
|
||||
return button;
|
||||
}
|
||||
});
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
button.performClick();
|
||||
mBiometricView.mConfirmButton.performClick();
|
||||
waitForIdleSync();
|
||||
|
||||
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED);
|
||||
verify(mCallback).onAction(ACTION_AUTHENTICATED);
|
||||
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
|
||||
Button button = new Button(mContext);
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
|
||||
@Override
|
||||
public Button getNegativeButton() {
|
||||
return button;
|
||||
}
|
||||
});
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
mBiometricView.onDialogAnimatedIn();
|
||||
button.performClick();
|
||||
mBiometricView.mNegativeButton.performClick();
|
||||
waitForIdleSync();
|
||||
|
||||
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE);
|
||||
@@ -158,25 +130,14 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() {
|
||||
Button cancelButton = new Button(mContext);
|
||||
Button negativeButton = new Button(mContext);
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
|
||||
@Override
|
||||
public Button getNegativeButton() {
|
||||
return negativeButton;
|
||||
}
|
||||
@Override
|
||||
public Button getCancelButton() {
|
||||
return cancelButton;
|
||||
}
|
||||
});
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
mBiometricView.setRequireConfirmation(true);
|
||||
mBiometricView.onAuthenticationSucceeded();
|
||||
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
|
||||
|
||||
assertEquals(View.GONE, negativeButton.getVisibility());
|
||||
assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
|
||||
|
||||
cancelButton.performClick();
|
||||
mBiometricView.mCancelButton.performClick();
|
||||
waitForIdleSync();
|
||||
|
||||
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED);
|
||||
@@ -184,15 +145,9 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void testTryAgainButton_sendsActionTryAgain() {
|
||||
Button button = new Button(mContext);
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
|
||||
@Override
|
||||
public Button getTryAgainButton() {
|
||||
return button;
|
||||
}
|
||||
});
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
button.performClick();
|
||||
mBiometricView.mTryAgainButton.performClick();
|
||||
waitForIdleSync();
|
||||
|
||||
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
|
||||
@@ -202,7 +157,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
@Test
|
||||
@Ignore("flaky, b/189031816")
|
||||
public void testError_sendsActionError() {
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
final String testError = "testError";
|
||||
mBiometricView.onError(TYPE_FACE, testError);
|
||||
waitForIdleSync();
|
||||
@@ -213,7 +168,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void testBackgroundClicked_sendsActionUserCanceled() {
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
View view = new View(mContext);
|
||||
mBiometricView.setBackgroundView(view);
|
||||
@@ -223,18 +178,18 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() {
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
View view = new View(mContext);
|
||||
mBiometricView.setBackgroundView(view);
|
||||
mBiometricView.onAuthenticationSucceeded();
|
||||
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
|
||||
view.performClick();
|
||||
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() {
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
mBiometricView.mLayoutParams = new AuthDialog.LayoutParams(0, 0);
|
||||
mBiometricView.updateSize(AuthDialog.SIZE_SMALL);
|
||||
|
||||
@@ -246,7 +201,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void testIgnoresUselessHelp() {
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
|
||||
initDialog(false /* allowDeviceCredential */, mCallback);
|
||||
|
||||
mBiometricView.onDialogAnimatedIn();
|
||||
waitForIdleSync();
|
||||
@@ -256,33 +211,16 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
mBiometricView.onHelp(TYPE_FINGERPRINT, "");
|
||||
waitForIdleSync();
|
||||
|
||||
verify(mIndicatorView, never()).setText(any());
|
||||
assertEquals("", mBiometricView.mIndicatorView.getText());
|
||||
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR));
|
||||
assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoresState() {
|
||||
final boolean requireConfirmation = true; // set/init from AuthController
|
||||
final boolean requireConfirmation = true;
|
||||
|
||||
Button tryAgainButton = new Button(mContext);
|
||||
TextView indicatorView = new TextView(mContext);
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
|
||||
@Override
|
||||
public Button getTryAgainButton() {
|
||||
return tryAgainButton;
|
||||
}
|
||||
@Override
|
||||
public TextView getIndicatorView() {
|
||||
return indicatorView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDelayAfterError() {
|
||||
// keep a real delay to test saving in the error state
|
||||
return BiometricPrompt.HIDE_DIALOG_DELAY;
|
||||
}
|
||||
});
|
||||
initDialog(false /* allowDeviceCredential */, mCallback, null, 10000);
|
||||
|
||||
final String failureMessage = "testFailureMessage";
|
||||
mBiometricView.setRequireConfirmation(requireConfirmation);
|
||||
@@ -292,8 +230,8 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
Bundle state = new Bundle();
|
||||
mBiometricView.onSaveState(state);
|
||||
|
||||
assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
|
||||
assertEquals(View.VISIBLE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
|
||||
assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility());
|
||||
assertEquals(View.GONE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
|
||||
|
||||
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
|
||||
assertEquals(AuthBiometricView.STATE_ERROR, state.getInt(AuthDialog.KEY_BIOMETRIC_STATE));
|
||||
@@ -307,25 +245,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
// TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle
|
||||
|
||||
// Create new dialog and restore the previous state into it
|
||||
Button tryAgainButton2 = new Button(mContext);
|
||||
TextView indicatorView2 = new TextView(mContext);
|
||||
initDialog(mContext, false /* allowDeviceCredential */, mCallback, state,
|
||||
new MockInjector() {
|
||||
@Override
|
||||
public Button getTryAgainButton() {
|
||||
return tryAgainButton2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView getIndicatorView() {
|
||||
return indicatorView2;
|
||||
}
|
||||
});
|
||||
initDialog(false /* allowDeviceCredential */, mCallback, state, 10000);
|
||||
mBiometricView.mAnimationDurationHideDialog = 10000;
|
||||
mBiometricView.setRequireConfirmation(requireConfirmation);
|
||||
waitForIdleSync();
|
||||
|
||||
// Test restored state
|
||||
assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
|
||||
assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility());
|
||||
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
|
||||
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
|
||||
|
||||
@@ -334,23 +259,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialButton_whenDeviceCredentialAllowed() {
|
||||
final Button negativeButton = new Button(mContext);
|
||||
final Button useCredentialButton = new Button(mContext);
|
||||
initDialog(mContext, true /* allowDeviceCredential */, mCallback, new MockInjector() {
|
||||
@Override
|
||||
public Button getNegativeButton() {
|
||||
return negativeButton;
|
||||
}
|
||||
public void testCredentialButton_whenDeviceCredentialAllowed() throws InterruptedException {
|
||||
initDialog(true /* allowDeviceCredential */, mCallback);
|
||||
|
||||
@Override
|
||||
public Button getUseCredentialButton() {
|
||||
return useCredentialButton;
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals(View.GONE, negativeButton.getVisibility());
|
||||
useCredentialButton.performClick();
|
||||
assertEquals(View.VISIBLE, mBiometricView.mUseCredentialButton.getVisibility());
|
||||
assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
|
||||
mBiometricView.mUseCredentialButton.performClick();
|
||||
waitForIdleSync();
|
||||
|
||||
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL);
|
||||
@@ -369,120 +283,30 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
return promptInfo;
|
||||
}
|
||||
|
||||
private void initDialog(Context context, boolean allowDeviceCredential,
|
||||
AuthBiometricView.Callback callback,
|
||||
Bundle savedState, MockInjector injector) {
|
||||
mBiometricView = new TestableBiometricView(context, null, injector);
|
||||
private void initDialog(boolean allowDeviceCredential, AuthBiometricView.Callback callback) {
|
||||
initDialog(allowDeviceCredential, callback,
|
||||
null /* savedState */, 0 /* hideDelay */);
|
||||
}
|
||||
|
||||
private void initDialog(boolean allowDeviceCredential,
|
||||
AuthBiometricView.Callback callback, Bundle savedState, int hideDelay) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
mBiometricView = (AuthBiometricView) inflater.inflate(
|
||||
R.layout.auth_biometric_view, null, false);
|
||||
mBiometricView.mAnimationDurationLong = 0;
|
||||
mBiometricView.mAnimationDurationShort = 0;
|
||||
mBiometricView.mAnimationDurationHideDialog = hideDelay;
|
||||
mBiometricView.setPromptInfo(buildPromptInfo(allowDeviceCredential));
|
||||
mBiometricView.setCallback(callback);
|
||||
mBiometricView.restoreState(savedState);
|
||||
mBiometricView.onFinishInflateInternal();
|
||||
mBiometricView.onAttachedToWindowInternal();
|
||||
|
||||
ViewUtils.attachView(mBiometricView);
|
||||
mBiometricView.setPanelController(mPanelController);
|
||||
waitForIdleSync();
|
||||
}
|
||||
|
||||
private void initDialog(Context context, boolean allowDeviceCredential,
|
||||
AuthBiometricView.Callback callback, MockInjector injector) {
|
||||
initDialog(context, allowDeviceCredential, callback, null /* savedState */, injector);
|
||||
}
|
||||
|
||||
private class MockInjector extends AuthBiometricView.Injector {
|
||||
@Override
|
||||
public Button getNegativeButton() {
|
||||
return mNegativeButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Button getCancelButton() {
|
||||
return mCancelButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Button getUseCredentialButton() {
|
||||
return mUseCredentialButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Button getConfirmButton() {
|
||||
return mPositiveButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Button getTryAgainButton() {
|
||||
return mTryAgainButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView getTitleView() {
|
||||
return mTitleView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView getSubtitleView() {
|
||||
return mSubtitleView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView getDescriptionView() {
|
||||
return mDescriptionView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView getIndicatorView() {
|
||||
return mIndicatorView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageView getIconView() {
|
||||
return mIconView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getIconHolderView() {
|
||||
return mIconHolderView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDelayAfterError() {
|
||||
return 0; // Keep this at 0 for tests to invoke callback immediately.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMediumToLargeAnimationDurationMs() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestableBiometricView extends AuthBiometricView {
|
||||
TestableBiometricView(Context context, AttributeSet attrs,
|
||||
Injector injector) {
|
||||
super(context, attrs, injector);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDelayAfterAuthenticatedDurationMs() {
|
||||
return 0; // Keep this at 0 for tests to invoke callback immediately.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getStateForAfterError() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleResetAfterError() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleResetAfterHelp() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsSmallDialog() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
protected void waitForIdleSync() {
|
||||
TestableLooper.get(this).processAllMessages();
|
||||
super.waitForIdleSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -16,8 +16,9 @@
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.Authenticators;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNull;
|
||||
@@ -62,6 +63,7 @@ import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserManager;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableContext;
|
||||
import android.testing.TestableLooper;
|
||||
@@ -71,6 +73,7 @@ import android.view.WindowManager;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.keyguard.WakefulnessLifecycle;
|
||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||
@@ -79,6 +82,8 @@ import com.android.systemui.util.concurrency.Execution;
|
||||
import com.android.systemui.util.concurrency.FakeExecution;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.AdditionalMatchers;
|
||||
@@ -86,7 +91,8 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -94,11 +100,15 @@ import java.util.Random;
|
||||
|
||||
import javax.inject.Provider;
|
||||
|
||||
@Ignore
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@RunWithLooper
|
||||
@SmallTest
|
||||
public class AuthControllerTest extends SysuiTestCase {
|
||||
|
||||
@Rule
|
||||
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
|
||||
@Mock
|
||||
private PackageManager mPackageManager;
|
||||
@Mock
|
||||
@@ -128,6 +138,10 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
@Mock
|
||||
private WakefulnessLifecycle mWakefulnessLifecycle;
|
||||
@Mock
|
||||
private UserManager mUserManager;
|
||||
@Mock
|
||||
private LockPatternUtils mLockPatternUtils;
|
||||
@Mock
|
||||
private StatusBarStateController mStatusBarStateController;
|
||||
@Captor
|
||||
ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mAuthenticatorsRegisteredCaptor;
|
||||
@@ -144,8 +158,6 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
|
||||
@Before
|
||||
public void setup() throws RemoteException {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mContextSpy = spy(mContext);
|
||||
mExecution = new FakeExecution();
|
||||
mTestableLooper = TestableLooper.get(this);
|
||||
@@ -343,8 +355,8 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
@Test
|
||||
public void testOnAuthenticationSucceededInvoked_whenSystemRequested() {
|
||||
showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
|
||||
mAuthController.onBiometricAuthenticated();
|
||||
verify(mDialog1).onAuthenticationSucceeded();
|
||||
mAuthController.onBiometricAuthenticated(TYPE_FINGERPRINT);
|
||||
verify(mDialog1).onAuthenticationSucceeded(eq(TYPE_FINGERPRINT));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -528,8 +540,7 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
doAnswer(invocation -> {
|
||||
Object[] args = invocation.getArguments();
|
||||
Bundle savedState = (Bundle) args[0];
|
||||
savedState.putInt(
|
||||
AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING);
|
||||
savedState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false);
|
||||
return null; // onSaveState returns void
|
||||
}).when(mDialog1).onSaveState(any());
|
||||
|
||||
@@ -558,8 +569,7 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
doAnswer(invocation -> {
|
||||
Object[] args = invocation.getArguments();
|
||||
Bundle savedState = (Bundle) args[0];
|
||||
savedState.putInt(
|
||||
AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING);
|
||||
savedState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false);
|
||||
savedState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, true);
|
||||
return null; // onSaveState returns void
|
||||
}).when(mDialog1).onSaveState(any());
|
||||
@@ -697,7 +707,7 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
0 /* operationId */,
|
||||
"testPackage",
|
||||
1 /* requestId */,
|
||||
BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT);
|
||||
BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE);
|
||||
}
|
||||
|
||||
private PromptInfo createTestPromptInfo() {
|
||||
@@ -739,15 +749,16 @@ public class AuthControllerTest extends SysuiTestCase {
|
||||
super(context, execution, commandQueue, activityTaskManager, windowManager,
|
||||
fingerprintManager, faceManager, udfpsControllerFactory,
|
||||
sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle,
|
||||
statusBarStateController, mHandler);
|
||||
mUserManager, mLockPatternUtils, statusBarStateController, mHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthDialog buildDialog(PromptInfo promptInfo,
|
||||
boolean requireConfirmation, int userId, int[] sensorIds, boolean credentialAllowed,
|
||||
boolean requireConfirmation, int userId, int[] sensorIds,
|
||||
String opPackageName, boolean skipIntro, long operationId, long requestId,
|
||||
@BiometricManager.BiometricMultiSensorMode int multiSensorConfig,
|
||||
WakefulnessLifecycle wakefulnessLifecycle) {
|
||||
WakefulnessLifecycle wakefulnessLifecycle, UserManager userManager,
|
||||
LockPatternUtils lockPatternUtils) {
|
||||
|
||||
mLastBiometricPromptInfo = promptInfo;
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
package com.android.systemui.biometrics
|
||||
|
||||
import android.animation.Animator
|
||||
import android.graphics.Insets
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityTaskManager
|
||||
import android.content.ComponentName
|
||||
import android.graphics.Insets
|
||||
import android.graphics.Rect
|
||||
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
|
||||
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
|
||||
@@ -65,8 +65,8 @@ import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.anyFloat
|
||||
import org.mockito.Mockito.anyLong
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.anyLong
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
|
||||
@@ -57,9 +57,9 @@ import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
|
||||
@@ -38,11 +38,11 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.nullable
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.Mockito.nullable
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
private const val DISPLAY_ID = "" // default display id
|
||||
private const val SENSOR_X = 50
|
||||
|
||||
@@ -448,9 +448,10 @@ public class CommandQueueTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void testOnBiometricAuthenticated() {
|
||||
mCommandQueue.onBiometricAuthenticated();
|
||||
final int id = 12;
|
||||
mCommandQueue.onBiometricAuthenticated(id);
|
||||
waitForIdleSync();
|
||||
verify(mCallbacks).onBiometricAuthenticated();
|
||||
verify(mCallbacks).onBiometricAuthenticated(eq(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -20,12 +20,8 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
|
||||
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FACE_SCANNING;
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FP_SCANNING;
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_SWITCHING;
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_UNKNOWN;
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE;
|
||||
@@ -100,14 +96,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface SessionState {}
|
||||
|
||||
/** Defined in biometrics.proto */
|
||||
@IntDef({
|
||||
MULTI_SENSOR_STATE_UNKNOWN,
|
||||
MULTI_SENSOR_STATE_FACE_SCANNING,
|
||||
MULTI_SENSOR_STATE_FP_SCANNING})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface MultiSensorState {}
|
||||
|
||||
/**
|
||||
* Notify the holder of the AuthSession that the caller/client's binder has died. The
|
||||
* holder (BiometricService) should schedule {@link AuthSession#onClientDied()} to be run
|
||||
@@ -119,7 +107,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
|
||||
private final Context mContext;
|
||||
private final IStatusBarService mStatusBarService;
|
||||
private final IBiometricSysuiReceiver mSysuiReceiver;
|
||||
@VisibleForTesting final IBiometricSysuiReceiver mSysuiReceiver;
|
||||
private final KeyStore mKeyStore;
|
||||
private final Random mRandom;
|
||||
private final ClientDeathReceiver mClientDeathReceiver;
|
||||
@@ -133,7 +121,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
private final long mRequestId;
|
||||
private final long mOperationId;
|
||||
private final int mUserId;
|
||||
private final IBiometricSensorReceiver mSensorReceiver;
|
||||
@VisibleForTesting final IBiometricSensorReceiver mSensorReceiver;
|
||||
// Original receiver from BiometricPrompt.
|
||||
private final IBiometricServiceReceiver mClientReceiver;
|
||||
private final String mOpPackageName;
|
||||
@@ -143,10 +131,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
// The current state, which can be either idle, called, or started
|
||||
private @SessionState int mState = STATE_AUTH_IDLE;
|
||||
private @BiometricMultiSensorMode int mMultiSensorMode;
|
||||
private @MultiSensorState int mMultiSensorState;
|
||||
private int[] mSensors;
|
||||
// TODO(b/197265902): merge into state
|
||||
private boolean mCancelled;
|
||||
private int mAuthenticatedSensorId = -1;
|
||||
// For explicit confirmation, do not send to keystore until the user has confirmed
|
||||
// the authentication.
|
||||
private byte[] mTokenEscrow;
|
||||
@@ -232,8 +220,16 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
}
|
||||
|
||||
private void setSensorsToStateWaitingForCookie() throws RemoteException {
|
||||
private void setSensorsToStateWaitingForCookie(boolean isTryAgain) throws RemoteException {
|
||||
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
|
||||
@BiometricSensor.SensorState final int state = sensor.getSensorState();
|
||||
if (isTryAgain
|
||||
&& state != BiometricSensor.STATE_STOPPED
|
||||
&& state != BiometricSensor.STATE_CANCELING) {
|
||||
Slog.d(TAG, "Skip retry because sensor: " + sensor.id + " is: " + state);
|
||||
continue;
|
||||
}
|
||||
|
||||
final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
|
||||
final boolean requireConfirmation = isConfirmationRequired(sensor);
|
||||
|
||||
@@ -254,7 +250,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
|
||||
mSensors = new int[0];
|
||||
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
|
||||
|
||||
mStatusBarService.showAuthenticationDialog(
|
||||
mPromptInfo,
|
||||
@@ -269,7 +264,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
mMultiSensorMode);
|
||||
} else if (!mPreAuthInfo.eligibleSensors.isEmpty()) {
|
||||
// Some combination of biometric or biometric|credential is requested
|
||||
setSensorsToStateWaitingForCookie();
|
||||
setSensorsToStateWaitingForCookie(false /* isTryAgain */);
|
||||
mState = STATE_AUTH_CALLED;
|
||||
} else {
|
||||
// No authenticators requested. This should never happen - an exception should have
|
||||
@@ -283,6 +278,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
Slog.w(TAG, "Received cookie but already cancelled (ignoring): " + cookie);
|
||||
return;
|
||||
}
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onCookieReceived after successful auth");
|
||||
return;
|
||||
}
|
||||
|
||||
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
|
||||
sensor.goToStateCookieReturnedIfCookieMatches(cookie);
|
||||
@@ -307,7 +306,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
mMultiSensorMode = getMultiSensorModeForNewSession(
|
||||
mPreAuthInfo.eligibleSensors);
|
||||
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
|
||||
|
||||
mStatusBarService.showAuthenticationDialog(mPromptInfo,
|
||||
mSysuiReceiver,
|
||||
@@ -381,9 +379,8 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
// sending the final error callback to the application.
|
||||
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
|
||||
try {
|
||||
final boolean shouldCancel = filter.apply(sensor);
|
||||
Slog.d(TAG, "sensorId: " + sensor.id + ", shouldCancel: " + shouldCancel);
|
||||
if (shouldCancel) {
|
||||
if (filter.apply(sensor)) {
|
||||
Slog.d(TAG, "Cancelling sensorId: " + sensor.id);
|
||||
sensor.goToStateCancelling(mToken, mOpPackageName, mRequestId);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
@@ -412,10 +409,16 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
}
|
||||
|
||||
// do not propagate the error and let onAuthenticationSucceeded handle the new state
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onErrorReceived after successful auth (ignoring)");
|
||||
return false;
|
||||
}
|
||||
|
||||
mErrorEscrow = error;
|
||||
mVendorCodeEscrow = vendorCode;
|
||||
|
||||
final @BiometricAuthenticator.Modality int modality = sensorIdToModality(sensorId);
|
||||
@Modality final int modality = sensorIdToModality(sensorId);
|
||||
|
||||
switch (mState) {
|
||||
case STATE_AUTH_CALLED: {
|
||||
@@ -430,7 +433,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
|
||||
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
|
||||
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
|
||||
mSensors = new int[0];
|
||||
|
||||
mStatusBarService.showAuthenticationDialog(
|
||||
@@ -468,12 +470,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
return true;
|
||||
} else {
|
||||
mState = STATE_ERROR_PENDING_SYSUI;
|
||||
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT
|
||||
&& mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING) {
|
||||
// wait for the UI to signal when modality should switch
|
||||
Slog.d(TAG, "onErrorReceived: waiting for modality switch callback");
|
||||
mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING;
|
||||
}
|
||||
mStatusBarService.onBiometricError(modality, error, vendorCode);
|
||||
}
|
||||
break;
|
||||
@@ -505,6 +501,11 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
|
||||
void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onAcquired after successful auth");
|
||||
return;
|
||||
}
|
||||
|
||||
final String message = getAcquiredMessageForSensor(sensorId, acquiredInfo, vendorCode);
|
||||
Slog.d(TAG, "sensorId: " + sensorId + " acquiredInfo: " + acquiredInfo
|
||||
+ " message: " + message);
|
||||
@@ -520,6 +521,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
|
||||
void onSystemEvent(int event) {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onSystemEvent after successful auth");
|
||||
return;
|
||||
}
|
||||
if (!mPromptInfo.isReceiveSystemEvents()) {
|
||||
return;
|
||||
}
|
||||
@@ -538,53 +543,35 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
|
||||
mState = STATE_AUTH_STARTED_UI_SHOWING;
|
||||
|
||||
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
|
||||
mMultiSensorState = MULTI_SENSOR_STATE_FACE_SCANNING;
|
||||
} else {
|
||||
startFingerprintSensorsNow();
|
||||
}
|
||||
}
|
||||
|
||||
// call anytime after onDialogAnimatedIn() to indicate it's appropriate to start the
|
||||
// fingerprint sensor (i.e. face auth has failed or is not available)
|
||||
void onStartFingerprint() {
|
||||
if (mMultiSensorMode != BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
|
||||
Slog.e(TAG, "onStartFingerprint, unexpected mode: " + mMultiSensorMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mState != STATE_AUTH_STARTED
|
||||
&& mState != STATE_AUTH_STARTED_UI_SHOWING
|
||||
&& mState != STATE_AUTH_PAUSED
|
||||
&& mState != STATE_ERROR_PENDING_SYSUI) {
|
||||
Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
|
||||
}
|
||||
|
||||
mMultiSensorState = MULTI_SENSOR_STATE_FP_SCANNING;
|
||||
startFingerprintSensorsNow();
|
||||
}
|
||||
|
||||
// unguarded helper for the above methods only
|
||||
private void startFingerprintSensorsNow() {
|
||||
startAllPreparedFingerprintSensors();
|
||||
mState = STATE_AUTH_STARTED_UI_SHOWING;
|
||||
}
|
||||
|
||||
void onTryAgainPressed() {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onTryAgainPressed after successful auth");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mState != STATE_AUTH_PAUSED) {
|
||||
Slog.w(TAG, "onTryAgainPressed, state: " + mState);
|
||||
}
|
||||
|
||||
try {
|
||||
setSensorsToStateWaitingForCookie();
|
||||
setSensorsToStateWaitingForCookie(true /* isTryAgain */);
|
||||
mState = STATE_AUTH_PAUSED_RESUMING;
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "RemoteException: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
void onAuthenticationSucceeded(int sensorId, boolean strong,
|
||||
byte[] token) {
|
||||
void onAuthenticationSucceeded(int sensorId, boolean strong, byte[] token) {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onAuthenticationSucceeded after successful auth");
|
||||
return;
|
||||
}
|
||||
|
||||
mAuthenticatedSensorId = sensorId;
|
||||
if (strong) {
|
||||
mTokenEscrow = token;
|
||||
} else {
|
||||
@@ -596,7 +583,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
try {
|
||||
// Notify SysUI that the biometric has been authenticated. SysUI already knows
|
||||
// the implicit/explicit state and will react accordingly.
|
||||
mStatusBarService.onBiometricAuthenticated();
|
||||
mStatusBarService.onBiometricAuthenticated(sensorIdToModality(sensorId));
|
||||
|
||||
final boolean requireConfirmation = isConfirmationRequiredByAnyEligibleSensor();
|
||||
|
||||
@@ -609,20 +596,22 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "RemoteException", e);
|
||||
}
|
||||
|
||||
cancelAllSensors(sensor -> sensor.id != sensorId);
|
||||
}
|
||||
|
||||
void onAuthenticationRejected() {
|
||||
try {
|
||||
mStatusBarService.onBiometricError(TYPE_NONE,
|
||||
BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */);
|
||||
void onAuthenticationRejected(int sensorId) {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onAuthenticationRejected after successful auth");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This logic will need to be updated if BP is multi-modal
|
||||
if (hasPausableBiometric()) {
|
||||
// Pause authentication. onBiometricAuthenticated(false) causes the
|
||||
// dialog to show a "try again" button for passive modalities.
|
||||
try {
|
||||
mStatusBarService.onBiometricError(sensorIdToModality(sensorId),
|
||||
BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */);
|
||||
if (pauseSensorIfSupported(sensorId)) {
|
||||
mState = STATE_AUTH_PAUSED;
|
||||
}
|
||||
|
||||
mClientReceiver.onAuthenticationFailed();
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "RemoteException", e);
|
||||
@@ -630,15 +619,34 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
|
||||
void onAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onAuthenticationTimedOut after successful auth");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mStatusBarService.onBiometricError(sensorIdToModality(sensorId), error, vendorCode);
|
||||
pauseSensorIfSupported(sensorId);
|
||||
mState = STATE_AUTH_PAUSED;
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "RemoteException", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean pauseSensorIfSupported(int sensorId) {
|
||||
if (sensorIdToModality(sensorId) == TYPE_FACE) {
|
||||
cancelAllSensors(sensor -> sensor.id == sensorId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void onDeviceCredentialPressed() {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onDeviceCredentialPressed after successful auth");
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel authentication. Skip the token/package check since we are cancelling
|
||||
// from system server. The interface is permission protected so this is fine.
|
||||
cancelAllSensors();
|
||||
@@ -666,6 +674,10 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasAuthenticated() {
|
||||
return mAuthenticatedSensorId != -1;
|
||||
}
|
||||
|
||||
private void logOnDialogDismissed(@BiometricPrompt.DismissedReason int reason) {
|
||||
if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) {
|
||||
// Explicit auth, authentication confirmed.
|
||||
@@ -794,6 +806,11 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
* @return true if this AuthSession is finished, e.g. should be set to null
|
||||
*/
|
||||
boolean onCancelAuthSession(boolean force) {
|
||||
if (hasAuthenticated()) {
|
||||
Slog.d(TAG, "onCancelAuthSession after successful auth");
|
||||
return true;
|
||||
}
|
||||
|
||||
mCancelled = true;
|
||||
|
||||
final boolean authStarted = mState == STATE_AUTH_CALLED
|
||||
@@ -848,15 +865,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
return remainingCookies == 0;
|
||||
}
|
||||
|
||||
private boolean hasPausableBiometric() {
|
||||
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
|
||||
if (sensor.modality == TYPE_FACE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SessionState int getState() {
|
||||
return mState;
|
||||
}
|
||||
@@ -919,7 +927,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
|
||||
}
|
||||
|
||||
if (hasFace && hasFingerprint) {
|
||||
return BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
|
||||
return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
|
||||
}
|
||||
return BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
}
|
||||
|
||||
@@ -131,8 +131,10 @@ public abstract class BiometricSensor {
|
||||
|
||||
void goToStateCancelling(IBinder token, String opPackageName, long requestId)
|
||||
throws RemoteException {
|
||||
impl.cancelAuthenticationFromService(token, opPackageName, requestId);
|
||||
mSensorState = STATE_CANCELING;
|
||||
if (mSensorState != STATE_CANCELING) {
|
||||
impl.cancelAuthenticationFromService(token, opPackageName, requestId);
|
||||
mSensorState = STATE_CANCELING;
|
||||
}
|
||||
}
|
||||
|
||||
void goToStoppedStateIfCookieMatches(int cookie, int error) {
|
||||
|
||||
@@ -55,7 +55,6 @@ import android.os.DeadObjectException;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
@@ -84,6 +83,7 @@ import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* System service that arbitrates the modality for BiometricPrompt to use.
|
||||
@@ -92,22 +92,6 @@ public class BiometricService extends SystemService {
|
||||
|
||||
static final String TAG = "BiometricService";
|
||||
|
||||
private static final int MSG_ON_AUTHENTICATION_SUCCEEDED = 2;
|
||||
private static final int MSG_ON_AUTHENTICATION_REJECTED = 3;
|
||||
private static final int MSG_ON_ERROR = 4;
|
||||
private static final int MSG_ON_ACQUIRED = 5;
|
||||
private static final int MSG_ON_DISMISSED = 6;
|
||||
private static final int MSG_ON_TRY_AGAIN_PRESSED = 7;
|
||||
private static final int MSG_ON_READY_FOR_AUTHENTICATION = 8;
|
||||
private static final int MSG_AUTHENTICATE = 9;
|
||||
private static final int MSG_CANCEL_AUTHENTICATION = 10;
|
||||
private static final int MSG_ON_AUTHENTICATION_TIMED_OUT = 11;
|
||||
private static final int MSG_ON_DEVICE_CREDENTIAL_PRESSED = 12;
|
||||
private static final int MSG_ON_SYSTEM_EVENT = 13;
|
||||
private static final int MSG_CLIENT_DIED = 14;
|
||||
private static final int MSG_ON_DIALOG_ANIMATED_IN = 15;
|
||||
private static final int MSG_ON_START_FINGERPRINT_NOW = 16;
|
||||
|
||||
private final Injector mInjector;
|
||||
private final DevicePolicyManager mDevicePolicyManager;
|
||||
@VisibleForTesting
|
||||
@@ -116,7 +100,7 @@ public class BiometricService extends SystemService {
|
||||
final SettingObserver mSettingObserver;
|
||||
private final List<EnabledOnKeyguardCallback> mEnabledOnKeyguardCallbacks;
|
||||
private final Random mRandom = new Random();
|
||||
@NonNull private final AtomicLong mRequestCounter;
|
||||
@NonNull private final Supplier<Long> mRequestCounter;
|
||||
|
||||
@VisibleForTesting
|
||||
IStatusBarService mStatusBarService;
|
||||
@@ -128,133 +112,13 @@ public class BiometricService extends SystemService {
|
||||
// Get and cache the available biometric authenticators and their associated info.
|
||||
final ArrayList<BiometricSensor> mSensors = new ArrayList<>();
|
||||
|
||||
@VisibleForTesting
|
||||
BiometricStrengthController mBiometricStrengthController;
|
||||
|
||||
// The current authentication session, null if idle/done.
|
||||
@VisibleForTesting
|
||||
AuthSession mCurrentAuthSession;
|
||||
|
||||
@VisibleForTesting
|
||||
final Handler mHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_ON_AUTHENTICATION_SUCCEEDED: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
handleAuthenticationSucceeded(
|
||||
args.argi1 /* sensorId */,
|
||||
(byte[]) args.arg1 /* token */);
|
||||
args.recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_AUTHENTICATION_REJECTED: {
|
||||
handleAuthenticationRejected();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_ERROR: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
handleOnError(
|
||||
args.argi1 /* sensorId */,
|
||||
args.argi2 /* cookie */,
|
||||
args.argi3 /* error */,
|
||||
args.argi4 /* vendorCode */);
|
||||
args.recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_ACQUIRED: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
handleOnAcquired(
|
||||
args.argi1 /* sensorId */,
|
||||
args.argi2 /* acquiredInfo */,
|
||||
args.argi3 /* vendorCode */);
|
||||
args.recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_DISMISSED: {
|
||||
handleOnDismissed(msg.arg1, (byte[]) msg.obj);
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_TRY_AGAIN_PRESSED: {
|
||||
handleOnTryAgainPressed();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_READY_FOR_AUTHENTICATION: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
handleOnReadyForAuthentication(
|
||||
args.argi1 /* cookie */);
|
||||
args.recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_AUTHENTICATE: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
handleAuthenticate(
|
||||
(IBinder) args.arg1 /* token */,
|
||||
(long) args.arg6 /* requestId */,
|
||||
(long) args.arg2 /* operationId */,
|
||||
args.argi1 /* userid */,
|
||||
(IBiometricServiceReceiver) args.arg3 /* receiver */,
|
||||
(String) args.arg4 /* opPackageName */,
|
||||
(PromptInfo) args.arg5 /* promptInfo */);
|
||||
args.recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_CANCEL_AUTHENTICATION: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
handleCancelAuthentication((long) args.arg3 /* requestId */);
|
||||
args.recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_AUTHENTICATION_TIMED_OUT: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
handleAuthenticationTimedOut(
|
||||
args.argi1 /* sensorId */,
|
||||
args.argi2 /* cookie */,
|
||||
args.argi3 /* error */,
|
||||
args.argi4 /* vendorCode */);
|
||||
args.recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_DEVICE_CREDENTIAL_PRESSED: {
|
||||
handleOnDeviceCredentialPressed();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_SYSTEM_EVENT: {
|
||||
handleOnSystemEvent((int) msg.obj);
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_CLIENT_DIED: {
|
||||
handleClientDied();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_DIALOG_ANIMATED_IN: {
|
||||
handleOnDialogAnimatedIn();
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ON_START_FINGERPRINT_NOW: {
|
||||
handleOnStartFingerprintNow();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Slog.e(TAG, "Unknown message: " + msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
AuthSession mAuthSession;
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
/**
|
||||
* Tracks authenticatorId invalidation. For more details, see
|
||||
@@ -552,93 +416,74 @@ public class BiometricService extends SystemService {
|
||||
}
|
||||
|
||||
// Receives events from individual biometric sensors.
|
||||
@VisibleForTesting
|
||||
final IBiometricSensorReceiver mBiometricSensorReceiver = new IBiometricSensorReceiver.Stub() {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(int sensorId, byte[] token) {
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = sensorId;
|
||||
args.arg1 = token;
|
||||
mHandler.obtainMessage(MSG_ON_AUTHENTICATION_SUCCEEDED, args).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed(int sensorId) {
|
||||
Slog.v(TAG, "onAuthenticationFailed");
|
||||
mHandler.obtainMessage(MSG_ON_AUTHENTICATION_REJECTED).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int sensorId, int cookie, @BiometricConstants.Errors int error,
|
||||
int vendorCode) {
|
||||
// Determine if error is hard or soft error. Certain errors (such as TIMEOUT) are
|
||||
// soft errors and we should allow the user to try authenticating again instead of
|
||||
// dismissing BiometricPrompt.
|
||||
if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) {
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = sensorId;
|
||||
args.argi2 = cookie;
|
||||
args.argi3 = error;
|
||||
args.argi4 = vendorCode;
|
||||
mHandler.obtainMessage(MSG_ON_AUTHENTICATION_TIMED_OUT, args).sendToTarget();
|
||||
} else {
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = sensorId;
|
||||
args.argi2 = cookie;
|
||||
args.argi3 = error;
|
||||
args.argi4 = vendorCode;
|
||||
mHandler.obtainMessage(MSG_ON_ERROR, args).sendToTarget();
|
||||
private IBiometricSensorReceiver createBiometricSensorReceiver(final long requestId) {
|
||||
return new IBiometricSensorReceiver.Stub() {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(int sensorId, byte[] token) {
|
||||
mHandler.post(() -> handleAuthenticationSucceeded(requestId, sensorId, token));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = sensorId;
|
||||
args.argi2 = acquiredInfo;
|
||||
args.argi3 = vendorCode;
|
||||
mHandler.obtainMessage(MSG_ON_ACQUIRED, args).sendToTarget();
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onAuthenticationFailed(int sensorId) {
|
||||
Slog.v(TAG, "onAuthenticationFailed");
|
||||
mHandler.post(() -> handleAuthenticationRejected(requestId, sensorId));
|
||||
}
|
||||
|
||||
final IBiometricSysuiReceiver mSysuiReceiver = new IBiometricSysuiReceiver.Stub() {
|
||||
@Override
|
||||
public void onDialogDismissed(@BiometricPrompt.DismissedReason int reason,
|
||||
@Nullable byte[] credentialAttestation) {
|
||||
mHandler.obtainMessage(MSG_ON_DISMISSED,
|
||||
reason,
|
||||
0 /* arg2 */,
|
||||
credentialAttestation /* obj */).sendToTarget();
|
||||
}
|
||||
@Override
|
||||
public void onError(int sensorId, int cookie, @BiometricConstants.Errors int error,
|
||||
int vendorCode) {
|
||||
// Determine if error is hard or soft error. Certain errors (such as TIMEOUT) are
|
||||
// soft errors and we should allow the user to try authenticating again instead of
|
||||
// dismissing BiometricPrompt.
|
||||
if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) {
|
||||
mHandler.post(() -> handleAuthenticationTimedOut(
|
||||
requestId, sensorId, cookie, error, vendorCode));
|
||||
} else {
|
||||
mHandler.post(() -> handleOnError(
|
||||
requestId, sensorId, cookie, error, vendorCode));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTryAgainPressed() {
|
||||
mHandler.sendEmptyMessage(MSG_ON_TRY_AGAIN_PRESSED);
|
||||
}
|
||||
@Override
|
||||
public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
|
||||
mHandler.post(() -> handleOnAcquired(
|
||||
requestId, sensorId, acquiredInfo, vendorCode));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceCredentialPressed() {
|
||||
mHandler.sendEmptyMessage(MSG_ON_DEVICE_CREDENTIAL_PRESSED);
|
||||
}
|
||||
private IBiometricSysuiReceiver createSysuiReceiver(final long requestId) {
|
||||
return new IBiometricSysuiReceiver.Stub() {
|
||||
@Override
|
||||
public void onDialogDismissed(@BiometricPrompt.DismissedReason int reason,
|
||||
@Nullable byte[] credentialAttestation) {
|
||||
mHandler.post(() -> handleOnDismissed(requestId, reason, credentialAttestation));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemEvent(int event) {
|
||||
mHandler.obtainMessage(MSG_ON_SYSTEM_EVENT, event).sendToTarget();
|
||||
}
|
||||
@Override
|
||||
public void onTryAgainPressed() {
|
||||
mHandler.post(() -> handleOnTryAgainPressed(requestId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogAnimatedIn() {
|
||||
mHandler.obtainMessage(MSG_ON_DIALOG_ANIMATED_IN).sendToTarget();
|
||||
}
|
||||
@Override
|
||||
public void onDeviceCredentialPressed() {
|
||||
mHandler.post(() -> handleOnDeviceCredentialPressed(requestId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartFingerprintNow() {
|
||||
mHandler.obtainMessage(MSG_ON_START_FINGERPRINT_NOW).sendToTarget();
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onSystemEvent(int event) {
|
||||
mHandler.post(() -> handleOnSystemEvent(requestId, event));
|
||||
}
|
||||
|
||||
private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> {
|
||||
mHandler.sendEmptyMessage(MSG_CLIENT_DIED);
|
||||
@Override
|
||||
public void onDialogAnimatedIn() {
|
||||
mHandler.post(() -> handleOnDialogAnimatedIn(requestId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private AuthSession.ClientDeathReceiver createClientDeathReceiver(final long requestId) {
|
||||
return () -> mHandler.post(() -> handleClientDied(requestId));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -679,12 +524,10 @@ public class BiometricService extends SystemService {
|
||||
}
|
||||
|
||||
@Override // Binder call
|
||||
public void onReadyForAuthentication(int cookie) {
|
||||
public void onReadyForAuthentication(long requestId, int cookie) {
|
||||
checkInternalPermission();
|
||||
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = cookie;
|
||||
mHandler.obtainMessage(MSG_ON_READY_FOR_AUTHENTICATION, args).sendToTarget();
|
||||
mHandler.post(() -> handleOnReadyForAuthentication(requestId, cookie));
|
||||
}
|
||||
|
||||
@Override // Binder call
|
||||
@@ -711,18 +554,9 @@ public class BiometricService extends SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
final long requestId = mRequestCounter.incrementAndGet();
|
||||
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.arg1 = token;
|
||||
args.arg2 = operationId;
|
||||
args.argi1 = userId;
|
||||
args.arg3 = receiver;
|
||||
args.arg4 = opPackageName;
|
||||
args.arg5 = promptInfo;
|
||||
args.arg6 = requestId;
|
||||
|
||||
mHandler.obtainMessage(MSG_AUTHENTICATE, args).sendToTarget();
|
||||
final long requestId = mRequestCounter.get();
|
||||
mHandler.post(() -> handleAuthenticate(
|
||||
token, requestId, operationId, userId, receiver, opPackageName, promptInfo));
|
||||
|
||||
return requestId;
|
||||
}
|
||||
@@ -736,7 +570,7 @@ public class BiometricService extends SystemService {
|
||||
args.arg2 = opPackageName;
|
||||
args.arg3 = requestId;
|
||||
|
||||
mHandler.obtainMessage(MSG_CANCEL_AUTHENTICATION, args).sendToTarget();
|
||||
mHandler.post(() -> handleCancelAuthentication(requestId));
|
||||
}
|
||||
|
||||
@Override // Binder call
|
||||
@@ -1002,8 +836,7 @@ public class BiometricService extends SystemService {
|
||||
Slog.d(TAG, "ClearSchedulerBuffer: " + clearSchedulerBuffer);
|
||||
final ProtoOutputStream proto = new ProtoOutputStream(fd);
|
||||
proto.write(BiometricServiceStateProto.AUTH_SESSION_STATE,
|
||||
mCurrentAuthSession != null ? mCurrentAuthSession.getState()
|
||||
: STATE_AUTH_IDLE);
|
||||
mAuthSession != null ? mAuthSession.getState() : STATE_AUTH_IDLE);
|
||||
for (BiometricSensor sensor : mSensors) {
|
||||
byte[] serviceState = sensor.impl
|
||||
.dumpSensorServiceStateProto(clearSchedulerBuffer);
|
||||
@@ -1128,8 +961,9 @@ public class BiometricService extends SystemService {
|
||||
CoexCoordinator.FACE_HAPTIC_DISABLE, 1) != 0;
|
||||
}
|
||||
|
||||
public AtomicLong getRequestGenerator() {
|
||||
return new AtomicLong(0);
|
||||
public Supplier<Long> getRequestGenerator() {
|
||||
final AtomicLong generator = new AtomicLong(0);
|
||||
return () -> generator.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1202,172 +1036,184 @@ public class BiometricService extends SystemService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleAuthenticationSucceeded(int sensorId, byte[] token) {
|
||||
@Nullable
|
||||
private AuthSession getAuthSessionIfCurrent(long requestId) {
|
||||
final AuthSession session = mAuthSession;
|
||||
if (session != null && session.getRequestId() == requestId) {
|
||||
return session;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleAuthenticationSucceeded(long requestId, int sensorId, byte[] token) {
|
||||
Slog.v(TAG, "handleAuthenticationSucceeded(), sensorId: " + sensorId);
|
||||
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
|
||||
// after user dismissed/canceled dialog).
|
||||
if (mCurrentAuthSession == null) {
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.e(TAG, "handleAuthenticationSucceeded: AuthSession is null");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onAuthenticationSucceeded(sensorId, isStrongBiometric(sensorId), token);
|
||||
session.onAuthenticationSucceeded(sensorId, isStrongBiometric(sensorId), token);
|
||||
}
|
||||
|
||||
private void handleAuthenticationRejected() {
|
||||
private void handleAuthenticationRejected(long requestId, int sensorId) {
|
||||
Slog.v(TAG, "handleAuthenticationRejected()");
|
||||
|
||||
// Should never happen, log this to catch bad HAL behavior (e.g. auth rejected
|
||||
// after user dismissed/canceled dialog).
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleAuthenticationRejected: AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleAuthenticationRejected: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onAuthenticationRejected();
|
||||
session.onAuthenticationRejected(sensorId);
|
||||
}
|
||||
|
||||
private void handleAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) {
|
||||
private void handleAuthenticationTimedOut(long requestId, int sensorId, int cookie, int error,
|
||||
int vendorCode) {
|
||||
Slog.v(TAG, "handleAuthenticationTimedOut(), sensorId: " + sensorId
|
||||
+ ", cookie: " + cookie
|
||||
+ ", error: " + error
|
||||
+ ", vendorCode: " + vendorCode);
|
||||
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
|
||||
// after user dismissed/canceled dialog).
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleAuthenticationTimedOut: AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleAuthenticationTimedOut: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onAuthenticationTimedOut(sensorId, cookie, error, vendorCode);
|
||||
session.onAuthenticationTimedOut(sensorId, cookie, error, vendorCode);
|
||||
}
|
||||
|
||||
private void handleOnError(int sensorId, int cookie, @BiometricConstants.Errors int error,
|
||||
int vendorCode) {
|
||||
private void handleOnError(long requestId, int sensorId, int cookie,
|
||||
@BiometricConstants.Errors int error, int vendorCode) {
|
||||
Slog.d(TAG, "handleOnError() sensorId: " + sensorId
|
||||
+ ", cookie: " + cookie
|
||||
+ ", error: " + error
|
||||
+ ", vendorCode: " + vendorCode);
|
||||
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleOnError: AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleOnError: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final boolean finished = mCurrentAuthSession
|
||||
.onErrorReceived(sensorId, cookie, error, vendorCode);
|
||||
final boolean finished = session.onErrorReceived(sensorId, cookie, error, vendorCode);
|
||||
if (finished) {
|
||||
Slog.d(TAG, "handleOnError: AuthSession finished");
|
||||
mCurrentAuthSession = null;
|
||||
mAuthSession = null;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "RemoteException", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleOnAcquired(int sensorId, int acquiredInfo, int vendorCode) {
|
||||
private void handleOnAcquired(long requestId, int sensorId, int acquiredInfo, int vendorCode) {
|
||||
// Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded
|
||||
// after user dismissed/canceled dialog).
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "onAcquired: AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "onAcquired: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onAcquired(sensorId, acquiredInfo, vendorCode);
|
||||
session.onAcquired(sensorId, acquiredInfo, vendorCode);
|
||||
}
|
||||
|
||||
private void handleOnDismissed(@BiometricPrompt.DismissedReason int reason,
|
||||
private void handleOnDismissed(long requestId, @BiometricPrompt.DismissedReason int reason,
|
||||
@Nullable byte[] credentialAttestation) {
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "onDismissed: " + reason + ", AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.e(TAG, "onDismissed: " + reason + ", AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onDialogDismissed(reason, credentialAttestation);
|
||||
mCurrentAuthSession = null;
|
||||
session.onDialogDismissed(reason, credentialAttestation);
|
||||
mAuthSession = null;
|
||||
}
|
||||
|
||||
private void handleOnTryAgainPressed() {
|
||||
private void handleOnTryAgainPressed(long requestId) {
|
||||
Slog.d(TAG, "onTryAgainPressed");
|
||||
// No need to check permission, since it can only be invoked by SystemUI
|
||||
// (or system server itself).
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleOnTryAgainPressed: AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleOnTryAgainPressed: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onTryAgainPressed();
|
||||
session.onTryAgainPressed();
|
||||
}
|
||||
|
||||
private void handleOnDeviceCredentialPressed() {
|
||||
private void handleOnDeviceCredentialPressed(long requestId) {
|
||||
Slog.d(TAG, "onDeviceCredentialPressed");
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleOnDeviceCredentialPressed: AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleOnDeviceCredentialPressed: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onDeviceCredentialPressed();
|
||||
session.onDeviceCredentialPressed();
|
||||
}
|
||||
|
||||
private void handleOnSystemEvent(int event) {
|
||||
private void handleOnSystemEvent(long requestId, int event) {
|
||||
Slog.d(TAG, "onSystemEvent: " + event);
|
||||
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleOnSystemEvent: AuthSession is null");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleOnSystemEvent: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onSystemEvent(event);
|
||||
session.onSystemEvent(event);
|
||||
}
|
||||
|
||||
private void handleClientDied() {
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleClientDied: AuthSession is null");
|
||||
private void handleClientDied(long requestId) {
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleClientDied: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
Slog.e(TAG, "Session: " + mCurrentAuthSession);
|
||||
final boolean finished = mCurrentAuthSession.onClientDied();
|
||||
Slog.e(TAG, "Session: " + session);
|
||||
final boolean finished = session.onClientDied();
|
||||
if (finished) {
|
||||
mCurrentAuthSession = null;
|
||||
mAuthSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleOnDialogAnimatedIn() {
|
||||
private void handleOnDialogAnimatedIn(long requestId) {
|
||||
Slog.d(TAG, "handleOnDialogAnimatedIn");
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleOnDialogAnimatedIn: AuthSession is null");
|
||||
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleOnDialogAnimatedIn: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onDialogAnimatedIn();
|
||||
}
|
||||
|
||||
private void handleOnStartFingerprintNow() {
|
||||
Slog.d(TAG, "handleOnStartFingerprintNow");
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleOnStartFingerprintNow: AuthSession is null");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onStartFingerprint();
|
||||
session.onDialogAnimatedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when each service has notified that its client is ready to be started. When
|
||||
* all biometrics are ready, this invokes the SystemUI dialog through StatusBar.
|
||||
*/
|
||||
private void handleOnReadyForAuthentication(int cookie) {
|
||||
if (mCurrentAuthSession == null) {
|
||||
private void handleOnReadyForAuthentication(long requestId, int cookie) {
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
// Only should happen if a biometric was locked out when authenticate() was invoked.
|
||||
// In that case, if device credentials are allowed, the UI is already showing. If not
|
||||
// allowed, the error has already been returned to the caller.
|
||||
Slog.w(TAG, "handleOnReadyForAuthentication: AuthSession is null");
|
||||
Slog.w(TAG, "handleOnReadyForAuthentication: AuthSession is not current");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentAuthSession.onCookieReceived(cookie);
|
||||
session.onCookieReceived(cookie);
|
||||
}
|
||||
|
||||
private void handleAuthenticate(IBinder token, long requestId, long operationId, int userId,
|
||||
@@ -1428,47 +1274,41 @@ public class BiometricService extends SystemService {
|
||||
|
||||
// No need to dismiss dialog / send error yet if we're continuing authentication, e.g.
|
||||
// "Try again" is showing due to something like ERROR_TIMEOUT.
|
||||
if (mCurrentAuthSession != null) {
|
||||
if (mAuthSession != null) {
|
||||
// Forcefully cancel authentication. Dismiss the UI, and immediately send
|
||||
// ERROR_CANCELED to the client. Note that we should/will ignore HAL ERROR_CANCELED.
|
||||
// Expect to see some harmless "unknown cookie" errors.
|
||||
Slog.w(TAG, "Existing AuthSession: " + mCurrentAuthSession);
|
||||
mCurrentAuthSession.onCancelAuthSession(true /* force */);
|
||||
mCurrentAuthSession = null;
|
||||
Slog.w(TAG, "Existing AuthSession: " + mAuthSession);
|
||||
mAuthSession.onCancelAuthSession(true /* force */);
|
||||
mAuthSession = null;
|
||||
}
|
||||
|
||||
final boolean debugEnabled = mInjector.isDebugEnabled(getContext(), userId);
|
||||
mCurrentAuthSession = new AuthSession(getContext(), mStatusBarService, mSysuiReceiver,
|
||||
mKeyStore, mRandom, mClientDeathReceiver, preAuthInfo, token, requestId,
|
||||
operationId, userId, mBiometricSensorReceiver, receiver, opPackageName, promptInfo,
|
||||
debugEnabled, mInjector.getFingerprintSensorProperties(getContext()));
|
||||
mAuthSession = new AuthSession(getContext(), mStatusBarService,
|
||||
createSysuiReceiver(requestId), mKeyStore, mRandom,
|
||||
createClientDeathReceiver(requestId), preAuthInfo, token, requestId,
|
||||
operationId, userId, createBiometricSensorReceiver(requestId), receiver,
|
||||
opPackageName, promptInfo, debugEnabled,
|
||||
mInjector.getFingerprintSensorProperties(getContext()));
|
||||
try {
|
||||
mCurrentAuthSession.goToInitialState();
|
||||
mAuthSession.goToInitialState();
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "RemoteException", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCancelAuthentication(long requestId) {
|
||||
if (mCurrentAuthSession == null) {
|
||||
Slog.e(TAG, "handleCancelAuthentication: AuthSession is null");
|
||||
return;
|
||||
}
|
||||
if (mCurrentAuthSession.getRequestId() != requestId) {
|
||||
// TODO: actually cancel the operation
|
||||
// This can happen if the operation has been queued, but is cancelled before
|
||||
// it reaches the head of the scheduler. Consider it a programming error for now
|
||||
// and ignore it.
|
||||
Slog.e(TAG, "handleCancelAuthentication: AuthSession mismatch current requestId: "
|
||||
+ mCurrentAuthSession.getRequestId() + " cancel for: " + requestId
|
||||
+ " (ignoring cancellation)");
|
||||
final AuthSession session = getAuthSessionIfCurrent(requestId);
|
||||
if (session == null) {
|
||||
Slog.w(TAG, "handleCancelAuthentication: AuthSession is not current");
|
||||
// TODO: actually cancel the operation?
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean finished = mCurrentAuthSession.onCancelAuthSession(false /* force */);
|
||||
final boolean finished = session.onCancelAuthSession(false /* force */);
|
||||
if (finished) {
|
||||
Slog.d(TAG, "handleCancelAuthentication: AuthSession finished");
|
||||
mCurrentAuthSession = null;
|
||||
mAuthSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1491,7 +1331,7 @@ public class BiometricService extends SystemService {
|
||||
pw.println(" " + sensor);
|
||||
}
|
||||
pw.println();
|
||||
pw.println("CurrentSession: " + mCurrentAuthSession);
|
||||
pw.println("CurrentSession: " + mAuthSession);
|
||||
pw.println();
|
||||
pw.println("CoexCoordinator: " + CoexCoordinator.getInstance().toString());
|
||||
pw.println();
|
||||
|
||||
@@ -86,6 +86,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
|
||||
private long mStartTimeMs;
|
||||
|
||||
private boolean mAuthAttempted;
|
||||
private boolean mAuthSuccess = false;
|
||||
|
||||
// TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update
|
||||
// the state. We should think of a way to improve this in the future.
|
||||
@@ -237,6 +238,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
|
||||
"Successful background authentication!");
|
||||
}
|
||||
|
||||
mAuthSuccess = true;
|
||||
markAlreadyDone();
|
||||
|
||||
if (mTaskStackListener != null) {
|
||||
@@ -502,6 +504,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
|
||||
return mAuthAttempted;
|
||||
}
|
||||
|
||||
/** If an auth attempt completed successfully. */
|
||||
public boolean wasAuthSuccessful() {
|
||||
return mAuthSuccess;
|
||||
}
|
||||
|
||||
protected int getShowOverlayReason() {
|
||||
if (isKeyguard()) {
|
||||
return BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
|
||||
|
||||
@@ -316,7 +316,8 @@ public class BiometricScheduler {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
mBiometricService.onReadyForAuthentication(cookie);
|
||||
mBiometricService.onReadyForAuthentication(
|
||||
mCurrentOperation.getClientMonitor().getRequestId(), cookie);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(getTag(), "Remote exception when contacting BiometricService", e);
|
||||
}
|
||||
|
||||
@@ -173,18 +173,13 @@ public class CoexCoordinator {
|
||||
}
|
||||
|
||||
// SensorType to AuthenticationClient map
|
||||
private final Map<Integer, AuthenticationClient<?>> mClientMap;
|
||||
@VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths;
|
||||
private final Map<Integer, AuthenticationClient<?>> mClientMap = new HashMap<>();
|
||||
@VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths = new LinkedList<>();
|
||||
private boolean mAdvancedLogicEnabled;
|
||||
private boolean mFaceHapticDisabledWhenNonBypass;
|
||||
private final Handler mHandler;
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private CoexCoordinator() {
|
||||
// Singleton
|
||||
mClientMap = new HashMap<>();
|
||||
mSuccessfulAuths = new LinkedList<>();
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
private CoexCoordinator() {}
|
||||
|
||||
public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType,
|
||||
@NonNull AuthenticationClient<?> client) {
|
||||
@@ -221,8 +216,14 @@ public class CoexCoordinator {
|
||||
public void onAuthenticationSucceeded(long currentTimeMillis,
|
||||
@NonNull AuthenticationClient<?> client,
|
||||
@NonNull Callback callback) {
|
||||
final boolean isUsingSingleModality = isSingleAuthOnly(client);
|
||||
|
||||
if (client.isBiometricPrompt()) {
|
||||
callback.sendHapticFeedback();
|
||||
if (!isUsingSingleModality && hasMultipleSuccessfulAuthentications()) {
|
||||
// only send feedback on the first one
|
||||
} else {
|
||||
callback.sendHapticFeedback();
|
||||
}
|
||||
// For BP, BiometricService will add the authToken to Keystore.
|
||||
callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
|
||||
callback.handleLifecycleAfterAuth();
|
||||
@@ -234,7 +235,7 @@ public class CoexCoordinator {
|
||||
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
|
||||
callback.handleLifecycleAfterAuth();
|
||||
} else if (mAdvancedLogicEnabled && client.isKeyguard()) {
|
||||
if (isSingleAuthOnly(client)) {
|
||||
if (isUsingSingleModality) {
|
||||
// Single sensor authentication
|
||||
callback.sendHapticFeedback();
|
||||
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
|
||||
@@ -295,10 +296,10 @@ public class CoexCoordinator {
|
||||
@NonNull AuthenticationClient<?> client,
|
||||
@LockoutTracker.LockoutMode int lockoutMode,
|
||||
@NonNull Callback callback) {
|
||||
final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
|
||||
final boolean isUsingSingleModality = isSingleAuthOnly(client);
|
||||
|
||||
if (keyguardAdvancedLogic) {
|
||||
if (isSingleAuthOnly(client)) {
|
||||
if (mAdvancedLogicEnabled && client.isKeyguard()) {
|
||||
if (isUsingSingleModality) {
|
||||
callback.sendHapticFeedback();
|
||||
callback.handleLifecycleAfterAuth();
|
||||
} else {
|
||||
@@ -319,8 +320,7 @@ public class CoexCoordinator {
|
||||
// also done now.
|
||||
callback.sendHapticFeedback();
|
||||
callback.handleLifecycleAfterAuth();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// UDFPS auth has never been attempted.
|
||||
if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) {
|
||||
Slog.w(TAG, "Skipping face reject haptic");
|
||||
@@ -360,6 +360,11 @@ public class CoexCoordinator {
|
||||
callback.handleLifecycleAfterAuth();
|
||||
}
|
||||
}
|
||||
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
|
||||
if (!isCurrentFaceAuth(client)) {
|
||||
callback.sendHapticFeedback();
|
||||
}
|
||||
callback.handleLifecycleAfterAuth();
|
||||
} else {
|
||||
callback.sendHapticFeedback();
|
||||
callback.handleLifecycleAfterAuth();
|
||||
@@ -380,6 +385,8 @@ public class CoexCoordinator {
|
||||
*/
|
||||
public void onAuthenticationError(@NonNull AuthenticationClient<?> client,
|
||||
@BiometricConstants.Errors int error, @NonNull ErrorCallback callback) {
|
||||
final boolean isUsingSingleModality = isSingleAuthOnly(client);
|
||||
|
||||
// Figure out non-coex state
|
||||
final boolean shouldUsuallyVibrate;
|
||||
if (isCurrentFaceAuth(client)) {
|
||||
@@ -401,25 +408,26 @@ public class CoexCoordinator {
|
||||
}
|
||||
|
||||
// Figure out coex state
|
||||
final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
|
||||
final boolean hapticSuppressedByCoex;
|
||||
|
||||
if (keyguardAdvancedLogic) {
|
||||
if (isSingleAuthOnly(client)) {
|
||||
if (mAdvancedLogicEnabled && client.isKeyguard()) {
|
||||
if (isUsingSingleModality) {
|
||||
hapticSuppressedByCoex = false;
|
||||
} else {
|
||||
hapticSuppressedByCoex = isCurrentFaceAuth(client)
|
||||
&& !client.isKeyguardBypassEnabled();
|
||||
}
|
||||
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
|
||||
hapticSuppressedByCoex = isCurrentFaceAuth(client);
|
||||
} else {
|
||||
hapticSuppressedByCoex = false;
|
||||
}
|
||||
|
||||
// Combine and send feedback if appropriate
|
||||
Slog.d(TAG, "shouldUsuallyVibrate: " + shouldUsuallyVibrate
|
||||
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
|
||||
if (shouldUsuallyVibrate && !hapticSuppressedByCoex) {
|
||||
callback.sendHapticFeedback();
|
||||
} else {
|
||||
Slog.v(TAG, "no haptic shouldUsuallyVibrate: " + shouldUsuallyVibrate
|
||||
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +512,19 @@ public class CoexCoordinator {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasMultipleSuccessfulAuthentications() {
|
||||
int count = 0;
|
||||
for (AuthenticationClient<?> c : mClientMap.values()) {
|
||||
if (c.wasAuthSuccessful()) {
|
||||
count++;
|
||||
}
|
||||
if (count > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -856,11 +856,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBiometricAuthenticated() {
|
||||
public void onBiometricAuthenticated(@Modality int modality) {
|
||||
enforceBiometricDialog();
|
||||
if (mBar != null) {
|
||||
try {
|
||||
mBar.onBiometricAuthenticated();
|
||||
mBar.onBiometricAuthenticated(modality);
|
||||
} catch (RemoteException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ public class AuthSessionTest {
|
||||
public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes()
|
||||
throws Exception {
|
||||
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
|
||||
testMultiAuth_fingerprintSensorStartsAfter(false /* fingerprintStartsAfterDelay */);
|
||||
testMultiAuth_fingerprintSensorStartsAfterUINotifies();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -227,10 +227,10 @@ public class AuthSessionTest {
|
||||
throws Exception {
|
||||
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
|
||||
setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
|
||||
testMultiAuth_fingerprintSensorStartsAfter(true /* fingerprintStartsAfterDelay */);
|
||||
testMultiAuth_fingerprintSensorStartsAfterUINotifies();
|
||||
}
|
||||
|
||||
public void testMultiAuth_fingerprintSensorStartsAfter(boolean fingerprintStartsAfterDelay)
|
||||
public void testMultiAuth_fingerprintSensorStartsAfterUINotifies()
|
||||
throws Exception {
|
||||
final long operationId = 123;
|
||||
final int userId = 10;
|
||||
@@ -274,12 +274,6 @@ public class AuthSessionTest {
|
||||
|
||||
// Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started.
|
||||
session.onDialogAnimatedIn();
|
||||
if (fingerprintStartsAfterDelay) {
|
||||
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
|
||||
assertEquals(BiometricSensor.STATE_COOKIE_RETURNED,
|
||||
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
|
||||
session.onStartFingerprint();
|
||||
}
|
||||
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
|
||||
assertEquals(BiometricSensor.STATE_AUTHENTICATING,
|
||||
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.server.biometrics;
|
||||
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.Authenticators;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
|
||||
@@ -85,14 +86,11 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Presubmit
|
||||
@SmallTest
|
||||
public class BiometricServiceTest {
|
||||
|
||||
private static final String TAG = "BiometricServiceTest";
|
||||
|
||||
private static final String TEST_PACKAGE_NAME = "test_package";
|
||||
private static final long TEST_REQUEST_ID = 44;
|
||||
|
||||
@@ -153,7 +151,7 @@ public class BiometricServiceTest {
|
||||
.thenReturn(mock(BiometricStrengthController.class));
|
||||
when(mInjector.getTrustManager()).thenReturn(mTrustManager);
|
||||
when(mInjector.getDevicePolicyManager(any())).thenReturn(mDevicePolicyManager);
|
||||
when(mInjector.getRequestGenerator()).thenReturn(new AtomicLong(TEST_REQUEST_ID - 1));
|
||||
when(mInjector.getRequestGenerator()).thenReturn(() -> TEST_REQUEST_ID);
|
||||
|
||||
when(mResources.getString(R.string.biometric_error_hw_unavailable))
|
||||
.thenReturn(ERROR_HW_UNAVAILABLE);
|
||||
@@ -178,22 +176,22 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
true /* requireConfirmation */, null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession),
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
|
||||
anyInt());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
mBiometricService.mCurrentAuthSession.binderDied();
|
||||
mBiometricService.mAuthSession.binderDied();
|
||||
waitForIdle();
|
||||
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
@@ -205,31 +203,31 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
true /* requireConfirmation */, null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession),
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
|
||||
anyInt());
|
||||
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mCurrentAuthSession.binderDied();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
mBiometricService.mAuthSession.binderDied();
|
||||
waitForIdle();
|
||||
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
|
||||
assertEquals(STATE_CLIENT_DIED_CANCELLING,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
|
||||
verify(mBiometricService.mCurrentAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl)
|
||||
verify(mBiometricService.mAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl)
|
||||
.cancelAuthenticationFromService(any(), any(), anyLong());
|
||||
|
||||
// Simulate ERROR_CANCELED received from HAL
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -265,12 +263,12 @@ public class BiometricServiceTest {
|
||||
Authenticators.DEVICE_CREDENTIAL);
|
||||
waitForIdle();
|
||||
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
// StatusBar showBiometricDialog invoked
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -304,21 +302,21 @@ public class BiometricServiceTest {
|
||||
mBiometricService = new BiometricService(mContext, mInjector);
|
||||
mBiometricService.onStart();
|
||||
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate_notStrongEnough_returnsHardwareNotPresent() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
Authenticators.BIOMETRIC_STRONG);
|
||||
@@ -335,7 +333,7 @@ public class BiometricServiceTest {
|
||||
// is able to proceed.
|
||||
|
||||
final int[] modalities = new int[] {
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT,
|
||||
TYPE_FINGERPRINT,
|
||||
BiometricAuthenticator.TYPE_FACE,
|
||||
};
|
||||
|
||||
@@ -356,7 +354,7 @@ public class BiometricServiceTest {
|
||||
|
||||
// StatusBar showBiometricDialog invoked with face, which was set up to be STRONG
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[] {SENSOR_ID_FACE}),
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -377,14 +375,14 @@ public class BiometricServiceTest {
|
||||
mBiometricService = new BiometricService(mContext, mInjector);
|
||||
mBiometricService.onStart();
|
||||
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
@@ -415,13 +413,13 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
final byte[] HAT = generateRandomHAT();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Confirmation is required
|
||||
assertEquals(STATE_AUTH_PENDING_CONFIRM,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Enrolled, not disabled in settings, user doesn't require confirmation in settings
|
||||
resetReceivers();
|
||||
@@ -431,25 +429,25 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
null /* authenticators */);
|
||||
waitForIdle();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Confirmation not required, waiting for dialog to dismiss
|
||||
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate_happyPathWithoutConfirmation_strongBiometric() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
testAuthenticate_happyPathWithoutConfirmation(true /* isStrongBiometric */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate_happyPathWithoutConfirmation_weakBiometric() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
testAuthenticate_happyPathWithoutConfirmation(false /* isStrongBiometric */);
|
||||
}
|
||||
|
||||
@@ -461,7 +459,7 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
// Creates a pending auth session with the correct initial states
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Invokes <Modality>Service#prepareForAuthentication
|
||||
ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
@@ -477,19 +475,19 @@ public class BiometricServiceTest {
|
||||
cookieCaptor.capture() /* cookie */,
|
||||
anyBoolean() /* allowBackgroundAuthentication */);
|
||||
|
||||
// onReadyForAuthentication, mCurrentAuthSession state OK
|
||||
mBiometricService.mImpl.onReadyForAuthentication(cookieCaptor.getValue());
|
||||
// onReadyForAuthentication, mAuthSession state OK
|
||||
mBiometricService.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookieCaptor.getValue());
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// startPreparedClient invoked
|
||||
mBiometricService.mCurrentAuthSession.onDialogAnimatedIn();
|
||||
mBiometricService.mAuthSession.onDialogAnimatedIn();
|
||||
verify(mBiometricService.mSensors.get(0).impl)
|
||||
.startPreparedClient(cookieCaptor.getValue());
|
||||
|
||||
// StatusBar showBiometricDialog invoked
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
any(),
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -502,18 +500,18 @@ public class BiometricServiceTest {
|
||||
|
||||
// Hardware authenticated
|
||||
final byte[] HAT = generateRandomHAT();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Waiting for SystemUI to send dismissed callback
|
||||
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
// Notify SystemUI hardware authenticated
|
||||
verify(mBiometricService.mStatusBarService).onBiometricAuthenticated();
|
||||
verify(mBiometricService.mStatusBarService).onBiometricAuthenticated(TYPE_FINGERPRINT);
|
||||
|
||||
// SystemUI sends callback with dismissed reason
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED,
|
||||
null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
@@ -527,7 +525,7 @@ public class BiometricServiceTest {
|
||||
verify(mReceiver1).onAuthenticationSucceeded(
|
||||
BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC);
|
||||
// Current session becomes null
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -542,11 +540,11 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
assertEquals(Authenticators.DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
|
||||
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -578,16 +576,16 @@ public class BiometricServiceTest {
|
||||
// Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not
|
||||
// sent to KeyStore yet
|
||||
final byte[] HAT = generateRandomHAT();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Waiting for SystemUI to send confirmation callback
|
||||
assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
|
||||
|
||||
// SystemUI sends confirm, HAT is sent to keystore and client is notified.
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED,
|
||||
null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
@@ -624,33 +622,34 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE);
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE);
|
||||
waitForIdle();
|
||||
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_NONE),
|
||||
eq(BiometricAuthenticator.TYPE_FACE),
|
||||
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mReceiver1).onAuthenticationFailed();
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FINGERPRINT);
|
||||
mBiometricService.mAuthSession.mSensorReceiver
|
||||
.onAuthenticationFailed(SENSOR_ID_FINGERPRINT);
|
||||
waitForIdle();
|
||||
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_NONE),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mReceiver1).onAuthenticationFailed();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -678,14 +677,14 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FACE),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT),
|
||||
@@ -694,15 +693,15 @@ public class BiometricServiceTest {
|
||||
verify(mReceiver1, never()).onAuthenticationFailed();
|
||||
|
||||
// No auth session. Pressing try again will create one.
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Pressing "Try again" on SystemUI
|
||||
mBiometricService.mSysuiReceiver.onTryAgainPressed();
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onTryAgainPressed();
|
||||
waitForIdle();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
|
||||
// AuthSession is now resuming
|
||||
assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Test resuming when hardware becomes ready. SystemUI should not be requested to
|
||||
// show another dialog since it's already showing.
|
||||
@@ -728,14 +727,14 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
@@ -748,7 +747,7 @@ public class BiometricServiceTest {
|
||||
// Dialog is hidden immediately
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
// Auth session is over
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -757,61 +756,61 @@ public class BiometricServiceTest {
|
||||
// For errors that show in SystemUI, BiometricService stays in STATE_ERROR_PENDING_SYSUI
|
||||
// until SystemUI notifies us that the dialog is dismissed at which point the current
|
||||
// session is done.
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// Sends error to SystemUI and does not notify client yet
|
||||
assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
|
||||
// SystemUI animation completed, client is notified, auth session is over
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_ERROR, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
|
||||
eq(0 /* vendorCode */));
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */,
|
||||
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForPendingSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForPendingSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// We should be showing device credential now
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
assertEquals(Authenticators.DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
|
||||
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -826,23 +825,23 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
waitForIdle();
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForPendingSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForPendingSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// Error is sent to client
|
||||
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
|
||||
eq(0) /* vendorCode */);
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -861,7 +860,7 @@ public class BiometricServiceTest {
|
||||
|
||||
private void testBiometricAuth_whenLockout(@LockoutTracker.LockoutMode int lockoutMode,
|
||||
int biometricPromptError) throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
|
||||
.thenReturn(lockoutMode);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
@@ -869,16 +868,15 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
// Modality and error are sent
|
||||
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
|
||||
eq(biometricPromptError), eq(0) /* vendorCode */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricOrCredentialAuth_whenBiometricLockout_showsCredential()
|
||||
throws Exception {
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
.thenReturn(true);
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
|
||||
.thenReturn(LockoutTracker.LOCKOUT_PERMANENT);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
@@ -887,13 +885,13 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
assertEquals(Authenticators.DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
|
||||
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -959,73 +957,73 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */,
|
||||
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
mBiometricService.mSysuiReceiver.onDeviceCredentialPressed();
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDeviceCredentialPressed();
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockout_whileAuthenticating_credentialAllowed() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */,
|
||||
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockout_whenAuthenticating_credentialNotAllowed() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_ERROR_PENDING_SYSUI,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
@@ -1033,20 +1031,20 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mSensors.get(0).impl).cancelAuthenticationFromService(
|
||||
any(), any(), anyLong());
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1055,12 +1053,12 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
|
||||
@@ -1069,18 +1067,17 @@ public class BiometricServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws
|
||||
Exception {
|
||||
public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
|
||||
@@ -1094,10 +1091,10 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
true /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
new byte[69] /* HAT */);
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
|
||||
@@ -1108,19 +1105,19 @@ public class BiometricServiceTest {
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception {
|
||||
when(mContext.getResources().getString(anyInt())).thenReturn("test string");
|
||||
|
||||
final int modality = BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
final int modality = TYPE_FINGERPRINT;
|
||||
setupAuthForOnly(modality, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAcquired(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAcquired(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
|
||||
0 /* vendorCode */);
|
||||
@@ -1130,29 +1127,29 @@ public class BiometricServiceTest {
|
||||
// string is retrieved for now, but it's also very unlikely to break anyway.
|
||||
verify(mBiometricService.mStatusBarService)
|
||||
.onBiometricHelp(eq(modality), anyString());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancel_whenAuthenticating() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mImpl.cancelAuthentication(mBiometricService.mCurrentAuthSession.mToken,
|
||||
mBiometricService.mImpl.cancelAuthentication(mBiometricService.mAuthSession.mToken,
|
||||
TEST_PACKAGE_NAME, TEST_REQUEST_ID);
|
||||
waitForIdle();
|
||||
|
||||
// Pretend that the HAL has responded to cancel with ERROR_CANCELED
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// Hides system dialog and invokes the onError callback
|
||||
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
@@ -1161,7 +1158,7 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testCanAuthenticate_whenDeviceHasRequestedBiometricStrength() throws Exception {
|
||||
// When only biometric is requested, and sensor is strong enough
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
|
||||
assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
|
||||
invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG));
|
||||
@@ -1170,7 +1167,7 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testCanAuthenticate_whenDeviceDoesNotHaveRequestedBiometricStrength()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
// When only biometric is requested, and sensor is not strong enough
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
@@ -1208,9 +1205,8 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testCanAuthenticate_whenNoBiometricsEnrolled() throws Exception {
|
||||
// With credential set up, test the following.
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
.thenReturn(true);
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
false /* enrolled */);
|
||||
|
||||
// When only biometric is requested
|
||||
@@ -1277,7 +1273,7 @@ public class BiometricServiceTest {
|
||||
private void testCanAuthenticate_whenLockedOut(@LockoutTracker.LockoutMode int lockoutMode)
|
||||
throws Exception {
|
||||
// When only biometric is requested, and sensor is strong enough
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
|
||||
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
|
||||
.thenReturn(lockoutMode);
|
||||
@@ -1311,7 +1307,7 @@ public class BiometricServiceTest {
|
||||
for (int i = 0; i < testCases.length; i++) {
|
||||
final BiometricSensor sensor =
|
||||
new BiometricSensor(mContext, 0 /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT,
|
||||
TYPE_FINGERPRINT,
|
||||
testCases[i][0],
|
||||
mock(IBiometricAuthenticator.class)) {
|
||||
@Override
|
||||
@@ -1341,7 +1337,7 @@ public class BiometricServiceTest {
|
||||
.thenReturn(true);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
mBiometricService.mImpl.registerAuthenticator(0 /* testId */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
verify(mBiometricService.mBiometricStrengthController).updateStrengths();
|
||||
@@ -1360,7 +1356,7 @@ public class BiometricServiceTest {
|
||||
.thenReturn(true);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
mBiometricService.mImpl.registerAuthenticator(testId /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
// Downgrade the authenticator
|
||||
@@ -1378,7 +1374,7 @@ public class BiometricServiceTest {
|
||||
false /* requireConfirmation */, authenticators);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED),
|
||||
eq(0) /* vendorCode */);
|
||||
|
||||
@@ -1392,7 +1388,7 @@ public class BiometricServiceTest {
|
||||
authenticators);
|
||||
waitForIdle();
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[] {testId}),
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -1414,9 +1410,9 @@ public class BiometricServiceTest {
|
||||
false /* requireConfirmation */,
|
||||
authenticators);
|
||||
waitForIdle();
|
||||
assertTrue(Utils.isCredentialRequested(mBiometricService.mCurrentAuthSession.mPromptInfo));
|
||||
assertTrue(Utils.isCredentialRequested(mBiometricService.mAuthSession.mPromptInfo));
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -1442,7 +1438,7 @@ public class BiometricServiceTest {
|
||||
false /* requireConfirmation */, authenticators);
|
||||
waitForIdle();
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[] {testId}) /* sensorIds */,
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -1495,29 +1491,29 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testWorkAuthentication_fingerprintWorksIfNotDisabledByDevicePolicyManager()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mDevicePolicyManager
|
||||
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
|
||||
.thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
|
||||
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
|
||||
Authenticators.BIOMETRIC_STRONG);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
startPendingAuthSession(mBiometricService);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthentication_normalAppIgnoresDevicePolicy() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mDevicePolicyManager
|
||||
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
|
||||
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, Authenticators.BIOMETRIC_STRONG);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1530,18 +1526,17 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
|
||||
Authenticators.BIOMETRIC_STRONG);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
startPendingAuthSession(mBiometricService);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkAuthentication_fingerprintFailsIfDisabledByDevicePolicyManager()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
.thenReturn(true);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
|
||||
when(mDevicePolicyManager
|
||||
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
|
||||
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
|
||||
@@ -1555,9 +1550,9 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver2,
|
||||
Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL);
|
||||
waitForIdle();
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mReceiver2, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@@ -1580,7 +1575,7 @@ public class BiometricServiceTest {
|
||||
|
||||
when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
|
||||
|
||||
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) {
|
||||
if ((modality & TYPE_FINGERPRINT) != 0) {
|
||||
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
|
||||
.thenReturn(enrolled);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
@@ -1614,7 +1609,7 @@ public class BiometricServiceTest {
|
||||
final int modality = modalities[i];
|
||||
final int strength = strengths[i];
|
||||
|
||||
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) {
|
||||
if ((modality & TYPE_FINGERPRINT) != 0) {
|
||||
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
|
||||
.thenReturn(true);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
@@ -1654,8 +1649,9 @@ public class BiometricServiceTest {
|
||||
startPendingAuthSession(mBiometricService);
|
||||
waitForIdle();
|
||||
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(TEST_REQUEST_ID, mBiometricService.mAuthSession.getRequestId());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
return requestId;
|
||||
}
|
||||
@@ -1663,14 +1659,14 @@ public class BiometricServiceTest {
|
||||
private static void startPendingAuthSession(BiometricService service) throws Exception {
|
||||
// Get the cookie so we can pretend the hardware is ready to authenticate
|
||||
// Currently we only support single modality per auth
|
||||
final PreAuthInfo preAuthInfo = service.mCurrentAuthSession.mPreAuthInfo;
|
||||
final PreAuthInfo preAuthInfo = service.mAuthSession.mPreAuthInfo;
|
||||
assertEquals(preAuthInfo.eligibleSensors.size(), 1);
|
||||
assertEquals(preAuthInfo.numSensorsWaitingForCookie(), 1);
|
||||
|
||||
final int cookie = preAuthInfo.eligibleSensors.get(0).getCookie();
|
||||
assertNotEquals(cookie, 0);
|
||||
|
||||
service.mImpl.onReadyForAuthentication(cookie);
|
||||
service.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookie);
|
||||
}
|
||||
|
||||
private static long invokeAuthenticate(IBiometricService.Stub service,
|
||||
|
||||
@@ -29,12 +29,8 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.biometrics.BiometricConstants;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
@@ -43,9 +39,11 @@ import androidx.test.filters.SmallTest;
|
||||
import com.android.server.biometrics.sensors.fingerprint.Udfps;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
@@ -53,39 +51,38 @@ import java.util.LinkedList;
|
||||
@SmallTest
|
||||
public class CoexCoordinatorTest {
|
||||
|
||||
private static final String TAG = "CoexCoordinatorTest";
|
||||
@Rule
|
||||
public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
|
||||
private CoexCoordinator mCoexCoordinator;
|
||||
private Handler mHandler;
|
||||
|
||||
@Mock
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private CoexCoordinator.Callback mCallback;
|
||||
@Mock
|
||||
private CoexCoordinator.ErrorCallback mErrorCallback;
|
||||
@Mock
|
||||
private AuthenticationClient mFaceClient;
|
||||
@Mock
|
||||
private AuthenticationClient mFingerprintClient;
|
||||
@Mock(extraInterfaces = {Udfps.class})
|
||||
private AuthenticationClient mUdfpsClient;
|
||||
|
||||
private CoexCoordinator mCoexCoordinator;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
mCoexCoordinator = CoexCoordinator.getInstance();
|
||||
mCoexCoordinator.setAdvancedLogicEnabled(true);
|
||||
mCoexCoordinator.setFaceHapticDisabledWhenNonBypass(true);
|
||||
mCoexCoordinator.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_authSuccess() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -93,15 +90,12 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_authReject_whenNotLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
client, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -109,30 +103,97 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_authReject_whenLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
client, LockoutTracker.LOCKOUT_TIMED, mCallback);
|
||||
mFaceClient, LockoutTracker.LOCKOUT_TIMED, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_success() {
|
||||
testBiometricPrompt_coex_success(false /* twice */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_successWithoutDouble() {
|
||||
testBiometricPrompt_coex_success(true /* twice */);
|
||||
}
|
||||
|
||||
private void testBiometricPrompt_coex_success(boolean twice) {
|
||||
initFaceAndFingerprintForBiometricPrompt();
|
||||
when(mFaceClient.wasAuthSuccessful()).thenReturn(true);
|
||||
when(mUdfpsClient.wasAuthSuccessful()).thenReturn(twice, true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mUdfpsClient, mCallback);
|
||||
|
||||
if (twice) {
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
} else {
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_reject() {
|
||||
initFaceAndFingerprintForBiometricPrompt();
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_errorNoHaptics() {
|
||||
initFaceAndFingerprintForBiometricPrompt();
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
mCoexCoordinator.onAuthenticationError(mUdfpsClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
|
||||
verify(mErrorCallback, never()).sendHapticFeedback();
|
||||
}
|
||||
|
||||
private void initFaceAndFingerprintForBiometricPrompt() {
|
||||
when(mFaceClient.isKeyguard()).thenReturn(false);
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(false);
|
||||
when(mUdfpsClient.isBiometricPrompt()).thenReturn(true);
|
||||
when(mUdfpsClient.wasAuthAttempted()).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyguard_faceAuthOnly_success() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isKeyguard()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -140,21 +201,16 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_faceAuth_udfpsNotTouching_faceSuccess() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
|
||||
mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
// Haptics tested in #testKeyguard_bypass_haptics. Let's leave this commented out (instead
|
||||
// of removed) to keep this context.
|
||||
// verify(mCallback).sendHapticFeedback();
|
||||
@@ -192,25 +248,19 @@ public class CoexCoordinatorTest {
|
||||
|
||||
private void testKeyguard_bypass_haptics(boolean bypassEnabled, boolean faceAccepted,
|
||||
boolean shouldReceiveHaptics) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
if (faceAccepted) {
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
|
||||
mCallback);
|
||||
} else {
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
}
|
||||
|
||||
@@ -244,24 +294,18 @@ public class CoexCoordinatorTest {
|
||||
|
||||
private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted,
|
||||
long udfpsRejectedAfterMs) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
when (udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
// For easier reading
|
||||
final CoexCoordinator.Callback faceCallback = mCallback;
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
|
||||
faceCallback);
|
||||
verify(faceCallback, never()).sendHapticFeedback();
|
||||
verify(faceCallback, never()).sendAuthenticationResult(anyBoolean());
|
||||
@@ -272,9 +316,9 @@ public class CoexCoordinatorTest {
|
||||
// Reset the mock
|
||||
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
|
||||
assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size());
|
||||
assertEquals(faceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
|
||||
assertEquals(mFaceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
|
||||
if (thenUdfpsAccepted) {
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
|
||||
udfpsCallback);
|
||||
verify(udfpsCallback).sendHapticFeedback();
|
||||
verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */);
|
||||
@@ -284,7 +328,7 @@ public class CoexCoordinatorTest {
|
||||
|
||||
assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
|
||||
} else {
|
||||
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, mUdfpsClient,
|
||||
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
|
||||
if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) {
|
||||
verify(udfpsCallback, never()).sendHapticFeedback();
|
||||
@@ -310,56 +354,44 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_udfpsAuthSuccess_whileFaceScanning() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
|
||||
mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(true));
|
||||
verify(faceClient).cancel();
|
||||
verify(mFaceClient).cancel();
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyguard_faceRejectedWhenUdfpsTouching_thenUdfpsRejected() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
|
||||
// BiometricScheduler removes the face authentication client after rejection
|
||||
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
// Then UDFPS rejected
|
||||
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mUdfpsClient,
|
||||
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
|
||||
verify(udfpsCallback).sendHapticFeedback();
|
||||
verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
@@ -368,26 +400,20 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_udfpsRejected_thenFaceRejected_noKeyguardBypass() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
|
||||
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(faceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, udfpsClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
// Auth was attempted
|
||||
when(udfpsClient.getState())
|
||||
when(mUdfpsClient.getState())
|
||||
.thenReturn(AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED);
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -395,7 +421,7 @@ public class CoexCoordinatorTest {
|
||||
// Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since
|
||||
// unlike face, its lifecycle becomes "paused" instead of "finished".
|
||||
CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class);
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, faceCallback);
|
||||
verify(faceCallback).sendHapticFeedback();
|
||||
verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
@@ -404,20 +430,16 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_capacitiveAccepted_whenFaceScanning() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
|
||||
|
||||
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class);
|
||||
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(fpClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, fpClient, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFingerprintClient, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -425,21 +447,16 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_capacitiveRejected_whenFaceScanning() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
|
||||
|
||||
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class);
|
||||
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(fpClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, fpClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mFingerprintClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -447,14 +464,11 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testNonKeyguard_rejectAndNotLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(false);
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(false);
|
||||
when(faceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
@@ -464,14 +478,11 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testNonKeyguard_rejectLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(false);
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(false);
|
||||
when(faceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_TIMED, mCallback);
|
||||
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
@@ -496,16 +507,13 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_FaceError() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
when(client.wasAuthAttempted()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
mErrorCallback);
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
verify(mErrorCallback).sendHapticFeedback();
|
||||
}
|
||||
|
||||
@@ -520,18 +528,15 @@ public class CoexCoordinatorTest {
|
||||
}
|
||||
|
||||
private void testKeyguard_faceAuthOnly(boolean bypassEnabled) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(mFaceClient.wasUserDetected()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isKeyguard()).thenReturn(true);
|
||||
when(client.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(client.wasAuthAttempted()).thenReturn(true);
|
||||
when(client.wasUserDetected()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
mErrorCallback);
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
verify(mErrorCallback).sendHapticFeedback();
|
||||
}
|
||||
|
||||
@@ -546,23 +551,17 @@ public class CoexCoordinatorTest {
|
||||
}
|
||||
|
||||
private void testKeyguard_coex_faceError(boolean bypassEnabled) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(mFaceClient.wasUserDetected()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(faceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(faceClient.wasUserDetected()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(faceClient,
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
|
||||
if (bypassEnabled) {
|
||||
|
||||
Reference in New Issue
Block a user