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:
@@ -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
|
||||
|
||||
@@ -159,7 +159,7 @@ oneway interface IStatusBar
|
||||
/**
|
||||
* Used to notify the authentication dialog that a biometric has been authenticated.
|
||||
*/
|
||||
void onBiometricAuthenticated();
|
||||
void onBiometricAuthenticated(int modality);
|
||||
/**
|
||||
* Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc.
|
||||
*/
|
||||
|
||||
@@ -125,7 +125,7 @@ interface IStatusBarService
|
||||
int multiSensorConfig);
|
||||
|
||||
// Used to notify the authentication dialog that a biometric has been authenticated
|
||||
void onBiometricAuthenticated();
|
||||
void onBiometricAuthenticated(int modality);
|
||||
// Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc
|
||||
void onBiometricHelp(int modality, String message);
|
||||
// Used to show an error - the dialog will dismiss after a certain amount of time
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -49,8 +49,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/biometric_icon"
|
||||
android:layout_width="@dimen/biometric_dialog_biometric_icon_size"
|
||||
android:layout_height="@dimen/biometric_dialog_biometric_icon_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.systemui.biometrics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
|
||||
|
||||
private const val TAG = "AuthBiometricFaceIconController"
|
||||
|
||||
/** Face only icon animator for BiometricPrompt. */
|
||||
class AuthBiometricFaceIconController(
|
||||
context: Context,
|
||||
iconView: ImageView
|
||||
) : AuthIconController(context, iconView) {
|
||||
|
||||
// false = dark to light, true = light to dark
|
||||
private var lastPulseLightToDark = false
|
||||
|
||||
@BiometricState
|
||||
private var state = 0
|
||||
|
||||
init {
|
||||
val size = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
|
||||
iconView.layoutParams.width = size
|
||||
iconView.layoutParams.height = size
|
||||
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
|
||||
}
|
||||
|
||||
private fun startPulsing() {
|
||||
lastPulseLightToDark = false
|
||||
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true)
|
||||
}
|
||||
|
||||
private fun pulseInNextDirection() {
|
||||
val iconRes = if (lastPulseLightToDark) {
|
||||
R.drawable.face_dialog_pulse_dark_to_light
|
||||
} else {
|
||||
R.drawable.face_dialog_pulse_light_to_dark
|
||||
}
|
||||
animateIcon(iconRes, true /* repeat */)
|
||||
lastPulseLightToDark = !lastPulseLightToDark
|
||||
}
|
||||
|
||||
override fun handleAnimationEnd(drawable: Drawable) {
|
||||
if (state == STATE_AUTHENTICATING || state == STATE_HELP) {
|
||||
pulseInNextDirection()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateIcon(@BiometricState oldState: Int, @BiometricState newState: Int) {
|
||||
val lastStateIsErrorIcon = (oldState == STATE_ERROR || oldState == STATE_HELP)
|
||||
if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
|
||||
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light)
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticating
|
||||
)
|
||||
} else if (newState == STATE_AUTHENTICATING) {
|
||||
startPulsing()
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticating
|
||||
)
|
||||
} else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
|
||||
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_confirmed
|
||||
)
|
||||
} else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
|
||||
animateIconOnce(R.drawable.face_dialog_error_to_idle)
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_idle
|
||||
)
|
||||
} else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
|
||||
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticated
|
||||
)
|
||||
} else if (newState == STATE_ERROR && oldState != STATE_ERROR) {
|
||||
animateIconOnce(R.drawable.face_dialog_dark_to_error)
|
||||
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
|
||||
animateIconOnce(R.drawable.face_dialog_dark_to_checkmark)
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticated
|
||||
)
|
||||
} else if (newState == STATE_PENDING_CONFIRMATION) {
|
||||
animateIconOnce(R.drawable.face_dialog_wink_from_dark)
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_authenticated
|
||||
)
|
||||
} else if (newState == STATE_IDLE) {
|
||||
showStaticDrawable(R.drawable.face_dialog_idle_static)
|
||||
iconView.contentDescription = context.getString(
|
||||
R.string.biometric_dialog_face_icon_description_idle
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "Unhandled state: $newState")
|
||||
}
|
||||
state = newState
|
||||
}
|
||||
}
|
||||
@@ -1,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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.systemui.biometrics
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.biometrics.BiometricAuthenticator.Modality
|
||||
import android.util.AttributeSet
|
||||
|
||||
/** Face only view for BiometricPrompt. */
|
||||
class AuthBiometricFaceView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : AuthBiometricView(context, attrs) {
|
||||
|
||||
override fun getDelayAfterAuthenticatedDurationMs() = HIDE_DELAY_MS
|
||||
|
||||
override fun getStateForAfterError() = STATE_IDLE
|
||||
|
||||
override fun handleResetAfterError() = resetErrorView()
|
||||
|
||||
override fun handleResetAfterHelp() = resetErrorView()
|
||||
|
||||
override fun supportsSmallDialog() = true
|
||||
|
||||
override fun supportsManualRetry() = true
|
||||
|
||||
override fun supportsRequireConfirmation() = true
|
||||
|
||||
override fun createIconController(): AuthIconController =
|
||||
AuthBiometricFaceIconController(mContext, mIconView)
|
||||
|
||||
override fun updateState(@BiometricState newState: Int) {
|
||||
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
|
||||
newState == STATE_AUTHENTICATING && size == AuthDialog.SIZE_MEDIUM) {
|
||||
resetErrorView()
|
||||
}
|
||||
|
||||
// Do this last since the state variable gets updated.
|
||||
super.updateState(newState)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed(
|
||||
@Modality modality: Int,
|
||||
failureReason: String?
|
||||
) {
|
||||
if (size == AuthDialog.SIZE_MEDIUM) {
|
||||
if (supportsManualRetry()) {
|
||||
mTryAgainButton.visibility = VISIBLE
|
||||
mConfirmButton.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
// Do this last since we want to know if the button is being animated (in the case of
|
||||
// small -> medium dialog)
|
||||
super.onAuthenticationFailed(modality, failureReason)
|
||||
}
|
||||
|
||||
private fun resetErrorView() {
|
||||
mIndicatorView.setTextColor(mTextColorHint)
|
||||
mIndicatorView.visibility = INVISIBLE
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Delay before dismissing after being authenticated/confirmed. */
|
||||
const val HIDE_DELAY_MS = 500
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.biometrics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
|
||||
|
||||
/** Face/Fingerprint combined icon animator for BiometricPrompt. */
|
||||
class AuthBiometricFingerprintAndFaceIconController(
|
||||
context: Context,
|
||||
iconView: ImageView
|
||||
) : AuthBiometricFingerprintIconController(context, iconView) {
|
||||
|
||||
override val actsAsConfirmButton: Boolean = true
|
||||
|
||||
override fun shouldAnimateForTransition(
|
||||
@BiometricState oldState: Int,
|
||||
@BiometricState newState: Int
|
||||
): Boolean = when (newState) {
|
||||
STATE_PENDING_CONFIRMATION -> true
|
||||
STATE_AUTHENTICATED -> false
|
||||
else -> super.shouldAnimateForTransition(oldState, newState)
|
||||
}
|
||||
|
||||
override fun getAnimationForTransition(
|
||||
@BiometricState oldState: Int,
|
||||
@BiometricState newState: Int
|
||||
): Drawable? = when (newState) {
|
||||
STATE_PENDING_CONFIRMATION -> {
|
||||
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
|
||||
context.getDrawable(R.drawable.fingerprint_dialog_error_to_unlock)
|
||||
} else {
|
||||
context.getDrawable(R.drawable.fingerprint_dialog_fp_to_unlock)
|
||||
}
|
||||
}
|
||||
STATE_AUTHENTICATED -> null
|
||||
else -> super.getAnimationForTransition(oldState, newState)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.biometrics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
|
||||
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
|
||||
|
||||
/** Fingerprint only icon animator for BiometricPrompt. */
|
||||
open class AuthBiometricFingerprintIconController(
|
||||
context: Context,
|
||||
iconView: ImageView
|
||||
) : AuthIconController(context, iconView) {
|
||||
|
||||
init {
|
||||
val size = context.resources.getDimensionPixelSize(
|
||||
R.dimen.biometric_dialog_fingerprint_icon_size
|
||||
)
|
||||
iconView.layoutParams.width = size
|
||||
iconView.layoutParams.height = size
|
||||
}
|
||||
|
||||
override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) {
|
||||
val icon = getAnimationForTransition(lastState, newState) ?: return
|
||||
|
||||
iconView.setImageDrawable(icon)
|
||||
|
||||
val iconContentDescription = getIconContentDescription(newState)
|
||||
if (iconContentDescription != null) {
|
||||
iconView.contentDescription = iconContentDescription
|
||||
}
|
||||
|
||||
(icon as? AnimatedVectorDrawable)?.apply {
|
||||
reset()
|
||||
if (shouldAnimateForTransition(lastState, newState)) {
|
||||
forceAnimationOnUI()
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconContentDescription(@BiometricState newState: Int): CharSequence? {
|
||||
val id = when (newState) {
|
||||
STATE_IDLE,
|
||||
STATE_AUTHENTICATING_ANIMATING_IN,
|
||||
STATE_AUTHENTICATING,
|
||||
STATE_PENDING_CONFIRMATION,
|
||||
STATE_AUTHENTICATED -> R.string.accessibility_fingerprint_dialog_fingerprint_icon
|
||||
STATE_ERROR,
|
||||
STATE_HELP -> R.string.biometric_dialog_try_again
|
||||
else -> null
|
||||
}
|
||||
return if (id != null) context.getString(id) else null
|
||||
}
|
||||
|
||||
protected open fun shouldAnimateForTransition(
|
||||
@BiometricState oldState: Int,
|
||||
@BiometricState newState: Int
|
||||
) = when (newState) {
|
||||
STATE_HELP,
|
||||
STATE_ERROR -> true
|
||||
STATE_AUTHENTICATING_ANIMATING_IN,
|
||||
STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP
|
||||
STATE_AUTHENTICATED -> false
|
||||
else -> false
|
||||
}
|
||||
|
||||
protected open fun getAnimationForTransition(
|
||||
@BiometricState oldState: Int,
|
||||
@BiometricState newState: Int
|
||||
): Drawable? {
|
||||
val id = when (newState) {
|
||||
STATE_HELP,
|
||||
STATE_ERROR -> R.drawable.fingerprint_dialog_fp_to_error
|
||||
STATE_AUTHENTICATING_ANIMATING_IN,
|
||||
STATE_AUTHENTICATING -> {
|
||||
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
|
||||
R.drawable.fingerprint_dialog_error_to_fp
|
||||
} else {
|
||||
R.drawable.fingerprint_dialog_fp_to_error
|
||||
}
|
||||
}
|
||||
STATE_AUTHENTICATED -> R.drawable.fingerprint_dialog_fp_to_error
|
||||
else -> return null
|
||||
}
|
||||
return if (id != null) context.getDrawable(id) else null
|
||||
}
|
||||
}
|
||||
@@ -1,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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.systemui.biometrics
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import com.android.systemui.R
|
||||
|
||||
private const val TAG = "AuthBiometricFingerprintView"
|
||||
|
||||
/** Fingerprint only view for BiometricPrompt. */
|
||||
open class AuthBiometricFingerprintView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : AuthBiometricView(context, attrs) {
|
||||
/** If this view is for a UDFPS sensor. */
|
||||
var isUdfps = false
|
||||
private set
|
||||
|
||||
private var udfpsAdapter: UdfpsDialogMeasureAdapter? = null
|
||||
|
||||
/** Set the [sensorProps] of this sensor so the view can be customized prior to layout. */
|
||||
fun setSensorProperties(sensorProps: FingerprintSensorPropertiesInternal) {
|
||||
isUdfps = sensorProps.isAnyUdfpsType
|
||||
udfpsAdapter = if (isUdfps) UdfpsDialogMeasureAdapter(this, sensorProps) else null
|
||||
}
|
||||
|
||||
override fun onMeasureInternal(width: Int, height: Int): AuthDialog.LayoutParams {
|
||||
val layoutParams = super.onMeasureInternal(width, height)
|
||||
return udfpsAdapter?.onMeasureInternal(width, height, layoutParams) ?: layoutParams
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
val adapter = udfpsAdapter
|
||||
if (adapter != null) {
|
||||
// Move the UDFPS icon and indicator text if necessary. This probably only needs to happen
|
||||
// for devices where the UDFPS sensor is too low.
|
||||
// TODO(b/201510778): Update this logic to support cases where the sensor or text overlap
|
||||
// the button bar area.
|
||||
val bottomSpacerHeight = adapter.bottomSpacerHeight
|
||||
Log.w(TAG, "bottomSpacerHeight: $bottomSpacerHeight")
|
||||
if (bottomSpacerHeight < 0) {
|
||||
val iconFrame = findViewById<FrameLayout>(R.id.biometric_icon_frame)!!
|
||||
iconFrame.translationY = -bottomSpacerHeight.toFloat()
|
||||
val indicator = findViewById<TextView>(R.id.indicator)!!
|
||||
indicator.translationY = -bottomSpacerHeight.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDelayAfterAuthenticatedDurationMs() = 0
|
||||
|
||||
override fun getStateForAfterError() = STATE_AUTHENTICATING
|
||||
|
||||
override fun handleResetAfterError() = showTouchSensorString()
|
||||
|
||||
override fun handleResetAfterHelp() = showTouchSensorString()
|
||||
|
||||
override fun supportsSmallDialog() = false
|
||||
|
||||
override fun createIconController(): AuthIconController =
|
||||
AuthBiometricFingerprintIconController(mContext, mIconView)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
showTouchSensorString()
|
||||
}
|
||||
|
||||
private fun showTouchSensorString() {
|
||||
mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor)
|
||||
mIndicatorView.setTextColor(mTextColorHint)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.biometrics
|
||||
|
||||
import android.annotation.DrawableRes
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable2
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
|
||||
|
||||
private const val TAG = "AuthIconController"
|
||||
|
||||
/** Controller for animating the BiometricPrompt icon/affordance. */
|
||||
abstract class AuthIconController(
|
||||
protected val context: Context,
|
||||
protected val iconView: ImageView
|
||||
) : Animatable2.AnimationCallback() {
|
||||
|
||||
/** If this controller should ignore events and pause. */
|
||||
var deactivated: Boolean = false
|
||||
|
||||
/** If the icon view should be treated as an alternate "confirm" button. */
|
||||
open val actsAsConfirmButton: Boolean = false
|
||||
|
||||
final override fun onAnimationStart(drawable: Drawable) {
|
||||
super.onAnimationStart(drawable)
|
||||
}
|
||||
|
||||
final override fun onAnimationEnd(drawable: Drawable) {
|
||||
super.onAnimationEnd(drawable)
|
||||
|
||||
if (!deactivated) {
|
||||
handleAnimationEnd(drawable)
|
||||
}
|
||||
}
|
||||
|
||||
/** Set the icon to a static image. */
|
||||
protected fun showStaticDrawable(@DrawableRes iconRes: Int) {
|
||||
iconView.setImageDrawable(context.getDrawable(iconRes))
|
||||
}
|
||||
|
||||
/** Animate a resource. */
|
||||
protected fun animateIconOnce(@DrawableRes iconRes: Int) {
|
||||
animateIcon(iconRes, false)
|
||||
}
|
||||
|
||||
/** Animate a resource. */
|
||||
protected fun animateIcon(@DrawableRes iconRes: Int, repeat: Boolean) {
|
||||
if (!deactivated) {
|
||||
val icon = context.getDrawable(iconRes) as AnimatedVectorDrawable
|
||||
iconView.setImageDrawable(icon)
|
||||
icon.forceAnimationOnUI()
|
||||
if (repeat) {
|
||||
icon.registerAnimationCallback(this)
|
||||
}
|
||||
icon.start()
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the icon to reflect the [newState]. */
|
||||
fun updateState(@BiometricState lastState: Int, @BiometricState newState: Int) {
|
||||
if (deactivated) {
|
||||
Log.w(TAG, "Ignoring updateState when deactivated: $newState")
|
||||
} else {
|
||||
updateIcon(lastState, newState)
|
||||
}
|
||||
}
|
||||
|
||||
/** If the icon should act as a "retry" button in the [currentState]. */
|
||||
fun iconTapSendsRetryWhen(@BiometricState currentState: Int): Boolean = false
|
||||
|
||||
/** Call during [updateState] if the controller is not [deactivated]. */
|
||||
abstract fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int)
|
||||
|
||||
/** Called during [onAnimationEnd] if the controller is not [deactivated]. */
|
||||
open fun handleAnimationEnd(drawable: Drawable) {}
|
||||
}
|
||||
@@ -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 */);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -310,11 +310,11 @@ public class CommandQueue extends IStatusBar.Stub implements
|
||||
long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
|
||||
}
|
||||
|
||||
/** @see IStatusBar#onBiometricAuthenticated() */
|
||||
default void onBiometricAuthenticated() {
|
||||
/** @see IStatusBar#onBiometricAuthenticated(int) */
|
||||
default void onBiometricAuthenticated(@Modality int modality) {
|
||||
}
|
||||
|
||||
/** @see IStatusBar#onBiometricHelp(String) */
|
||||
/** @see IStatusBar#onBiometricHelp(int, String) */
|
||||
default void onBiometricHelp(@Modality int modality, String message) {
|
||||
}
|
||||
|
||||
@@ -963,9 +963,11 @@ public class CommandQueue extends IStatusBar.Stub implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBiometricAuthenticated() {
|
||||
public void onBiometricAuthenticated(@Modality int modality) {
|
||||
synchronized (mLock) {
|
||||
mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED).sendToTarget();
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.argi1 = modality;
|
||||
mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1465,9 +1467,11 @@ public class CommandQueue extends IStatusBar.Stub implements
|
||||
break;
|
||||
}
|
||||
case MSG_BIOMETRIC_AUTHENTICATED: {
|
||||
SomeArgs someArgs = (SomeArgs) msg.obj;
|
||||
for (int i = 0; i < mCallbacks.size(); i++) {
|
||||
mCallbacks.get(i).onBiometricAuthenticated();
|
||||
mCallbacks.get(i).onBiometricAuthenticated(someArgs.argi1 /* modality */);
|
||||
}
|
||||
someArgs.recycle();
|
||||
break;
|
||||
}
|
||||
case MSG_BIOMETRIC_HELP: {
|
||||
|
||||
@@ -1,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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -131,8 +131,10 @@ public abstract class BiometricSensor {
|
||||
|
||||
void goToStateCancelling(IBinder token, String opPackageName, long requestId)
|
||||
throws RemoteException {
|
||||
impl.cancelAuthenticationFromService(token, opPackageName, requestId);
|
||||
mSensorState = STATE_CANCELING;
|
||||
if (mSensorState != STATE_CANCELING) {
|
||||
impl.cancelAuthenticationFromService(token, opPackageName, requestId);
|
||||
mSensorState = STATE_CANCELING;
|
||||
}
|
||||
}
|
||||
|
||||
void goToStoppedStateIfCookieMatches(int cookie, int error) {
|
||||
|
||||
@@ -55,7 +55,6 @@ import android.os.DeadObjectException;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
@@ -84,6 +83,7 @@ import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* System service that arbitrates the modality for BiometricPrompt to use.
|
||||
@@ -92,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();
|
||||
|
||||
@@ -86,6 +86,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
|
||||
private long mStartTimeMs;
|
||||
|
||||
private boolean mAuthAttempted;
|
||||
private boolean mAuthSuccess = false;
|
||||
|
||||
// TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update
|
||||
// the state. We should think of a way to improve this in the future.
|
||||
@@ -237,6 +238,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
|
||||
"Successful background authentication!");
|
||||
}
|
||||
|
||||
mAuthSuccess = true;
|
||||
markAlreadyDone();
|
||||
|
||||
if (mTaskStackListener != null) {
|
||||
@@ -502,6 +504,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
|
||||
return mAuthAttempted;
|
||||
}
|
||||
|
||||
/** If an auth attempt completed successfully. */
|
||||
public boolean wasAuthSuccessful() {
|
||||
return mAuthSuccess;
|
||||
}
|
||||
|
||||
protected int getShowOverlayReason() {
|
||||
if (isKeyguard()) {
|
||||
return BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
|
||||
|
||||
@@ -316,7 +316,8 @@ public class BiometricScheduler {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
mBiometricService.onReadyForAuthentication(cookie);
|
||||
mBiometricService.onReadyForAuthentication(
|
||||
mCurrentOperation.getClientMonitor().getRequestId(), cookie);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(getTag(), "Remote exception when contacting BiometricService", e);
|
||||
}
|
||||
|
||||
@@ -173,18 +173,13 @@ public class CoexCoordinator {
|
||||
}
|
||||
|
||||
// SensorType to AuthenticationClient map
|
||||
private final Map<Integer, AuthenticationClient<?>> mClientMap;
|
||||
@VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths;
|
||||
private final Map<Integer, AuthenticationClient<?>> mClientMap = new HashMap<>();
|
||||
@VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths = new LinkedList<>();
|
||||
private boolean mAdvancedLogicEnabled;
|
||||
private boolean mFaceHapticDisabledWhenNonBypass;
|
||||
private final Handler mHandler;
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private CoexCoordinator() {
|
||||
// Singleton
|
||||
mClientMap = new HashMap<>();
|
||||
mSuccessfulAuths = new LinkedList<>();
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
private CoexCoordinator() {}
|
||||
|
||||
public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType,
|
||||
@NonNull AuthenticationClient<?> client) {
|
||||
@@ -221,8 +216,14 @@ public class CoexCoordinator {
|
||||
public void onAuthenticationSucceeded(long currentTimeMillis,
|
||||
@NonNull AuthenticationClient<?> client,
|
||||
@NonNull Callback callback) {
|
||||
final boolean isUsingSingleModality = isSingleAuthOnly(client);
|
||||
|
||||
if (client.isBiometricPrompt()) {
|
||||
callback.sendHapticFeedback();
|
||||
if (!isUsingSingleModality && hasMultipleSuccessfulAuthentications()) {
|
||||
// only send feedback on the first one
|
||||
} else {
|
||||
callback.sendHapticFeedback();
|
||||
}
|
||||
// For BP, BiometricService will add the authToken to Keystore.
|
||||
callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */);
|
||||
callback.handleLifecycleAfterAuth();
|
||||
@@ -234,7 +235,7 @@ public class CoexCoordinator {
|
||||
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
|
||||
callback.handleLifecycleAfterAuth();
|
||||
} else if (mAdvancedLogicEnabled && client.isKeyguard()) {
|
||||
if (isSingleAuthOnly(client)) {
|
||||
if (isUsingSingleModality) {
|
||||
// Single sensor authentication
|
||||
callback.sendHapticFeedback();
|
||||
callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */);
|
||||
@@ -295,10 +296,10 @@ public class CoexCoordinator {
|
||||
@NonNull AuthenticationClient<?> client,
|
||||
@LockoutTracker.LockoutMode int lockoutMode,
|
||||
@NonNull Callback callback) {
|
||||
final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
|
||||
final boolean isUsingSingleModality = isSingleAuthOnly(client);
|
||||
|
||||
if (keyguardAdvancedLogic) {
|
||||
if (isSingleAuthOnly(client)) {
|
||||
if (mAdvancedLogicEnabled && client.isKeyguard()) {
|
||||
if (isUsingSingleModality) {
|
||||
callback.sendHapticFeedback();
|
||||
callback.handleLifecycleAfterAuth();
|
||||
} else {
|
||||
@@ -319,8 +320,7 @@ public class CoexCoordinator {
|
||||
// also done now.
|
||||
callback.sendHapticFeedback();
|
||||
callback.handleLifecycleAfterAuth();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// UDFPS auth has never been attempted.
|
||||
if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) {
|
||||
Slog.w(TAG, "Skipping face reject haptic");
|
||||
@@ -360,6 +360,11 @@ public class CoexCoordinator {
|
||||
callback.handleLifecycleAfterAuth();
|
||||
}
|
||||
}
|
||||
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
|
||||
if (!isCurrentFaceAuth(client)) {
|
||||
callback.sendHapticFeedback();
|
||||
}
|
||||
callback.handleLifecycleAfterAuth();
|
||||
} else {
|
||||
callback.sendHapticFeedback();
|
||||
callback.handleLifecycleAfterAuth();
|
||||
@@ -380,6 +385,8 @@ public class CoexCoordinator {
|
||||
*/
|
||||
public void onAuthenticationError(@NonNull AuthenticationClient<?> client,
|
||||
@BiometricConstants.Errors int error, @NonNull ErrorCallback callback) {
|
||||
final boolean isUsingSingleModality = isSingleAuthOnly(client);
|
||||
|
||||
// Figure out non-coex state
|
||||
final boolean shouldUsuallyVibrate;
|
||||
if (isCurrentFaceAuth(client)) {
|
||||
@@ -401,25 +408,26 @@ public class CoexCoordinator {
|
||||
}
|
||||
|
||||
// Figure out coex state
|
||||
final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard();
|
||||
final boolean hapticSuppressedByCoex;
|
||||
|
||||
if (keyguardAdvancedLogic) {
|
||||
if (isSingleAuthOnly(client)) {
|
||||
if (mAdvancedLogicEnabled && client.isKeyguard()) {
|
||||
if (isUsingSingleModality) {
|
||||
hapticSuppressedByCoex = false;
|
||||
} else {
|
||||
hapticSuppressedByCoex = isCurrentFaceAuth(client)
|
||||
&& !client.isKeyguardBypassEnabled();
|
||||
}
|
||||
} else if (client.isBiometricPrompt() && !isUsingSingleModality) {
|
||||
hapticSuppressedByCoex = isCurrentFaceAuth(client);
|
||||
} else {
|
||||
hapticSuppressedByCoex = false;
|
||||
}
|
||||
|
||||
// Combine and send feedback if appropriate
|
||||
Slog.d(TAG, "shouldUsuallyVibrate: " + shouldUsuallyVibrate
|
||||
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
|
||||
if (shouldUsuallyVibrate && !hapticSuppressedByCoex) {
|
||||
callback.sendHapticFeedback();
|
||||
} else {
|
||||
Slog.v(TAG, "no haptic shouldUsuallyVibrate: " + shouldUsuallyVibrate
|
||||
+ ", hapticSuppressedByCoex: " + hapticSuppressedByCoex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +512,19 @@ public class CoexCoordinator {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasMultipleSuccessfulAuthentications() {
|
||||
int count = 0;
|
||||
for (AuthenticationClient<?> c : mClientMap.values()) {
|
||||
if (c.wasAuthSuccessful()) {
|
||||
count++;
|
||||
}
|
||||
if (count > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -856,11 +856,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBiometricAuthenticated() {
|
||||
public void onBiometricAuthenticated(@Modality int modality) {
|
||||
enforceBiometricDialog();
|
||||
if (mBar != null) {
|
||||
try {
|
||||
mBar.onBiometricAuthenticated();
|
||||
mBar.onBiometricAuthenticated(modality);
|
||||
} catch (RemoteException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.server.biometrics;
|
||||
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.Authenticators;
|
||||
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
|
||||
|
||||
@@ -85,14 +86,11 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Presubmit
|
||||
@SmallTest
|
||||
public class BiometricServiceTest {
|
||||
|
||||
private static final String TAG = "BiometricServiceTest";
|
||||
|
||||
private static final String TEST_PACKAGE_NAME = "test_package";
|
||||
private static final long TEST_REQUEST_ID = 44;
|
||||
|
||||
@@ -153,7 +151,7 @@ public class BiometricServiceTest {
|
||||
.thenReturn(mock(BiometricStrengthController.class));
|
||||
when(mInjector.getTrustManager()).thenReturn(mTrustManager);
|
||||
when(mInjector.getDevicePolicyManager(any())).thenReturn(mDevicePolicyManager);
|
||||
when(mInjector.getRequestGenerator()).thenReturn(new AtomicLong(TEST_REQUEST_ID - 1));
|
||||
when(mInjector.getRequestGenerator()).thenReturn(() -> TEST_REQUEST_ID);
|
||||
|
||||
when(mResources.getString(R.string.biometric_error_hw_unavailable))
|
||||
.thenReturn(ERROR_HW_UNAVAILABLE);
|
||||
@@ -178,22 +176,22 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
true /* requireConfirmation */, null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession),
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
|
||||
anyInt());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
mBiometricService.mCurrentAuthSession.binderDied();
|
||||
mBiometricService.mAuthSession.binderDied();
|
||||
waitForIdle();
|
||||
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
@@ -205,31 +203,31 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
true /* requireConfirmation */, null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession),
|
||||
verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession),
|
||||
anyInt());
|
||||
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mCurrentAuthSession.binderDied();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
mBiometricService.mAuthSession.binderDied();
|
||||
waitForIdle();
|
||||
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
|
||||
assertEquals(STATE_CLIENT_DIED_CANCELLING,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
|
||||
verify(mBiometricService.mCurrentAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl)
|
||||
verify(mBiometricService.mAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl)
|
||||
.cancelAuthenticationFromService(any(), any(), anyLong());
|
||||
|
||||
// Simulate ERROR_CANCELED received from HAL
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -265,12 +263,12 @@ public class BiometricServiceTest {
|
||||
Authenticators.DEVICE_CREDENTIAL);
|
||||
waitForIdle();
|
||||
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
// StatusBar showBiometricDialog invoked
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -304,21 +302,21 @@ public class BiometricServiceTest {
|
||||
mBiometricService = new BiometricService(mContext, mInjector);
|
||||
mBiometricService.onStart();
|
||||
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate_notStrongEnough_returnsHardwareNotPresent() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
Authenticators.BIOMETRIC_STRONG);
|
||||
@@ -335,7 +333,7 @@ public class BiometricServiceTest {
|
||||
// is able to proceed.
|
||||
|
||||
final int[] modalities = new int[] {
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT,
|
||||
TYPE_FINGERPRINT,
|
||||
BiometricAuthenticator.TYPE_FACE,
|
||||
};
|
||||
|
||||
@@ -356,7 +354,7 @@ public class BiometricServiceTest {
|
||||
|
||||
// StatusBar showBiometricDialog invoked with face, which was set up to be STRONG
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[] {SENSOR_ID_FACE}),
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -377,14 +375,14 @@ public class BiometricServiceTest {
|
||||
mBiometricService = new BiometricService(mContext, mInjector);
|
||||
mBiometricService.onStart();
|
||||
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
null /* authenticators */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
@@ -415,13 +413,13 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
final byte[] HAT = generateRandomHAT();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Confirmation is required
|
||||
assertEquals(STATE_AUTH_PENDING_CONFIRM,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Enrolled, not disabled in settings, user doesn't require confirmation in settings
|
||||
resetReceivers();
|
||||
@@ -431,25 +429,25 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
|
||||
null /* authenticators */);
|
||||
waitForIdle();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Confirmation not required, waiting for dialog to dismiss
|
||||
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate_happyPathWithoutConfirmation_strongBiometric() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
testAuthenticate_happyPathWithoutConfirmation(true /* isStrongBiometric */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate_happyPathWithoutConfirmation_weakBiometric() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
testAuthenticate_happyPathWithoutConfirmation(false /* isStrongBiometric */);
|
||||
}
|
||||
|
||||
@@ -461,7 +459,7 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
// Creates a pending auth session with the correct initial states
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Invokes <Modality>Service#prepareForAuthentication
|
||||
ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
@@ -477,19 +475,19 @@ public class BiometricServiceTest {
|
||||
cookieCaptor.capture() /* cookie */,
|
||||
anyBoolean() /* allowBackgroundAuthentication */);
|
||||
|
||||
// onReadyForAuthentication, mCurrentAuthSession state OK
|
||||
mBiometricService.mImpl.onReadyForAuthentication(cookieCaptor.getValue());
|
||||
// onReadyForAuthentication, mAuthSession state OK
|
||||
mBiometricService.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookieCaptor.getValue());
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// startPreparedClient invoked
|
||||
mBiometricService.mCurrentAuthSession.onDialogAnimatedIn();
|
||||
mBiometricService.mAuthSession.onDialogAnimatedIn();
|
||||
verify(mBiometricService.mSensors.get(0).impl)
|
||||
.startPreparedClient(cookieCaptor.getValue());
|
||||
|
||||
// StatusBar showBiometricDialog invoked
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
any(),
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -502,18 +500,18 @@ public class BiometricServiceTest {
|
||||
|
||||
// Hardware authenticated
|
||||
final byte[] HAT = generateRandomHAT();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Waiting for SystemUI to send dismissed callback
|
||||
assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
// Notify SystemUI hardware authenticated
|
||||
verify(mBiometricService.mStatusBarService).onBiometricAuthenticated();
|
||||
verify(mBiometricService.mStatusBarService).onBiometricAuthenticated(TYPE_FINGERPRINT);
|
||||
|
||||
// SystemUI sends callback with dismissed reason
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED,
|
||||
null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
@@ -527,7 +525,7 @@ public class BiometricServiceTest {
|
||||
verify(mReceiver1).onAuthenticationSucceeded(
|
||||
BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC);
|
||||
// Current session becomes null
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -542,11 +540,11 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
assertEquals(Authenticators.DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
|
||||
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -578,16 +576,16 @@ public class BiometricServiceTest {
|
||||
// Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not
|
||||
// sent to KeyStore yet
|
||||
final byte[] HAT = generateRandomHAT();
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
HAT);
|
||||
waitForIdle();
|
||||
// Waiting for SystemUI to send confirmation callback
|
||||
assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
|
||||
|
||||
// SystemUI sends confirm, HAT is sent to keystore and client is notified.
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED,
|
||||
null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
@@ -624,33 +622,34 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE);
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE);
|
||||
waitForIdle();
|
||||
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_NONE),
|
||||
eq(BiometricAuthenticator.TYPE_FACE),
|
||||
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mReceiver1).onAuthenticationFailed();
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FINGERPRINT);
|
||||
mBiometricService.mAuthSession.mSensorReceiver
|
||||
.onAuthenticationFailed(SENSOR_ID_FINGERPRINT);
|
||||
waitForIdle();
|
||||
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_NONE),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mReceiver1).onAuthenticationFailed();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -678,14 +677,14 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FACE),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT),
|
||||
@@ -694,15 +693,15 @@ public class BiometricServiceTest {
|
||||
verify(mReceiver1, never()).onAuthenticationFailed();
|
||||
|
||||
// No auth session. Pressing try again will create one.
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Pressing "Try again" on SystemUI
|
||||
mBiometricService.mSysuiReceiver.onTryAgainPressed();
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onTryAgainPressed();
|
||||
waitForIdle();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
|
||||
// AuthSession is now resuming
|
||||
assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mAuthSession.getState());
|
||||
|
||||
// Test resuming when hardware becomes ready. SystemUI should not be requested to
|
||||
// show another dialog since it's already showing.
|
||||
@@ -728,14 +727,14 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
@@ -748,7 +747,7 @@ public class BiometricServiceTest {
|
||||
// Dialog is hidden immediately
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
// Auth session is over
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -757,61 +756,61 @@ public class BiometricServiceTest {
|
||||
// For errors that show in SystemUI, BiometricService stays in STATE_ERROR_PENDING_SYSUI
|
||||
// until SystemUI notifies us that the dialog is dismissed at which point the current
|
||||
// session is done.
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// Sends error to SystemUI and does not notify client yet
|
||||
assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog();
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
|
||||
// SystemUI animation completed, client is notified, auth session is over
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_ERROR, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
|
||||
eq(0 /* vendorCode */));
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */,
|
||||
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForPendingSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForPendingSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// We should be showing device credential now
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
assertEquals(Authenticators.DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
|
||||
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -826,23 +825,23 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
waitForIdle();
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForPendingSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForPendingSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// Error is sent to client
|
||||
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
|
||||
eq(0) /* vendorCode */);
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -861,7 +860,7 @@ public class BiometricServiceTest {
|
||||
|
||||
private void testBiometricAuth_whenLockout(@LockoutTracker.LockoutMode int lockoutMode,
|
||||
int biometricPromptError) throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
|
||||
.thenReturn(lockoutMode);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
@@ -869,16 +868,15 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
// Modality and error are sent
|
||||
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
|
||||
eq(biometricPromptError), eq(0) /* vendorCode */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricOrCredentialAuth_whenBiometricLockout_showsCredential()
|
||||
throws Exception {
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
.thenReturn(true);
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
|
||||
.thenReturn(LockoutTracker.LOCKOUT_PERMANENT);
|
||||
invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
|
||||
@@ -887,13 +885,13 @@ public class BiometricServiceTest {
|
||||
waitForIdle();
|
||||
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
assertEquals(Authenticators.DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators());
|
||||
mBiometricService.mAuthSession.mPromptInfo.getAuthenticators());
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -959,73 +957,73 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */,
|
||||
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
mBiometricService.mSysuiReceiver.onDeviceCredentialPressed();
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDeviceCredentialPressed();
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockout_whileAuthenticating_credentialAllowed() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */,
|
||||
Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockout_whenAuthenticating_credentialNotAllowed() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
assertEquals(STATE_ERROR_PENDING_SYSUI,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mBiometricService.mStatusBarService).onBiometricError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS),
|
||||
eq(0 /* vendorCode */));
|
||||
}
|
||||
@@ -1033,20 +1031,20 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mSensors.get(0).impl).cancelAuthenticationFromService(
|
||||
any(), any(), anyLong());
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1055,12 +1053,12 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
|
||||
@@ -1069,18 +1067,17 @@ public class BiometricServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws
|
||||
Exception {
|
||||
public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FACE,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
0 /* vendorCode */);
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
|
||||
@@ -1094,10 +1091,10 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
true /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded(
|
||||
SENSOR_ID_FACE,
|
||||
new byte[69] /* HAT */);
|
||||
mBiometricService.mSysuiReceiver.onDialogDismissed(
|
||||
mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
|
||||
BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */);
|
||||
waitForIdle();
|
||||
|
||||
@@ -1108,19 +1105,19 @@ public class BiometricServiceTest {
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
|
||||
assertNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNull(mBiometricService.mAuthSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception {
|
||||
when(mContext.getResources().getString(anyInt())).thenReturn("test string");
|
||||
|
||||
final int modality = BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
final int modality = TYPE_FINGERPRINT;
|
||||
setupAuthForOnly(modality, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mBiometricSensorReceiver.onAcquired(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onAcquired(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
|
||||
0 /* vendorCode */);
|
||||
@@ -1130,29 +1127,29 @@ public class BiometricServiceTest {
|
||||
// string is retrieved for now, but it's also very unlikely to break anyway.
|
||||
verify(mBiometricService.mStatusBarService)
|
||||
.onBiometricHelp(eq(modality), anyString());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancel_whenAuthenticating() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, null /* authenticators */);
|
||||
|
||||
mBiometricService.mImpl.cancelAuthentication(mBiometricService.mCurrentAuthSession.mToken,
|
||||
mBiometricService.mImpl.cancelAuthentication(mBiometricService.mAuthSession.mToken,
|
||||
TEST_PACKAGE_NAME, TEST_REQUEST_ID);
|
||||
waitForIdle();
|
||||
|
||||
// Pretend that the HAL has responded to cancel with ERROR_CANCELED
|
||||
mBiometricService.mBiometricSensorReceiver.onError(
|
||||
mBiometricService.mAuthSession.mSensorReceiver.onError(
|
||||
SENSOR_ID_FINGERPRINT,
|
||||
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
|
||||
getCookieForCurrentSession(mBiometricService.mAuthSession),
|
||||
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
|
||||
0 /* vendorCode */);
|
||||
waitForIdle();
|
||||
|
||||
// Hides system dialog and invokes the onError callback
|
||||
verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
verify(mReceiver1).onError(eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
|
||||
eq(0 /* vendorCode */));
|
||||
verify(mBiometricService.mStatusBarService).hideAuthenticationDialog();
|
||||
@@ -1161,7 +1158,7 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testCanAuthenticate_whenDeviceHasRequestedBiometricStrength() throws Exception {
|
||||
// When only biometric is requested, and sensor is strong enough
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
|
||||
assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
|
||||
invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG));
|
||||
@@ -1170,7 +1167,7 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testCanAuthenticate_whenDeviceDoesNotHaveRequestedBiometricStrength()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK);
|
||||
|
||||
// When only biometric is requested, and sensor is not strong enough
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
@@ -1208,9 +1205,8 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testCanAuthenticate_whenNoBiometricsEnrolled() throws Exception {
|
||||
// With credential set up, test the following.
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
.thenReturn(true);
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
false /* enrolled */);
|
||||
|
||||
// When only biometric is requested
|
||||
@@ -1277,7 +1273,7 @@ public class BiometricServiceTest {
|
||||
private void testCanAuthenticate_whenLockedOut(@LockoutTracker.LockoutMode int lockoutMode)
|
||||
throws Exception {
|
||||
// When only biometric is requested, and sensor is strong enough
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
|
||||
when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt()))
|
||||
.thenReturn(lockoutMode);
|
||||
@@ -1311,7 +1307,7 @@ public class BiometricServiceTest {
|
||||
for (int i = 0; i < testCases.length; i++) {
|
||||
final BiometricSensor sensor =
|
||||
new BiometricSensor(mContext, 0 /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT,
|
||||
TYPE_FINGERPRINT,
|
||||
testCases[i][0],
|
||||
mock(IBiometricAuthenticator.class)) {
|
||||
@Override
|
||||
@@ -1341,7 +1337,7 @@ public class BiometricServiceTest {
|
||||
.thenReturn(true);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
mBiometricService.mImpl.registerAuthenticator(0 /* testId */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
verify(mBiometricService.mBiometricStrengthController).updateStrengths();
|
||||
@@ -1360,7 +1356,7 @@ public class BiometricServiceTest {
|
||||
.thenReturn(true);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
mBiometricService.mImpl.registerAuthenticator(testId /* id */,
|
||||
BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
|
||||
mFingerprintAuthenticator);
|
||||
|
||||
// Downgrade the authenticator
|
||||
@@ -1378,7 +1374,7 @@ public class BiometricServiceTest {
|
||||
false /* requireConfirmation */, authenticators);
|
||||
waitForIdle();
|
||||
verify(mReceiver1).onError(
|
||||
eq(BiometricAuthenticator.TYPE_FINGERPRINT),
|
||||
eq(TYPE_FINGERPRINT),
|
||||
eq(BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED),
|
||||
eq(0) /* vendorCode */);
|
||||
|
||||
@@ -1392,7 +1388,7 @@ public class BiometricServiceTest {
|
||||
authenticators);
|
||||
waitForIdle();
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[] {testId}),
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -1414,9 +1410,9 @@ public class BiometricServiceTest {
|
||||
false /* requireConfirmation */,
|
||||
authenticators);
|
||||
waitForIdle();
|
||||
assertTrue(Utils.isCredentialRequested(mBiometricService.mCurrentAuthSession.mPromptInfo));
|
||||
assertTrue(Utils.isCredentialRequested(mBiometricService.mAuthSession.mPromptInfo));
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[0]) /* sensorIds */,
|
||||
eq(true) /* credentialAllowed */,
|
||||
@@ -1442,7 +1438,7 @@ public class BiometricServiceTest {
|
||||
false /* requireConfirmation */, authenticators);
|
||||
waitForIdle();
|
||||
verify(mBiometricService.mStatusBarService).showAuthenticationDialog(
|
||||
eq(mBiometricService.mCurrentAuthSession.mPromptInfo),
|
||||
eq(mBiometricService.mAuthSession.mPromptInfo),
|
||||
any(IBiometricSysuiReceiver.class),
|
||||
AdditionalMatchers.aryEq(new int[] {testId}) /* sensorIds */,
|
||||
eq(false) /* credentialAllowed */,
|
||||
@@ -1495,29 +1491,29 @@ public class BiometricServiceTest {
|
||||
@Test
|
||||
public void testWorkAuthentication_fingerprintWorksIfNotDisabledByDevicePolicyManager()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mDevicePolicyManager
|
||||
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
|
||||
.thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
|
||||
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
|
||||
Authenticators.BIOMETRIC_STRONG);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
startPendingAuthSession(mBiometricService);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthentication_normalAppIgnoresDevicePolicy() throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mDevicePolicyManager
|
||||
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
|
||||
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
|
||||
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
|
||||
false /* requireConfirmation */, Authenticators.BIOMETRIC_STRONG);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1530,18 +1526,17 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1,
|
||||
Authenticators.BIOMETRIC_STRONG);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState());
|
||||
startPendingAuthSession(mBiometricService);
|
||||
waitForIdle();
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkAuthentication_fingerprintFailsIfDisabledByDevicePolicyManager()
|
||||
throws Exception {
|
||||
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
|
||||
.thenReturn(true);
|
||||
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
|
||||
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
|
||||
when(mDevicePolicyManager
|
||||
.getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */))
|
||||
.thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
|
||||
@@ -1555,9 +1550,9 @@ public class BiometricServiceTest {
|
||||
invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver2,
|
||||
Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL);
|
||||
waitForIdle();
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL,
|
||||
mBiometricService.mCurrentAuthSession.getState());
|
||||
mBiometricService.mAuthSession.getState());
|
||||
verify(mReceiver2, never()).onError(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@@ -1580,7 +1575,7 @@ public class BiometricServiceTest {
|
||||
|
||||
when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
|
||||
|
||||
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) {
|
||||
if ((modality & TYPE_FINGERPRINT) != 0) {
|
||||
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
|
||||
.thenReturn(enrolled);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
@@ -1614,7 +1609,7 @@ public class BiometricServiceTest {
|
||||
final int modality = modalities[i];
|
||||
final int strength = strengths[i];
|
||||
|
||||
if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) {
|
||||
if ((modality & TYPE_FINGERPRINT) != 0) {
|
||||
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any()))
|
||||
.thenReturn(true);
|
||||
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
|
||||
@@ -1654,8 +1649,9 @@ public class BiometricServiceTest {
|
||||
startPendingAuthSession(mBiometricService);
|
||||
waitForIdle();
|
||||
|
||||
assertNotNull(mBiometricService.mCurrentAuthSession);
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState());
|
||||
assertNotNull(mBiometricService.mAuthSession);
|
||||
assertEquals(TEST_REQUEST_ID, mBiometricService.mAuthSession.getRequestId());
|
||||
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
|
||||
|
||||
return requestId;
|
||||
}
|
||||
@@ -1663,14 +1659,14 @@ public class BiometricServiceTest {
|
||||
private static void startPendingAuthSession(BiometricService service) throws Exception {
|
||||
// Get the cookie so we can pretend the hardware is ready to authenticate
|
||||
// Currently we only support single modality per auth
|
||||
final PreAuthInfo preAuthInfo = service.mCurrentAuthSession.mPreAuthInfo;
|
||||
final PreAuthInfo preAuthInfo = service.mAuthSession.mPreAuthInfo;
|
||||
assertEquals(preAuthInfo.eligibleSensors.size(), 1);
|
||||
assertEquals(preAuthInfo.numSensorsWaitingForCookie(), 1);
|
||||
|
||||
final int cookie = preAuthInfo.eligibleSensors.get(0).getCookie();
|
||||
assertNotEquals(cookie, 0);
|
||||
|
||||
service.mImpl.onReadyForAuthentication(cookie);
|
||||
service.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookie);
|
||||
}
|
||||
|
||||
private static long invokeAuthenticate(IBiometricService.Stub service,
|
||||
|
||||
@@ -29,12 +29,8 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.biometrics.BiometricConstants;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
@@ -43,9 +39,11 @@ import androidx.test.filters.SmallTest;
|
||||
import com.android.server.biometrics.sensors.fingerprint.Udfps;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
@@ -53,39 +51,38 @@ import java.util.LinkedList;
|
||||
@SmallTest
|
||||
public class CoexCoordinatorTest {
|
||||
|
||||
private static final String TAG = "CoexCoordinatorTest";
|
||||
@Rule
|
||||
public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
|
||||
private CoexCoordinator mCoexCoordinator;
|
||||
private Handler mHandler;
|
||||
|
||||
@Mock
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private CoexCoordinator.Callback mCallback;
|
||||
@Mock
|
||||
private CoexCoordinator.ErrorCallback mErrorCallback;
|
||||
@Mock
|
||||
private AuthenticationClient mFaceClient;
|
||||
@Mock
|
||||
private AuthenticationClient mFingerprintClient;
|
||||
@Mock(extraInterfaces = {Udfps.class})
|
||||
private AuthenticationClient mUdfpsClient;
|
||||
|
||||
private CoexCoordinator mCoexCoordinator;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
mCoexCoordinator = CoexCoordinator.getInstance();
|
||||
mCoexCoordinator.setAdvancedLogicEnabled(true);
|
||||
mCoexCoordinator.setFaceHapticDisabledWhenNonBypass(true);
|
||||
mCoexCoordinator.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_authSuccess() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -93,15 +90,12 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_authReject_whenNotLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
client, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -109,30 +103,97 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_authReject_whenLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
client, LockoutTracker.LOCKOUT_TIMED, mCallback);
|
||||
mFaceClient, LockoutTracker.LOCKOUT_TIMED, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback, never()).sendAuthenticationResult(anyBoolean());
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_success() {
|
||||
testBiometricPrompt_coex_success(false /* twice */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_successWithoutDouble() {
|
||||
testBiometricPrompt_coex_success(true /* twice */);
|
||||
}
|
||||
|
||||
private void testBiometricPrompt_coex_success(boolean twice) {
|
||||
initFaceAndFingerprintForBiometricPrompt();
|
||||
when(mFaceClient.wasAuthSuccessful()).thenReturn(true);
|
||||
when(mUdfpsClient.wasAuthSuccessful()).thenReturn(twice, true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mUdfpsClient, mCallback);
|
||||
|
||||
if (twice) {
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
} else {
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_reject() {
|
||||
initFaceAndFingerprintForBiometricPrompt();
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_coex_errorNoHaptics() {
|
||||
initFaceAndFingerprintForBiometricPrompt();
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
mCoexCoordinator.onAuthenticationError(mUdfpsClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
|
||||
verify(mErrorCallback, never()).sendHapticFeedback();
|
||||
}
|
||||
|
||||
private void initFaceAndFingerprintForBiometricPrompt() {
|
||||
when(mFaceClient.isKeyguard()).thenReturn(false);
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(false);
|
||||
when(mUdfpsClient.isBiometricPrompt()).thenReturn(true);
|
||||
when(mUdfpsClient.wasAuthAttempted()).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyguard_faceAuthOnly_success() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isKeyguard()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -140,21 +201,16 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_faceAuth_udfpsNotTouching_faceSuccess() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
|
||||
mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFaceClient, mCallback);
|
||||
// Haptics tested in #testKeyguard_bypass_haptics. Let's leave this commented out (instead
|
||||
// of removed) to keep this context.
|
||||
// verify(mCallback).sendHapticFeedback();
|
||||
@@ -192,25 +248,19 @@ public class CoexCoordinatorTest {
|
||||
|
||||
private void testKeyguard_bypass_haptics(boolean bypassEnabled, boolean faceAccepted,
|
||||
boolean shouldReceiveHaptics) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
if (faceAccepted) {
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
|
||||
mCallback);
|
||||
} else {
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
}
|
||||
|
||||
@@ -244,24 +294,18 @@ public class CoexCoordinatorTest {
|
||||
|
||||
private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted,
|
||||
long udfpsRejectedAfterMs) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
when (udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
// For easier reading
|
||||
final CoexCoordinator.Callback faceCallback = mCallback;
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient,
|
||||
faceCallback);
|
||||
verify(faceCallback, never()).sendHapticFeedback();
|
||||
verify(faceCallback, never()).sendAuthenticationResult(anyBoolean());
|
||||
@@ -272,9 +316,9 @@ public class CoexCoordinatorTest {
|
||||
// Reset the mock
|
||||
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
|
||||
assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size());
|
||||
assertEquals(faceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
|
||||
assertEquals(mFaceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient);
|
||||
if (thenUdfpsAccepted) {
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
|
||||
udfpsCallback);
|
||||
verify(udfpsCallback).sendHapticFeedback();
|
||||
verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */);
|
||||
@@ -284,7 +328,7 @@ public class CoexCoordinatorTest {
|
||||
|
||||
assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty());
|
||||
} else {
|
||||
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, mUdfpsClient,
|
||||
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
|
||||
if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) {
|
||||
verify(udfpsCallback, never()).sendHapticFeedback();
|
||||
@@ -310,56 +354,44 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_udfpsAuthSuccess_whileFaceScanning() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient,
|
||||
mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(true));
|
||||
verify(faceClient).cancel();
|
||||
verify(mFaceClient).cancel();
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyguard_faceRejectedWhenUdfpsTouching_thenUdfpsRejected() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
|
||||
// BiometricScheduler removes the face authentication client after rejection
|
||||
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
// Then UDFPS rejected
|
||||
CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class);
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, udfpsClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mUdfpsClient,
|
||||
LockoutTracker.LOCKOUT_NONE, udfpsCallback);
|
||||
verify(udfpsCallback).sendHapticFeedback();
|
||||
verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
@@ -368,26 +400,20 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_udfpsRejected_thenFaceRejected_noKeyguardBypass() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
|
||||
when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(faceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, udfpsClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
// Auth was attempted
|
||||
when(udfpsClient.getState())
|
||||
when(mUdfpsClient.getState())
|
||||
.thenReturn(AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED);
|
||||
verify(mCallback, never()).sendHapticFeedback();
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -395,7 +421,7 @@ public class CoexCoordinatorTest {
|
||||
// Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since
|
||||
// unlike face, its lifecycle becomes "paused" instead of "finished".
|
||||
CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class);
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, faceCallback);
|
||||
verify(faceCallback).sendHapticFeedback();
|
||||
verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
@@ -404,20 +430,16 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_capacitiveAccepted_whenFaceScanning() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
|
||||
|
||||
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class);
|
||||
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(fpClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, fpClient, mCallback);
|
||||
mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */,
|
||||
mFingerprintClient, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -425,21 +447,16 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testKeyguard_capacitiveRejected_whenFaceScanning() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(mFingerprintClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient);
|
||||
|
||||
AuthenticationClient<?> fpClient = mock(AuthenticationClient.class);
|
||||
when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED);
|
||||
when(fpClient.isKeyguard()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, fpClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */,
|
||||
mFingerprintClient, LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */);
|
||||
verify(mCallback).handleLifecycleAfterAuth();
|
||||
@@ -447,14 +464,11 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testNonKeyguard_rejectAndNotLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(false);
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(false);
|
||||
when(faceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_NONE, mCallback);
|
||||
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
@@ -464,14 +478,11 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testNonKeyguard_rejectLockedOut() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(false);
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(false);
|
||||
when(faceClient.isBiometricPrompt()).thenReturn(true);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient,
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient,
|
||||
LockoutTracker.LOCKOUT_TIMED, mCallback);
|
||||
|
||||
verify(mCallback).sendHapticFeedback();
|
||||
@@ -496,16 +507,13 @@ public class CoexCoordinatorTest {
|
||||
|
||||
@Test
|
||||
public void testBiometricPrompt_FaceError() {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isBiometricPrompt()).thenReturn(true);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isBiometricPrompt()).thenReturn(true);
|
||||
when(client.wasAuthAttempted()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
mErrorCallback);
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
verify(mErrorCallback).sendHapticFeedback();
|
||||
}
|
||||
|
||||
@@ -520,18 +528,15 @@ public class CoexCoordinatorTest {
|
||||
}
|
||||
|
||||
private void testKeyguard_faceAuthOnly(boolean bypassEnabled) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(mFaceClient.wasUserDetected()).thenReturn(true);
|
||||
|
||||
AuthenticationClient<?> client = mock(AuthenticationClient.class);
|
||||
when(client.isKeyguard()).thenReturn(true);
|
||||
when(client.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(client.wasAuthAttempted()).thenReturn(true);
|
||||
when(client.wasUserDetected()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT,
|
||||
mErrorCallback);
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
verify(mErrorCallback).sendHapticFeedback();
|
||||
}
|
||||
|
||||
@@ -546,23 +551,17 @@ public class CoexCoordinatorTest {
|
||||
}
|
||||
|
||||
private void testKeyguard_coex_faceError(boolean bypassEnabled) {
|
||||
mCoexCoordinator.reset();
|
||||
when(mFaceClient.isKeyguard()).thenReturn(true);
|
||||
when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(mFaceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(mFaceClient.wasUserDetected()).thenReturn(true);
|
||||
when(mUdfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
AuthenticationClient<?> faceClient = mock(AuthenticationClient.class);
|
||||
when(faceClient.isKeyguard()).thenReturn(true);
|
||||
when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled);
|
||||
when(faceClient.wasAuthAttempted()).thenReturn(true);
|
||||
when(faceClient.wasUserDetected()).thenReturn(true);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient);
|
||||
|
||||
AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class,
|
||||
withSettings().extraInterfaces(Udfps.class));
|
||||
when(udfpsClient.isKeyguard()).thenReturn(true);
|
||||
when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false);
|
||||
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient);
|
||||
mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient);
|
||||
|
||||
mCoexCoordinator.onAuthenticationError(faceClient,
|
||||
mCoexCoordinator.onAuthenticationError(mFaceClient,
|
||||
BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback);
|
||||
|
||||
if (bypassEnabled) {
|
||||
|
||||
Reference in New Issue
Block a user