<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