1 /*
2  * 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             )
466             .map { FaceManager.getErrorString(applicationContext, it, 0 /* vendorCode */) }
467 
468     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 
493     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 
502     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 
514     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 
526     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 
548     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 
565     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 
581     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 
589     override fun startTransitionToCredentialUI(isError: Boolean) {
590         applicationScope.launch {
591             viewModel.onSwitchToCredential()
592             legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
593         }
594     }
595 
596     override fun requestLayout() {
597         // nothing, for legacy view...
598     }
599 
600     override fun restoreState(bundle: Bundle?) {
601         // nothing, for legacy view...
602     }
603 
604     override fun onSaveState(bundle: Bundle?) {
605         // nothing, for legacy view...
606     }
607 
608     override fun onOrientationChanged() {
609         // nothing, for legacy view...
610     }
611 
612     override fun cancelAnimation() {
613         view.animate()?.cancel()
614     }
615 
616     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 
627 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 
639 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