1 /*
<lambda>null2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.biometrics.ui.binder
18
19 import android.animation.Animator
20 import android.annotation.SuppressLint
21 import android.content.Context
22 import android.hardware.biometrics.BiometricAuthenticator
23 import android.hardware.biometrics.BiometricConstants
24 import android.hardware.biometrics.BiometricPrompt
25 import android.hardware.face.FaceManager
26 import android.os.Bundle
27 import android.text.method.ScrollingMovementMethod
28 import android.util.Log
29 import android.view.HapticFeedbackConstants
30 import android.view.MotionEvent
31 import android.view.View
32 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
33 import android.view.accessibility.AccessibilityManager
34 import android.widget.Button
35 import android.widget.TextView
36 import androidx.lifecycle.DefaultLifecycleObserver
37 import androidx.lifecycle.Lifecycle
38 import androidx.lifecycle.LifecycleOwner
39 import androidx.lifecycle.lifecycleScope
40 import androidx.lifecycle.repeatOnLifecycle
41 import com.airbnb.lottie.LottieAnimationView
42 import com.android.systemui.R
43 import com.android.systemui.biometrics.AuthBiometricFaceIconController
44 import com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceIconController
45 import com.android.systemui.biometrics.AuthBiometricFingerprintIconController
46 import com.android.systemui.biometrics.AuthBiometricView
47 import com.android.systemui.biometrics.AuthBiometricView.Callback
48 import com.android.systemui.biometrics.AuthBiometricViewAdapter
49 import com.android.systemui.biometrics.AuthIconController
50 import com.android.systemui.biometrics.AuthPanelController
51 import com.android.systemui.biometrics.domain.model.BiometricModalities
52 import com.android.systemui.biometrics.shared.model.BiometricModality
53 import com.android.systemui.biometrics.shared.model.PromptKind
54 import com.android.systemui.biometrics.shared.model.asBiometricModality
55 import com.android.systemui.biometrics.ui.BiometricPromptLayout
56 import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
57 import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
58 import com.android.systemui.biometrics.ui.viewmodel.PromptSize
59 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
60 import com.android.systemui.flags.FeatureFlags
61 import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
62 import com.android.systemui.lifecycle.repeatWhenAttached
63 import com.android.systemui.statusbar.VibratorHelper
64 import kotlinx.coroutines.CoroutineScope
65 import kotlinx.coroutines.delay
66 import kotlinx.coroutines.flow.collect
67 import kotlinx.coroutines.flow.combine
68 import kotlinx.coroutines.flow.first
69 import kotlinx.coroutines.flow.map
70 import kotlinx.coroutines.launch
71
72 private const val TAG = "BiometricViewBinder"
73
74 /** Top-most view binder for BiometricPrompt views. */
75 object BiometricViewBinder {
76
77 /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */
78 @SuppressLint("ClickableViewAccessibility")
79 @JvmStatic
80 fun bind(
81 view: BiometricPromptLayout,
82 viewModel: PromptViewModel,
83 panelViewController: AuthPanelController,
84 jankListener: BiometricJankListener,
85 backgroundView: View,
86 legacyCallback: Callback,
87 applicationScope: CoroutineScope,
88 vibratorHelper: VibratorHelper,
89 featureFlags: FeatureFlags,
90 ): AuthBiometricViewAdapter {
91 val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
92
93 val textColorError =
94 view.resources.getColor(R.color.biometric_dialog_error, view.context.theme)
95 val textColorHint =
96 view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
97
98 val titleView = view.requireViewById<TextView>(R.id.title)
99 val subtitleView = view.requireViewById<TextView>(R.id.subtitle)
100 val descriptionView = view.requireViewById<TextView>(R.id.description)
101
102 // set selected to enable marquee unless a screen reader is enabled
103 titleView.isSelected =
104 !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled
105 subtitleView.isSelected =
106 !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled
107 descriptionView.movementMethod = ScrollingMovementMethod()
108
109 val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
110 val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon)
111
112 PromptFingerprintIconViewBinder.bind(iconView, viewModel.fingerprintIconViewModel)
113
114 val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator)
115
116 // Negative-side (left) buttons
117 val negativeButton = view.requireViewById<Button>(R.id.button_negative)
118 val cancelButton = view.requireViewById<Button>(R.id.button_cancel)
119 val credentialFallbackButton = view.requireViewById<Button>(R.id.button_use_credential)
120
121 // Positive-side (right) buttons
122 val confirmationButton = view.requireViewById<Button>(R.id.button_confirm)
123 val retryButton = view.requireViewById<Button>(R.id.button_try_again)
124
125 // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers
126 val adapter =
127 Spaghetti(
128 view = view,
129 viewModel = viewModel,
130 applicationContext = view.context.applicationContext,
131 applicationScope = applicationScope,
132 )
133
134 // bind to prompt
135 var boundSize = false
136 view.repeatWhenAttached {
137 // these do not change and need to be set before any size transitions
138 val modalities = viewModel.modalities.first()
139 titleView.text = viewModel.title.first()
140 descriptionView.text = viewModel.description.first()
141 subtitleView.text = viewModel.subtitle.first()
142
143 // set button listeners
144 negativeButton.setOnClickListener {
145 legacyCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE)
146 }
147 cancelButton.setOnClickListener {
148 legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
149 }
150 credentialFallbackButton.setOnClickListener {
151 viewModel.onSwitchToCredential()
152 legacyCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
153 }
154 confirmationButton.setOnClickListener { viewModel.confirmAuthenticated() }
155 retryButton.setOnClickListener {
156 viewModel.showAuthenticating(isRetry = true)
157 legacyCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN)
158 }
159
160 // TODO(b/251476085): migrate legacy icon controllers and remove
161 var legacyState: Int = viewModel.legacyState.value
162 val iconController =
163 modalities.asIconController(
164 view.context,
165 iconView,
166 iconViewOverlay,
167 )
168 adapter.attach(this, iconController, modalities, legacyCallback)
169 if (iconController is AuthBiometricFingerprintIconController) {
170 view.updateFingerprintAffordanceSize(iconController)
171 }
172 if (iconController is HackyCoexIconController) {
173 iconController.faceMode = !viewModel.isConfirmationRequired.first()
174 }
175
176 // the icon controller must be created before this happens for the legacy
177 // sizing code in BiometricPromptLayout to work correctly. Simplify this
178 // when those are also migrated. (otherwise the icon size may not be set to
179 // a pixel value before the view is measured and WRAP_CONTENT will be incorrectly
180 // used as part of the measure spec)
181 if (!boundSize) {
182 boundSize = true
183 BiometricViewSizeBinder.bind(
184 view = view,
185 viewModel = viewModel,
186 viewsToHideWhenSmall =
187 listOf(
188 titleView,
189 subtitleView,
190 descriptionView,
191 ),
192 viewsToFadeInOnSizeChange =
193 listOf(
194 titleView,
195 subtitleView,
196 descriptionView,
197 indicatorMessageView,
198 negativeButton,
199 cancelButton,
200 retryButton,
201 confirmationButton,
202 credentialFallbackButton,
203 ),
204 panelViewController = panelViewController,
205 jankListener = jankListener,
206 )
207 }
208
209 // TODO(b/251476085): migrate legacy icon controllers and remove
210 // The fingerprint sensor is started by the legacy
211 // AuthContainerView#onDialogAnimatedIn in all cases but the implicit coex flow
212 // (delayed mode). In that case, start it on the first transition to delayed
213 // which will be triggered by any auth failure.
214 lifecycleScope.launch {
215 val oldMode = viewModel.fingerprintStartMode.first()
216 viewModel.fingerprintStartMode.collect { newMode ->
217 // trigger sensor to start
218 if (
219 oldMode == FingerprintStartMode.Pending &&
220 newMode == FingerprintStartMode.Delayed
221 ) {
222 legacyCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)
223 }
224
225 if (newMode.isStarted) {
226 // do wonky switch from implicit to explicit flow
227 (iconController as? HackyCoexIconController)?.faceMode = false
228 viewModel.showAuthenticating(
229 modalities.asDefaultHelpMessage(view.context),
230 )
231 }
232 }
233 }
234
235 repeatOnLifecycle(Lifecycle.State.STARTED) {
236 // handle background clicks
237 launch {
238 combine(viewModel.isAuthenticated, viewModel.size) { (authenticated, _), size ->
239 when {
240 authenticated -> false
241 size == PromptSize.SMALL -> false
242 size == PromptSize.LARGE -> false
243 else -> true
244 }
245 }
246 .collect { dismissOnClick ->
247 backgroundView.setOnClickListener {
248 if (dismissOnClick) {
249 legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
250 } else {
251 Log.w(TAG, "Ignoring background click")
252 }
253 }
254 }
255 }
256
257 // set messages
258 launch {
259 viewModel.isIndicatorMessageVisible.collect { show ->
260 indicatorMessageView.visibility = show.asVisibleOrHidden()
261 }
262 }
263
264 // set padding
265 launch {
266 viewModel.promptPadding.collect { promptPadding ->
267 view.setPadding(
268 promptPadding.left,
269 promptPadding.top,
270 promptPadding.right,
271 promptPadding.bottom
272 )
273 }
274 }
275
276 // configure & hide/disable buttons
277 launch {
278 viewModel.credentialKind
279 .map { kind ->
280 when (kind) {
281 PromptKind.Pin ->
282 view.resources.getString(R.string.biometric_dialog_use_pin)
283 PromptKind.Password ->
284 view.resources.getString(R.string.biometric_dialog_use_password)
285 PromptKind.Pattern ->
286 view.resources.getString(R.string.biometric_dialog_use_pattern)
287 else -> ""
288 }
289 }
290 .collect { credentialFallbackButton.text = it }
291 }
292 launch { viewModel.negativeButtonText.collect { negativeButton.text = it } }
293 launch {
294 viewModel.isConfirmButtonVisible.collect { show ->
295 confirmationButton.visibility = show.asVisibleOrGone()
296 }
297 }
298 launch {
299 viewModel.isCancelButtonVisible.collect { show ->
300 cancelButton.visibility = show.asVisibleOrGone()
301 }
302 }
303 launch {
304 viewModel.isNegativeButtonVisible.collect { show ->
305 negativeButton.visibility = show.asVisibleOrGone()
306 }
307 }
308 launch {
309 viewModel.isTryAgainButtonVisible.collect { show ->
310 retryButton.visibility = show.asVisibleOrGone()
311 }
312 }
313 launch {
314 viewModel.isCredentialButtonVisible.collect { show ->
315 credentialFallbackButton.visibility = show.asVisibleOrGone()
316 }
317 }
318
319 // reuse the icon as a confirm button
320 launch {
321 viewModel.isIconConfirmButton
322 .map { isPending ->
323 when {
324 isPending && iconController.actsAsConfirmButton ->
325 View.OnTouchListener { _: View, event: MotionEvent ->
326 viewModel.onOverlayTouch(event)
327 }
328 else -> null
329 }
330 }
331 .collect { onTouch ->
332 iconViewOverlay.setOnTouchListener(onTouch)
333 iconView.setOnTouchListener(onTouch)
334 }
335 }
336
337 // TODO(b/251476085): remove w/ legacy icon controllers
338 // set icon affordance using legacy states
339 // like the old code, this causes animations to repeat on config changes :(
340 // but keep behavior for now as no one has complained...
341 launch {
342 viewModel.legacyState.collect { newState ->
343 iconController.updateState(legacyState, newState)
344 legacyState = newState
345 }
346 }
347
348 // dismiss prompt when authenticated and confirmed
349 launch {
350 viewModel.isAuthenticated.collect { authState ->
351 // Disable background view for cancelling authentication once authenticated,
352 // and remove from talkback
353 if (authState.isAuthenticated) {
354 // Prevents Talkback from speaking subtitle after already authenticated
355 subtitleView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
356 backgroundView.setOnClickListener(null)
357 backgroundView.importantForAccessibility =
358 IMPORTANT_FOR_ACCESSIBILITY_NO
359
360 // Allow icon to be used as confirmation button with a11y enabled
361 if (accessibilityManager.isTouchExplorationEnabled) {
362 iconViewOverlay.setOnClickListener {
363 viewModel.confirmAuthenticated()
364 }
365 iconView.setOnClickListener { viewModel.confirmAuthenticated() }
366 }
367 }
368 if (authState.isAuthenticatedAndConfirmed) {
369 view.announceForAccessibility(
370 view.resources.getString(R.string.biometric_dialog_authenticated)
371 )
372
373 launch {
374 delay(authState.delay)
375 legacyCallback.onAction(
376 if (authState.isAuthenticatedAndExplicitlyConfirmed) {
377 Callback.ACTION_AUTHENTICATED_AND_CONFIRMED
378 } else {
379 Callback.ACTION_AUTHENTICATED
380 }
381 )
382 }
383 }
384 }
385 }
386
387 // show error & help messages
388 launch {
389 viewModel.message.collect { promptMessage ->
390 val isError = promptMessage is PromptMessage.Error
391
392 indicatorMessageView.text = promptMessage.message
393 indicatorMessageView.setTextColor(
394 if (isError) textColorError else textColorHint
395 )
396
397 // select to enable marquee unless a screen reader is enabled
398 // TODO(wenhuiy): this may have recently changed per UX - verify and remove
399 indicatorMessageView.isSelected =
400 !accessibilityManager.isEnabled ||
401 !accessibilityManager.isTouchExplorationEnabled
402
403 /**
404 * Note: Talkback 14.0 has new rate-limitation design to reduce frequency of
405 * TYPE_WINDOW_CONTENT_CHANGED events to once every 30 seconds. (context:
406 * b/281765653#comment18) Using {@link View#announceForAccessibility}
407 * instead as workaround since sending events exceeding this frequency is
408 * required.
409 */
410 indicatorMessageView?.text?.let {
411 if (it.isNotBlank()) {
412 view.announceForAccessibility(it)
413 }
414 }
415 }
416 }
417
418 // Play haptics
419 if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
420 launch {
421 viewModel.hapticsToPlay.collect { hapticFeedbackConstant ->
422 if (hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) {
423 vibratorHelper.performHapticFeedback(view, hapticFeedbackConstant)
424 viewModel.clearHaptics()
425 }
426 }
427 }
428 }
429 }
430 }
431
432 return adapter
433 }
434 }
435
436 /**
437 * Adapter for legacy events. Remove once legacy controller can be replaced by flagged code.
438 *
439 * These events can be dispatched when the view is being recreated so they need to be delivered to
440 * the view model (which will be retained) via the application scope.
441 *
442 * Do not reference the [view] for anything other than [asView].
443 *
444 * TODO(b/251476085): remove after replacing AuthContainerView
445 */
446 private class Spaghetti(
447 private val view: View,
448 private val viewModel: PromptViewModel,
449 private val applicationContext: Context,
450 private val applicationScope: CoroutineScope,
451 ) : AuthBiometricViewAdapter {
452
453 private var lifecycleScope: CoroutineScope? = null
454 private var modalities: BiometricModalities = BiometricModalities()
455 private var legacyCallback: Callback? = null
456
457 override var legacyIconController: AuthIconController? = null
458 private set
459
460 // hacky way to suppress lockout errors
461 private val lockoutErrorStrings =
462 listOf(
463 BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
464 BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
465 )
<lambda>null466 .map { FaceManager.getErrorString(applicationContext, it, 0 /* vendorCode */) }
467
attachnull468 fun attach(
469 lifecycleOwner: LifecycleOwner,
470 iconController: AuthIconController,
471 activeModalities: BiometricModalities,
472 callback: Callback,
473 ) {
474 modalities = activeModalities
475 legacyIconController = iconController
476 legacyCallback = callback
477
478 lifecycleOwner.lifecycle.addObserver(
479 object : DefaultLifecycleObserver {
480 override fun onCreate(owner: LifecycleOwner) {
481 lifecycleScope = owner.lifecycleScope
482 iconController.deactivated = false
483 }
484
485 override fun onDestroy(owner: LifecycleOwner) {
486 lifecycleScope = null
487 iconController.deactivated = true
488 }
489 }
490 )
491 }
492
onDialogAnimatedInnull493 override fun onDialogAnimatedIn(fingerprintWasStarted: Boolean) {
494 if (fingerprintWasStarted) {
495 viewModel.ensureFingerprintHasStarted(isDelayed = false)
496 viewModel.showAuthenticating(modalities.asDefaultHelpMessage(applicationContext))
497 } else {
498 viewModel.showAuthenticating()
499 }
500 }
501
onAuthenticationSucceedednull502 override fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int) {
503 applicationScope.launch {
504 val authenticatedModality = modality.asBiometricModality()
505 val msgId = getHelpForSuccessfulAuthentication(authenticatedModality)
506 viewModel.showAuthenticated(
507 modality = authenticatedModality,
508 dismissAfterDelay = 500,
509 helpMessage = if (msgId != null) applicationContext.getString(msgId) else ""
510 )
511 }
512 }
513
getHelpForSuccessfulAuthenticationnull514 private suspend fun getHelpForSuccessfulAuthentication(
515 authenticatedModality: BiometricModality,
516 ): Int? =
517 when {
518 // for coex, show a message when face succeeds after fingerprint has also started
519 modalities.hasFaceAndFingerprint &&
520 (viewModel.fingerprintStartMode.first() != FingerprintStartMode.Pending) &&
521 (authenticatedModality == BiometricModality.Face) ->
522 R.string.biometric_dialog_tap_confirm_with_face_alt_1
523 else -> null
524 }
525
onAuthenticationFailednull526 override fun onAuthenticationFailed(
527 @BiometricAuthenticator.Modality modality: Int,
528 failureReason: String,
529 ) {
530 val failedModality = modality.asBiometricModality()
531 viewModel.ensureFingerprintHasStarted(isDelayed = true)
532
533 applicationScope.launch {
534 viewModel.showTemporaryError(
535 failureReason,
536 messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
537 authenticateAfterError = modalities.hasFingerprint,
538 suppressIf = { currentMessage, history ->
539 modalities.hasFaceAndFingerprint &&
540 failedModality == BiometricModality.Face &&
541 (currentMessage.isError || history.faceFailed)
542 },
543 failedModality = failedModality,
544 )
545 }
546 }
547
onErrornull548 override fun onError(modality: Int, error: String) {
549 val errorModality = modality.asBiometricModality()
550 if (ignoreUnsuccessfulEventsFrom(errorModality, error)) {
551 return
552 }
553
554 applicationScope.launch {
555 viewModel.showTemporaryError(
556 error,
557 messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
558 authenticateAfterError = modalities.hasFingerprint,
559 )
560 delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
561 legacyCallback?.onAction(Callback.ACTION_ERROR)
562 }
563 }
564
onHelpnull565 override fun onHelp(modality: Int, help: String) {
566 if (ignoreUnsuccessfulEventsFrom(modality.asBiometricModality(), "")) {
567 return
568 }
569
570 applicationScope.launch {
571 // help messages from the HAL should be displayed as temporary (i.e. soft) errors
572 viewModel.showTemporaryError(
573 help,
574 messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
575 authenticateAfterError = modalities.hasFingerprint,
576 hapticFeedback = false,
577 )
578 }
579 }
580
ignoreUnsuccessfulEventsFromnull581 private fun ignoreUnsuccessfulEventsFrom(modality: BiometricModality, message: String) =
582 when {
583 modalities.hasFaceAndFingerprint ->
584 (modality == BiometricModality.Face) &&
585 !(modalities.isFaceStrong && lockoutErrorStrings.contains(message))
586 else -> false
587 }
588
startTransitionToCredentialUInull589 override fun startTransitionToCredentialUI(isError: Boolean) {
590 applicationScope.launch {
591 viewModel.onSwitchToCredential()
592 legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
593 }
594 }
595
requestLayoutnull596 override fun requestLayout() {
597 // nothing, for legacy view...
598 }
599
restoreStatenull600 override fun restoreState(bundle: Bundle?) {
601 // nothing, for legacy view...
602 }
603
onSaveStatenull604 override fun onSaveState(bundle: Bundle?) {
605 // nothing, for legacy view...
606 }
607
onOrientationChangednull608 override fun onOrientationChanged() {
609 // nothing, for legacy view...
610 }
611
cancelAnimationnull612 override fun cancelAnimation() {
613 view.animate()?.cancel()
614 }
615
isCoexnull616 override fun isCoex() = modalities.hasFaceAndFingerprint
617
618 override fun asView() = view
619 }
620
621 private fun BiometricModalities.asDefaultHelpMessage(context: Context): String =
622 when {
623 hasFingerprint -> context.getString(R.string.fingerprint_dialog_touch_sensor)
624 else -> ""
625 }
626
BiometricModalitiesnull627 private fun BiometricModalities.asIconController(
628 context: Context,
629 iconView: LottieAnimationView,
630 iconViewOverlay: LottieAnimationView,
631 ): AuthIconController =
632 when {
633 hasFaceAndFingerprint -> HackyCoexIconController(context, iconView, iconViewOverlay)
634 hasFingerprint -> AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay)
635 hasFace -> AuthBiometricFaceIconController(context, iconView)
636 else -> throw IllegalStateException("unexpected view type :$this")
637 }
638
Booleannull639 private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE
640
641 private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE
642
643 // TODO(b/251476085): proper type?
644 typealias BiometricJankListener = Animator.AnimatorListener
645
646 // TODO(b/251476085): delete - temporary until the legacy icon controllers are replaced
647 private class HackyCoexIconController(
648 context: Context,
649 iconView: LottieAnimationView,
650 iconViewOverlay: LottieAnimationView,
651 ) : AuthBiometricFingerprintAndFaceIconController(context, iconView, iconViewOverlay) {
652
653 private var state: Int? = null
654 private val faceController = AuthBiometricFaceIconController(context, iconView)
655
656 var faceMode: Boolean = true
657 set(value) {
658 if (field != value) {
659 field = value
660
661 faceController.deactivated = !value
662 iconView.setImageIcon(null)
663 iconViewOverlay.setImageIcon(null)
664 state?.let { updateIcon(AuthBiometricView.STATE_IDLE, it) }
665 }
666 }
667
668 override fun updateIcon(lastState: Int, newState: Int) {
669 if (deactivated) {
670 return
671 }
672
673 if (faceMode) {
674 faceController.updateIcon(lastState, newState)
675 } else {
676 super.updateIcon(lastState, newState)
677 }
678
679 state = newState
680 }
681 }
682