Remove face to fingerprint multi-sensor behavior.

Refactor existing tests to remove unnecessary mocks and boilerplate code to
cover more of the real behavior. This is prep for adding new tests in another
change to add the new face and fingerprint behavior.

Bug: 217393533
Test: atest AuthBiometricViewTest AuthContainerViewTest
Test: manual (authenticate using test app)

Change-Id: If984d1c07fee98d2d7a20cb0025ad434ad9977f4
This commit is contained in:
Joe Bolinger
2022-02-03 10:08:24 -08:00
parent b7e2433127
commit 1c9a06276a
30 changed files with 952 additions and 1982 deletions

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
~ limitations under the License.
-->
<com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView
<com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -22,4 +22,4 @@
<include layout="@layout/auth_biometric_contents"/>
</com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView>
</com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView>

View File

@@ -0,0 +1,26 @@
<!--
~ 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.
-->
<com.android.systemui.biometrics.AuthBiometricView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/auth_biometric_contents"/>
</com.android.systemui.biometrics.AuthBiometricView>

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import android.content.Context;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
class AuthBiometricFaceIconController extends Animatable2.AnimationCallback {
private static final String TAG = "AuthBiometricFaceIconController";
protected Context mContext;
protected ImageView mIconView;
protected TextView mTextView;
protected Handler mHandler;
protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
@AuthBiometricView.BiometricState protected int mState;
protected boolean mDeactivated;
protected AuthBiometricFaceIconController(Context context, ImageView iconView,
TextView textView) {
mContext = context;
mIconView = iconView;
mTextView = textView;
mHandler = new Handler(Looper.getMainLooper());
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
}
protected void animateOnce(int iconRes) {
animateIcon(iconRes, false);
}
protected void showStaticDrawable(int iconRes) {
mIconView.setImageDrawable(mContext.getDrawable(iconRes));
}
protected void animateIcon(int iconRes, boolean repeat) {
Log.d(TAG, "animateIcon, state: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
final AnimatedVectorDrawable icon =
(AnimatedVectorDrawable) mContext.getDrawable(iconRes);
mIconView.setImageDrawable(icon);
icon.forceAnimationOnUI();
if (repeat) {
icon.registerAnimationCallback(this);
}
icon.start();
}
protected void startPulsing() {
mLastPulseLightToDark = false;
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true);
}
protected void pulseInNextDirection() {
int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light
: R.drawable.face_dialog_pulse_light_to_dark;
animateIcon(iconRes, true /* repeat */);
mLastPulseLightToDark = !mLastPulseLightToDark;
}
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
Log.d(TAG, "onAnimationEnd, mState: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
if (mState == AuthBiometricView.STATE_AUTHENTICATING
|| mState == AuthBiometricView.STATE_HELP) {
pulseInNextDirection();
}
}
protected void deactivate() {
mDeactivated = true;
}
protected void updateState(int lastState, int newState) {
if (mDeactivated) {
Log.w(TAG, "Ignoring updateState when deactivated: " + newState);
return;
}
final boolean lastStateIsErrorIcon =
lastState == AuthBiometricView.STATE_ERROR
|| lastState == AuthBiometricView.STATE_HELP;
if (newState == AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN) {
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (newState == AuthBiometricView.STATE_AUTHENTICATING) {
startPulsing();
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (lastState == AuthBiometricView.STATE_PENDING_CONFIRMATION
&& newState == AuthBiometricView.STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_confirmed));
} else if (lastStateIsErrorIcon && newState == AuthBiometricView.STATE_IDLE) {
animateOnce(R.drawable.face_dialog_error_to_idle);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_idle));
} else if (lastStateIsErrorIcon && newState == AuthBiometricView.STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == AuthBiometricView.STATE_ERROR
&& lastState != AuthBiometricView.STATE_ERROR) {
animateOnce(R.drawable.face_dialog_dark_to_error);
} else if (lastState == AuthBiometricView.STATE_AUTHENTICATING
&& newState == AuthBiometricView.STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == AuthBiometricView.STATE_PENDING_CONFIRMATION) {
animateOnce(R.drawable.face_dialog_wink_from_dark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == AuthBiometricView.STATE_IDLE) {
showStaticDrawable(R.drawable.face_dialog_idle_static);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_idle));
} else {
Log.w(TAG, "Unhandled state: " + newState);
}
mState = newState;
}
}

View File

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

View File

@@ -17,152 +17,23 @@
package com.android.systemui.biometrics;
import android.content.Context;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
public class AuthBiometricFaceView extends AuthBiometricView {
private static final String TAG = "BiometricPrompt/AuthBiometricFaceView";
private static final String TAG = "AuthBiometricFaceView";
// Delay before dismissing after being authenticated/confirmed.
private static final int HIDE_DELAY_MS = 500;
protected static class IconController extends Animatable2.AnimationCallback {
protected Context mContext;
protected ImageView mIconView;
protected TextView mTextView;
protected Handler mHandler;
protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
protected @BiometricState int mState;
protected boolean mDeactivated;
protected IconController(Context context, ImageView iconView, TextView textView) {
mContext = context;
mIconView = iconView;
mTextView = textView;
mHandler = new Handler(Looper.getMainLooper());
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
}
protected void animateOnce(int iconRes) {
animateIcon(iconRes, false);
}
protected void showStaticDrawable(int iconRes) {
mIconView.setImageDrawable(mContext.getDrawable(iconRes));
}
protected void animateIcon(int iconRes, boolean repeat) {
Log.d(TAG, "animateIcon, state: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
final AnimatedVectorDrawable icon =
(AnimatedVectorDrawable) mContext.getDrawable(iconRes);
mIconView.setImageDrawable(icon);
icon.forceAnimationOnUI();
if (repeat) {
icon.registerAnimationCallback(this);
}
icon.start();
}
protected void startPulsing() {
mLastPulseLightToDark = false;
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true);
}
protected void pulseInNextDirection() {
int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light
: R.drawable.face_dialog_pulse_light_to_dark;
animateIcon(iconRes, true /* repeat */);
mLastPulseLightToDark = !mLastPulseLightToDark;
}
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
Log.d(TAG, "onAnimationEnd, mState: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
if (mState == STATE_AUTHENTICATING || mState == STATE_HELP) {
pulseInNextDirection();
}
}
protected void deactivate() {
mDeactivated = true;
}
protected void updateState(int lastState, int newState) {
if (mDeactivated) {
Log.w(TAG, "Ignoring updateState when deactivated: " + newState);
return;
}
final boolean lastStateIsErrorIcon =
lastState == STATE_ERROR || lastState == STATE_HELP;
if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (newState == STATE_AUTHENTICATING) {
startPulsing();
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (lastState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_confirmed));
} else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
animateOnce(R.drawable.face_dialog_error_to_idle);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_idle));
} else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_ERROR && lastState != STATE_ERROR) {
animateOnce(R.drawable.face_dialog_dark_to_error);
} else if (lastState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_PENDING_CONFIRMATION) {
animateOnce(R.drawable.face_dialog_wink_from_dark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_IDLE) {
showStaticDrawable(R.drawable.face_dialog_idle_static);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_idle));
} else {
Log.w(TAG, "Unhandled state: " + newState);
}
mState = newState;
}
}
@Nullable @VisibleForTesting IconController mFaceIconController;
@Nullable @VisibleForTesting AuthBiometricFaceIconController mFaceIconController;
@NonNull private final OnAttachStateChangeListener mOnAttachStateChangeListener =
new OnAttachStateChangeListener() {
@Override
@@ -184,15 +55,10 @@ public class AuthBiometricFaceView extends AuthBiometricView {
super(context, attrs);
}
@VisibleForTesting
AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector) {
super(context, attrs, injector);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFaceIconController = new IconController(mContext, mIconView, mIndicatorView);
mFaceIconController = new AuthBiometricFaceIconController(mContext, mIconView, mIndicatorView);
addOnAttachStateChangeListener(mOnAttachStateChangeListener);
}

View File

@@ -0,0 +1,29 @@
/*
* 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.util.AttributeSet
class AuthBiometricFingerprintAndFaceView(
context: Context,
attrs: AttributeSet?
) : AuthBiometricFingerprintView(context, attrs) {
constructor (context: Context) : this(context, null)
}

View File

@@ -33,7 +33,7 @@ import com.android.systemui.R;
public class AuthBiometricFingerprintView extends AuthBiometricView {
private static final String TAG = "BiometricPrompt/AuthBiometricFingerprintView";
private static final String TAG = "AuthBiometricFingerprintView";
private boolean mIsUdfps = false;
@Nullable private UdfpsDialogMeasureAdapter mUdfpsAdapter;
@@ -65,8 +65,8 @@ public class AuthBiometricFingerprintView extends AuthBiometricView {
}
@Override
void onLayoutInternal() {
super.onLayoutInternal();
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
@@ -124,8 +124,8 @@ public class AuthBiometricFingerprintView extends AuthBiometricView {
}
@Override
void onAttachedToWindowInternal() {
super.onAttachedToWindowInternal();
protected void onAttachedToWindow() {
super.onAttachedToWindow();
showTouchSensorString();
}

View File

@@ -44,6 +44,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import java.lang.annotation.Retention;
@@ -52,11 +53,11 @@ import java.util.ArrayList;
import java.util.List;
/**
* Contains the Biometric views (title, subtitle, icon, buttons, etc) and its controllers.
* Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers.
*/
public abstract class AuthBiometricView extends LinearLayout {
public class AuthBiometricView extends LinearLayout {
private static final String TAG = "BiometricPrompt/AuthBiometricView";
private static final String TAG = "AuthBiometricView";
/**
* Authentication hardware idle.
@@ -102,13 +103,6 @@ public abstract class AuthBiometricView extends LinearLayout {
int ACTION_BUTTON_TRY_AGAIN = 4;
int ACTION_ERROR = 5;
int ACTION_USE_DEVICE_CREDENTIAL = 6;
/**
* Notify the receiver to start the fingerprint sensor.
*
* This is only applicable to multi-sensor devices that need to delay fingerprint auth
* (i.e face -> fingerprint).
*/
int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7;
/**
* When an action has occurred. The caller will only invoke this when the callback should
@@ -118,66 +112,9 @@ public abstract class AuthBiometricView extends LinearLayout {
void onAction(int action);
}
@VisibleForTesting
static class Injector {
AuthBiometricView mBiometricView;
public Button getNegativeButton() {
return mBiometricView.findViewById(R.id.button_negative);
}
public Button getCancelButton() {
return mBiometricView.findViewById(R.id.button_cancel);
}
public Button getUseCredentialButton() {
return mBiometricView.findViewById(R.id.button_use_credential);
}
public Button getConfirmButton() {
return mBiometricView.findViewById(R.id.button_confirm);
}
public Button getTryAgainButton() {
return mBiometricView.findViewById(R.id.button_try_again);
}
public TextView getTitleView() {
return mBiometricView.findViewById(R.id.title);
}
public TextView getSubtitleView() {
return mBiometricView.findViewById(R.id.subtitle);
}
public TextView getDescriptionView() {
return mBiometricView.findViewById(R.id.description);
}
public TextView getIndicatorView() {
return mBiometricView.findViewById(R.id.indicator);
}
public ImageView getIconView() {
return mBiometricView.findViewById(R.id.biometric_icon);
}
public View getIconHolderView() {
return mBiometricView.findViewById(R.id.biometric_icon_frame);
}
public int getDelayAfterError() {
return BiometricPrompt.HIDE_DIALOG_DELAY;
}
public int getMediumToLargeAnimationDurationMs() {
return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
}
}
private final Injector mInjector;
protected final Handler mHandler;
private final AccessibilityManager mAccessibilityManager;
private final LockPatternUtils mLockPatternUtils;
protected final int mTextColorError;
protected final int mTextColorHint;
@@ -195,6 +132,10 @@ public abstract class AuthBiometricView extends LinearLayout {
protected ImageView mIconView;
protected TextView mIndicatorView;
@VisibleForTesting int mAnimationDurationShort = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS;
@VisibleForTesting int mAnimationDurationLong = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
@VisibleForTesting int mAnimationDurationHideDialog = BiometricPrompt.HIDE_DIALOG_DELAY;
// Negative button position, exclusively for the app-specified behavior
@VisibleForTesting Button mNegativeButton;
// Negative button position, exclusively for cancelling auth after passive auth success
@@ -217,30 +158,7 @@ public abstract class AuthBiometricView extends LinearLayout {
protected boolean mDialogSizeAnimating;
protected Bundle mSavedState;
/**
* Delay after authentication is confirmed, before the dialog should be animated away.
*/
protected abstract int getDelayAfterAuthenticatedDurationMs();
/**
* State that the dialog/icon should be in after showing a help message.
*/
protected abstract int getStateForAfterError();
/**
* Invoked when the error message is being cleared.
*/
protected abstract void handleResetAfterError();
/**
* Invoked when the help message is being cleared.
*/
protected abstract void handleResetAfterHelp();
/**
* @return true if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}
*/
protected abstract boolean supportsSmallDialog();
private final Runnable mResetErrorRunnable;
private final Runnable mResetHelpRunnable;
private final OnClickListener mBackgroundClickListener = (view) -> {
@@ -262,11 +180,6 @@ public abstract class AuthBiometricView extends LinearLayout {
}
public AuthBiometricView(Context context, AttributeSet attrs) {
this(context, attrs, new Injector());
}
@VisibleForTesting
AuthBiometricView(Context context, AttributeSet attrs, Injector injector) {
super(context, attrs);
mHandler = new Handler(Looper.getMainLooper());
mTextColorError = getResources().getColor(
@@ -274,10 +187,8 @@ public abstract class AuthBiometricView extends LinearLayout {
mTextColorHint = getResources().getColor(
R.color.biometric_dialog_gray, context.getTheme());
mInjector = injector;
mInjector.mBiometricView = this;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mLockPatternUtils = new LockPatternUtils(context);
mResetErrorRunnable = () -> {
updateState(getStateForAfterError());
@@ -292,31 +203,52 @@ public abstract class AuthBiometricView extends LinearLayout {
};
}
public void setPanelController(AuthPanelController panelController) {
/** Delay after authentication is confirmed, before the dialog should be animated away. */
protected int getDelayAfterAuthenticatedDurationMs() {
return 0;
}
/** State that the dialog/icon should be in after showing a help message. */
protected int getStateForAfterError() {
return STATE_IDLE;
}
/** Invoked when the error message is being cleared. */
protected void handleResetAfterError() {}
/** Invoked when the help message is being cleared. */
protected void handleResetAfterHelp() {}
/** True if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}. */
protected boolean supportsSmallDialog() {
return false;
}
void setPanelController(AuthPanelController panelController) {
mPanelController = panelController;
}
public void setPromptInfo(PromptInfo promptInfo) {
void setPromptInfo(PromptInfo promptInfo) {
mPromptInfo = promptInfo;
}
public void setCallback(Callback callback) {
void setCallback(Callback callback) {
mCallback = callback;
}
public void setBackgroundView(View backgroundView) {
void setBackgroundView(View backgroundView) {
backgroundView.setOnClickListener(mBackgroundClickListener);
}
public void setUserId(int userId) {
void setUserId(int userId) {
mUserId = userId;
}
public void setEffectiveUserId(int effectiveUserId) {
void setEffectiveUserId(int effectiveUserId) {
mEffectiveUserId = effectiveUserId;
}
public void setRequireConfirmation(boolean requireConfirmation) {
void setRequireConfirmation(boolean requireConfirmation) {
mRequireConfirmation = requireConfirmation;
}
@@ -376,7 +308,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Choreograph together
final AnimatorSet as = new AnimatorSet();
as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
as.setDuration(mAnimationDurationShort);
as.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -429,7 +361,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Translate at full duration
final ValueAnimator translationAnimator = ValueAnimator.ofFloat(
biometricView.getY(), biometricView.getY() - translationY);
translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs());
translationAnimator.setDuration(mAnimationDurationLong);
translationAnimator.addUpdateListener((animation) -> {
final float translation = (float) animation.getAnimatedValue();
biometricView.setTranslationY(translation);
@@ -438,7 +370,7 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (biometricView.getParent() != null) {
if (biometricView.getParent() instanceof ViewGroup) {
((ViewGroup) biometricView.getParent()).removeView(biometricView);
}
mSize = newSize;
@@ -447,7 +379,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Opacity to 0 in half duration
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2);
opacityAnimator.setDuration(mAnimationDurationLong / 2);
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
biometricView.setAlpha(opacity);
@@ -457,7 +389,7 @@ public abstract class AuthBiometricView extends LinearLayout {
mPanelController.updateForContentDimensions(
mPanelController.getContainerWidth(),
mPanelController.getContainerHeight(),
mInjector.getMediumToLargeAnimationDurationMs());
mAnimationDurationLong);
// Start the animations together
AnimatorSet as = new AnimatorSet();
@@ -466,7 +398,7 @@ public abstract class AuthBiometricView extends LinearLayout {
animators.add(opacityAnimator);
as.playTogether(animators);
as.setDuration(mInjector.getMediumToLargeAnimationDurationMs() * 2 / 3);
as.setDuration(mAnimationDurationLong * 2 / 3);
as.start();
} else {
Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
@@ -567,9 +499,8 @@ public abstract class AuthBiometricView extends LinearLayout {
showTemporaryMessage(error, mResetErrorRunnable);
updateState(STATE_ERROR);
mHandler.postDelayed(() -> {
mCallback.onAction(Callback.ACTION_ERROR);
}, mInjector.getDelayAfterError());
mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_ERROR),
mAnimationDurationHideDialog);
}
/**
@@ -639,7 +570,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// select to enable marquee unless a screen reader is enabled
mIndicatorView.setSelected(!mAccessibilityManager.isEnabled()
|| !mAccessibilityManager.isTouchExplorationEnabled());
mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError());
mHandler.postDelayed(resetMessageRunnable, mAnimationDurationHideDialog);
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
}
@@ -647,29 +578,22 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
onFinishInflateInternal();
}
/**
* After inflation, but before things like restoreState, onAttachedToWindow, etc.
*/
@VisibleForTesting
void onFinishInflateInternal() {
mTitleView = mInjector.getTitleView();
mSubtitleView = mInjector.getSubtitleView();
mDescriptionView = mInjector.getDescriptionView();
mIconView = mInjector.getIconView();
mIconHolderView = mInjector.getIconHolderView();
mIndicatorView = mInjector.getIndicatorView();
mTitleView = findViewById(R.id.title);
mSubtitleView = findViewById(R.id.subtitle);
mDescriptionView = findViewById(R.id.description);
mIconView = findViewById(R.id.biometric_icon);
mIconHolderView = findViewById(R.id.biometric_icon_frame);
mIndicatorView = findViewById(R.id.indicator);
// Negative-side (left) buttons
mNegativeButton = mInjector.getNegativeButton();
mCancelButton = mInjector.getCancelButton();
mUseCredentialButton = mInjector.getUseCredentialButton();
mNegativeButton = findViewById(R.id.button_negative);
mCancelButton = findViewById(R.id.button_cancel);
mUseCredentialButton = findViewById(R.id.button_use_credential);
// Positive-side (right) buttons
mConfirmButton = mInjector.getConfirmButton();
mTryAgainButton = mInjector.getTryAgainButton();
mConfirmButton = findViewById(R.id.button_confirm);
mTryAgainButton = findViewById(R.id.button_try_again);
mNegativeButton.setOnClickListener((view) -> {
mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
@@ -706,21 +630,13 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onAttachedToWindowInternal();
}
/**
* Contains all the testable logic that should be invoked when {@link #onAttachedToWindow()} is
* invoked.
*/
@VisibleForTesting
void onAttachedToWindowInternal() {
mTitleView.setText(mPromptInfo.getTitle());
if (isDeviceCredentialAllowed()) {
final CharSequence credentialButtonText;
final @Utils.CredentialType int credentialType =
Utils.getCredentialType(mContext, mEffectiveUserId);
@Utils.CredentialType final int credentialType =
Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId);
switch (credentialType) {
case Utils.CREDENTIAL_PIN:
credentialButtonText =
@@ -731,9 +647,6 @@ public abstract class AuthBiometricView extends LinearLayout {
getResources().getString(R.string.biometric_dialog_use_pattern);
break;
case Utils.CREDENTIAL_PASSWORD:
credentialButtonText =
getResources().getString(R.string.biometric_dialog_use_password);
break;
default:
credentialButtonText =
getResources().getString(R.string.biometric_dialog_use_password);
@@ -749,7 +662,6 @@ public abstract class AuthBiometricView extends LinearLayout {
}
setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle());
setTextOrHide(mDescriptionView, mPromptInfo.getDescription());
if (mSavedState == null) {
@@ -856,15 +768,7 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLayoutInternal();
}
/**
* Contains all the testable logic that should be invoked when
* {@link #onLayout(boolean, int, int, int, int)}, is invoked.
*/
@VisibleForTesting
void onLayoutInternal() {
// Start with initial size only once. Subsequent layout changes don't matter since we
// only care about the initial icon position.
if (mIconOriginalY == 0) {

View File

@@ -16,9 +16,10 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -52,6 +53,7 @@ import android.widget.LinearLayout;
import android.widget.ScrollView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -66,54 +68,51 @@ import java.util.List;
public class AuthContainerView extends LinearLayout
implements AuthDialog, WakefulnessLifecycle.Observer {
private static final String TAG = "BiometricPrompt/AuthContainerView";
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms
private static final String TAG = "AuthContainerView";
static final int STATE_UNKNOWN = 0;
static final int STATE_ANIMATING_IN = 1;
static final int STATE_PENDING_DISMISS = 2;
static final int STATE_SHOWING = 3;
static final int STATE_ANIMATING_OUT = 4;
static final int STATE_GONE = 5;
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350;
private static final int STATE_UNKNOWN = 0;
private static final int STATE_ANIMATING_IN = 1;
private static final int STATE_PENDING_DISMISS = 2;
private static final int STATE_SHOWING = 3;
private static final int STATE_ANIMATING_OUT = 4;
private static final int STATE_GONE = 5;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
STATE_ANIMATING_OUT, STATE_GONE})
@interface ContainerState {}
private @interface ContainerState {}
final Config mConfig;
final int mEffectiveUserId;
@Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps;
@Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
private final Config mConfig;
private final int mEffectiveUserId;
private final Handler mHandler;
private final Injector mInjector;
private final IBinder mWindowToken = new Binder();
private final WindowManager mWindowManager;
private final AuthPanelController mPanelController;
private final Interpolator mLinearOutSlowIn;
@VisibleForTesting final BiometricCallback mBiometricCallback;
private final CredentialCallback mCredentialCallback;
@VisibleForTesting final FrameLayout mFrameLayout;
@VisibleForTesting @Nullable AuthBiometricView mBiometricView;
@VisibleForTesting @Nullable AuthCredentialView mCredentialView;
@VisibleForTesting final ImageView mBackgroundView;
@VisibleForTesting final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
@VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
@VisibleForTesting final BiometricCallback mBiometricCallback;
@Nullable private AuthBiometricView mBiometricView;
@Nullable private AuthCredentialView mCredentialView;
private final AuthPanelController mPanelController;
private final FrameLayout mFrameLayout;
private final ImageView mBackgroundView;
private final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
@ContainerState private int mContainerState = STATE_UNKNOWN;
// Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
@Nullable @AuthDialogCallback.DismissedReason Integer mPendingCallbackReason;
@Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
// HAT received from LockSettingsService when credential is verified.
@Nullable byte[] mCredentialAttestation;
@Nullable private byte[] mCredentialAttestation;
@VisibleForTesting
static class Config {
Context mContext;
AuthDialogCallback mCallback;
@@ -122,11 +121,11 @@ public class AuthContainerView extends LinearLayout
int mUserId;
String mOpPackageName;
int[] mSensorIds;
boolean mCredentialAllowed;
boolean mSkipIntro;
long mOperationId;
long mRequestId;
@BiometricMultiSensorMode int mMultiSensorConfig;
boolean mSkipAnimation = false;
@BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
}
public static class Builder {
@@ -167,7 +166,7 @@ public class AuthContainerView extends LinearLayout
return this;
}
public Builder setOperationId(long operationId) {
public Builder setOperationId(@DurationMillisLong long operationId) {
mConfig.mOperationId = operationId;
return this;
}
@@ -178,55 +177,27 @@ public class AuthContainerView extends LinearLayout
return this;
}
@VisibleForTesting
public Builder setSkipAnimationDuration(boolean skip) {
mConfig.mSkipAnimation = skip;
return this;
}
/** The multi-sensor mode. */
public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
mConfig.mMultiSensorConfig = multiSensorConfig;
return this;
}
public AuthContainerView build(int[] sensorIds, boolean credentialAllowed,
public AuthContainerView build(int[] sensorIds,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
WakefulnessLifecycle wakefulnessLifecycle) {
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils) {
mConfig.mSensorIds = sensorIds;
mConfig.mCredentialAllowed = credentialAllowed;
return new AuthContainerView(
mConfig, new Injector(), fpProps, faceProps, wakefulnessLifecycle);
}
}
public static class Injector {
ScrollView getBiometricScrollView(FrameLayout parent) {
return parent.findViewById(R.id.biometric_scrollview);
}
FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) {
return (FrameLayout) factory.inflate(
R.layout.auth_container_view, root, false /* attachToRoot */);
}
AuthPanelController getPanelController(Context context, View panelView) {
return new AuthPanelController(context, panelView);
}
ImageView getBackgroundView(FrameLayout parent) {
return parent.findViewById(R.id.background);
}
View getPanelView(FrameLayout parent) {
return parent.findViewById(R.id.panel);
}
int getAnimateCredentialStartDelayMs() {
return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS;
}
UserManager getUserManager(Context context) {
return UserManager.get(context);
}
int getCredentialType(Context context, int effectiveUserId) {
return Utils.getCredentialType(context, effectiveUserId);
return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
userManager, lockPatternUtils, new Handler(Looper.getMainLooper()));
}
}
@@ -255,10 +226,7 @@ public class AuthContainerView extends LinearLayout
mConfig.mCallback.onDeviceCredentialPressed();
mHandler.postDelayed(() -> {
addCredentialView(false /* animatePanel */, true /* animateContents */);
}, mInjector.getAnimateCredentialStartDelayMs());
break;
case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR:
mConfig.mCallback.onStartFingerprintNow();
}, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
break;
default:
Log.e(TAG, "Unhandled action: " + action);
@@ -275,21 +243,19 @@ public class AuthContainerView extends LinearLayout
}
@VisibleForTesting
AuthContainerView(Config config, Injector injector,
AuthContainerView(Config config,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
WakefulnessLifecycle wakefulnessLifecycle) {
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull Handler mainHandler) {
super(config.mContext);
mConfig = config;
mInjector = injector;
mFpProps = fpProps;
mFaceProps = faceProps;
mEffectiveUserId = mInjector.getUserManager(mContext)
.getCredentialOwnerProfile(mConfig.mUserId);
mHandler = new Handler(Looper.getMainLooper());
mLockPatternUtils = lockPatternUtils;
mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
mHandler = mainHandler;
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -299,96 +265,42 @@ public class AuthContainerView extends LinearLayout
mBiometricCallback = new BiometricCallback();
mCredentialCallback = new CredentialCallback();
final LayoutInflater factory = LayoutInflater.from(mContext);
mFrameLayout = mInjector.inflateContainerView(factory, this);
mPanelView = mInjector.getPanelView(mFrameLayout);
mPanelController = mInjector.getPanelController(mContext, mPanelView);
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
mFrameLayout = (FrameLayout) layoutInflater.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
addView(mFrameLayout);
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
// Inflate biometric view only if necessary.
final int sensorCount = config.mSensorIds.length;
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
if (sensorCount == 1) {
final int singleSensorAuthId = config.mSensorIds[0];
if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) {
FingerprintSensorPropertiesInternal sensorProps = null;
for (FingerprintSensorPropertiesInternal prop : mFpProps) {
if (prop.sensorId == singleSensorAuthId) {
sensorProps = prop;
break;
}
}
final FingerprintSensorPropertiesInternal fpProperties =
Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds);
final FaceSensorPropertiesInternal faceProperties =
Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds);
final AuthBiometricFingerprintView fpView = (AuthBiometricFingerprintView)
factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false);
fpView.setSensorProperties(sensorProps);
mBiometricView = fpView;
} else if (Utils.containsSensorId(mFaceProps, singleSensorAuthId)) {
mBiometricView = (AuthBiometricFaceView)
factory.inflate(R.layout.auth_biometric_face_view, null, false);
} else {
// Unknown sensorId
Log.e(TAG, "Unknown sensorId: " + singleSensorAuthId);
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
}
} else if (sensorCount == 2) {
final int[] allSensors = findFaceAndFingerprintSensors();
final int faceSensorId = allSensors[0];
final int fingerprintSensorId = allSensors[1];
if (fingerprintSensorId == -1 || faceSensorId == -1) {
Log.e(TAG, "Missing fingerprint or face for dual-sensor config");
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
}
FingerprintSensorPropertiesInternal fingerprintSensorProps = null;
for (FingerprintSensorPropertiesInternal prop : mFpProps) {
if (prop.sensorId == fingerprintSensorId) {
fingerprintSensorProps = prop;
break;
}
}
if (fingerprintSensorProps != null) {
final AuthBiometricFaceToFingerprintView faceToFingerprintView =
(AuthBiometricFaceToFingerprintView) factory.inflate(
R.layout.auth_biometric_face_to_fingerprint_view, null, false);
faceToFingerprintView.setFingerprintSensorProps(fingerprintSensorProps);
faceToFingerprintView.setModalityListener(new ModalityListener() {
@Override
public void onModalitySwitched(int oldModality, int newModality) {
maybeUpdatePositionForUdfps(true /* invalidate */);
}
});
mBiometricView = faceToFingerprintView;
} else {
Log.e(TAG, "Fingerprint props not found for sensor ID: " + fingerprintSensorId);
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
}
if (fpProperties != null && faceProperties != null) {
final AuthBiometricFingerprintAndFaceView fingerprintAndFaceView =
(AuthBiometricFingerprintAndFaceView) layoutInflater.inflate(
R.layout.auth_biometric_fingerprint_and_face_view, null, false);
fingerprintAndFaceView.setSensorProperties(fpProperties);
mBiometricView = fingerprintAndFaceView;
} else if (fpProperties != null) {
final AuthBiometricFingerprintView fpView =
(AuthBiometricFingerprintView) layoutInflater.inflate(
R.layout.auth_biometric_fingerprint_view, null, false);
fpView.setSensorProperties(fpProperties);
mBiometricView = fpView;
} else if (faceProperties != null) {
mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate(
R.layout.auth_biometric_face_view, null, false);
} else {
Log.e(TAG, "Unsupported sensor array, length: " + sensorCount);
mBiometricView = null;
mBackgroundView = null;
mBiometricScrollView = null;
return;
Log.e(TAG, "No sensors found!");
}
}
mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout);
mBackgroundView = mInjector.getBackgroundView(mFrameLayout);
addView(mFrameLayout);
// init view before showing
if (mBiometricView != null) {
mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
@@ -427,10 +339,6 @@ public class AuthContainerView extends LinearLayout
return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
}
private void addBiometricView() {
mBiometricScrollView.addView(mBiometricView);
}
/**
* Adds the credential view. When going from biometric to credential view, the biometric
* view starts the panel expansion animation. If the credential view is being shown first,
@@ -440,8 +348,8 @@ public class AuthContainerView extends LinearLayout
private void addCredentialView(boolean animatePanel, boolean animateContents) {
final LayoutInflater factory = LayoutInflater.from(mContext);
final @Utils.CredentialType int credentialType = mInjector.getCredentialType(
mContext, mEffectiveUserId);
@Utils.CredentialType final int credentialType = Utils.getCredentialType(
mLockPatternUtils, mEffectiveUserId);
switch (credentialType) {
case Utils.CREDENTIAL_PATTERN:
@@ -489,15 +397,11 @@ public class AuthContainerView extends LinearLayout
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
onAttachedToWindowInternal();
}
@VisibleForTesting
void onAttachedToWindowInternal() {
mWakefulnessLifecycle.addObserver(this);
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
addBiometricView();
mBiometricScrollView.addView(mBiometricView);
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */);
} else {
@@ -517,17 +421,18 @@ public class AuthContainerView extends LinearLayout
mBiometricScrollView.setY(mTranslationY);
setAlpha(0f);
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
postOnAnimation(() -> {
mPanelView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.withEndAction(this::onDialogAnimatedIn)
.start();
mBiometricScrollView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
@@ -535,14 +440,14 @@ public class AuthContainerView extends LinearLayout
mCredentialView.setY(mTranslationY);
mCredentialView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
}
animate()
.alpha(1f)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
@@ -555,13 +460,6 @@ public class AuthContainerView extends LinearLayout
return ((AuthBiometricFingerprintView) view).isUdfps();
}
if (view instanceof AuthBiometricFaceToFingerprintView) {
AuthBiometricFaceToFingerprintView faceToFingerprintView =
(AuthBiometricFaceToFingerprintView) view;
return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT
&& faceToFingerprintView.isFingerprintUdfps();
}
return false;
}
@@ -669,7 +567,8 @@ public class AuthContainerView extends LinearLayout
@Override
public void onSaveState(@NonNull Bundle outState) {
outState.putInt(AuthDialog.KEY_CONTAINER_STATE, mContainerState);
outState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY,
mContainerState == STATE_ANIMATING_OUT);
// In the case where biometric and credential are both allowed, we can assume that
// biometric isn't showing if credential is showing since biometric is shown first.
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING,
@@ -691,8 +590,7 @@ public class AuthContainerView extends LinearLayout
mBiometricView.startTransitionToCredentialUI();
}
@VisibleForTesting
void animateAway(int reason) {
void animateAway(@AuthDialogCallback.DismissedReason int reason) {
animateAway(true /* sendReason */, reason);
}
@@ -720,31 +618,32 @@ public class AuthContainerView extends LinearLayout
removeWindowIfAttached();
};
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
postOnAnimation(() -> {
mPanelView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.withEndAction(endActionRunnable)
.start();
mBiometricScrollView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
mCredentialView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
}
animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
@@ -769,8 +668,7 @@ public class AuthContainerView extends LinearLayout
mWindowManager.removeView(this);
}
@VisibleForTesting
void onDialogAnimatedIn() {
private void onDialogAnimatedIn() {
if (mContainerState == STATE_PENDING_DISMISS) {
Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
@@ -784,8 +682,7 @@ public class AuthContainerView extends LinearLayout
}
@VisibleForTesting
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken,
CharSequence title) {
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
| WindowManager.LayoutParams.FLAG_SECURE;
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
@@ -801,24 +698,4 @@ public class AuthContainerView extends LinearLayout
lp.token = windowToken;
return lp;
}
// returns [face, fingerprint] sensor ids (id is -1 if not present)
private int[] findFaceAndFingerprintSensors() {
int faceSensorId = -1;
int fingerprintSensorId = -1;
for (final int sensorId : mConfig.mSensorIds) {
if (Utils.containsSensorId(mFpProps, sensorId)) {
fingerprintSensorId = sensorId;
} else if (Utils.containsSensorId(mFaceProps, sensorId)) {
faceSensorId = sensorId;
}
if (fingerprintSensorId != -1 && faceSensorId != -1) {
break;
}
}
return new int[] {faceSensorId, fingerprintSensorId};
}
}

View File

@@ -51,6 +51,7 @@ import android.hardware.fingerprint.IUdfpsHbmListener;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserManager;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.MotionEvent;
@@ -59,6 +60,7 @@ import android.view.WindowManager;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.CoreStartable;
import com.android.systemui.assist.ui.DisplayUtils;
import com.android.systemui.dagger.SysUISingleton;
@@ -137,6 +139,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private boolean mAllAuthenticatorsRegistered;
@NonNull private final UserManager mUserManager;
@NonNull private final LockPatternUtils mLockPatternUtils;
private class BiometricTaskStackListener extends TaskStackListener {
@Override
@@ -359,20 +363,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
}
}
@Override
public void onStartFingerprintNow() {
if (mReceiver == null) {
Log.e(TAG, "onStartUdfpsNow: Receiver is null");
return;
}
try {
mReceiver.onStartFingerprintNow();
} catch (RemoteException e) {
Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
}
}
@Override
public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) {
switch (reason) {
@@ -503,12 +493,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
Provider<UdfpsController> udfpsControllerFactory,
Provider<SidefpsController> sidefpsControllerFactory,
@NonNull DisplayManager displayManager,
WakefulnessLifecycle wakefulnessLifecycle,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull StatusBarStateController statusBarStateController,
@Main Handler handler) {
super(context);
mExecution = execution;
mWakefulnessLifecycle = wakefulnessLifecycle;
mUserManager = userManager;
mLockPatternUtils = lockPatternUtils;
mHandler = handler;
mCommandQueue = commandQueue;
mActivityTaskManager = activityTaskManager;
@@ -827,7 +821,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
final String opPackageName = (String) args.arg6;
final long operationId = args.argl1;
final long requestId = args.argl2;
final @BiometricMultiSensorMode int multiSensorConfig = args.argi2;
@BiometricMultiSensorMode final int multiSensorConfig = args.argi2;
// Create a new dialog but do not replace the current one yet.
final AuthDialog newDialog = buildDialog(
@@ -835,13 +829,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
requireConfirmation,
userId,
sensorIds,
credentialAllowed,
opPackageName,
skipAnimation,
operationId,
requestId,
multiSensorConfig,
mWakefulnessLifecycle);
mWakefulnessLifecycle,
mUserManager,
mLockPatternUtils);
if (newDialog == null) {
Log.e(TAG, "Unsupported type configuration");
@@ -902,8 +897,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
// Only show the dialog if necessary. If it was animating out, the dialog is supposed
// to send its pending callback immediately.
if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE)
!= AuthContainerView.STATE_ANIMATING_OUT) {
if (!savedState.getBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false)) {
final boolean credentialShowing =
savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING);
if (credentialShowing) {
@@ -927,10 +921,12 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
}
protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation,
int userId, int[] sensorIds, boolean credentialAllowed, String opPackageName,
int userId, int[] sensorIds, String opPackageName,
boolean skipIntro, long operationId, long requestId,
@BiometricMultiSensorMode int multiSensorConfig,
WakefulnessLifecycle wakefulnessLifecycle) {
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils) {
return new AuthContainerView.Builder(mContext)
.setCallback(this)
.setPromptInfo(promptInfo)
@@ -941,7 +937,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba
.setOperationId(operationId)
.setRequestId(requestId)
.setMultiSensorConfig(multiSensorConfig)
.build(sensorIds, credentialAllowed, mFpProps, mFaceProps, wakefulnessLifecycle);
.build(sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle, userManager,
lockPatternUtils);
}
/**

View File

@@ -31,7 +31,7 @@ import java.lang.annotation.RetentionPolicy;
*/
public interface AuthDialog {
String KEY_CONTAINER_STATE = "container_state";
String KEY_CONTAINER_GOING_AWAY = "container_going_away";
String KEY_BIOMETRIC_SHOWING = "biometric_showing";
String KEY_CREDENTIAL_SHOWING = "credential_showing";
@@ -64,7 +64,7 @@ public interface AuthDialog {
@interface DialogSize {}
/**
* Parameters used when laying out {@link AuthBiometricView}, its sublclasses, and
* Parameters used when laying out {@link AuthBiometricView}, its subclasses, and
* {@link AuthPanelController}.
*/
class LayoutParams {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.Manifest
import android.annotation.IntDef
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.biometrics.BiometricManager.Authenticators
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorPropertiesInternal
import android.os.UserManager
import android.util.DisplayMetrics
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import com.android.internal.widget.LockPatternUtils
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
object Utils {
const val CREDENTIAL_PIN = 1
const val CREDENTIAL_PATTERN = 2
const val CREDENTIAL_PASSWORD = 3
/** Base set of layout flags for fingerprint overlay widgets. */
const val FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS =
(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
@JvmStatic
fun dpToPixels(context: Context, dp: Float): Float {
return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
}
@JvmStatic
fun notifyAccessibilityContentChanged(am: AccessibilityManager, view: ViewGroup) {
if (!am.isEnabled) {
return
}
val event = AccessibilityEvent.obtain()
event.eventType = AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
event.contentChangeTypes =
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
view.sendAccessibilityEventUnchecked(event)
view.notifySubtreeAccessibilityStateChanged(
view,
view,
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
)
}
@JvmStatic
fun isDeviceCredentialAllowed(promptInfo: PromptInfo): Boolean =
(promptInfo.authenticators and Authenticators.DEVICE_CREDENTIAL) != 0
@JvmStatic
fun isBiometricAllowed(promptInfo: PromptInfo): Boolean =
(promptInfo.authenticators and Authenticators.BIOMETRIC_WEAK) != 0
@JvmStatic
@CredentialType
fun getCredentialType(utils: LockPatternUtils, userId: Int): Int =
when (utils.getKeyguardStoredPasswordQuality(userId)) {
PASSWORD_QUALITY_SOMETHING -> CREDENTIAL_PATTERN
PASSWORD_QUALITY_NUMERIC, PASSWORD_QUALITY_NUMERIC_COMPLEX -> CREDENTIAL_PIN
PASSWORD_QUALITY_ALPHABETIC, PASSWORD_QUALITY_ALPHANUMERIC, PASSWORD_QUALITY_COMPLEX, PASSWORD_QUALITY_MANAGED -> CREDENTIAL_PASSWORD
else -> CREDENTIAL_PASSWORD
}
@JvmStatic
fun isManagedProfile(context: Context, userId: Int): Boolean =
context.getSystemService(UserManager::class.java)?.isManagedProfile(userId) ?: false
@JvmStatic
fun <T : SensorPropertiesInternal> findFirstSensorProperties(
properties: List<T>?,
sensorIds: IntArray
): T? = properties?.firstOrNull { sensorIds.contains(it.sensorId) }
@JvmStatic
fun isSystem(context: Context, clientPackage: String?): Boolean {
val hasPermission =
(context.checkCallingOrSelfPermission(Manifest.permission.USE_BIOMETRIC_INTERNAL)
== PackageManager.PERMISSION_GRANTED)
return hasPermission && "android" == clientPackage
}
@Retention(RetentionPolicy.SOURCE)
@IntDef(CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD)
internal annotation class CredentialType
}

View File

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

View File

@@ -37,8 +37,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.android.systemui.R;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
@@ -58,11 +56,14 @@ public class AuthBiometricFaceViewTest extends SysuiTestCase {
@Mock private TextView mErrorView;
@Mock
private TestableFaceView.TestableIconController mIconController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mFaceView = new TestableFaceView(mContext);
mFaceView.mFaceIconController = mock(TestableFaceView.TestableIconController.class);
mFaceView.mFaceIconController = mIconController;
mFaceView.setCallback(mCallback);
mFaceView.mNegativeButton = mNegativeButton;
@@ -96,7 +97,7 @@ public class AuthBiometricFaceViewTest extends SysuiTestCase {
public class TestableFaceView extends AuthBiometricFaceView {
public class TestableIconController extends IconController {
public class TestableIconController extends AuthBiometricFaceIconController {
TestableIconController(Context context, ImageView iconView) {
super(context, iconView, mock(TextView.class));
}

View File

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

View File

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

View File

@@ -0,0 +1,323 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.app.admin.DevicePolicyManager
import android.hardware.biometrics.BiometricConstants
import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorProperties
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.os.Handler
import android.os.IBinder
import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.testing.ViewUtils
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
import android.widget.ScrollView
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.eq
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
@SmallTest
class AuthContainerViewTest : SysuiTestCase() {
@JvmField @Rule
var rule = MockitoJUnit.rule()
@Mock
lateinit var callback: AuthDialogCallback
@Mock
lateinit var userManager: UserManager
@Mock
lateinit var lockPatternUtils: LockPatternUtils
@Mock
lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Mock
lateinit var windowToken: IBinder
private lateinit var authContainer: TestAuthContainerView
@Test
fun testActionAuthenticated_sendsDismissedAuthenticated() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_AUTHENTICATED
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionUserCanceled_sendsDismissedUserCanceled() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USER_CANCELED
)
waitForIdleSync()
verify(callback).onSystemEvent(
eq(BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL)
)
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionButtonNegative_sendsDismissedButtonNegative() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionTryAgain_sendsTryAgain() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN
)
waitForIdleSync()
verify(callback).onTryAgainPressed()
}
@Test
fun testActionError_sendsDismissedError() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_ERROR
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_ERROR),
eq<ByteArray?>(null) /* credentialAttestation */
)
assertThat(authContainer.parent).isNull()
}
@Test
fun testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
initializeContainer(
BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
authContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL
)
waitForIdleSync()
verify(callback).onDeviceCredentialPressed()
assertThat(authContainer.hasCredentialView()).isTrue()
}
@Test
fun testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() {
initializeContainer(
BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
authContainer.animateToCredentialUI()
waitForIdleSync()
assertThat(authContainer.hasCredentialView()).isTrue()
}
@Test
fun testShowBiometricUI() {
initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK)
waitForIdleSync()
assertThat(authContainer.hasCredentialView()).isFalse()
assertThat(authContainer.hasBiometricPrompt()).isTrue()
}
@Test
fun testShowCredentialUI() {
initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
waitForIdleSync()
assertThat(authContainer.hasCredentialView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
}
@Test
fun testCredentialViewUsesEffectiveUserId() {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200)
whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200))).thenReturn(
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
)
initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
waitForIdleSync()
assertThat(authContainer.hasCredentialPatternView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
}
@Test
fun testCredentialUI_disablesClickingOnBackground() {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn(
DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
)
// In the credential view, clicking on the background (to cancel authentication) is not
// valid. Thus, the listener should be null, and it should not be in the accessibility
// hierarchy.
initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
waitForIdleSync()
assertThat(authContainer.hasCredentialPasswordView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
assertThat(
authContainer.findViewById<View>(R.id.background)?.isImportantForAccessibility
).isFalse()
authContainer.findViewById<View>(R.id.background)?.performClick()
waitForIdleSync()
assertThat(authContainer.hasCredentialPasswordView()).isTrue()
assertThat(authContainer.hasBiometricPrompt()).isFalse()
}
@Test
fun testLayoutParams_hasSecureWindowFlag() {
val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0).isTrue()
}
@Test
fun testLayoutParams_excludesImeInsets() {
val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.ime()) == 0).isTrue()
}
private fun initializeContainer(authenticators: Int) {
val config = AuthContainerView.Config()
config.mContext = mContext
config.mCallback = callback
config.mSensorIds = intArrayOf(0)
config.mSkipAnimation = true
config.mPromptInfo = PromptInfo()
config.mPromptInfo.authenticators = authenticators
val componentInfo = listOf(
ComponentInfoInternal(
"faceSensor" /* componentId */,
"vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
"00000001" /* serialNumber */, "" /* softwareVersion */
),
ComponentInfoInternal(
"matchingAlgorithm" /* componentId */,
"" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
"vendor/version/revision" /* softwareVersion */
)
)
val fpProps = listOf(
FingerprintSensorPropertiesInternal(
0,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
componentInfo,
FingerprintSensorProperties.TYPE_REAR,
false /* resetLockoutRequiresHardwareAuthToken */
)
)
authContainer = TestAuthContainerView(
config,
fpProps,
listOf(),
wakefulnessLifecycle,
userManager,
lockPatternUtils,
Handler(TestableLooper.get(this).looper)
)
ViewUtils.attachView(authContainer)
}
private inner class TestAuthContainerView(
config: Config,
fpProps: List<FingerprintSensorPropertiesInternal>,
faceProps: List<FaceSensorPropertiesInternal>,
wakefulnessLifecycle: WakefulnessLifecycle,
userManager: UserManager,
lockPatternUtils: LockPatternUtils,
mainHandler: Handler
) : AuthContainerView(
config, fpProps, faceProps,
wakefulnessLifecycle, userManager, lockPatternUtils, mainHandler
) {
override fun postOnAnimation(runnable: Runnable) {
runnable.run()
}
}
override fun waitForIdleSync() {
TestableLooper.get(this).processAllMessages()
super.waitForIdleSync()
}
}
private fun AuthContainerView.hasBiometricPrompt() =
(findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0
private fun AuthContainerView.hasCredentialView() =
hasCredentialPatternView() || hasCredentialPasswordView()
private fun AuthContainerView.hasCredentialPatternView() =
findViewById<View>(R.id.lockPattern) != null
private fun AuthContainerView.hasCredentialPasswordView() =
findViewById<View>(R.id.lockPassword) != null

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,12 +20,8 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FACE_SCANNING;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FP_SCANNING;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_SWITCHING;
import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_UNKNOWN;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE;
@@ -100,14 +96,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
@Retention(RetentionPolicy.SOURCE)
@interface SessionState {}
/** Defined in biometrics.proto */
@IntDef({
MULTI_SENSOR_STATE_UNKNOWN,
MULTI_SENSOR_STATE_FACE_SCANNING,
MULTI_SENSOR_STATE_FP_SCANNING})
@Retention(RetentionPolicy.SOURCE)
@interface MultiSensorState {}
/**
* Notify the holder of the AuthSession that the caller/client's binder has died. The
* holder (BiometricService) should schedule {@link AuthSession#onClientDied()} to be run
@@ -143,7 +131,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
// The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE;
private @BiometricMultiSensorMode int mMultiSensorMode;
private @MultiSensorState int mMultiSensorState;
private int[] mSensors;
// TODO(b/197265902): merge into state
private boolean mCancelled;
@@ -254,7 +241,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mSensors = new int[0];
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mStatusBarService.showAuthenticationDialog(
mPromptInfo,
@@ -307,7 +293,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
mMultiSensorMode = getMultiSensorModeForNewSession(
mPreAuthInfo.eligibleSensors);
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mStatusBarService.showAuthenticationDialog(mPromptInfo,
mSysuiReceiver,
@@ -415,7 +400,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
mErrorEscrow = error;
mVendorCodeEscrow = vendorCode;
final @BiometricAuthenticator.Modality int modality = sensorIdToModality(sensorId);
@Modality final int modality = sensorIdToModality(sensorId);
switch (mState) {
case STATE_AUTH_CALLED: {
@@ -430,7 +415,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN;
mSensors = new int[0];
mStatusBarService.showAuthenticationDialog(
@@ -468,12 +452,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
return true;
} else {
mState = STATE_ERROR_PENDING_SYSUI;
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT
&& mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING) {
// wait for the UI to signal when modality should switch
Slog.d(TAG, "onErrorReceived: waiting for modality switch callback");
mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING;
}
mStatusBarService.onBiometricError(modality, error, vendorCode);
}
break;
@@ -538,34 +516,6 @@ public final class AuthSession implements IBinder.DeathRecipient {
mState = STATE_AUTH_STARTED_UI_SHOWING;
if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
mMultiSensorState = MULTI_SENSOR_STATE_FACE_SCANNING;
} else {
startFingerprintSensorsNow();
}
}
// call anytime after onDialogAnimatedIn() to indicate it's appropriate to start the
// fingerprint sensor (i.e. face auth has failed or is not available)
void onStartFingerprint() {
if (mMultiSensorMode != BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) {
Slog.e(TAG, "onStartFingerprint, unexpected mode: " + mMultiSensorMode);
return;
}
if (mState != STATE_AUTH_STARTED
&& mState != STATE_AUTH_STARTED_UI_SHOWING
&& mState != STATE_AUTH_PAUSED
&& mState != STATE_ERROR_PENDING_SYSUI) {
Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
}
mMultiSensorState = MULTI_SENSOR_STATE_FP_SCANNING;
startFingerprintSensorsNow();
}
// unguarded helper for the above methods only
private void startFingerprintSensorsNow() {
startAllPreparedFingerprintSensors();
mState = STATE_AUTH_STARTED_UI_SHOWING;
}
@@ -919,7 +869,7 @@ public final class AuthSession implements IBinder.DeathRecipient {
}
if (hasFace && hasFingerprint) {
return BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT;
return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
}
return BIOMETRIC_MULTI_SENSOR_DEFAULT;
}

View File

@@ -106,7 +106,6 @@ public class BiometricService extends SystemService {
private static final int MSG_ON_SYSTEM_EVENT = 13;
private static final int MSG_CLIENT_DIED = 14;
private static final int MSG_ON_DIALOG_ANIMATED_IN = 15;
private static final int MSG_ON_START_FINGERPRINT_NOW = 16;
private final Injector mInjector;
private final DevicePolicyManager mDevicePolicyManager;
@@ -244,11 +243,6 @@ public class BiometricService extends SystemService {
break;
}
case MSG_ON_START_FINGERPRINT_NOW: {
handleOnStartFingerprintNow();
break;
}
default:
Slog.e(TAG, "Unknown message: " + msg);
break;
@@ -630,11 +624,6 @@ public class BiometricService extends SystemService {
public void onDialogAnimatedIn() {
mHandler.obtainMessage(MSG_ON_DIALOG_ANIMATED_IN).sendToTarget();
}
@Override
public void onStartFingerprintNow() {
mHandler.obtainMessage(MSG_ON_START_FINGERPRINT_NOW).sendToTarget();
}
};
private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> {
@@ -1344,16 +1333,6 @@ public class BiometricService extends SystemService {
mCurrentAuthSession.onDialogAnimatedIn();
}
private void handleOnStartFingerprintNow() {
Slog.d(TAG, "handleOnStartFingerprintNow");
if (mCurrentAuthSession == null) {
Slog.e(TAG, "handleOnStartFingerprintNow: AuthSession is null");
return;
}
mCurrentAuthSession.onStartFingerprint();
}
/**
* Invoked when each service has notified that its client is ready to be started. When
* all biometrics are ready, this invokes the SystemUI dialog through StatusBar.

View File

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