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