diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt index db46ccf6a827fdef17ef602289352ef4bf698abf..80f70a0cd2f2eaaf12466a33089be83d7949136d 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt @@ -33,6 +33,10 @@ data class BiometricModalities( val hasFingerprint: Boolean get() = fingerprintProperties != null + /** If SFPS authentication is available. */ + val hasSfps: Boolean + get() = hasFingerprint && fingerprintProperties!!.isAnySidefpsType + /** If fingerprint authentication is available (and [faceProperties] is non-null). */ val hasFace: Boolean get() = faceProperties != null diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt deleted file mode 100644 index 3f2da5e144c57cdab04daf2086cc621534789aa1..0000000000000000000000000000000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.biometrics - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.Log -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState - -private const val TAG = "AuthBiometricFaceIconController" - -/** Face only icon animator for BiometricPrompt. */ -class AuthBiometricFaceIconController( - context: Context, - iconView: LottieAnimationView -) : AuthIconController(context, iconView) { - - // false = dark to light, true = light to dark - private var lastPulseLightToDark = false - - private var state: BiometricState = BiometricState.STATE_IDLE - - 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 == BiometricState.STATE_AUTHENTICATING || state == BiometricState.STATE_HELP) { - pulseInNextDirection() - } - } - - override fun updateIcon(oldState: BiometricState, newState: BiometricState) { - val lastStateIsErrorIcon = (oldState == BiometricState.STATE_ERROR || oldState == BiometricState.STATE_HELP) - if (newState == BiometricState.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 == BiometricState.STATE_AUTHENTICATING) { - startPulsing() - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_authenticating - ) - } else if (oldState == BiometricState.STATE_PENDING_CONFIRMATION && newState == BiometricState.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 == BiometricState.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 == BiometricState.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 == BiometricState.STATE_ERROR && oldState != BiometricState.STATE_ERROR) { - animateIconOnce(R.drawable.face_dialog_dark_to_error) - iconView.contentDescription = context.getString( - R.string.keyguard_face_failed - ) - } else if (oldState == BiometricState.STATE_AUTHENTICATING && newState == BiometricState.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 == BiometricState.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 == BiometricState.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/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt deleted file mode 100644 index 09eabf2aa430e0fcc61ef3f86191646a3e535480..0000000000000000000000000000000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics - -import android.annotation.RawRes -import android.content.Context -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATED -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_ERROR -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_HELP -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - -/** Face/Fingerprint combined icon animator for BiometricPrompt. */ -open class AuthBiometricFingerprintAndFaceIconController( - context: Context, - iconView: LottieAnimationView, - iconViewOverlay: LottieAnimationView, -) : AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) { - - override val actsAsConfirmButton: Boolean = true - - override fun shouldAnimateIconViewForTransition( - oldState: BiometricState, - newState: BiometricState - ): Boolean = when (newState) { - STATE_PENDING_CONFIRMATION -> true - else -> super.shouldAnimateIconViewForTransition(oldState, newState) - } - - @RawRes - override fun getAnimationForTransition( - oldState: BiometricState, - newState: BiometricState - ): Int? = when (newState) { - STATE_AUTHENTICATED -> { - if (oldState == STATE_PENDING_CONFIRMATION) { - R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie - } else { - super.getAnimationForTransition(oldState, newState) - } - } - STATE_PENDING_CONFIRMATION -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - R.raw.fingerprint_dialogue_error_to_unlock_lottie - } else { - R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie - } - } - else -> super.getAnimationForTransition(oldState, newState) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt deleted file mode 100644 index 0ad3848299f939ac4b031c00001958d3a4dd23a3..0000000000000000000000000000000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics - -import android.annotation.RawRes -import android.content.Context -import android.content.Context.FINGERPRINT_SERVICE -import android.hardware.fingerprint.FingerprintManager -import android.view.DisplayInfo -import android.view.Surface -import android.view.View -import androidx.annotation.VisibleForTesting -import com.airbnb.lottie.LottieAnimationView -import com.android.settingslib.widget.LottieColorUtils -import com.android.systemui.res.R -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATED -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATING -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATING_ANIMATING_IN -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_ERROR -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_HELP -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_IDLE -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - - -/** Fingerprint only icon animator for BiometricPrompt. */ -open class AuthBiometricFingerprintIconController( - context: Context, - iconView: LottieAnimationView, - protected val iconViewOverlay: LottieAnimationView -) : AuthIconController(context, iconView) { - - private val isSideFps: Boolean - private val isReverseDefaultRotation = - context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) - - var iconLayoutParamSize: Pair<Int, Int> = Pair(1, 1) - set(value) { - if (field == value) { - return - } - iconViewOverlay.layoutParams.width = value.first - iconViewOverlay.layoutParams.height = value.second - iconView.layoutParams.width = value.first - iconView.layoutParams.height = value.second - field = value - } - - init { - iconLayoutParamSize = Pair(context.resources.getDimensionPixelSize( - R.dimen.biometric_dialog_fingerprint_icon_width), - context.resources.getDimensionPixelSize( - R.dimen.biometric_dialog_fingerprint_icon_height)) - isSideFps = - (context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager?)?.let { fpm -> - fpm.sensorPropertiesInternal.any { it.isAnySidefpsType } - } ?: false - preloadAssets(context) - val displayInfo = DisplayInfo() - context.display?.getDisplayInfo(displayInfo) - if (isSideFps && getRotationFromDefault(displayInfo.rotation) == Surface.ROTATION_180) { - iconView.rotation = 180f - } - } - - private fun updateIconSideFps(lastState: BiometricState, newState: BiometricState) { - val displayInfo = DisplayInfo() - context.display?.getDisplayInfo(displayInfo) - val rotation = getRotationFromDefault(displayInfo.rotation) - val iconViewOverlayAnimation = - getSideFpsOverlayAnimationForTransition(lastState, newState, rotation) ?: return - - if (!(lastState == STATE_AUTHENTICATING_ANIMATING_IN && newState == STATE_AUTHENTICATING)) { - iconViewOverlay.setAnimation(iconViewOverlayAnimation) - } - - val iconContentDescription = getIconContentDescription(newState) - if (iconContentDescription != null) { - iconView.contentDescription = iconContentDescription - } - - iconView.frame = 0 - iconViewOverlay.frame = 0 - if (shouldAnimateSfpsIconViewForTransition(lastState, newState)) { - iconView.playAnimation() - } - - if (shouldAnimateIconViewOverlayForTransition(lastState, newState)) { - iconViewOverlay.playAnimation() - } - - LottieColorUtils.applyDynamicColors(context, iconView) - LottieColorUtils.applyDynamicColors(context, iconViewOverlay) - } - - private fun updateIconNormal(lastState: BiometricState, newState: BiometricState) { - val icon = getAnimationForTransition(lastState, newState) ?: return - - if (!(lastState == STATE_AUTHENTICATING_ANIMATING_IN && newState == STATE_AUTHENTICATING)) { - iconView.setAnimation(icon) - } - - val iconContentDescription = getIconContentDescription(newState) - if (iconContentDescription != null) { - iconView.contentDescription = iconContentDescription - } - - iconView.frame = 0 - if (shouldAnimateIconViewForTransition(lastState, newState)) { - iconView.playAnimation() - } - LottieColorUtils.applyDynamicColors(context, iconView) - } - - override fun updateIcon(lastState: BiometricState, newState: BiometricState) { - if (isSideFps) { - updateIconSideFps(lastState, newState) - } else { - iconViewOverlay.visibility = View.GONE - updateIconNormal(lastState, newState) - } - } - - @VisibleForTesting - fun getIconContentDescription(newState: BiometricState): CharSequence? { - val id = when (newState) { - STATE_IDLE, - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING, - STATE_AUTHENTICATED -> - if (isSideFps) { - R.string.security_settings_sfps_enroll_find_sensor_message - } else { - R.string.fingerprint_dialog_touch_sensor - } - STATE_PENDING_CONFIRMATION -> - if (isSideFps) { - R.string.security_settings_sfps_enroll_find_sensor_message - } else { - R.string.fingerprint_dialog_authenticated_confirmation - } - STATE_ERROR, - STATE_HELP -> R.string.biometric_dialog_try_again - else -> null - } - return if (id != null) context.getString(id) else null - } - - protected open fun shouldAnimateIconViewForTransition( - oldState: BiometricState, - newState: BiometricState - ) = when (newState) { - STATE_HELP, - STATE_ERROR -> true - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP - STATE_AUTHENTICATED -> true - else -> false - } - - private fun shouldAnimateSfpsIconViewForTransition( - oldState: BiometricState, - newState: BiometricState - ) = when (newState) { - STATE_HELP, - STATE_ERROR -> true - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> - oldState == STATE_ERROR || oldState == STATE_HELP || oldState == STATE_IDLE - STATE_AUTHENTICATED -> true - else -> false - } - - protected open fun shouldAnimateIconViewOverlayForTransition( - oldState: BiometricState, - newState: BiometricState - ) = when (newState) { - STATE_HELP, - STATE_ERROR -> true - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP - STATE_AUTHENTICATED -> true - else -> false - } - - @RawRes - protected open fun getAnimationForTransition( - oldState: BiometricState, - newState: BiometricState - ): Int? { - val id = when (newState) { - STATE_HELP, - STATE_ERROR -> { - R.raw.fingerprint_dialogue_fingerprint_to_error_lottie - } - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - R.raw.fingerprint_dialogue_error_to_fingerprint_lottie - } else { - R.raw.fingerprint_dialogue_fingerprint_to_error_lottie - } - } - STATE_AUTHENTICATED -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - R.raw.fingerprint_dialogue_error_to_success_lottie - } else { - R.raw.fingerprint_dialogue_fingerprint_to_success_lottie - } - } - else -> return null - } - return if (id != null) return id else null - } - - private fun getRotationFromDefault(rotation: Int): Int = - if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation - - @RawRes - private fun getSideFpsOverlayAnimationForTransition( - oldState: BiometricState, - newState: BiometricState, - rotation: Int - ): Int? = when (newState) { - STATE_HELP, - STATE_ERROR -> { - when (rotation) { - Surface.ROTATION_0 -> R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright - else -> R.raw.biometricprompt_fingerprint_to_error_landscape - } - } - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - when (rotation) { - Surface.ROTATION_0 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright - else -> R.raw.biometricprompt_symbol_error_to_fingerprint_landscape - } - } else { - when (rotation) { - Surface.ROTATION_0 -> R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright - else -> R.raw.biometricprompt_fingerprint_to_error_landscape - } - } - } - STATE_AUTHENTICATED -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - when (rotation) { - Surface.ROTATION_0 -> - R.raw.biometricprompt_symbol_error_to_success_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_error_to_success_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_symbol_error_to_success_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright - else -> R.raw.biometricprompt_symbol_error_to_success_landscape - } - } else { - when (rotation) { - Surface.ROTATION_0 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright - else -> R.raw.biometricprompt_symbol_fingerprint_to_success_landscape - } - } - } - else -> null - } - - private fun preloadAssets(context: Context) { - if (isSideFps) { - cacheLottieAssetsInContext( - context, - R.raw.biometricprompt_fingerprint_to_error_landscape, - R.raw.biometricprompt_folded_base_bottomright, - R.raw.biometricprompt_folded_base_default, - R.raw.biometricprompt_folded_base_topleft, - R.raw.biometricprompt_landscape_base, - R.raw.biometricprompt_portrait_base_bottomright, - R.raw.biometricprompt_portrait_base_topleft, - R.raw.biometricprompt_symbol_error_to_fingerprint_landscape, - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright, - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft, - R.raw.biometricprompt_symbol_error_to_success_landscape, - R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright, - R.raw.biometricprompt_symbol_error_to_success_portrait_topleft, - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright, - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft, - R.raw.biometricprompt_symbol_fingerprint_to_success_landscape, - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright, - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft - ) - } else { - cacheLottieAssetsInContext( - context, - R.raw.fingerprint_dialogue_error_to_fingerprint_lottie, - R.raw.fingerprint_dialogue_error_to_success_lottie, - R.raw.fingerprint_dialogue_fingerprint_to_error_lottie, - R.raw.fingerprint_dialogue_fingerprint_to_success_lottie - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt deleted file mode 100644 index 958213afacdf209757d37d5f9bdf004f63c71d86..0000000000000000000000000000000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics - -import android.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 com.airbnb.lottie.LottieAnimationView -import com.airbnb.lottie.LottieCompositionFactory -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState - -private const val TAG = "AuthIconController" - -/** Controller for animating the BiometricPrompt icon/affordance. */ -abstract class AuthIconController( - protected val context: Context, - protected val iconView: LottieAnimationView -) : 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(lastState: BiometricState, newState: BiometricState) { - if (deactivated) { - Log.w(TAG, "Ignoring updateState when deactivated: $newState") - } else { - updateIcon(lastState, newState) - } - } - - /** Call during [updateState] if the controller is not [deactivated]. */ - abstract fun updateIcon(lastState: BiometricState, newState: BiometricState) - - /** Called during [onAnimationEnd] if the controller is not [deactivated]. */ - open fun handleAnimationEnd(drawable: Drawable) {} - - // TODO(b/251476085): Migrate this to an extension at the appropriate level? - /** Load the given [rawResources] immediately so they are cached for use in the [context]. */ - protected fun cacheLottieAssetsInContext(context: Context, vararg rawResources: Int) { - for (res in rawResources) { - LottieCompositionFactory.fromRawRes(context, res) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt index c4c52e8b358ec93826e4e4004988c2ef25885c5f..050b399fd3e80d5059fc0928040b495e612f9c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt @@ -42,8 +42,11 @@ import kotlinx.coroutines.flow.stateIn /** Repository for the current state of the display */ interface DisplayStateRepository { /** - * Whether or not the direction rotation is applied to get to an application's requested - * orientation is reversed. + * If true, the direction rotation is applied to get to an application's requested orientation + * is reversed. Normally, the model is that landscape is clockwise from portrait; thus on a + * portrait device an app requesting landscape will cause a clockwise rotation, and on a + * landscape device an app requesting portrait will cause a counter-clockwise rotation. Setting + * true here reverses that logic. See go/natural-orientation for context. */ val isReverseDefaultRotation: Boolean diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt index a317a068405514844fa0162e891c627e4b2d484a..427361d4b17a631e0146fde3f2a777dfa26906bc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt @@ -57,6 +57,15 @@ interface DisplayStateInteractor { /** Display change event indicating a change to the given displayId has occurred. */ val displayChanges: Flow<Int> + /** + * If true, the direction rotation is applied to get to an application's requested orientation + * is reversed. Normally, the model is that landscape is clockwise from portrait; thus on a + * portrait device an app requesting landscape will cause a clockwise rotation, and on a + * landscape device an app requesting portrait will cause a counter-clockwise rotation. Setting + * true here reverses that logic. See go/natural-orientation for context. + */ + val isReverseDefaultRotation: Boolean + /** Called on configuration changes, used to keep the display state in sync */ fun onConfigurationChanged(newConfig: Configuration) } @@ -112,6 +121,8 @@ constructor( override val currentRotation: StateFlow<DisplayRotation> = displayStateRepository.currentRotation + override val isReverseDefaultRotation: Boolean = displayStateRepository.isReverseDefaultRotation + override fun onConfigurationChanged(newConfig: Configuration) { screenSizeFoldProvider.onConfigurationChange(newConfig) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java index cef0be09d3ce1e377273fbdef23c1d21ce2c88b5..0d72b9c07d7a53f3b30894d29d85afb5ffd6f1de 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java @@ -29,11 +29,10 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; -import com.android.systemui.res.R; -import com.android.systemui.biometrics.AuthBiometricFingerprintIconController; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.AuthDialog; import com.android.systemui.biometrics.UdfpsDialogMeasureAdapter; +import com.android.systemui.res.R; import kotlin.Pair; @@ -85,13 +84,13 @@ public class BiometricPromptLayout extends LinearLayout { } @Deprecated - public void updateFingerprintAffordanceSize( - @NonNull AuthBiometricFingerprintIconController iconController) { + public Pair<Integer, Integer> getUpdatedFingerprintAffordanceSize() { if (mUdfpsAdapter != null) { final int sensorDiameter = mUdfpsAdapter.getSensorDiameter( mScaleFactorProvider.provide()); - iconController.setIconLayoutParamSize(new Pair(sensorDiameter, sensorDiameter)); + return new Pair(sensorDiameter, sensorDiameter); } + return null; } @NonNull diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index c29efc0fcab96efc82b95bb3f596985afff0e510..ac48b6a2b11e6d9151410c52b7c652b5dd6eacd6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -38,11 +38,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.biometrics.AuthBiometricFaceIconController -import com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceIconController -import com.android.systemui.biometrics.AuthBiometricFingerprintIconController -import com.android.systemui.biometrics.AuthIconController +import com.airbnb.lottie.LottieCompositionFactory import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality @@ -56,6 +52,7 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -101,10 +98,15 @@ object BiometricViewBinder { !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled descriptionView.movementMethod = ScrollingMovementMethod() - val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay) + val iconOverlayView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay) val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon) - PromptFingerprintIconViewBinder.bind(iconView, viewModel.fingerprintIconViewModel) + PromptIconViewBinder.bind( + iconView, + iconOverlayView, + view.getUpdatedFingerprintAffordanceSize(), + viewModel.iconViewModel + ) val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) @@ -128,9 +130,21 @@ object BiometricViewBinder { // bind to prompt var boundSize = false + view.repeatWhenAttached { // these do not change and need to be set before any size transitions val modalities = viewModel.modalities.first() + if (modalities.hasFingerprint) { + /** + * Load the given [rawResources] immediately so they are cached for use in the + * [context]. + */ + val rawResources = viewModel.iconViewModel.getRawAssets(modalities.hasSfps) + for (res in rawResources) { + LottieCompositionFactory.fromRawRes(view.context, res) + } + } + titleView.text = viewModel.title.first() descriptionView.text = viewModel.description.first() subtitleView.text = viewModel.subtitle.first() @@ -148,27 +162,8 @@ object BiometricViewBinder { legacyCallback.onButtonTryAgain() } - // TODO(b/251476085): migrate legacy icon controllers and remove - var legacyState = viewModel.legacyState.value - val iconController = - modalities.asIconController( - view.context, - iconView, - iconViewOverlay, - ) - adapter.attach(this, iconController, modalities, legacyCallback) - if (iconController is AuthBiometricFingerprintIconController) { - view.updateFingerprintAffordanceSize(iconController) - } - if (iconController is HackyCoexIconController) { - iconController.faceMode = !viewModel.isConfirmationRequired.first() - } + adapter.attach(this, modalities, legacyCallback) - // the icon controller must be created before this happens for the legacy - // sizing code in BiometricPromptLayout to work correctly. Simplify this - // when those are also migrated. (otherwise the icon size may not be set to - // a pixel value before the view is measured and WRAP_CONTENT will be incorrectly - // used as part of the measure spec) if (!boundSize) { boundSize = true BiometricViewSizeBinder.bind( @@ -212,14 +207,6 @@ object BiometricViewBinder { ) { legacyCallback.onStartDelayedFingerprintSensor() } - - if (newMode.isStarted) { - // do wonky switch from implicit to explicit flow - (iconController as? HackyCoexIconController)?.faceMode = false - viewModel.showAuthenticating( - modalities.asDefaultHelpMessage(view.context), - ) - } } } @@ -312,7 +299,7 @@ object BiometricViewBinder { viewModel.isIconConfirmButton .map { isPending -> when { - isPending && iconController.actsAsConfirmButton -> + isPending && modalities.hasFaceAndFingerprint -> View.OnTouchListener { _: View, event: MotionEvent -> viewModel.onOverlayTouch(event) } @@ -320,22 +307,11 @@ object BiometricViewBinder { } } .collect { onTouch -> - iconViewOverlay.setOnTouchListener(onTouch) + iconOverlayView.setOnTouchListener(onTouch) iconView.setOnTouchListener(onTouch) } } - // TODO(b/251476085): remove w/ legacy icon controllers - // set icon affordance using legacy states - // like the old code, this causes animations to repeat on config changes :( - // but keep behavior for now as no one has complained... - launch { - viewModel.legacyState.collect { newState -> - iconController.updateState(legacyState, newState) - legacyState = newState - } - } - // dismiss prompt when authenticated and confirmed launch { viewModel.isAuthenticated.collect { authState -> @@ -350,7 +326,7 @@ object BiometricViewBinder { // Allow icon to be used as confirmation button with a11y enabled if (accessibilityManager.isTouchExplorationEnabled) { - iconViewOverlay.setOnClickListener { + iconOverlayView.setOnClickListener { viewModel.confirmAuthenticated() } iconView.setOnClickListener { viewModel.confirmAuthenticated() } @@ -377,7 +353,6 @@ object BiometricViewBinder { launch { viewModel.message.collect { promptMessage -> val isError = promptMessage is PromptMessage.Error - indicatorMessageView.text = promptMessage.message indicatorMessageView.setTextColor( if (isError) textColorError else textColorHint @@ -472,9 +447,6 @@ class Spaghetti( private var modalities: BiometricModalities = BiometricModalities() private var legacyCallback: Callback? = null - var legacyIconController: AuthIconController? = null - private set - // hacky way to suppress lockout errors private val lockoutErrorStrings = listOf( @@ -485,24 +457,20 @@ class Spaghetti( fun attach( lifecycleOwner: LifecycleOwner, - iconController: AuthIconController, activeModalities: BiometricModalities, callback: Callback, ) { modalities = activeModalities - legacyIconController = iconController legacyCallback = callback lifecycleOwner.lifecycle.addObserver( object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { lifecycleScope = owner.lifecycleScope - iconController.deactivated = false } override fun onDestroy(owner: LifecycleOwner) { lifecycleScope = null - iconController.deactivated = true } } ) @@ -626,61 +594,9 @@ private fun BiometricModalities.asDefaultHelpMessage(context: Context): String = else -> "" } -private fun BiometricModalities.asIconController( - context: Context, - iconView: LottieAnimationView, - iconViewOverlay: LottieAnimationView, -): AuthIconController = - when { - hasFaceAndFingerprint -> HackyCoexIconController(context, iconView, iconViewOverlay) - hasFingerprint -> AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) - hasFace -> AuthBiometricFaceIconController(context, iconView) - else -> throw IllegalStateException("unexpected view type :$this") - } - private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE // TODO(b/251476085): proper type? typealias BiometricJankListener = Animator.AnimatorListener - -// TODO(b/251476085): delete - temporary until the legacy icon controllers are replaced -private class HackyCoexIconController( - context: Context, - iconView: LottieAnimationView, - iconViewOverlay: LottieAnimationView, -) : AuthBiometricFingerprintAndFaceIconController(context, iconView, iconViewOverlay) { - - private var state: Spaghetti.BiometricState? = null - private val faceController = AuthBiometricFaceIconController(context, iconView) - - var faceMode: Boolean = true - set(value) { - if (field != value) { - field = value - - faceController.deactivated = !value - iconView.setImageIcon(null) - iconViewOverlay.setImageIcon(null) - state?.let { updateIcon(Spaghetti.BiometricState.STATE_IDLE, it) } - } - } - - override fun updateIcon( - lastState: Spaghetti.BiometricState, - newState: Spaghetti.BiometricState, - ) { - if (deactivated) { - return - } - - if (faceMode) { - faceController.updateIcon(lastState, newState) - } else { - super.updateIcon(lastState, newState) - } - - state = newState - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt deleted file mode 100644 index d28f1dc78c1333d6cdb6957c27121f2f19a990e6..0000000000000000000000000000000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2023 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.ui.binder - -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.biometrics.ui.viewmodel.PromptFingerprintIconViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.launch - -/** Sub-binder for [BiometricPromptLayout.iconView]. */ -object PromptFingerprintIconViewBinder { - - /** Binds [BiometricPromptLayout.iconView] to [PromptFingerprintIconViewModel]. */ - @JvmStatic - fun bind(view: LottieAnimationView, viewModel: PromptFingerprintIconViewModel) { - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.onConfigurationChanged(view.context.resources.configuration) - launch { - viewModel.iconAsset.collect { iconAsset -> - if (iconAsset != -1) { - view.setAnimation(iconAsset) - // TODO: must replace call below once non-sfps asset logic and - // shouldAnimateIconView logic is migrated to this ViewModel. - view.playAnimation() - } - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt new file mode 100644 index 0000000000000000000000000000000000000000..475ef18e50993e2b048ae0355b5422b6d5a03b98 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2023 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.ui.binder + +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.airbnb.lottie.LottieAnimationView +import com.android.settingslib.widget.LottieColorUtils +import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel +import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.util.kotlin.Utils.Companion.toQuad +import com.android.systemui.util.kotlin.Utils.Companion.toQuint +import com.android.systemui.util.kotlin.Utils.Companion.toTriple +import com.android.systemui.util.kotlin.sample +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Sub-binder for [BiometricPromptLayout.iconView]. */ +object PromptIconViewBinder { + /** + * Binds [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay] to + * [PromptIconViewModel]. + */ + @JvmStatic + fun bind( + iconView: LottieAnimationView, + iconOverlayView: LottieAnimationView, + iconViewLayoutParamSizeOverride: Pair<Int, Int>?, + viewModel: PromptIconViewModel + ) { + iconView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.onConfigurationChanged(iconView.context.resources.configuration) + if (iconViewLayoutParamSizeOverride != null) { + iconView.layoutParams.width = iconViewLayoutParamSizeOverride.first + iconView.layoutParams.height = iconViewLayoutParamSizeOverride.second + + iconOverlayView.layoutParams.width = iconViewLayoutParamSizeOverride.first + iconOverlayView.layoutParams.height = iconViewLayoutParamSizeOverride.second + } + + var faceIcon: AnimatedVectorDrawable? = null + val faceIconCallback = + object : Animatable2.AnimationCallback() { + override fun onAnimationStart(drawable: Drawable) { + viewModel.onAnimationStart() + } + + override fun onAnimationEnd(drawable: Drawable) { + viewModel.onAnimationEnd() + } + } + + launch { + viewModel.activeAuthType.collect { activeAuthType -> + if (iconViewLayoutParamSizeOverride == null) { + val width: Int + val height: Int + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> { + width = viewModel.fingerprintIconWidth + height = viewModel.fingerprintIconHeight + } + AuthType.Face -> { + width = viewModel.faceIconWidth + height = viewModel.faceIconHeight + } + } + + iconView.layoutParams.width = width + iconView.layoutParams.height = height + + iconOverlayView.layoutParams.width = width + iconOverlayView.layoutParams.height = height + } + } + } + + launch { + viewModel.iconAsset + .sample( + combine( + viewModel.activeAuthType, + viewModel.shouldAnimateIconView, + viewModel.shouldRepeatAnimation, + viewModel.showingError, + ::toQuad + ), + ::toQuint + ) + .collect { + ( + iconAsset, + activeAuthType, + shouldAnimateIconView, + shouldRepeatAnimation, + showingError) -> + if (iconAsset != -1) { + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> { + iconView.setAnimation(iconAsset) + iconView.frame = 0 + + if (shouldAnimateIconView) { + iconView.playAnimation() + } + } + AuthType.Face -> { + faceIcon?.apply { + unregisterAnimationCallback(faceIconCallback) + stop() + } + faceIcon = + iconView.context.getDrawable(iconAsset) + as AnimatedVectorDrawable + faceIcon?.apply { + iconView.setImageDrawable(this) + if (shouldAnimateIconView) { + forceAnimationOnUI() + if (shouldRepeatAnimation) { + registerAnimationCallback(faceIconCallback) + } + start() + } + } + } + } + LottieColorUtils.applyDynamicColors(iconView.context, iconView) + viewModel.setPreviousIconWasError(showingError) + } + } + } + + launch { + viewModel.iconOverlayAsset + .sample( + combine( + viewModel.shouldAnimateIconOverlay, + viewModel.showingError, + ::Pair + ), + ::toTriple + ) + .collect { (iconOverlayAsset, shouldAnimateIconOverlay, showingError) -> + if (iconOverlayAsset != -1) { + iconOverlayView.setAnimation(iconOverlayAsset) + iconOverlayView.frame = 0 + LottieColorUtils.applyDynamicColors( + iconOverlayView.context, + iconOverlayView + ) + + if (shouldAnimateIconOverlay) { + iconOverlayView.playAnimation() + } + viewModel.setPreviousIconOverlayWasError(showingError) + } + } + } + + launch { + viewModel.shouldFlipIconView.collect { shouldFlipIconView -> + if (shouldFlipIconView) { + iconView.rotation = 180f + } + } + } + + launch { + viewModel.contentDescriptionId.collect { id -> + if (id != -1) { + iconView.contentDescription = iconView.context.getString(id) + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt deleted file mode 100644 index dfd3a9b8aebe9257a32af14af9630301b9769b78..0000000000000000000000000000000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2023 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.ui.viewmodel - -import android.annotation.RawRes -import android.content.res.Configuration -import com.android.systemui.res.R -import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor -import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor -import com.android.systemui.biometrics.shared.model.DisplayRotation -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine - -/** Models UI of [BiometricPromptLayout.iconView] */ -class PromptFingerprintIconViewModel -@Inject -constructor( - private val displayStateInteractor: DisplayStateInteractor, - promptSelectorInteractor: PromptSelectorInteractor, -) { - /** Current BiometricPromptLayout.iconView asset. */ - val iconAsset: Flow<Int> = - combine( - displayStateInteractor.currentRotation, - displayStateInteractor.isFolded, - displayStateInteractor.isInRearDisplayMode, - promptSelectorInteractor.sensorType, - ) { - rotation: DisplayRotation, - isFolded: Boolean, - isInRearDisplayMode: Boolean, - sensorType: FingerprintSensorType -> - when (sensorType) { - FingerprintSensorType.POWER_BUTTON -> - getSideFpsAnimationAsset(rotation, isFolded, isInRearDisplayMode) - // Replace below when non-SFPS iconAsset logic is migrated to this ViewModel - else -> -1 - } - } - - @RawRes - private fun getSideFpsAnimationAsset( - rotation: DisplayRotation, - isDeviceFolded: Boolean, - isInRearDisplayMode: Boolean, - ): Int = - when (rotation) { - DisplayRotation.ROTATION_90 -> - if (isInRearDisplayMode) { - R.raw.biometricprompt_rear_portrait_reverse_base - } else if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_topleft - } else { - R.raw.biometricprompt_portrait_base_topleft - } - DisplayRotation.ROTATION_270 -> - if (isInRearDisplayMode) { - R.raw.biometricprompt_rear_portrait_base - } else if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_bottomright - } else { - R.raw.biometricprompt_portrait_base_bottomright - } - else -> - if (isInRearDisplayMode) { - R.raw.biometricprompt_rear_landscape_base - } else if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_default - } else { - R.raw.biometricprompt_landscape_base - } - } - - /** Called on configuration changes */ - fun onConfigurationChanged(newConfig: Configuration) { - displayStateInteractor.onConfigurationChanged(newConfig) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..11a5d8b578df9b27a9ccbf6f9b24a17d3af81ad6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt @@ -0,0 +1,721 @@ +/* + * Copyright (C) 2023 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.ui.viewmodel + +import android.annotation.DrawableRes +import android.annotation.RawRes +import android.content.res.Configuration +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor +import com.android.systemui.biometrics.shared.model.DisplayRotation +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.res.R +import com.android.systemui.util.kotlin.combine +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** + * Models UI of [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay] + */ +class PromptIconViewModel +constructor( + promptViewModel: PromptViewModel, + private val displayStateInteractor: DisplayStateInteractor, + promptSelectorInteractor: PromptSelectorInteractor +) { + + /** Auth types for the UI to display. */ + enum class AuthType { + Fingerprint, + Face, + Coex + } + + /** + * Indicates what auth type the UI currently displays. + * Fingerprint-only auth -> Fingerprint + * Face-only auth -> Face + * Co-ex auth, implicit flow -> Face + * Co-ex auth, explicit flow -> Coex + */ + val activeAuthType: Flow<AuthType> = + combine( + promptViewModel.modalities.distinctUntilChanged(), + promptViewModel.faceMode.distinctUntilChanged() + ) { modalities, faceMode -> + if (modalities.hasFaceAndFingerprint && !faceMode) { + AuthType.Coex + } else if (modalities.hasFaceOnly || faceMode) { + AuthType.Face + } else if (modalities.hasFingerprintOnly) { + AuthType.Fingerprint + } else { + throw IllegalStateException("unexpected modality: $modalities") + } + } + + /** Whether an error message is currently being shown. */ + val showingError = promptViewModel.showingError + + /** Whether the previous icon shown displayed an error. */ + private val _previousIconWasError: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** Whether the previous icon overlay shown displayed an error. */ + private val _previousIconOverlayWasError: MutableStateFlow<Boolean> = MutableStateFlow(false) + + fun setPreviousIconWasError(previousIconWasError: Boolean) { + _previousIconWasError.value = previousIconWasError + } + + fun setPreviousIconOverlayWasError(previousIconOverlayWasError: Boolean) { + _previousIconOverlayWasError.value = previousIconOverlayWasError + } + + /** Called when iconView begins animating. */ + fun onAnimationStart() { + _animationEnded.value = false + } + + /** Called when iconView ends animating. */ + fun onAnimationEnd() { + _animationEnded.value = true + } + + private val _animationEnded: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** + * Whether a face iconView should pulse (i.e. while isAuthenticating and previous animation + * ended). + */ + val shouldPulseAnimation: Flow<Boolean> = + combine(_animationEnded, promptViewModel.isAuthenticating) { + animationEnded, + isAuthenticating -> + animationEnded && isAuthenticating + } + .distinctUntilChanged() + + private val _lastPulseLightToDark: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** Tracks whether a face iconView last pulsed light to dark (vs. dark to light) */ + val lastPulseLightToDark: Flow<Boolean> = _lastPulseLightToDark.asStateFlow() + + /** Layout params for fingerprint iconView */ + val fingerprintIconWidth: Int = promptViewModel.fingerprintIconWidth + val fingerprintIconHeight: Int = promptViewModel.fingerprintIconHeight + + /** Layout params for face iconView */ + val faceIconWidth: Int = promptViewModel.faceIconWidth + val faceIconHeight: Int = promptViewModel.faceIconHeight + + /** Current BiometricPromptLayout.iconView asset. */ + val iconAsset: Flow<Int> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint -> + combine( + displayStateInteractor.currentRotation, + displayStateInteractor.isFolded, + displayStateInteractor.isInRearDisplayMode, + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + rotation: DisplayRotation, + isFolded: Boolean, + isInRearDisplayMode: Boolean, + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + getSfpsIconViewAsset(rotation, isFolded, isInRearDisplayMode) + else -> + getFingerprintIconViewAsset( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + } + } + AuthType.Face -> + shouldPulseAnimation.flatMapLatest { shouldPulseAnimation: Boolean -> + if (shouldPulseAnimation) { + val iconAsset = + if (_lastPulseLightToDark.value) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + _lastPulseLightToDark.value = !_lastPulseLightToDark.value + flowOf(iconAsset) + } else { + combine( + promptViewModel.isAuthenticated.distinctUntilChanged(), + promptViewModel.isAuthenticating.distinctUntilChanged(), + promptViewModel.isPendingConfirmation.distinctUntilChanged(), + promptViewModel.showingError.distinctUntilChanged() + ) { + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + getFaceIconViewAsset( + authState, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } + } + AuthType.Coex -> + combine( + displayStateInteractor.currentRotation, + displayStateInteractor.isFolded, + displayStateInteractor.isInRearDisplayMode, + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.isPendingConfirmation, + promptViewModel.showingError, + ) { + rotation: DisplayRotation, + isFolded: Boolean, + isInRearDisplayMode: Boolean, + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + getSfpsIconViewAsset(rotation, isFolded, isInRearDisplayMode) + else -> + getCoexIconViewAsset( + authState, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } + } + } + + private fun getFingerprintIconViewAsset( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ): Int = + if (isAuthenticated) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_success_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_success_lottie + } + } else if (isAuthenticating) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_fingerprint_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } + } else if (showingError) { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } else { + -1 + } + + @RawRes + private fun getSfpsIconViewAsset( + rotation: DisplayRotation, + isDeviceFolded: Boolean, + isInRearDisplayMode: Boolean, + ): Int = + when (rotation) { + DisplayRotation.ROTATION_90 -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_portrait_reverse_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_topleft + } else { + R.raw.biometricprompt_portrait_base_topleft + } + DisplayRotation.ROTATION_270 -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_portrait_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_bottomright + } else { + R.raw.biometricprompt_portrait_base_bottomright + } + else -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_landscape_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_default + } else { + R.raw.biometricprompt_landscape_base + } + } + + @DrawableRes + private fun getFaceIconViewAsset( + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ): Int = + if (authState.isAuthenticated && isPendingConfirmation) { + R.drawable.face_dialog_wink_from_dark + } else if (authState.isAuthenticated) { + R.drawable.face_dialog_dark_to_checkmark + } else if (isAuthenticating) { + _lastPulseLightToDark.value = false + R.drawable.face_dialog_pulse_dark_to_light + } else if (showingError) { + R.drawable.face_dialog_dark_to_error + } else if (_previousIconWasError.value) { + R.drawable.face_dialog_error_to_idle + } else { + R.drawable.face_dialog_idle_static + } + + @RawRes + private fun getCoexIconViewAsset( + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ): Int = + if (authState.isAuthenticatedAndExplicitlyConfirmed) { + R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie + } else if (isPendingConfirmation) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_unlock_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie + } + } else if (authState.isAuthenticated) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_success_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_success_lottie + } + } else if (isAuthenticating) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_fingerprint_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } + } else if (showingError) { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } else { + -1 + } + + /** Current BiometricPromptLayout.biometric_icon_overlay asset. */ + var iconOverlayAsset: Flow<Int> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + displayStateInteractor.currentRotation, + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + rotation: DisplayRotation, + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + getSfpsIconOverlayAsset( + rotation, + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> -1 + } + } + AuthType.Face -> flowOf(-1) + } + } + + @RawRes + private fun getSfpsIconOverlayAsset( + rotation: DisplayRotation, + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ): Int = + if (isAuthenticated) { + if (_previousIconOverlayWasError.value) { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_error_to_success_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_error_to_success_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_error_to_success_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright + } + } else { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright + } + } + } else if (isAuthenticating) { + if (_previousIconOverlayWasError.value) { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright + } + } else { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + } + } + } else if (showingError) { + when (rotation) { + DisplayRotation.ROTATION_0 -> R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + } + } else { + -1 + } + + /** Content description for iconView */ + val contentDescriptionId: Flow<Int> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.isPendingConfirmation, + promptViewModel.showingError + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + getFingerprintIconContentDescriptionId( + sensorType, + authState.isAuthenticated, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + AuthType.Face -> + combine( + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError, + ) { authState: PromptAuthState, isAuthenticating: Boolean, showingError: Boolean + -> + getFaceIconContentDescriptionId(authState, isAuthenticating, showingError) + } + } + } + + private fun getFingerprintIconContentDescriptionId( + sensorType: FingerprintSensorType, + isAuthenticated: Boolean, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ): Int = + if (isPendingConfirmation) { + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + R.string.security_settings_sfps_enroll_find_sensor_message + else -> R.string.fingerprint_dialog_authenticated_confirmation + } + } else if (isAuthenticating || isAuthenticated) { + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + R.string.security_settings_sfps_enroll_find_sensor_message + else -> R.string.fingerprint_dialog_touch_sensor + } + } else if (showingError) { + R.string.biometric_dialog_try_again + } else { + -1 + } + + private fun getFaceIconContentDescriptionId( + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean + ): Int = + if (authState.isAuthenticatedAndExplicitlyConfirmed) { + R.string.biometric_dialog_face_icon_description_confirmed + } else if (authState.isAuthenticated) { + R.string.biometric_dialog_face_icon_description_authenticated + } else if (isAuthenticating) { + R.string.biometric_dialog_face_icon_description_authenticating + } else if (showingError) { + R.string.keyguard_face_failed + } else { + R.string.biometric_dialog_face_icon_description_idle + } + + /** Whether the current BiometricPromptLayout.iconView asset animation should be playing. */ + val shouldAnimateIconView: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + shouldAnimateSfpsIconView( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> + shouldAnimateFingerprintIconView( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + } + } + AuthType.Face -> + combine( + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { authState: PromptAuthState, isAuthenticating: Boolean, showingError: Boolean + -> + isAuthenticating || + authState.isAuthenticated || + showingError || + _previousIconWasError.value + } + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.isPendingConfirmation, + promptViewModel.showingError, + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + shouldAnimateSfpsIconView( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> + shouldAnimateCoexIconView( + authState.isAuthenticated, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } + } + } + + private fun shouldAnimateFingerprintIconView( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ) = (isAuthenticating && _previousIconWasError.value) || isAuthenticated || showingError + + private fun shouldAnimateSfpsIconView( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ) = isAuthenticated || isAuthenticating || showingError + + private fun shouldAnimateCoexIconView( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ) = + (isAuthenticating && _previousIconWasError.value) || + isPendingConfirmation || + isAuthenticated || + showingError + + /** Whether the current iconOverlayAsset animation should be playing. */ + val shouldAnimateIconOverlay: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + shouldAnimateSfpsIconOverlay( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> false + } + } + AuthType.Face -> flowOf(false) + } + } + + private fun shouldAnimateSfpsIconOverlay( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ) = (isAuthenticating && _previousIconOverlayWasError.value) || isAuthenticated || showingError + + /** Whether the iconView should be flipped due to a device using reverse default rotation . */ + val shouldFlipIconView: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + displayStateInteractor.currentRotation + ) { sensorType: FingerprintSensorType, rotation: DisplayRotation -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + (rotation == DisplayRotation.ROTATION_180) + else -> false + } + } + AuthType.Face -> flowOf(false) + } + } + + /** Whether the current BiometricPromptLayout.iconView asset animation should be repeated. */ + val shouldRepeatAnimation: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> flowOf(false) + AuthType.Face -> promptViewModel.isAuthenticating.map { it } + } + } + + /** Called on configuration changes */ + fun onConfigurationChanged(newConfig: Configuration) { + displayStateInteractor.onConfigurationChanged(newConfig) + } + + /** iconView assets for caching */ + fun getRawAssets(hasSfps: Boolean): List<Int> { + return if (hasSfps) { + listOf( + R.raw.biometricprompt_fingerprint_to_error_landscape, + R.raw.biometricprompt_folded_base_bottomright, + R.raw.biometricprompt_folded_base_default, + R.raw.biometricprompt_folded_base_topleft, + R.raw.biometricprompt_landscape_base, + R.raw.biometricprompt_portrait_base_bottomright, + R.raw.biometricprompt_portrait_base_topleft, + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape, + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright, + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft, + R.raw.biometricprompt_symbol_error_to_success_landscape, + R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright, + R.raw.biometricprompt_symbol_error_to_success_portrait_topleft, + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright, + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft, + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape, + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright, + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft + ) + } else { + listOf( + R.raw.fingerprint_dialogue_error_to_fingerprint_lottie, + R.raw.fingerprint_dialogue_error_to_success_lottie, + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie, + R.raw.fingerprint_dialogue_fingerprint_to_success_lottie + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 267afae118e6ff499400d27e7bb474f509c2f30b..e49b4a7bbce9de7bfa61269305f5ea27ef7ad084 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -28,10 +28,10 @@ import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.PromptKind -import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import javax.inject.Inject import kotlinx.coroutines.Job @@ -39,7 +39,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -51,25 +50,29 @@ import kotlinx.coroutines.launch class PromptViewModel @Inject constructor( - private val displayStateInteractor: DisplayStateInteractor, - private val promptSelectorInteractor: PromptSelectorInteractor, + displayStateInteractor: DisplayStateInteractor, + promptSelectorInteractor: PromptSelectorInteractor, private val vibrator: VibratorHelper, @Application context: Context, private val featureFlags: FeatureFlags, ) { - /** Models UI of [BiometricPromptLayout.iconView] */ - val fingerprintIconViewModel: PromptFingerprintIconViewModel = - PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor) - /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = promptSelectorInteractor.prompt .map { it?.modalities ?: BiometricModalities() } .distinctUntilChanged() - // TODO(b/251476085): remove after icon controllers are migrated - do not keep this state - private var _legacyState = MutableStateFlow(Spaghetti.BiometricState.STATE_IDLE) - val legacyState: StateFlow<Spaghetti.BiometricState> = _legacyState.asStateFlow() + /** Layout params for fingerprint iconView */ + val fingerprintIconWidth: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_fingerprint_icon_width) + val fingerprintIconHeight: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_fingerprint_icon_height) + + /** Layout params for face iconView */ + val faceIconWidth: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) + val faceIconHeight: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -82,6 +85,12 @@ constructor( /** If the user has successfully authenticated and confirmed (when explicitly required). */ val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() + /** If the auth is pending confirmation. */ + val isPendingConfirmation: Flow<Boolean> = + isAuthenticated.map { authState -> + authState.isAuthenticated && authState.needsUserConfirmation + } + private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false) /** The kind of credential the user has. */ @@ -96,6 +105,9 @@ constructor( /** A message to show the user, if there is an error, hint, or help to show. */ val message: Flow<PromptMessage> = _message.asStateFlow() + /** Whether an error message is currently being shown. */ + val showingError: Flow<Boolean> = message.map { it.isError }.distinctUntilChanged() + private val isRetrySupported: Flow<Boolean> = modalities.map { it.hasFace } private val _fingerprintStartMode = MutableStateFlow(FingerprintStartMode.Pending) @@ -141,6 +153,38 @@ constructor( !isOverlayTouched && size.isNotSmall } + /** + * When fingerprint and face modalities are enrolled, indicates whether only face auth has + * started. + * + * True when fingerprint and face modalities are enrolled and implicit flow is active. This + * occurs in co-ex auth when confirmation is not required and only face auth is started, then + * becomes false when device transitions to explicit flow after a first error, when the + * fingerprint sensor is started. + * + * False when the dialog opens in explicit flow (fingerprint and face modalities enrolled but + * confirmation is required), or if user has only fingerprint enrolled, or only face enrolled. + */ + val faceMode: Flow<Boolean> = + combine(modalities, isConfirmationRequired, fingerprintStartMode) { + modalities: BiometricModalities, + isConfirmationRequired: Boolean, + fingerprintStartMode: FingerprintStartMode -> + if (modalities.hasFaceAndFingerprint) { + if (isConfirmationRequired) { + false + } else { + !fingerprintStartMode.isStarted + } + } else { + false + } + } + .distinctUntilChanged() + + val iconViewModel: PromptIconViewModel = + PromptIconViewModel(this, displayStateInteractor, promptSelectorInteractor) + /** Padding for prompt UI elements */ val promptPadding: Flow<Rect> = combine(size, displayStateInteractor.currentRotation) { size, rotation -> @@ -184,9 +228,9 @@ constructor( val isConfirmButtonVisible: Flow<Boolean> = combine( size, - isAuthenticated, - ) { size, authState -> - size.isNotSmall && authState.isAuthenticated && authState.needsUserConfirmation + isPendingConfirmation, + ) { size, isPendingConfirmation -> + size.isNotSmall && isPendingConfirmation } .distinctUntilChanged() @@ -293,7 +337,6 @@ constructor( _isAuthenticated.value = PromptAuthState(false) _forceMediumSize.value = true _message.value = PromptMessage.Error(message) - _legacyState.value = Spaghetti.BiometricState.STATE_ERROR if (hapticFeedback) { vibrator.error(failedModality) @@ -305,7 +348,7 @@ constructor( if (authenticateAfterError) { showAuthenticating(messageAfterError) } else { - showInfo(messageAfterError) + showHelp(messageAfterError) } } } @@ -325,15 +368,12 @@ constructor( private fun supportsRetry(failedModality: BiometricModality) = failedModality == BiometricModality.Face - suspend fun showHelp(message: String) = showHelp(message, clearIconError = false) - suspend fun showInfo(message: String) = showHelp(message, clearIconError = true) - /** * Show a persistent help message. * * Will be show even if the user has already authenticated. */ - private suspend fun showHelp(message: String, clearIconError: Boolean) { + suspend fun showHelp(message: String) { val alreadyAuthenticated = _isAuthenticated.value.isAuthenticated if (!alreadyAuthenticated) { _isAuthenticating.value = false @@ -343,16 +383,6 @@ constructor( _message.value = if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty _forceMediumSize.value = true - _legacyState.value = - if (alreadyAuthenticated && isConfirmationRequired.first()) { - Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - } else if (alreadyAuthenticated && !isConfirmationRequired.first()) { - Spaghetti.BiometricState.STATE_AUTHENTICATED - } else if (clearIconError) { - Spaghetti.BiometricState.STATE_IDLE - } else { - Spaghetti.BiometricState.STATE_HELP - } messageJob?.cancel() messageJob = null @@ -376,7 +406,6 @@ constructor( _message.value = if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty _forceMediumSize.value = true - _legacyState.value = Spaghetti.BiometricState.STATE_HELP messageJob?.cancel() messageJob = launch { @@ -396,7 +425,6 @@ constructor( _isAuthenticating.value = true _isAuthenticated.value = PromptAuthState(false) _message.value = if (message.isBlank()) PromptMessage.Empty else PromptMessage.Help(message) - _legacyState.value = Spaghetti.BiometricState.STATE_AUTHENTICATING // reset the try again button(s) after the user attempts a retry if (isRetry) { @@ -427,12 +455,6 @@ constructor( _isAuthenticated.value = PromptAuthState(true, modality, needsUserConfirmation, dismissAfterDelay) _message.value = PromptMessage.Empty - _legacyState.value = - if (needsUserConfirmation) { - Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - } else { - Spaghetti.BiometricState.STATE_AUTHENTICATED - } if (!needsUserConfirmation) { vibrator.success(modality) @@ -472,7 +494,6 @@ constructor( _isAuthenticated.value = authState.asExplicitlyConfirmed() _message.value = PromptMessage.Empty - _legacyState.value = Spaghetti.BiometricState.STATE_AUTHENTICATED vibrator.success(authState.authenticatedModality) diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt index 83ff789808801d1a4f19e7b0891277a70b6afbfb..b3834f58be2f865fff83d72a576d3b9998f4d5c1 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt @@ -269,3 +269,108 @@ inline fun <T> CoroutineScope.stateFlow( crossinline getValue: () -> T, ): StateFlow<T> = changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue()) + +inline fun <T1, T2, T3, T4, T5, T6, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { + args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6 + ) + } +} + +inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { + args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7 + ) + } +} + +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { + args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8 + ) + } +} + +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + flow9: Flow<T9>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9 + ) { args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[6] as T8, + args[6] as T9, + ) + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconControllerTest.kt deleted file mode 100644 index 215d63508306674c772647766d7a7058bb9b7460..0000000000000000000000000000000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconControllerTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2023 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.SensorProperties -import android.hardware.fingerprint.FingerprintManager -import android.hardware.fingerprint.FingerprintSensorProperties -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal -import android.view.ViewGroup.LayoutParams -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` as whenEver -import org.mockito.junit.MockitoJUnit - -private const val SENSOR_ID = 1 - -@SmallTest -@RunWith(AndroidJUnit4::class) -class AuthBiometricFingerprintIconControllerTest : SysuiTestCase() { - - @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var iconView: LottieAnimationView - @Mock private lateinit var iconViewOverlay: LottieAnimationView - @Mock private lateinit var layoutParam: LayoutParams - @Mock private lateinit var fingerprintManager: FingerprintManager - - private lateinit var controller: AuthBiometricFingerprintIconController - - @Before - fun setUp() { - context.addMockSystemService(Context.FINGERPRINT_SERVICE, fingerprintManager) - whenEver(iconView.layoutParams).thenReturn(layoutParam) - whenEver(iconViewOverlay.layoutParams).thenReturn(layoutParam) - } - - @Test - fun testIconContentDescription_SfpsDevice() { - setupFingerprintSensorProperties(FingerprintSensorProperties.TYPE_POWER_BUTTON) - controller = AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) - - assertThat(controller.getIconContentDescription(BiometricState.STATE_AUTHENTICATING)) - .isEqualTo( - context.resources.getString( - R.string.security_settings_sfps_enroll_find_sensor_message - ) - ) - } - - @Test - fun testIconContentDescription_NonSfpsDevice() { - setupFingerprintSensorProperties(FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) - controller = AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) - - assertThat(controller.getIconContentDescription(BiometricState.STATE_AUTHENTICATING)) - .isEqualTo(context.resources.getString(R.string.fingerprint_dialog_touch_sensor)) - } - - private fun setupFingerprintSensorProperties(sensorType: Int) { - whenEver(fingerprintManager.sensorPropertiesInternal) - .thenReturn( - listOf( - FingerprintSensorPropertiesInternal( - SENSOR_ID, - SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - listOf() /* componentInfo */, - sensorType, - true /* halControlsIllumination */, - true /* resetLockoutRequiresHardwareAuthToken */, - listOf() /* sensorLocations */ - ) - ) - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 9f24a9f553a16cd66486ea3267b3ffaca9ab7338..15633d1baed1ce18894487d7ca6bd9ff72328247 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -30,6 +30,7 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal internal fun fingerprintSensorPropertiesInternal( ids: List<Int> = listOf(0), strong: Boolean = true, + sensorType: Int = FingerprintSensorProperties.TYPE_REAR ): List<FingerprintSensorPropertiesInternal> { val componentInfo = listOf( @@ -54,7 +55,7 @@ internal fun fingerprintSensorPropertiesInternal( if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, componentInfo, - FingerprintSensorProperties.TYPE_REAR, + sensorType, false /* resetLockoutRequiresHardwareAuthToken */ ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt deleted file mode 100644 index fd86486aeff87bb71c1eebd16ffeae6dce9c6e5a..0000000000000000000000000000000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.android.systemui.biometrics.ui.viewmodel - -import android.content.res.Configuration -import androidx.test.filters.SmallTest -import com.android.internal.widget.LockPatternUtils -import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository -import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.biometrics.data.repository.FakePromptRepository -import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor -import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl -import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor -import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import com.android.systemui.biometrics.shared.model.SensorStrength -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.display.data.repository.FakeDisplayRepository -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.Mock -import org.mockito.junit.MockitoJUnit - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class PromptFingerprintIconViewModelTest : SysuiTestCase() { - - @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var lockPatternUtils: LockPatternUtils - - private lateinit var displayRepository: FakeDisplayRepository - private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository - private lateinit var promptRepository: FakePromptRepository - private lateinit var displayStateRepository: FakeDisplayStateRepository - - private val testScope = TestScope(StandardTestDispatcher()) - private val fakeExecutor = FakeExecutor(FakeSystemClock()) - - private lateinit var promptSelectorInteractor: PromptSelectorInteractor - private lateinit var displayStateInteractor: DisplayStateInteractor - private lateinit var viewModel: PromptFingerprintIconViewModel - - @Before - fun setup() { - displayRepository = FakeDisplayRepository() - fingerprintRepository = FakeFingerprintPropertyRepository() - promptRepository = FakePromptRepository() - displayStateRepository = FakeDisplayStateRepository() - - promptSelectorInteractor = - PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils) - displayStateInteractor = - DisplayStateInteractorImpl( - testScope.backgroundScope, - mContext, - fakeExecutor, - displayStateRepository, - displayRepository, - ) - viewModel = PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor) - } - - @Test - fun sfpsIconUpdates_onConfigurationChanged() { - testScope.runTest { - runCurrent() - configureFingerprintPropertyRepository(FingerprintSensorType.POWER_BUTTON) - val testConfig = Configuration() - val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1 - val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1 - val currentIcon = collectLastValue(viewModel.iconAsset) - - testConfig.smallestScreenWidthDp = folded - viewModel.onConfigurationChanged(testConfig) - val foldedIcon = currentIcon() - - testConfig.smallestScreenWidthDp = unfolded - viewModel.onConfigurationChanged(testConfig) - val unfoldedIcon = currentIcon() - - assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) - } - } - - private fun configureFingerprintPropertyRepository(sensorType: FingerprintSensorType) { - fingerprintRepository.setProperties(0, SensorStrength.STRONG, sensorType, mapOf()) - } -} - -internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600 diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index ca6df4027ea9a15346eb99ef45d4a8bea490a2c2..b695a0ee1fa36dd8e0eaf4b752c8639b8ea6d6f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -16,8 +16,10 @@ package com.android.systemui.biometrics.ui.viewmodel +import android.content.res.Configuration import android.hardware.biometrics.PromptInfo import android.hardware.face.FaceSensorPropertiesInternal +import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.view.HapticFeedbackConstants import android.view.MotionEvent @@ -36,12 +38,15 @@ import com.android.systemui.biometrics.faceSensorPropertiesInternal import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState +import com.android.systemui.biometrics.shared.model.DisplayRotation +import com.android.systemui.biometrics.shared.model.toSensorStrength +import com.android.systemui.biometrics.shared.model.toSensorType import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -66,6 +71,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 4 private const val CHALLENGE = 2L +private const val DELAY = 1000L @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -88,11 +94,22 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private lateinit var selector: PromptSelectorInteractor private lateinit var viewModel: PromptViewModel + private lateinit var iconViewModel: PromptIconViewModel private val featureFlags = FakeFeatureFlags() @Before fun setup() { fingerprintRepository = FakeFingerprintPropertyRepository() + testCase.fingerprint?.let { + fingerprintRepository.setProperties( + it.sensorId, + it.sensorStrength.toSensorStrength(), + it.sensorType.toSensorType(), + it.allLocations.associateBy { sensorLocationInternal -> + sensorLocationInternal.displayId + } + ) + } promptRepository = FakePromptRepository() displayStateRepository = FakeDisplayStateRepository() displayRepository = FakeDisplayRepository() @@ -110,6 +127,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel = PromptViewModel(displayStateInteractor, selector, vibrator, mContext, featureFlags) + iconViewModel = viewModel.iconViewModel featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) } @@ -123,7 +141,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val modalities by collectLastValue(viewModel.modalities) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) assertThat(authenticating).isFalse() assertThat(authenticated?.isNotAuthenticated).isTrue() @@ -133,7 +150,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } assertThat(message).isEqualTo(PromptMessage.Empty) assertThat(size).isEqualTo(expectedSize) - assertThat(legacyState).isEqualTo(BiometricState.STATE_IDLE) val startMessage = "here we go" viewModel.showAuthenticating(startMessage, isRetry = false) @@ -143,7 +159,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticated?.isNotAuthenticated).isTrue() assertThat(size).isEqualTo(expectedSize) assertButtonsVisible(negative = expectedSize != PromptSize.SMALL) - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATING) } @Test @@ -205,6 +220,472 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.REJECT) } + @Test + fun start_idle_and_show_authenticating_iconUpdate() = + runGenericTest(doNotStart = true) { + val currentRotation by collectLastValue(displayStateInteractor.currentRotation) + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + val forceExplicitFlow = testCase.isCoex && testCase.authenticatedByFingerprint + if (forceExplicitFlow) { + viewModel.ensureFingerprintHasStarted(isDelayed = true) + } + + val startMessage = "here we go" + viewModel.showAuthenticating(startMessage, isRetry = false) + + if (testCase.isFingerprintOnly) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(false) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } + assertThat(iconAsset).isEqualTo(expectedIconAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) + } + + if (testCase.isCoex) { + if (testCase.confirmationRequested || forceExplicitFlow) { + // explicit flow + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(false) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } else { + // implicit flow + val shouldRepeatAnimation by + collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } + assertThat(iconAsset).isEqualTo(expectedIconAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) + } + } + } + + @Test + fun start_authenticating_show_and_clear_error_iconUpdate() = runGenericTest { + val currentRotation by collectLastValue(displayStateInteractor.currentRotation) + + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + val forceExplicitFlow = testCase.isCoex && testCase.authenticatedByFingerprint + if (forceExplicitFlow) { + viewModel.ensureFingerprintHasStarted(isDelayed = true) + } + + val errorJob = launch { + viewModel.showTemporaryError( + "so sad", + messageAfterError = "", + authenticateAfterError = testCase.isFingerprintOnly || testCase.isCoex, + ) + // Usually done by binder + iconViewModel.setPreviousIconWasError(true) + iconViewModel.setPreviousIconOverlayWasError(true) + } + + if (testCase.isFingerprintOnly) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + + // Clear error, restart authenticating + errorJob.join() + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_fingerprint_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_error) + assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + + // Clear error, go to idle + errorJob.join() + + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_error_to_idle) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_idle) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + + if (testCase.isCoex) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + + // Clear error, restart authenticating + errorJob.join() + + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_fingerprint_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + } + + @Test + fun shows_authenticated_no_errors_no_confirmation_required_iconUpdate() = runGenericTest { + if (!testCase.confirmationRequested) { + val currentRotation by collectLastValue(displayStateInteractor.currentRotation) + + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + viewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + if (testCase.isFingerprintOnly) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + val isAuthenticated by collectLastValue(viewModel.isAuthenticated) + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + + // If co-ex, using implicit flow (explicit flow always requires confirmation) + if (testCase.isFaceOnly || testCase.isCoex) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + } + } + + @Test + fun shows_pending_confirmation_iconUpdate() = runGenericTest { + if ( + (testCase.isFaceOnly || testCase.isCoex) && + testCase.authenticatedByFace && + testCase.confirmationRequested + ) { + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + viewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_wink_from_dark) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + + // explicit flow because confirmation requested + if (testCase.isCoex) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + } + } + + @Test + fun shows_authenticated_explicitly_confirmed_iconUpdate() = runGenericTest { + if ( + (testCase.isFaceOnly || testCase.isCoex) && + testCase.authenticatedByFace && + testCase.confirmationRequested + ) { + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + viewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + viewModel.confirmAuthenticated() + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + + // explicit flow because confirmation requested + if (testCase.isCoex) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + } + } + + @Test + fun sfpsIconUpdates_onConfigurationChanged() = runGenericTest { + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val testConfig = Configuration() + val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1 + val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1 + val currentIcon by collectLastValue(iconViewModel.iconAsset) + + testConfig.smallestScreenWidthDp = folded + iconViewModel.onConfigurationChanged(testConfig) + val foldedIcon = currentIcon + + testConfig.smallestScreenWidthDp = unfolded + iconViewModel.onConfigurationChanged(testConfig) + val unfoldedIcon = currentIcon + + assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) + } + } + + @Test + fun sfpsIconUpdates_onRotation() = runGenericTest { + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val currentIcon by collectLastValue(iconViewModel.iconAsset) + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) + val iconRotation0 = currentIcon + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + val iconRotation90 = currentIcon + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) + val iconRotation180 = currentIcon + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + val iconRotation270 = currentIcon + + assertThat(iconRotation0).isEqualTo(iconRotation180) + assertThat(iconRotation0).isNotEqualTo(iconRotation90) + assertThat(iconRotation0).isNotEqualTo(iconRotation270) + } + } + + @Test + fun sfpsIconUpdates_onRearDisplayMode() = runGenericTest { + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val currentIcon by collectLastValue(iconViewModel.iconAsset) + + displayStateRepository.setIsInRearDisplayMode(false) + val iconNotRearDisplayMode = currentIcon + + displayStateRepository.setIsInRearDisplayMode(true) + val iconRearDisplayMode = currentIcon + + assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode) + } + } + private suspend fun TestScope.showAuthenticated( authenticatedModality: BiometricModality, expectConfirmation: Boolean, @@ -213,7 +694,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val fpStartMode by collectLastValue(viewModel.fingerprintStartMode) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val authWithSmallPrompt = testCase.shouldStartAsImplicitFlow && @@ -221,14 +701,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticating).isTrue() assertThat(authenticated?.isNotAuthenticated).isTrue() assertThat(size).isEqualTo(if (authWithSmallPrompt) PromptSize.SMALL else PromptSize.MEDIUM) - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATING) assertButtonsVisible(negative = !authWithSmallPrompt) - val delay = 1000L - viewModel.showAuthenticated(authenticatedModality, delay) + viewModel.showAuthenticated(authenticatedModality, DELAY) assertThat(authenticated?.isAuthenticated).isTrue() - assertThat(authenticated?.delay).isEqualTo(delay) + assertThat(authenticated?.delay).isEqualTo(DELAY) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) assertThat(size) .isEqualTo( @@ -238,14 +716,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa PromptSize.SMALL } ) - assertThat(legacyState) - .isEqualTo( - if (expectConfirmation) { - BiometricState.STATE_PENDING_CONFIRMATION - } else { - BiometricState.STATE_AUTHENTICATED - } - ) + assertButtonsVisible( cancel = expectConfirmation, confirm = expectConfirmation, @@ -298,7 +769,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val message by collectLastValue(viewModel.message) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgainNow by collectLastValue(viewModel.canTryAgainNow) val errorJob = launch { @@ -312,7 +782,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(size).isEqualTo(PromptSize.MEDIUM) assertThat(message).isEqualTo(PromptMessage.Error(errorMessage)) assertThat(messageVisible).isTrue() - assertThat(legacyState).isEqualTo(BiometricState.STATE_ERROR) // temporary error should disappear after a delay errorJob.join() @@ -323,17 +792,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(message).isEqualTo(PromptMessage.Empty) assertThat(messageVisible).isFalse() } - val clearIconError = !restart - assertThat(legacyState) - .isEqualTo( - if (restart) { - BiometricState.STATE_AUTHENTICATING - } else if (clearIconError) { - BiometricState.STATE_IDLE - } else { - BiometricState.STATE_HELP - } - ) assertThat(authenticating).isEqualTo(restart) assertThat(authenticated?.isNotAuthenticated).isTrue() @@ -488,7 +946,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) @@ -506,7 +963,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticating).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) assertThat(canTryAgain).isFalse() } @@ -524,7 +980,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticating).isFalse() @@ -532,8 +987,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticated?.isAuthenticated).isTrue() if (testCase.isFaceOnly && expectConfirmation) { - assertThat(legacyState).isEqualTo(BiometricState.STATE_PENDING_CONFIRMATION) - assertThat(size).isEqualTo(PromptSize.MEDIUM) assertButtonsVisible( cancel = true, @@ -543,8 +996,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) assertButtonsVisible() - } else { - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) } } @@ -563,7 +1014,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) @@ -581,7 +1031,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticating).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) assertThat(canTryAgain).isFalse() } @@ -610,12 +1059,10 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val message by collectLastValue(viewModel.message) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) viewModel.showHelp(helpMessage) assertThat(size).isEqualTo(PromptSize.MEDIUM) - assertThat(legacyState).isEqualTo(BiometricState.STATE_HELP) assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(messageVisible).isTrue() @@ -632,7 +1079,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val message by collectLastValue(viewModel.message) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val confirmationRequired by collectLastValue(viewModel.isConfirmationRequired) if (testCase.isCoex && testCase.authenticatedByFingerprint) { @@ -642,11 +1088,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.showHelp(helpMessage) assertThat(size).isEqualTo(PromptSize.MEDIUM) - if (confirmationRequired == true) { - assertThat(legacyState).isEqualTo(BiometricState.STATE_PENDING_CONFIRMATION) - } else { - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) - } + assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(messageVisible).isTrue() assertThat(authenticating).isFalse() @@ -784,6 +1226,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(), authenticatedModality = BiometricModality.Fingerprint, ), + TestCase( + fingerprint = + fingerprintSensorPropertiesInternal( + strong = true, + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + ) + .first(), + authenticatedModality = BiometricModality.Fingerprint, + ), TestCase( face = faceSensorPropertiesInternal(strong = true).first(), authenticatedModality = BiometricModality.Face, @@ -794,6 +1245,16 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa authenticatedModality = BiometricModality.Fingerprint, confirmationRequested = true, ), + TestCase( + fingerprint = + fingerprintSensorPropertiesInternal( + strong = true, + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + ) + .first(), + authenticatedModality = BiometricModality.Fingerprint, + confirmationRequested = true, + ), ) private val coexTestCases = @@ -834,7 +1295,9 @@ internal data class TestCase( val modality = when { fingerprint != null && face != null -> "coex" - fingerprint != null -> "fingerprint only" + fingerprint != null && fingerprint.isAnySidefpsType -> "fingerprint only, sideFps" + fingerprint != null && !fingerprint.isAnySidefpsType -> + "fingerprint only, non-sideFps" face != null -> "face only" else -> "?" } @@ -864,6 +1327,8 @@ internal data class TestCase( val isCoex: Boolean get() = face != null && fingerprint != null + @FingerprintSensorProperties.SensorType val sensorType: Int? = fingerprint?.sensorType + val shouldStartAsImplicitFlow: Boolean get() = (isFaceOnly || isCoex) && !confirmationRequested } @@ -890,3 +1355,5 @@ private fun PromptSelectorInteractor.initializePrompt( BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), ) } + +internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600