• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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