<lambda>null1 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
animateCredentialViewInnull121 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