Add Face and Fingerprint (Co-Ex) support to biometric prompt.

Migrate simple views to Kotlin & remove useless test. Will add new tests in a follow up change.

Bug: 217393533
Test: atest com.android.systemui.biometrics com.android.server.biometrics CommandQueueTest
Test: manual (via BP test app)
Change-Id: Ic8c80344d75f34f4b5f65ef14b61bbb5cddb0349
This commit is contained in:
Joe Bolinger
2022-02-09 20:44:31 -08:00
parent 1c9a06276a
commit 4f75555ac1
40 changed files with 1438 additions and 1920 deletions

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -16,6 +16,7 @@
<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">

View File

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

View File

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

View File

@@ -1,158 +0,0 @@
/*
* 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.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
class AuthBiometricFaceIconController extends Animatable2.AnimationCallback {
private static final String TAG = "AuthBiometricFaceIconController";
protected Context mContext;
protected ImageView mIconView;
protected TextView mTextView;
protected Handler mHandler;
protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
@AuthBiometricView.BiometricState protected int mState;
protected boolean mDeactivated;
protected AuthBiometricFaceIconController(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 == AuthBiometricView.STATE_AUTHENTICATING
|| mState == AuthBiometricView.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 == AuthBiometricView.STATE_ERROR
|| lastState == AuthBiometricView.STATE_HELP;
if (newState == AuthBiometricView.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 == AuthBiometricView.STATE_AUTHENTICATING) {
startPulsing();
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (lastState == AuthBiometricView.STATE_PENDING_CONFIRMATION
&& newState == AuthBiometricView.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 == AuthBiometricView.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 == AuthBiometricView.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 == AuthBiometricView.STATE_ERROR
&& lastState != AuthBiometricView.STATE_ERROR) {
animateOnce(R.drawable.face_dialog_dark_to_error);
} else if (lastState == AuthBiometricView.STATE_AUTHENTICATING
&& newState == AuthBiometricView.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 == AuthBiometricView.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 == AuthBiometricView.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;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.ImageView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
private const val TAG = "AuthBiometricFaceIconController"
/** Face only icon animator for BiometricPrompt. */
class AuthBiometricFaceIconController(
context: Context,
iconView: ImageView
) : AuthIconController(context, iconView) {
// false = dark to light, true = light to dark
private var lastPulseLightToDark = false
@BiometricState
private var state = 0
init {
val size = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
iconView.layoutParams.width = size
iconView.layoutParams.height = size
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
}
private fun startPulsing() {
lastPulseLightToDark = false
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true)
}
private fun pulseInNextDirection() {
val iconRes = if (lastPulseLightToDark) {
R.drawable.face_dialog_pulse_dark_to_light
} else {
R.drawable.face_dialog_pulse_light_to_dark
}
animateIcon(iconRes, true /* repeat */)
lastPulseLightToDark = !lastPulseLightToDark
}
override fun handleAnimationEnd(drawable: Drawable) {
if (state == STATE_AUTHENTICATING || state == STATE_HELP) {
pulseInNextDirection()
}
}
override fun updateIcon(@BiometricState oldState: Int, @BiometricState newState: Int) {
val lastStateIsErrorIcon = (oldState == STATE_ERROR || oldState == STATE_HELP)
if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticating
)
} else if (newState == STATE_AUTHENTICATING) {
startPulsing()
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticating
)
} else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_confirmed
)
} else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
animateIconOnce(R.drawable.face_dialog_error_to_idle)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_idle
)
} else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticated
)
} else if (newState == STATE_ERROR && oldState != STATE_ERROR) {
animateIconOnce(R.drawable.face_dialog_dark_to_error)
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticated
)
} else if (newState == STATE_PENDING_CONFIRMATION) {
animateIconOnce(R.drawable.face_dialog_wink_from_dark)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_authenticated
)
} else if (newState == STATE_IDLE) {
showStaticDrawable(R.drawable.face_dialog_idle_static)
iconView.contentDescription = context.getString(
R.string.biometric_dialog_face_icon_description_idle
)
} else {
Log.w(TAG, "Unhandled state: $newState")
}
state = newState
}
}

View File

@@ -1,127 +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.hardware.biometrics.BiometricAuthenticator.Modality;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
public class AuthBiometricFaceView extends AuthBiometricView {
private static final String TAG = "AuthBiometricFaceView";
// Delay before dismissing after being authenticated/confirmed.
private static final int HIDE_DELAY_MS = 500;
@Nullable @VisibleForTesting AuthBiometricFaceIconController 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);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFaceIconController = new AuthBiometricFaceIconController(mContext, mIconView, mIndicatorView);
addOnAttachStateChangeListener(mOnAttachStateChangeListener);
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return HIDE_DELAY_MS;
}
@Override
protected int getStateForAfterError() {
return STATE_IDLE;
}
@Override
protected void handleResetAfterError() {
resetErrorView();
}
@Override
protected void handleResetAfterHelp() {
resetErrorView();
}
@Override
protected boolean supportsSmallDialog() {
return true;
}
@Override
protected boolean supportsManualRetry() {
return true;
}
@Override
public void updateState(@BiometricState int newState) {
mFaceIconController.updateState(mState, newState);
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
(newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) {
resetErrorView();
}
// Do this last since the state variable gets updated.
super.updateState(newState);
}
@Override
public void onAuthenticationFailed(@Modality int modality, @Nullable String failureReason) {
if (getSize() == AuthDialog.SIZE_MEDIUM) {
if (supportsManualRetry()) {
mTryAgainButton.setVisibility(View.VISIBLE);
mConfirmButton.setVisibility(View.GONE);
}
}
// Do this last since we want to know if the button is being animated (in the case of
// small -> medium dialog)
super.onAuthenticationFailed(modality, failureReason);
}
private void resetErrorView() {
mIndicatorView.setTextColor(mTextColorHint);
mIndicatorView.setVisibility(View.INVISIBLE);
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.content.Context
import android.hardware.biometrics.BiometricAuthenticator.Modality
import android.util.AttributeSet
/** Face only view for BiometricPrompt. */
class AuthBiometricFaceView(
context: Context,
attrs: AttributeSet? = null
) : AuthBiometricView(context, attrs) {
override fun getDelayAfterAuthenticatedDurationMs() = HIDE_DELAY_MS
override fun getStateForAfterError() = STATE_IDLE
override fun handleResetAfterError() = resetErrorView()
override fun handleResetAfterHelp() = resetErrorView()
override fun supportsSmallDialog() = true
override fun supportsManualRetry() = true
override fun supportsRequireConfirmation() = true
override fun createIconController(): AuthIconController =
AuthBiometricFaceIconController(mContext, mIconView)
override fun updateState(@BiometricState newState: Int) {
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
newState == STATE_AUTHENTICATING && size == AuthDialog.SIZE_MEDIUM) {
resetErrorView()
}
// Do this last since the state variable gets updated.
super.updateState(newState)
}
override fun onAuthenticationFailed(
@Modality modality: Int,
failureReason: String?
) {
if (size == AuthDialog.SIZE_MEDIUM) {
if (supportsManualRetry()) {
mTryAgainButton.visibility = VISIBLE
mConfirmButton.visibility = GONE
}
}
// Do this last since we want to know if the button is being animated (in the case of
// small -> medium dialog)
super.onAuthenticationFailed(modality, failureReason)
}
private fun resetErrorView() {
mIndicatorView.setTextColor(mTextColorHint)
mIndicatorView.visibility = INVISIBLE
}
companion object {
/** Delay before dismissing after being authenticated/confirmed. */
const val HIDE_DELAY_MS = 500
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.content.Context
import android.graphics.drawable.Drawable
import android.widget.ImageView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
/** Face/Fingerprint combined icon animator for BiometricPrompt. */
class AuthBiometricFingerprintAndFaceIconController(
context: Context,
iconView: ImageView
) : AuthBiometricFingerprintIconController(context, iconView) {
override val actsAsConfirmButton: Boolean = true
override fun shouldAnimateForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
): Boolean = when (newState) {
STATE_PENDING_CONFIRMATION -> true
STATE_AUTHENTICATED -> false
else -> super.shouldAnimateForTransition(oldState, newState)
}
override fun getAnimationForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
): Drawable? = when (newState) {
STATE_PENDING_CONFIRMATION -> {
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
context.getDrawable(R.drawable.fingerprint_dialog_error_to_unlock)
} else {
context.getDrawable(R.drawable.fingerprint_dialog_fp_to_unlock)
}
}
STATE_AUTHENTICATED -> null
else -> super.getAnimationForTransition(oldState, newState)
}
}

View File

@@ -17,8 +17,12 @@
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?
@@ -26,4 +30,14 @@ class AuthBiometricFingerprintAndFaceView(
constructor (context: Context) : this(context, null)
}
override fun getConfirmationPrompt() = R.string.biometric_dialog_tap_confirm_with_face
override fun forceRequireConfirmation(@Modality modality: Int) = modality == TYPE_FACE
override fun ignoreUnsuccessfulEventsFrom(@Modality modality: Int) = modality == TYPE_FACE
override fun onPointerDown(failedModalities: Set<Int>) = failedModalities.contains(TYPE_FACE)
override fun createIconController(): AuthIconController =
AuthBiometricFingerprintAndFaceIconController(mContext, mIconView)
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.content.Context
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.widget.ImageView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
/** Fingerprint only icon animator for BiometricPrompt. */
open class AuthBiometricFingerprintIconController(
context: Context,
iconView: ImageView
) : AuthIconController(context, iconView) {
init {
val size = context.resources.getDimensionPixelSize(
R.dimen.biometric_dialog_fingerprint_icon_size
)
iconView.layoutParams.width = size
iconView.layoutParams.height = size
}
override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) {
val icon = getAnimationForTransition(lastState, newState) ?: return
iconView.setImageDrawable(icon)
val iconContentDescription = getIconContentDescription(newState)
if (iconContentDescription != null) {
iconView.contentDescription = iconContentDescription
}
(icon as? AnimatedVectorDrawable)?.apply {
reset()
if (shouldAnimateForTransition(lastState, newState)) {
forceAnimationOnUI()
start()
}
}
}
private fun getIconContentDescription(@BiometricState newState: Int): CharSequence? {
val id = when (newState) {
STATE_IDLE,
STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING,
STATE_PENDING_CONFIRMATION,
STATE_AUTHENTICATED -> R.string.accessibility_fingerprint_dialog_fingerprint_icon
STATE_ERROR,
STATE_HELP -> R.string.biometric_dialog_try_again
else -> null
}
return if (id != null) context.getString(id) else null
}
protected open fun shouldAnimateForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
) = when (newState) {
STATE_HELP,
STATE_ERROR -> true
STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP
STATE_AUTHENTICATED -> false
else -> false
}
protected open fun getAnimationForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
): Drawable? {
val id = when (newState) {
STATE_HELP,
STATE_ERROR -> R.drawable.fingerprint_dialog_fp_to_error
STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING -> {
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
R.drawable.fingerprint_dialog_error_to_fp
} else {
R.drawable.fingerprint_dialog_fp_to_error
}
}
STATE_AUTHENTICATED -> R.drawable.fingerprint_dialog_fp_to_error
else -> return null
}
return if (id != null) context.getDrawable(id) else null
}
}

View File

@@ -1,225 +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.annotation.NonNull;
import android.content.Context;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.systemui.R;
public class AuthBiometricFingerprintView extends AuthBiometricView {
private static final String TAG = "AuthBiometricFingerprintView";
private boolean mIsUdfps = false;
@Nullable private UdfpsDialogMeasureAdapter mUdfpsAdapter;
public AuthBiometricFingerprintView(Context context) {
this(context, null);
}
public AuthBiometricFingerprintView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height);
return mUdfpsAdapter != null
? mUdfpsAdapter.onMeasureInternal(width, height, layoutParams)
: layoutParams;
}
/**
* Set the properties of this sensor so the view can be customized prior to layout.
*
* @param sensorProps sensor properties
*/
public void setSensorProperties(@NonNull FingerprintSensorPropertiesInternal sensorProps) {
mIsUdfps = sensorProps.isAnyUdfpsType();
mUdfpsAdapter = mIsUdfps ? new UdfpsDialogMeasureAdapter(this, sensorProps) : null;
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mUdfpsAdapter != 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.
final int bottomSpacerHeight = mUdfpsAdapter.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);
}
}
}
/** If this view is for a UDFPS sensor. */
public boolean isUdfps() {
return mIsUdfps;
}
@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
protected void onAttachedToWindow() {
super.onAttachedToWindow();
showTouchSensorString();
}
private void showTouchSensorString() {
mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor);
mIndicatorView.setTextColor(mTextColorHint);
}
private void updateIcon(int lastState, int newState) {
final Drawable icon = getAnimationForTransition(lastState, newState);
if (icon == null) {
Log.e(TAG, "Animation not found, " + lastState + " -> " + newState);
return;
}
final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
? (AnimatedVectorDrawable) icon
: null;
mIconView.setImageDrawable(icon);
final CharSequence iconContentDescription = getIconContentDescription(newState);
if (iconContentDescription != null) {
mIconView.setContentDescription(iconContentDescription);
}
if (animation != null && shouldAnimateForTransition(lastState, newState)) {
animation.forceAnimationOnUI();
animation.start();
}
}
@Nullable
private CharSequence getIconContentDescription(int newState) {
switch (newState) {
case STATE_IDLE:
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
case STATE_PENDING_CONFIRMATION:
case STATE_AUTHENTICATED:
return mContext.getString(
R.string.accessibility_fingerprint_dialog_fingerprint_icon);
case STATE_ERROR:
case STATE_HELP:
return mContext.getString(R.string.biometric_dialog_try_again);
default:
return null;
}
}
private boolean shouldAnimateForTransition(int oldState, int newState) {
switch (newState) {
case STATE_HELP:
case STATE_ERROR:
return true;
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
return true;
} else {
return false;
}
case STATE_AUTHENTICATED:
return false;
default:
return false;
}
}
private Drawable getAnimationForTransition(int oldState, int newState) {
int iconRes;
switch (newState) {
case STATE_HELP:
case STATE_ERROR:
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
break;
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
iconRes = R.drawable.fingerprint_dialog_error_to_fp;
} else {
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
}
break;
case STATE_AUTHENTICATED:
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
break;
default:
return null;
}
return mContext.getDrawable(iconRes);
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.content.Context
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.util.AttributeSet
import android.util.Log
import android.widget.FrameLayout
import android.widget.TextView
import com.android.systemui.R
private const val TAG = "AuthBiometricFingerprintView"
/** Fingerprint only view for BiometricPrompt. */
open class AuthBiometricFingerprintView(
context: Context,
attrs: AttributeSet? = null
) : AuthBiometricView(context, attrs) {
/** If this view is for a UDFPS sensor. */
var isUdfps = false
private set
private var udfpsAdapter: UdfpsDialogMeasureAdapter? = null
/** Set the [sensorProps] of this sensor so the view can be customized prior to layout. */
fun setSensorProperties(sensorProps: FingerprintSensorPropertiesInternal) {
isUdfps = sensorProps.isAnyUdfpsType
udfpsAdapter = if (isUdfps) UdfpsDialogMeasureAdapter(this, sensorProps) else null
}
override fun onMeasureInternal(width: Int, height: Int): AuthDialog.LayoutParams {
val layoutParams = super.onMeasureInternal(width, height)
return udfpsAdapter?.onMeasureInternal(width, height, layoutParams) ?: layoutParams
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val adapter = udfpsAdapter
if (adapter != null) {
// Move the UDFPS icon and indicator text if necessary. This probably only needs to happen
// for devices where the UDFPS sensor is too low.
// TODO(b/201510778): Update this logic to support cases where the sensor or text overlap
// the button bar area.
val bottomSpacerHeight = adapter.bottomSpacerHeight
Log.w(TAG, "bottomSpacerHeight: $bottomSpacerHeight")
if (bottomSpacerHeight < 0) {
val iconFrame = findViewById<FrameLayout>(R.id.biometric_icon_frame)!!
iconFrame.translationY = -bottomSpacerHeight.toFloat()
val indicator = findViewById<TextView>(R.id.indicator)!!
indicator.translationY = -bottomSpacerHeight.toFloat()
}
}
}
override fun getDelayAfterAuthenticatedDurationMs() = 0
override fun getStateForAfterError() = STATE_AUTHENTICATING
override fun handleResetAfterError() = showTouchSensorString()
override fun handleResetAfterHelp() = showTouchSensorString()
override fun supportsSmallDialog() = false
override fun createIconController(): AuthIconController =
AuthBiometricFingerprintIconController(mContext, mIconView)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
showTouchSensorString()
}
private fun showTouchSensorString() {
mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor)
mIndicatorView.setTextColor(mTextColorHint)
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.annotation.DrawableRes
import android.content.Context
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.ImageView
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
private const val TAG = "AuthIconController"
/** Controller for animating the BiometricPrompt icon/affordance. */
abstract class AuthIconController(
protected val context: Context,
protected val iconView: ImageView
) : Animatable2.AnimationCallback() {
/** If this controller should ignore events and pause. */
var deactivated: Boolean = false
/** If the icon view should be treated as an alternate "confirm" button. */
open val actsAsConfirmButton: Boolean = false
final override fun onAnimationStart(drawable: Drawable) {
super.onAnimationStart(drawable)
}
final override fun onAnimationEnd(drawable: Drawable) {
super.onAnimationEnd(drawable)
if (!deactivated) {
handleAnimationEnd(drawable)
}
}
/** Set the icon to a static image. */
protected fun showStaticDrawable(@DrawableRes iconRes: Int) {
iconView.setImageDrawable(context.getDrawable(iconRes))
}
/** Animate a resource. */
protected fun animateIconOnce(@DrawableRes iconRes: Int) {
animateIcon(iconRes, false)
}
/** Animate a resource. */
protected fun animateIcon(@DrawableRes iconRes: Int, repeat: Boolean) {
if (!deactivated) {
val icon = context.getDrawable(iconRes) as AnimatedVectorDrawable
iconView.setImageDrawable(icon)
icon.forceAnimationOnUI()
if (repeat) {
icon.registerAnimationCallback(this)
}
icon.start()
}
}
/** Update the icon to reflect the [newState]. */
fun updateState(@BiometricState lastState: Int, @BiometricState newState: Int) {
if (deactivated) {
Log.w(TAG, "Ignoring updateState when deactivated: $newState")
} else {
updateIcon(lastState, newState)
}
}
/** If the icon should act as a "retry" button in the [currentState]. */
fun iconTapSendsRetryWhen(@BiometricState currentState: Int): Boolean = false
/** Call during [updateState] if the controller is not [deactivated]. */
abstract fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int)
/** Called during [onAnimationEnd] if the controller is not [deactivated]. */
open fun handleAnimationEnd(drawable: Drawable) {}
}

View File

@@ -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;
@@ -51,6 +52,7 @@ 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.
@@ -132,6 +134,7 @@ public 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;
@@ -224,6 +227,40 @@ public class AuthBiometricView extends LinearLayout {
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;
}
@@ -249,11 +286,11 @@ public class AuthBiometricView extends LinearLayout {
}
void setRequireConfirmation(boolean requireConfirmation) {
mRequireConfirmation = 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);
@@ -413,6 +450,8 @@ public 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:
@@ -442,10 +481,11 @@ public 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;
@@ -468,9 +508,9 @@ public 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);
@@ -485,6 +525,10 @@ public class AuthBiometricView extends LinearLayout {
*/
public void onAuthenticationFailed(
@Modality int modality, @Nullable String failureReason) {
if (ignoreUnsuccessfulEventsFrom(modality)) {
return;
}
showTemporaryMessage(failureReason, mResetErrorRunnable);
updateState(STATE_ERROR);
}
@@ -496,6 +540,10 @@ public 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);
@@ -503,6 +551,18 @@ public class AuthBiometricView extends LinearLayout {
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;
}
/**
* Show a help message to the user.
*
@@ -510,6 +570,9 @@ public 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;
@@ -617,6 +680,15 @@ public 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);
}
});
}
}
/**
@@ -686,6 +758,8 @@ public 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 */);

View File

@@ -60,7 +60,9 @@ 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.
@@ -106,6 +108,7 @@ public class AuthContainerView extends LinearLayout
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 private Integer mPendingCallbackReason;
@@ -217,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:
@@ -546,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);
}
@@ -565,6 +570,14 @@ 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.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY,

View File

@@ -125,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;
@@ -142,12 +140,13 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
@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 =
@@ -260,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) {
@@ -577,7 +587,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
mFingerprintAuthenticatorsRegisteredCallback);
}
mTaskStackListener = new BiometricTaskStackListener();
mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
}
@@ -662,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");
}

View File

@@ -113,7 +113,7 @@ public interface AuthDialog {
/**
* Biometric authenticated. May be pending user confirmation, or completed.
*/
void onAuthenticationSucceeded();
void onAuthenticationSucceeded(@Modality int modality);
/**
* Authentication failed (reject, timeout). Dialog stays showing.
@@ -136,6 +136,9 @@ public interface AuthDialog {
*/
void onError(@Modality int modality, String error);
/** UDFPS pointer down event. */
void onPointerDown();
/**
* Save the current state.
* @param outState

View File

@@ -53,7 +53,8 @@ object Utils {
@JvmStatic
fun dpToPixels(context: Context, dp: Float): Float {
return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
val density = context.resources.displayMetrics.densityDpi.toFloat()
return dp * (density / DisplayMetrics.DENSITY_DEFAULT)
}
@JvmStatic
@@ -86,8 +87,12 @@ object Utils {
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
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
}

View File

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

View File

@@ -1,120 +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;
@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;
@Mock
private TestableFaceView.TestableIconController mIconController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mFaceView = new TestableFaceView(mContext);
mFaceView.mFaceIconController = mIconController;
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 AuthBiometricFaceIconController {
TestableIconController(Context context, ImageView iconView) {
super(context, iconView, mock(TextView.class));
}
public void startPulsing() {
// Stub for testing
}
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return 0; // Keep this at 0 for tests to invoke callback immediately.
}
public TestableFaceView(Context context) {
super(context);
}
}
}

View File

@@ -20,6 +20,8 @@ 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.anyInt;
@@ -68,10 +70,10 @@ public class AuthBiometricViewTest extends SysuiTestCase {
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
@@ -79,19 +81,28 @@ public class AuthBiometricViewTest extends SysuiTestCase {
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, mBiometricView.mNegativeButton.getVisibility());
assertEquals(View.VISIBLE, mBiometricView.mCancelButton.getVisibility());
assertTrue(mBiometricView.mCancelButton.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));
}
assertTrue(mBiometricView.mConfirmButton.isEnabled());
assertEquals(mContext.getText(R.string.biometric_dialog_tap_confirm),
mBiometricView.mIndicatorView.getText());
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
}
@Test
@@ -101,7 +112,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
mBiometricView.mConfirmButton.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED);
verify(mCallback).onAction(ACTION_AUTHENTICATED);
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
}
@@ -121,7 +132,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
initDialog(false /* allowDeviceCredential */, mCallback);
mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded();
mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT);
assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility());
@@ -170,7 +181,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
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));
}

View File

@@ -16,6 +16,7 @@
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_FINGERPRINT_AND_FACE;
@@ -352,8 +353,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

View File

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

View File

@@ -107,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;
@@ -121,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;
@@ -134,6 +134,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
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;
@@ -219,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);
@@ -255,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
@@ -269,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);
@@ -366,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) {
@@ -397,6 +409,12 @@ 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;
@@ -483,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);
@@ -498,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;
}
@@ -521,20 +548,30 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
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 {
@@ -546,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();
@@ -559,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);
@@ -580,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();
@@ -616,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.
@@ -744,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
@@ -798,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;
}

View File

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

View File

@@ -55,7 +55,6 @@ import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
@@ -84,6 +83,7 @@ import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
/**
* System service that arbitrates the modality for BiometricPrompt to use.
@@ -92,21 +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 final Injector mInjector;
private final DevicePolicyManager mDevicePolicyManager;
@VisibleForTesting
@@ -115,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;
@@ -127,128 +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;
}
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
@@ -546,88 +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));
}
private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> {
mHandler.sendEmptyMessage(MSG_CLIENT_DIED);
@Override
public void onSystemEvent(int event) {
mHandler.post(() -> handleOnSystemEvent(requestId, event));
}
@Override
public void onDialogAnimatedIn() {
mHandler.post(() -> handleOnDialogAnimatedIn(requestId));
}
};
}
private AuthSession.ClientDeathReceiver createClientDeathReceiver(final long requestId) {
return () -> mHandler.post(() -> handleClientDied(requestId));
};
/**
@@ -668,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
@@ -700,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;
}
@@ -725,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
@@ -991,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);
@@ -1117,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();
}
}
@@ -1191,162 +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();
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,
@@ -1407,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;
}
}
@@ -1470,7 +1331,7 @@ public class BiometricService extends SystemService {
pw.println(" " + sensor);
}
pw.println();
pw.println("CurrentSession: " + mCurrentAuthSession);
pw.println("CurrentSession: " + mAuthSession);
pw.println();
pw.println("CoexCoordinator: " + CoexCoordinator.getInstance().toString());
pw.println();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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