<lambda>null1 package com.android.systemui.biometrics.ui.binder
2
3 import android.view.View
4 import android.view.ViewGroup
5 import android.widget.Button
6 import android.widget.ImageView
7 import android.widget.LinearLayout
8 import android.widget.TextView
9 import androidx.lifecycle.Lifecycle
10 import androidx.lifecycle.repeatOnLifecycle
11 import com.android.app.animation.Interpolators
12 import com.android.app.tracing.coroutines.launchTraced as launch
13 import com.android.systemui.biometrics.AuthPanelController
14 import com.android.systemui.biometrics.plugins.AuthContextPlugins
15 import com.android.systemui.biometrics.ui.CredentialPasswordView
16 import com.android.systemui.biometrics.ui.CredentialPatternView
17 import com.android.systemui.biometrics.ui.CredentialView
18 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
19 import com.android.systemui.lifecycle.repeatWhenAttached
20 import com.android.systemui.plugins.AuthContextPlugin
21 import com.android.systemui.res.R
22 import kotlinx.coroutines.Job
23 import kotlinx.coroutines.awaitCancellation
24 import kotlinx.coroutines.delay
25 import kotlinx.coroutines.flow.filter
26 import kotlinx.coroutines.flow.onEach
27
28 private const val ANIMATE_CREDENTIAL_INITIAL_DURATION_MS = 150
29
30 /**
31 * View binder for all credential variants of BiometricPrompt, including [CredentialPatternView] and
32 * [CredentialPasswordView].
33 *
34 * This binder delegates to sub-binders for each variant, such as the [CredentialPasswordViewBinder]
35 * and [CredentialPatternViewBinder].
36 */
37 object CredentialViewBinder {
38
39 /** Binds a [CredentialPasswordView] or [CredentialPatternView] to a [CredentialViewModel]. */
40 @JvmStatic
41 fun bind(
42 view: ViewGroup,
43 host: CredentialView.Host,
44 viewModel: CredentialViewModel,
45 panelViewController: AuthPanelController,
46 animatePanel: Boolean,
47 legacyCallback: Spaghetti.Callback,
48 plugins: AuthContextPlugins?,
49 maxErrorDuration: Long = 3_000L,
50 requestFocusForInput: Boolean = true,
51 ) {
52 val titleView: TextView = view.requireViewById(R.id.title)
53 val subtitleView: TextView = view.requireViewById(R.id.subtitle)
54 val descriptionView: TextView = view.requireViewById(R.id.description)
55 val customizedViewContainer: LinearLayout =
56 view.requireViewById(R.id.customized_view_container)
57 val iconView: ImageView? = view.findViewById(R.id.icon)
58 val errorView: TextView = view.requireViewById(R.id.error)
59 val cancelButton: Button? = view.findViewById(R.id.cancel_button)
60 val emergencyButtonView: Button = view.requireViewById(R.id.emergencyCallButton)
61
62 var errorTimer: Job? = null
63
64 // bind common elements
65 view.repeatWhenAttached {
66 if (animatePanel) {
67 with(panelViewController) {
68 // Credential view is always full screen.
69 setUseFullScreen(true)
70 updateForContentDimensions(
71 containerWidth,
72 containerHeight,
73 0, // animateDurationMs
74 )
75 }
76 }
77
78 repeatOnLifecycle(Lifecycle.State.STARTED) {
79 if (plugins != null) {
80 launch { plugins.notifyShowingBPCredential(view) }
81 }
82
83 // show prompt metadata
84 launch {
85 viewModel.header.collect { header ->
86 titleView.text = header.title
87 view.announceForAccessibility(header.title)
88
89 subtitleView.textOrHide = header.subtitle
90 descriptionView.textOrHide = header.description
91 BiometricCustomizedViewBinder.bind(
92 customizedViewContainer,
93 header.contentView,
94 legacyCallback,
95 )
96
97 iconView?.setImageDrawable(header.icon)
98
99 if (header.showEmergencyCallButton) {
100 emergencyButtonView.visibility = View.VISIBLE
101 emergencyButtonView.setOnClickListener {
102 viewModel.doEmergencyCall(view.context)
103 }
104 }
105
106 // Only animate this if we're transitioning from a biometric view.
107 if (viewModel.animateContents.value) {
108 view.animateCredentialViewIn()
109 }
110 }
111 }
112
113 // show transient error messages
114 launch {
115 viewModel.errorMessage
116 .onEach { msg ->
117 errorTimer?.cancel()
118 if (msg.isNotBlank()) {
119 errorTimer = launch {
120 delay(maxErrorDuration)
121 viewModel.resetErrorMessage()
122 }
123 }
124 }
125 .collect { it ->
126 val hasError = !it.isNullOrBlank()
127 errorView.visibility =
128 if (hasError) {
129 View.VISIBLE
130 } else if (cancelButton != null) {
131 View.INVISIBLE
132 } else {
133 View.GONE
134 }
135 errorView.text = if (hasError) it else ""
136 }
137 }
138
139 // show an extra dialog if the remaining attempts becomes low
140 launch {
141 viewModel.remainingAttempts
142 .filter { it.remaining != null }
143 .collect { info ->
144 host.onCredentialAttemptsRemaining(info.remaining!!, info.message)
145 }
146 }
147
148 try {
149 awaitCancellation()
150 } catch (_: Throwable) {
151 plugins?.notifyHidingBPCredential()
152 }
153 }
154 }
155
156 cancelButton?.setOnClickListener { host.onCredentialAborted() }
157
158 // bind the auth widget
159 when (view) {
160 is CredentialPasswordView ->
161 CredentialPasswordViewBinder.bind(view, host, viewModel, requestFocusForInput)
162 is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel)
163 else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}")
164 }
165 }
166 }
167
animateCredentialViewInnull168 private fun View.animateCredentialViewIn() {
169 translationY = resources.getDimension(R.dimen.biometric_dialog_credential_translation_offset)
170 alpha = 0f
171 postOnAnimation {
172 animate()
173 .translationY(0f)
174 .setDuration(ANIMATE_CREDENTIAL_INITIAL_DURATION_MS.toLong())
175 .alpha(1f)
176 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
177 .withLayer()
178 .start()
179 }
180 }
181
182 private var TextView.textOrHide: String?
183 set(value) {
184 val gone = value.isNullOrBlank()
185 visibility = if (gone) View.GONE else View.VISIBLE
186 text = if (gone) "" else value
187 }
188 get() = text?.toString()
189
notifyShowingBPCredentialnull190 private suspend fun AuthContextPlugins.notifyShowingBPCredential(view: View) = use { plugin ->
191 plugin.onShowingSensitiveSurface(
192 AuthContextPlugin.SensitiveSurface.BiometricPrompt(view = view, isCredential = true)
193 )
194 }
195
pluginnull196 private fun AuthContextPlugins.notifyHidingBPCredential() = useInBackground { plugin ->
197 plugin.onHidingSensitiveSurface(
198 AuthContextPlugin.SensitiveSurface.BiometricPrompt(isCredential = true)
199 )
200 }
201