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