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