1 package com.android.systemui.biometrics.ui.binder 2 3 import android.view.View 4 import android.view.ViewGroup 5 import android.widget.ImageView 6 import android.widget.TextView 7 import androidx.lifecycle.Lifecycle 8 import androidx.lifecycle.repeatOnLifecycle 9 import com.android.app.animation.Interpolators 10 import com.android.systemui.R 11 import com.android.systemui.biometrics.AuthDialog 12 import com.android.systemui.biometrics.AuthPanelController 13 import com.android.systemui.biometrics.ui.CredentialPasswordView 14 import com.android.systemui.biometrics.ui.CredentialPatternView 15 import com.android.systemui.biometrics.ui.CredentialView 16 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel 17 import com.android.systemui.lifecycle.repeatWhenAttached 18 import kotlinx.coroutines.Job 19 import kotlinx.coroutines.delay 20 import kotlinx.coroutines.flow.filter 21 import kotlinx.coroutines.flow.onEach 22 import kotlinx.coroutines.launch 23 24 /** 25 * View binder for all credential variants of BiometricPrompt, including [CredentialPatternView] and 26 * [CredentialPasswordView]. 27 * 28 * This binder delegates to sub-binders for each variant, such as the [CredentialPasswordViewBinder] 29 * and [CredentialPatternViewBinder]. 30 */ 31 object CredentialViewBinder { 32 33 /** Binds a [CredentialPasswordView] or [CredentialPatternView] to a [CredentialViewModel]. */ 34 @JvmStatic 35 fun bind( 36 view: ViewGroup, 37 host: CredentialView.Host, 38 viewModel: CredentialViewModel, 39 panelViewController: AuthPanelController, 40 animatePanel: Boolean, 41 maxErrorDuration: Long = 3_000L, 42 requestFocusForInput: Boolean = true, 43 ) { 44 val titleView: TextView = view.requireViewById(R.id.title) 45 val subtitleView: TextView = view.requireViewById(R.id.subtitle) 46 val descriptionView: TextView = view.requireViewById(R.id.description) 47 val iconView: ImageView? = view.findViewById(R.id.icon) 48 val errorView: TextView = view.requireViewById(R.id.error) 49 50 var errorTimer: Job? = null 51 52 // bind common elements 53 view.repeatWhenAttached { 54 if (animatePanel) { 55 with(panelViewController) { 56 // Credential view is always full screen. 57 setUseFullScreen(true) 58 updateForContentDimensions( 59 containerWidth, 60 containerHeight, 61 0 /* animateDurationMs */ 62 ) 63 } 64 } 65 66 repeatOnLifecycle(Lifecycle.State.STARTED) { 67 // show prompt metadata 68 launch { 69 viewModel.header.collect { header -> 70 titleView.text = header.title 71 view.announceForAccessibility(header.title) 72 73 subtitleView.textOrHide = header.subtitle 74 descriptionView.textOrHide = header.description 75 76 iconView?.setImageDrawable(header.icon) 77 78 // Only animate this if we're transitioning from a biometric view. 79 if (viewModel.animateContents.value) { 80 view.animateCredentialViewIn() 81 } 82 } 83 } 84 85 // show transient error messages 86 launch { 87 viewModel.errorMessage 88 .onEach { msg -> 89 errorTimer?.cancel() 90 if (msg.isNotBlank()) { 91 errorTimer = launch { 92 delay(maxErrorDuration) 93 viewModel.resetErrorMessage() 94 } 95 } 96 } 97 .collect { errorView.textOrHide = it } 98 } 99 100 // show an extra dialog if the remaining attempts becomes low 101 launch { 102 viewModel.remainingAttempts 103 .filter { it.remaining != null } 104 .collect { info -> 105 host.onCredentialAttemptsRemaining(info.remaining!!, info.message) 106 } 107 } 108 } 109 } 110 111 // bind the auth widget 112 when (view) { 113 is CredentialPasswordView -> 114 CredentialPasswordViewBinder.bind(view, host, viewModel, requestFocusForInput) 115 is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel) 116 else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}") 117 } 118 } 119 } 120 121 private fun View.animateCredentialViewIn() { 122 translationY = resources.getDimension(R.dimen.biometric_dialog_credential_translation_offset) 123 alpha = 0f 124 postOnAnimation { 125 animate() 126 .translationY(0f) 127 .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS.toLong()) 128 .alpha(1f) 129 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) 130 .withLayer() 131 .start() 132 } 133 } 134 135 private var TextView.textOrHide: String? 136 set(value) { 137 val gone = value.isNullOrBlank() 138 visibility = if (gone) View.GONE else View.VISIBLE 139 text = if (gone) "" else value 140 } 141 get() = text?.toString() 142