Migrating all BiometricPrompt iconController logic
Migrating all AuthBiometricFingerprintIconController,
AuthBiometricFingerprintAndFaceIconController, and
AuthBiometricFaceIconController logic into the recommended sysUI
ViewModel-ViewBinder structure, removing legacyState
code, and adding comprehensive testing
Bug: 287311775
Bug: 288173697
Bug: 288174575
Fixes: 288174572
Test: atest PromptViewModelTest
Change-Id: I822610b4585c3d03c412bbf3d9685babdd49a9bf
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 db46ccf..80f70a0 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 @@
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 3f2da5e..0000000
--- 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 09eabf2..0000000
--- 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 0ad3848..0000000
--- 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 958213a..0000000
--- 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 c4c52e8b..050b399 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 @@
/** 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 a317a06..427361d 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 @@
/** 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 @@
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 cef0be0..0d72b9c 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.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 @@
}
@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 c29efc0..ac48b6a 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.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.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 @@
!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 @@
// 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 @@
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 @@
) {
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 @@
viewModel.isIconConfirmButton
.map { isPending ->
when {
- isPending && iconController.actsAsConfirmButton ->
+ isPending && modalities.hasFaceAndFingerprint ->
View.OnTouchListener { _: View, event: MotionEvent ->
viewModel.onOverlayTouch(event)
}
@@ -320,22 +307,11 @@
}
}
.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 @@
// 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 @@
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 @@
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 @@
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 @@
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 d28f1dc..0000000
--- 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 0000000..475ef18
--- /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 dfd3a9b..0000000
--- 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 0000000..11a5d8b
--- /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 267afae..e49b4a7 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.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.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 @@
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 @@
/** 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 @@
/** 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 @@
!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 @@
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 @@
_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 @@
if (authenticateAfterError) {
showAuthenticating(messageAfterError)
} else {
- showInfo(messageAfterError)
+ showHelp(messageAfterError)
}
}
}
@@ -325,15 +368,12 @@
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 @@
_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 @@
_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 @@
_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 @@
_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 @@
_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 83ff789..b3834f5 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 @@
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 215d635..0000000
--- 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 9f24a9f..15633d1 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 @@
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 @@
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 fd86486..0000000
--- 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 ca6df40..b695a0e 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.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 @@
private const val USER_ID = 4
private const val CHALLENGE = 2L
+private const val DELAY = 1000L
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -88,11 +94,22 @@
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 @@
viewModel =
PromptViewModel(displayStateInteractor, selector, vibrator, mContext, featureFlags)
+ iconViewModel = viewModel.iconViewModel
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
}
@@ -123,7 +141,6 @@
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 @@
}
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 @@
assertThat(authenticated?.isNotAuthenticated).isTrue()
assertThat(size).isEqualTo(expectedSize)
assertButtonsVisible(negative = expectedSize != PromptSize.SMALL)
- assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATING)
}
@Test
@@ -205,6 +220,472 @@
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 @@
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 @@
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 @@
PromptSize.SMALL
}
)
- assertThat(legacyState)
- .isEqualTo(
- if (expectConfirmation) {
- BiometricState.STATE_PENDING_CONFIRMATION
- } else {
- BiometricState.STATE_AUTHENTICATED
- }
- )
+
assertButtonsVisible(
cancel = expectConfirmation,
confirm = expectConfirmation,
@@ -298,7 +769,6 @@
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 @@
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 @@
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 @@
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 @@
assertThat(authenticating).isFalse()
assertThat(authenticated?.isAuthenticated).isTrue()
- assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED)
assertThat(canTryAgain).isFalse()
}
@@ -524,7 +980,6 @@
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 @@
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 @@
viewModel.confirmAuthenticated()
assertThat(message).isEqualTo(PromptMessage.Empty)
assertButtonsVisible()
- } else {
- assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED)
}
}
@@ -563,7 +1014,6 @@
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 @@
assertThat(authenticating).isFalse()
assertThat(authenticated?.isAuthenticated).isTrue()
- assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED)
assertThat(canTryAgain).isFalse()
}
@@ -610,12 +1059,10 @@
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 @@
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 @@
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()
@@ -785,6 +1227,15 @@
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,
confirmationRequested = true,
@@ -794,6 +1245,16 @@
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 @@
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 @@
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 @@
BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),
)
}
+
+internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600