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.viewmodel
18 
19 import android.app.ActivityTaskManager
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.pm.ActivityInfo
23 import android.content.pm.ApplicationInfo
24 import android.content.pm.PackageManager
25 import android.graphics.Rect
26 import android.graphics.drawable.BitmapDrawable
27 import android.graphics.drawable.Drawable
28 import android.hardware.biometrics.BiometricFingerprintConstants
29 import android.hardware.biometrics.BiometricPrompt
30 import android.hardware.biometrics.Flags.customBiometricPrompt
31 import android.hardware.biometrics.PromptContentView
32 import android.os.UserHandle
33 import android.text.TextPaint
34 import android.util.Log
35 import android.util.RotationUtils
36 import android.view.HapticFeedbackConstants
37 import android.view.MotionEvent
38 import com.android.launcher3.icons.IconProvider
39 import com.android.systemui.Flags.bpTalkback
40 import com.android.systemui.Flags.constraintBp
41 import com.android.systemui.biometrics.UdfpsUtils
42 import com.android.systemui.biometrics.Utils
43 import com.android.systemui.biometrics.Utils.isSystem
44 import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
45 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
46 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
47 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
48 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
49 import com.android.systemui.biometrics.shared.model.BiometricModalities
50 import com.android.systemui.biometrics.shared.model.BiometricModality
51 import com.android.systemui.biometrics.shared.model.DisplayRotation
52 import com.android.systemui.biometrics.shared.model.PromptKind
53 import com.android.systemui.dagger.qualifiers.Application
54 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
55 import com.android.systemui.res.R
56 import com.android.systemui.util.kotlin.combine
57 import javax.inject.Inject
58 import kotlinx.coroutines.Job
59 import kotlinx.coroutines.coroutineScope
60 import kotlinx.coroutines.delay
61 import kotlinx.coroutines.flow.Flow
62 import kotlinx.coroutines.flow.MutableSharedFlow
63 import kotlinx.coroutines.flow.MutableStateFlow
64 import kotlinx.coroutines.flow.StateFlow
65 import kotlinx.coroutines.flow.asSharedFlow
66 import kotlinx.coroutines.flow.asStateFlow
67 import kotlinx.coroutines.flow.combine
68 import kotlinx.coroutines.flow.distinctUntilChanged
69 import kotlinx.coroutines.flow.first
70 import kotlinx.coroutines.flow.map
71 import kotlinx.coroutines.flow.update
72 import kotlinx.coroutines.launch
73 
74 /** ViewModel for BiometricPrompt. */
75 class PromptViewModel
76 @Inject
77 constructor(
78     displayStateInteractor: DisplayStateInteractor,
79     private val promptSelectorInteractor: PromptSelectorInteractor,
80     @Application private val context: Context,
81     private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
82     private val biometricStatusInteractor: BiometricStatusInteractor,
83     private val udfpsUtils: UdfpsUtils,
84     private val iconProvider: IconProvider,
85     private val activityTaskManager: ActivityTaskManager,
86 ) {
87     /** The set of modalities available for this prompt */
88     val modalities: Flow<BiometricModalities> =
89         promptSelectorInteractor.prompt
90             .map { it?.modalities ?: BiometricModalities() }
91             .distinctUntilChanged()
92 
93     /** Layout params for fingerprint iconView */
94     val fingerprintIconWidth: Int =
95         context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_fingerprint_icon_width)
96     val fingerprintIconHeight: Int =
97         context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_fingerprint_icon_height)
98 
99     /** Layout params for face iconView */
100     val faceIconWidth: Int =
101         context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
102     val faceIconHeight: Int =
103         context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
104 
105     /** Padding for placing icons */
106     val portraitSmallBottomPadding =
107         context.resources.getDimensionPixelSize(
108             R.dimen.biometric_prompt_portrait_small_bottom_padding
109         )
110     val portraitMediumBottomPadding =
111         context.resources.getDimensionPixelSize(
112             R.dimen.biometric_prompt_portrait_medium_bottom_padding
113         )
114     val portraitLargeScreenBottomPadding =
115         context.resources.getDimensionPixelSize(
116             R.dimen.biometric_prompt_portrait_large_screen_bottom_padding
117         )
118     val landscapeSmallBottomPadding =
119         context.resources.getDimensionPixelSize(
120             R.dimen.biometric_prompt_landscape_small_bottom_padding
121         )
122     val landscapeSmallHorizontalPadding =
123         context.resources.getDimensionPixelSize(
124             R.dimen.biometric_prompt_landscape_small_horizontal_padding
125         )
126     val landscapeMediumBottomPadding =
127         context.resources.getDimensionPixelSize(
128             R.dimen.biometric_prompt_landscape_medium_bottom_padding
129         )
130     val landscapeMediumHorizontalPadding =
131         context.resources.getDimensionPixelSize(
132             R.dimen.biometric_prompt_landscape_medium_horizontal_padding
133         )
134 
135     private val udfpsSensorBounds: Flow<Rect> =
136         combine(
137                 udfpsOverlayInteractor.udfpsOverlayParams,
138                 displayStateInteractor.currentRotation
139             ) { params, rotation ->
140                 val rotatedBounds = Rect(params.sensorBounds)
141                 RotationUtils.rotateBounds(
142                     rotatedBounds,
143                     params.naturalDisplayWidth,
144                     params.naturalDisplayHeight,
145                     rotation.ordinal
146                 )
147                 Rect(
148                     rotatedBounds.left,
149                     rotatedBounds.top,
150                     params.logicalDisplayWidth - rotatedBounds.right,
151                     params.logicalDisplayHeight - rotatedBounds.bottom
152                 )
153             }
154             .distinctUntilChanged()
155 
156     val legacyFingerprintSensorWidth: Flow<Int> =
157         combine(modalities, udfpsOverlayInteractor.udfpsOverlayParams) { modalities, overlayParams
158             ->
159             if (modalities.hasUdfps) {
160                 overlayParams.sensorBounds.width()
161             } else {
162                 fingerprintIconWidth
163             }
164         }
165 
166     val legacyFingerprintSensorHeight: Flow<Int> =
167         combine(modalities, udfpsOverlayInteractor.udfpsOverlayParams) { modalities, overlayParams
168             ->
169             if (modalities.hasUdfps) {
170                 overlayParams.sensorBounds.height()
171             } else {
172                 fingerprintIconHeight
173             }
174         }
175 
176     val fingerprintSensorWidth: Int =
177         udfpsOverlayInteractor.udfpsOverlayParams.value.sensorBounds.width()
178 
179     val fingerprintSensorHeight: Int =
180         udfpsOverlayInteractor.udfpsOverlayParams.value.sensorBounds.height()
181 
182     private val _accessibilityHint = MutableSharedFlow<String>()
183 
184     /** Hint for talkback directional guidance */
185     val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow()
186 
187     private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
188 
189     /** If the user is currently authenticating (i.e. at least one biometric is scanning). */
190     val isAuthenticating: Flow<Boolean> = _isAuthenticating.asStateFlow()
191 
192     private val _isAuthenticated: MutableStateFlow<PromptAuthState> =
193         MutableStateFlow(PromptAuthState(false))
194 
195     /** If the user has successfully authenticated and confirmed (when explicitly required). */
196     val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow()
197 
198     /** If the auth is pending confirmation. */
199     val isPendingConfirmation: Flow<Boolean> =
200         isAuthenticated.map { authState ->
201             authState.isAuthenticated && authState.needsUserConfirmation
202         }
203 
204     private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false)
205 
206     /** The kind of credential the user has. */
207     val credentialKind: Flow<PromptKind> = promptSelectorInteractor.credentialKind
208 
209     /** The kind of prompt to use (biometric, pin, pattern, etc.). */
210     val promptKind: StateFlow<PromptKind> = promptSelectorInteractor.promptKind
211 
212     /** Whether the sensor icon on biometric prompt ui should be hidden. */
213     val hideSensorIcon: Flow<Boolean> = modalities.map { it.isEmpty }.distinctUntilChanged()
214 
215     /** The label to use for the cancel button. */
216     val negativeButtonText: Flow<String> =
217         promptSelectorInteractor.prompt.map { it?.negativeButtonText ?: "" }
218 
219     private val _message: MutableStateFlow<PromptMessage> = MutableStateFlow(PromptMessage.Empty)
220 
221     /** A message to show the user, if there is an error, hint, or help to show. */
222     val message: Flow<PromptMessage> = _message.asStateFlow()
223 
224     /** Whether an error message is currently being shown. */
225     val showingError: Flow<Boolean> = message.map { it.isError }.distinctUntilChanged()
226 
227     private val isRetrySupported: Flow<Boolean> = modalities.map { it.hasFace }
228 
229     private val _fingerprintStartMode = MutableStateFlow(FingerprintStartMode.Pending)
230 
231     /** Fingerprint sensor state. */
232     val fingerprintStartMode: Flow<FingerprintStartMode> = _fingerprintStartMode.asStateFlow()
233 
234     /** Whether a finger has been acquired by the sensor */
235     val hasFingerBeenAcquired: Flow<Boolean> =
236         combine(biometricStatusInteractor.fingerprintAcquiredStatus, modalities) {
237                 status,
238                 modalities ->
239                 modalities.hasSfps &&
240                     status is AcquiredFingerprintAuthenticationStatus &&
241                     status.acquiredInfo == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
242             }
243             .distinctUntilChanged()
244 
245     /** Whether there is currently a finger on the sensor */
246     val hasFingerOnSensor: Flow<Boolean> =
247         combine(hasFingerBeenAcquired, _isOverlayTouched) { hasFingerBeenAcquired, overlayTouched ->
248             hasFingerBeenAcquired || overlayTouched
249         }
250 
251     private val _forceLargeSize = MutableStateFlow(false)
252     private val _forceMediumSize = MutableStateFlow(false)
253 
254     private val _hapticsToPlay =
255         MutableStateFlow(HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, /* flag= */ null))
256 
257     /** Event fired to the view indicating a [HapticsToPlay] */
258     val hapticsToPlay = _hapticsToPlay.asStateFlow()
259 
260     /** The current position of the prompt */
261     val position: Flow<PromptPosition> =
262         combine(
263                 _forceLargeSize,
264                 promptKind,
265                 displayStateInteractor.isLargeScreen,
266                 displayStateInteractor.currentRotation,
267                 modalities
268             ) { forceLarge, promptKind, isLargeScreen, rotation, modalities ->
269                 when {
270                     forceLarge ||
271                         isLargeScreen ||
272                         promptKind.isOnePaneNoSensorLandscapeBiometric() -> PromptPosition.Bottom
273                     rotation == DisplayRotation.ROTATION_90 -> PromptPosition.Right
274                     rotation == DisplayRotation.ROTATION_270 -> PromptPosition.Left
275                     rotation == DisplayRotation.ROTATION_180 && modalities.hasUdfps ->
276                         PromptPosition.Top
277                     else -> PromptPosition.Bottom
278                 }
279             }
280             .distinctUntilChanged()
281 
282     /** The size of the prompt. */
283     val size: Flow<PromptSize> =
284         combine(
285                 _forceLargeSize,
286                 _forceMediumSize,
287                 modalities,
288                 promptSelectorInteractor.isConfirmationRequired,
289                 fingerprintStartMode,
290             ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode ->
291                 when {
292                     forceLarge -> PromptSize.LARGE
293                     forceMedium -> PromptSize.MEDIUM
294                     modalities.hasFaceOnly && !confirmationRequired -> PromptSize.SMALL
295                     modalities.hasFaceAndFingerprint &&
296                         !confirmationRequired &&
297                         fpStartMode == FingerprintStartMode.Pending -> PromptSize.SMALL
298                     else -> PromptSize.MEDIUM
299                 }
300             }
301             .distinctUntilChanged()
302 
303     /** Prompt panel size padding */
304     private val smallHorizontalGuidelinePadding =
305         context.resources.getDimensionPixelSize(
306             R.dimen.biometric_prompt_land_small_horizontal_guideline_padding
307         )
308     private val udfpsHorizontalGuidelinePadding =
309         context.resources.getDimensionPixelSize(
310             R.dimen.biometric_prompt_two_pane_udfps_horizontal_guideline_padding
311         )
312     private val udfpsHorizontalShorterGuidelinePadding =
313         context.resources.getDimensionPixelSize(
314             R.dimen.biometric_prompt_two_pane_udfps_shorter_horizontal_guideline_padding
315         )
316     private val mediumTopGuidelinePadding =
317         context.resources.getDimensionPixelSize(
318             R.dimen.biometric_prompt_one_pane_medium_top_guideline_padding
319         )
320     private val mediumHorizontalGuidelinePadding =
321         context.resources.getDimensionPixelSize(
322             R.dimen.biometric_prompt_two_pane_medium_horizontal_guideline_padding
323         )
324 
325     /** Rect for positioning biometric icon */
326     val iconPosition: Flow<Rect> =
327         combine(udfpsSensorBounds, size, position, modalities) {
328                 sensorBounds,
329                 size,
330                 position,
331                 modalities ->
332                 when (position) {
333                     PromptPosition.Bottom ->
334                         if (size.isSmall) {
335                             Rect(0, 0, 0, portraitSmallBottomPadding)
336                         } else if (size.isMedium && modalities.hasUdfps) {
337                             Rect(0, 0, 0, sensorBounds.bottom)
338                         } else if (size.isMedium) {
339                             Rect(0, 0, 0, portraitMediumBottomPadding)
340                         } else {
341                             // Large screen
342                             Rect(0, 0, 0, portraitLargeScreenBottomPadding)
343                         }
344                     PromptPosition.Right ->
345                         if (size.isSmall || modalities.hasFaceOnly) {
346                             Rect(0, 0, landscapeSmallHorizontalPadding, landscapeSmallBottomPadding)
347                         } else if (size.isMedium && modalities.hasUdfps) {
348                             Rect(0, 0, sensorBounds.right, sensorBounds.bottom)
349                         } else {
350                             // SFPS
351                             Rect(
352                                 0,
353                                 0,
354                                 landscapeMediumHorizontalPadding,
355                                 landscapeMediumBottomPadding
356                             )
357                         }
358                     PromptPosition.Left ->
359                         if (size.isSmall || modalities.hasFaceOnly) {
360                             Rect(landscapeSmallHorizontalPadding, 0, 0, landscapeSmallBottomPadding)
361                         } else if (size.isMedium && modalities.hasUdfps) {
362                             Rect(sensorBounds.left, 0, 0, sensorBounds.bottom)
363                         } else {
364                             // SFPS
365                             Rect(
366                                 landscapeMediumHorizontalPadding,
367                                 0,
368                                 0,
369                                 landscapeMediumBottomPadding
370                             )
371                         }
372                     PromptPosition.Top ->
373                         if (size.isSmall) {
374                             Rect(0, 0, 0, portraitSmallBottomPadding)
375                         } else if (size.isMedium && modalities.hasUdfps) {
376                             Rect(0, 0, 0, sensorBounds.bottom)
377                         } else {
378                             Rect(0, 0, 0, portraitMediumBottomPadding)
379                         }
380                 }
381             }
382             .distinctUntilChanged()
383 
384     /**
385      * If the API caller or the user's personal preferences require explicit confirmation after
386      * successful authentication. Confirmation always required when in explicit flow.
387      */
388     val isConfirmationRequired: Flow<Boolean> =
389         combine(_isOverlayTouched, size) { isOverlayTouched, size ->
390             !isOverlayTouched && size.isNotSmall
391         }
392 
393     /**
394      * When fingerprint and face modalities are enrolled, indicates whether only face auth has
395      * started.
396      *
397      * True when fingerprint and face modalities are enrolled and implicit flow is active. This
398      * occurs in co-ex auth when confirmation is not required and only face auth is started, then
399      * becomes false when device transitions to explicit flow after a first error, when the
400      * fingerprint sensor is started.
401      *
402      * False when the dialog opens in explicit flow (fingerprint and face modalities enrolled but
403      * confirmation is required), or if user has only fingerprint enrolled, or only face enrolled.
404      */
405     val faceMode: Flow<Boolean> =
406         combine(modalities, isConfirmationRequired, fingerprintStartMode) {
407                 modalities,
408                 isConfirmationRequired,
409                 fingerprintStartMode ->
410                 modalities.hasFaceAndFingerprint &&
411                     !isConfirmationRequired &&
412                     fingerprintStartMode == FingerprintStartMode.Pending
413             }
414             .distinctUntilChanged()
415 
416     val iconViewModel: PromptIconViewModel =
417         PromptIconViewModel(
418             this,
419             displayStateInteractor,
420             promptSelectorInteractor,
421             udfpsOverlayInteractor
422         )
423 
424     private val _isIconViewLoaded = MutableStateFlow(false)
425 
426     /**
427      * For prompts with an iconView, false until the prompt's iconView animation has been loaded in
428      * the view, otherwise true by default. Used for BiometricViewSizeBinder to wait for the icon
429      * asset to be loaded before determining the prompt size.
430      */
431     val isIconViewLoaded: Flow<Boolean> =
432         combine(hideSensorIcon, _isIconViewLoaded.asStateFlow()) { hideSensorIcon, isIconViewLoaded
433                 ->
434                 hideSensorIcon || isIconViewLoaded
435             }
436             .distinctUntilChanged()
437 
438     // Sets whether the prompt's iconView animation has been loaded in the view yet.
439     fun setIsIconViewLoaded(iconViewLoaded: Boolean) {
440         _isIconViewLoaded.value = iconViewLoaded
441     }
442 
443     /** The size of the biometric icon */
444     val iconSize: Flow<Pair<Int, Int>> =
445         combine(iconViewModel.activeAuthType, modalities) { activeAuthType, modalities ->
446             if (activeAuthType == PromptIconViewModel.AuthType.Face) {
447                 Pair(faceIconWidth, faceIconHeight)
448             } else {
449                 if (modalities.hasUdfps) {
450                     Pair(fingerprintSensorWidth, fingerprintSensorHeight)
451                 } else {
452                     Pair(fingerprintIconWidth, fingerprintIconHeight)
453                 }
454             }
455         }
456 
457     /** Padding for prompt UI elements */
458     val promptPadding: Flow<Rect> =
459         combine(size, displayStateInteractor.currentRotation) { size, rotation ->
460             if (size != PromptSize.LARGE) {
461                 val navBarInsets = Utils.getNavbarInsets(context)
462                 if (rotation == DisplayRotation.ROTATION_90) {
463                     Rect(0, 0, navBarInsets.right, 0)
464                 } else if (rotation == DisplayRotation.ROTATION_270) {
465                     Rect(navBarInsets.left, 0, 0, 0)
466                 } else {
467                     Rect(0, 0, 0, navBarInsets.bottom)
468                 }
469             } else {
470                 Rect(0, 0, 0, 0)
471             }
472         }
473 
474     /** Logo for the prompt. */
475     val logo: Flow<Drawable?> =
476         promptSelectorInteractor.prompt
477             .map {
478                 when {
479                     !(customBiometricPrompt() && constraintBp()) || it == null -> null
480                     it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap)
481                     else -> context.getUserBadgedIcon(it, iconProvider, activityTaskManager)
482                 }
483             }
484             .distinctUntilChanged()
485 
486     /** Logo description for the prompt. */
487     val logoDescription: Flow<String> =
488         promptSelectorInteractor.prompt
489             .map {
490                 when {
491                     !(customBiometricPrompt() && constraintBp()) || it == null -> ""
492                     !it.logoDescription.isNullOrEmpty() -> it.logoDescription
493                     else -> context.getUserBadgedLabel(it, activityTaskManager)
494                 }
495             }
496             .distinctUntilChanged()
497 
498     /** Title for the prompt. */
499     val title: Flow<String> =
500         promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged()
501 
502     /** Subtitle for the prompt. */
503     val subtitle: Flow<String> =
504         promptSelectorInteractor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
505 
506     /** Custom content view for the prompt. */
507     val contentView: Flow<PromptContentView?> =
508         promptSelectorInteractor.prompt
509             .map { if (customBiometricPrompt() && constraintBp()) it?.contentView else null }
510             .distinctUntilChanged()
511 
512     private val originalDescription =
513         promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
514     /**
515      * Description for the prompt. Description view and contentView is mutually exclusive. Pass
516      * description down only when contentView is null.
517      */
518     val description: Flow<String> =
519         combine(contentView, originalDescription) { contentView, description ->
520             if (contentView == null) description else ""
521         }
522 
523     private val hasOnlyOneLineTitle: Flow<Boolean> =
524         combine(title, subtitle, contentView, description) {
525             title,
526             subtitle,
527             contentView,
528             description ->
529             if (subtitle.isNotEmpty() || contentView != null || description.isNotEmpty()) {
530                 false
531             } else {
532                 val maxWidth =
533                     context.resources.getDimensionPixelSize(
534                         R.dimen.biometric_prompt_two_pane_udfps_shorter_content_width
535                     )
536                 val attributes =
537                     context.obtainStyledAttributes(
538                         R.style.TextAppearance_AuthCredential_Title,
539                         intArrayOf(android.R.attr.textSize)
540                     )
541                 val paint = TextPaint()
542                 paint.textSize = attributes.getDimensionPixelSize(0, 0).toFloat()
543                 val textWidth = paint.measureText(title)
544                 attributes.recycle()
545                 textWidth / maxWidth <= 1
546             }
547         }
548 
549     /**
550      * Rect for positioning prompt guidelines (left, top, right, unused)
551      *
552      * Negative values are used to signify that guideline measuring should be flipped, measuring
553      * from opposite side of the screen
554      */
555     val guidelineBounds: Flow<Rect> =
556         combine(iconPosition, promptKind, size, position, modalities, hasOnlyOneLineTitle) {
557                 _,
558                 promptKind,
559                 size,
560                 position,
561                 modalities,
562                 hasOnlyOneLineTitle ->
563                 var left = 0
564                 var top = 0
565                 var right = 0
566                 when (position) {
567                     PromptPosition.Bottom -> {
568                         val noSensorLandscape = promptKind.isOnePaneNoSensorLandscapeBiometric()
569                         top = if (noSensorLandscape) 0 else mediumTopGuidelinePadding
570                     }
571                     PromptPosition.Right ->
572                         left = getHorizontalPadding(size, modalities, hasOnlyOneLineTitle)
573                     PromptPosition.Left ->
574                         right = getHorizontalPadding(size, modalities, hasOnlyOneLineTitle)
575                     PromptPosition.Top -> {}
576                 }
577                 Rect(left, top, right, 0)
578             }
579             .distinctUntilChanged()
580 
581     private fun getHorizontalPadding(
582         size: PromptSize,
583         modalities: BiometricModalities,
584         hasOnlyOneLineTitle: Boolean
585     ) =
586         if (size.isSmall) {
587             -smallHorizontalGuidelinePadding
588         } else if (modalities.hasUdfps) {
589             if (hasOnlyOneLineTitle) {
590                 -udfpsHorizontalShorterGuidelinePadding
591             } else {
592                 udfpsHorizontalGuidelinePadding
593             }
594         } else {
595             -mediumHorizontalGuidelinePadding
596         }
597 
598     /** If the indicator (help, error) message should be shown. */
599     val isIndicatorMessageVisible: Flow<Boolean> =
600         combine(
601             size,
602             position,
603             message,
604         ) { size, _, message ->
605             size.isMedium && message.message.isNotBlank()
606         }
607 
608     /** If the auth is pending confirmation and the confirm button should be shown. */
609     val isConfirmButtonVisible: Flow<Boolean> =
610         combine(
611             size,
612             position,
613             isPendingConfirmation,
614         ) { size, _, isPendingConfirmation ->
615             size.isNotSmall && isPendingConfirmation
616         }
617 
618     /** If the icon can be used as a confirmation button. */
619     val isIconConfirmButton: Flow<Boolean> =
620         combine(modalities, size) { modalities, size -> modalities.hasUdfps && size.isNotSmall }
621 
622     /** If the negative button should be shown. */
623     val isNegativeButtonVisible: Flow<Boolean> =
624         combine(
625             size,
626             position,
627             isAuthenticated,
628             promptSelectorInteractor.isCredentialAllowed,
629         ) { size, _, authState, credentialAllowed ->
630             size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed
631         }
632 
633     /** If the cancel button should be shown (. */
634     val isCancelButtonVisible: Flow<Boolean> =
635         combine(
636             size,
637             position,
638             isAuthenticated,
639             isNegativeButtonVisible,
640             isConfirmButtonVisible,
641         ) { size, _, authState, showNegativeButton, showConfirmButton ->
642             size.isNotSmall && authState.isAuthenticated && !showNegativeButton && showConfirmButton
643         }
644 
645     private val _canTryAgainNow = MutableStateFlow(false)
646     /**
647      * If authentication can be manually restarted via the try again button or touching a
648      * fingerprint sensor.
649      */
650     val canTryAgainNow: Flow<Boolean> =
651         combine(
652             _canTryAgainNow,
653             size,
654             position,
655             isAuthenticated,
656             isRetrySupported,
657         ) { readyToTryAgain, size, _, authState, supportsRetry ->
658             readyToTryAgain && size.isNotSmall && supportsRetry && authState.isNotAuthenticated
659         }
660 
661     /** If the try again button show be shown (only the button, see [canTryAgainNow]). */
662     val isTryAgainButtonVisible: Flow<Boolean> =
663         combine(
664             canTryAgainNow,
665             modalities,
666         ) { tryAgainIsPossible, modalities ->
667             tryAgainIsPossible && modalities.hasFaceOnly
668         }
669 
670     /** If the credential fallback button show be shown. */
671     val isCredentialButtonVisible: Flow<Boolean> =
672         combine(
673             size,
674             position,
675             isAuthenticated,
676             promptSelectorInteractor.isCredentialAllowed,
677         ) { size, _, authState, credentialAllowed ->
678             size.isMedium && authState.isNotAuthenticated && credentialAllowed
679         }
680 
681     private val history = PromptHistoryImpl()
682     private var messageJob: Job? = null
683 
684     /**
685      * Show a temporary error [message] associated with an optional [failedModality] and play
686      * [hapticFeedback].
687      *
688      * The [messageAfterError] will be shown via [showAuthenticating] when [authenticateAfterError]
689      * is set (or via [showHelp] when not set) after the error is dismissed.
690      *
691      * The error is ignored if the user has already authenticated or if [suppressIf] is true given
692      * the currently showing [PromptMessage] and [PromptHistory].
693      */
694     suspend fun showTemporaryError(
695         message: String,
696         messageAfterError: String,
697         authenticateAfterError: Boolean,
698         suppressIf: (PromptMessage, PromptHistory) -> Boolean = { _, _ -> false },
699         hapticFeedback: Boolean = true,
700         failedModality: BiometricModality = BiometricModality.None,
701     ) = coroutineScope {
702         if (_isAuthenticated.value.isAuthenticated) {
703             if (_isAuthenticated.value.needsUserConfirmation && hapticFeedback) {
704                 vibrateOnError()
705             }
706             return@coroutineScope
707         }
708 
709         _canTryAgainNow.value = supportsRetry(failedModality)
710 
711         val suppress = suppressIf(_message.value, history)
712         history.failure(failedModality)
713         if (suppress) {
714             return@coroutineScope
715         }
716 
717         _isAuthenticating.value = false
718         _isAuthenticated.value = PromptAuthState(false)
719         _forceMediumSize.value = true
720         _message.value = PromptMessage.Error(message)
721 
722         if (hapticFeedback) {
723             vibrateOnError()
724         }
725 
726         messageJob?.cancel()
727         messageJob = launch {
728             delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
729             if (authenticateAfterError) {
730                 showAuthenticating(messageAfterError)
731             } else {
732                 showHelp(messageAfterError)
733             }
734         }
735     }
736 
737     /**
738      * Call to ensure the fingerprint sensor has started. Either when the dialog is first shown
739      * (most cases) or when it should be enabled after a first error (coex implicit flow).
740      */
741     fun ensureFingerprintHasStarted(isDelayed: Boolean) {
742         if (_fingerprintStartMode.value == FingerprintStartMode.Pending) {
743             _fingerprintStartMode.value =
744                 if (isDelayed) FingerprintStartMode.Delayed else FingerprintStartMode.Normal
745         }
746     }
747 
748     // enable retry only when face fails (fingerprint runs constantly)
749     private fun supportsRetry(failedModality: BiometricModality) =
750         failedModality == BiometricModality.Face
751 
752     /**
753      * Show a persistent help message.
754      *
755      * Will be show even if the user has already authenticated.
756      */
757     suspend fun showHelp(message: String) {
758         val alreadyAuthenticated = _isAuthenticated.value.isAuthenticated
759         if (!alreadyAuthenticated) {
760             _isAuthenticating.value = false
761             _isAuthenticated.value = PromptAuthState(false)
762         }
763 
764         _message.value =
765             if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
766         _forceMediumSize.value = true
767 
768         messageJob?.cancel()
769         messageJob = null
770     }
771 
772     /**
773      * Show a temporary help message and transition back to a fixed message.
774      *
775      * Ignored if the user has already authenticated.
776      */
777     suspend fun showTemporaryHelp(
778         message: String,
779         messageAfterHelp: String = "",
780     ) = coroutineScope {
781         if (_isAuthenticated.value.isAuthenticated) {
782             return@coroutineScope
783         }
784 
785         _isAuthenticating.value = false
786         _isAuthenticated.value = PromptAuthState(false)
787         _message.value =
788             if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
789         _forceMediumSize.value = true
790 
791         messageJob?.cancel()
792         messageJob = launch {
793             delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
794             showAuthenticating(messageAfterHelp)
795         }
796     }
797 
798     /** Show the user that biometrics are actively running and set [isAuthenticating]. */
799     fun showAuthenticating(message: String = "", isRetry: Boolean = false) {
800         if (_isAuthenticated.value.isAuthenticated) {
801             // TODO(jbolinger): convert to go/tex-apc?
802             Log.w(TAG, "Cannot show authenticating after authenticated")
803             return
804         }
805 
806         _isAuthenticating.value = true
807         _isAuthenticated.value = PromptAuthState(false)
808         _message.value = if (message.isBlank()) PromptMessage.Empty else PromptMessage.Help(message)
809 
810         // reset the try again button(s) after the user attempts a retry
811         if (isRetry) {
812             _canTryAgainNow.value = false
813         }
814 
815         messageJob?.cancel()
816         messageJob = null
817     }
818 
819     /**
820      * Show successfully authentication, set [isAuthenticated], and dismiss the prompt after a
821      * [dismissAfterDelay] or prompt for explicit confirmation (if required).
822      */
823     suspend fun showAuthenticated(
824         modality: BiometricModality,
825         dismissAfterDelay: Long,
826         helpMessage: String = "",
827     ) {
828         if (_isAuthenticated.value.isAuthenticated) {
829             // Treat second authentication with a different modality as confirmation for the first
830             if (
831                 _isAuthenticated.value.needsUserConfirmation &&
832                     modality != _isAuthenticated.value.authenticatedModality
833             ) {
834                 confirmAuthenticated()
835                 return
836             }
837             // TODO(jbolinger): convert to go/tex-apc?
838             Log.w(TAG, "Cannot show authenticated after authenticated")
839             return
840         }
841 
842         _isAuthenticating.value = false
843         val needsUserConfirmation = needsExplicitConfirmation(modality)
844         _isAuthenticated.value =
845             PromptAuthState(true, modality, needsUserConfirmation, dismissAfterDelay)
846         _message.value = PromptMessage.Empty
847 
848         if (!needsUserConfirmation) {
849             vibrateOnSuccess()
850         }
851 
852         messageJob?.cancel()
853         messageJob = null
854 
855         if (helpMessage.isNotBlank()) {
856             showHelp(helpMessage)
857         }
858     }
859 
860     private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
861         val confirmationRequired = isConfirmationRequired.first()
862 
863         // Only worry about confirmationRequired if face was used to unlock
864         if (modality == BiometricModality.Face) {
865             return confirmationRequired
866         }
867         // fingerprint only never requires confirmation
868         return false
869     }
870 
871     /**
872      * Set the prompt's auth state to authenticated and confirmed.
873      *
874      * This should only be used after [showAuthenticated] when the operation requires explicit user
875      * confirmation.
876      */
877     fun confirmAuthenticated() {
878         val authState = _isAuthenticated.value
879         if (authState.isNotAuthenticated) {
880             Log.w(TAG, "Cannot confirm authenticated when not authenticated")
881             return
882         }
883 
884         _isAuthenticated.value = authState.asExplicitlyConfirmed()
885         _message.value = PromptMessage.Empty
886 
887         vibrateOnSuccess()
888 
889         messageJob?.cancel()
890         messageJob = null
891     }
892 
893     /**
894      * Touch event occurred on the overlay
895      *
896      * Tracks whether a finger is currently down to set [_isOverlayTouched] to be used as user
897      * confirmation
898      */
899     fun onOverlayTouch(event: MotionEvent): Boolean {
900         if (event.actionMasked == MotionEvent.ACTION_DOWN) {
901             _isOverlayTouched.value = true
902 
903             if (_isAuthenticated.value.needsUserConfirmation) {
904                 confirmAuthenticated()
905             }
906             return true
907         } else if (event.actionMasked == MotionEvent.ACTION_UP) {
908             _isOverlayTouched.value = false
909         }
910         return false
911     }
912 
913     /** Sets the message used for UDFPS directional guidance */
914     suspend fun onAnnounceAccessibilityHint(
915         event: MotionEvent,
916         touchExplorationEnabled: Boolean,
917     ): Boolean {
918         if (bpTalkback() && modalities.first().hasUdfps && touchExplorationEnabled) {
919             // TODO(b/315184924): Remove uses of UdfpsUtils
920             val scaledTouch =
921                 udfpsUtils.getTouchInNativeCoordinates(
922                     event.getPointerId(0),
923                     event,
924                     udfpsOverlayInteractor.udfpsOverlayParams.value
925                 )
926             if (
927                 !udfpsUtils.isWithinSensorArea(
928                     event.getPointerId(0),
929                     event,
930                     udfpsOverlayInteractor.udfpsOverlayParams.value
931                 )
932             ) {
933                 _accessibilityHint.emit(
934                     udfpsUtils.onTouchOutsideOfSensorArea(
935                         touchExplorationEnabled,
936                         context,
937                         scaledTouch.x,
938                         scaledTouch.y,
939                         udfpsOverlayInteractor.udfpsOverlayParams.value
940                     )
941                 )
942             }
943         }
944         return false
945     }
946 
947     /**
948      * Switch to the credential view.
949      *
950      * TODO(b/251476085): this should be decoupled from the shared panel controller
951      */
952     fun onSwitchToCredential() {
953         _forceLargeSize.value = true
954         promptSelectorInteractor.onSwitchToCredential()
955     }
956 
957     private fun vibrateOnSuccess() {
958         _hapticsToPlay.value =
959             HapticsToPlay(
960                 HapticFeedbackConstants.CONFIRM,
961                 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
962             )
963     }
964 
965     private fun vibrateOnError() {
966         _hapticsToPlay.value =
967             HapticsToPlay(
968                 HapticFeedbackConstants.REJECT,
969                 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
970             )
971     }
972 
973     /** Clears the [hapticsToPlay] variable by setting its constant to the NO_HAPTICS default. */
974     fun clearHaptics() {
975         _hapticsToPlay.update { previous ->
976             HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, previous.flag)
977         }
978     }
979 
980     companion object {
981         const val TAG = "PromptViewModel"
982     }
983 }
984 
Contextnull985 private fun Context.getUserBadgedIcon(
986     prompt: BiometricPromptRequest.Biometric,
987     iconProvider: IconProvider,
988     activityTaskManager: ActivityTaskManager
989 ): Drawable? {
990     var icon: Drawable? = null
991     val componentName = prompt.getComponentNameForLogo(activityTaskManager)
992     if (componentName != null && shouldShowLogoWithOverrides(componentName)) {
993         val activityInfo = getActivityInfo(componentName)
994         icon = if (activityInfo == null) null else iconProvider.getIcon(activityInfo)
995     }
996     if (icon == null) {
997         val appInfo = prompt.getApplicationInfoForLogo(this, componentName)
998         if (appInfo == null) {
999             Log.w(PromptViewModel.TAG, "Cannot find app logo for package $opPackageName")
1000             return null
1001         } else {
1002             icon = packageManager.getApplicationIcon(appInfo)
1003         }
1004     }
1005     return packageManager.getUserBadgedIcon(icon, UserHandle.of(prompt.userInfo.userId))
1006 }
1007 
Contextnull1008 private fun Context.getUserBadgedLabel(
1009     prompt: BiometricPromptRequest.Biometric,
1010     activityTaskManager: ActivityTaskManager
1011 ): String {
1012     val componentName = prompt.getComponentNameForLogo(activityTaskManager)
1013     val appInfo = prompt.getApplicationInfoForLogo(this, componentName)
1014     return if (appInfo == null || packageManager.getApplicationLabel(appInfo).isNullOrEmpty()) {
1015         Log.w(PromptViewModel.TAG, "Cannot find app logo for package $opPackageName")
1016         ""
1017     } else {
1018         packageManager
1019             .getUserBadgedLabel(packageManager.getApplicationLabel(appInfo), UserHandle.of(userId))
1020             .toString()
1021     }
1022 }
1023 
BiometricPromptRequestnull1024 private fun BiometricPromptRequest.Biometric.getComponentNameForLogo(
1025     activityTaskManager: ActivityTaskManager
1026 ): ComponentName? {
1027     val topActivity: ComponentName? = activityTaskManager.getTasks(1).firstOrNull()?.topActivity
1028     return when {
1029         componentNameForConfirmDeviceCredentialActivity != null ->
1030             componentNameForConfirmDeviceCredentialActivity
1031         topActivity?.packageName.contentEquals(opPackageName) -> topActivity
1032         else -> {
1033             Log.w(PromptViewModel.TAG, "Top activity $topActivity is not the client $opPackageName")
1034             null
1035         }
1036     }
1037 }
1038 
getApplicationInfoForLogonull1039 private fun BiometricPromptRequest.Biometric.getApplicationInfoForLogo(
1040     context: Context,
1041     componentNameForLogo: ComponentName?
1042 ): ApplicationInfo? {
1043     val packageName =
1044         when {
1045             componentNameForLogo != null -> componentNameForLogo.packageName
1046             // TODO(b/339532378): We should check whether |allowBackgroundAuthentication| should be
1047             // removed.
1048             // This is being consistent with the check in [AuthController.showDialog()].
1049             allowBackgroundAuthentication || isSystem(context, opPackageName) -> opPackageName
1050             else -> null
1051         }
1052     return if (packageName == null) {
1053         Log.w(PromptViewModel.TAG, "Cannot find application info for $opPackageName")
1054         null
1055     } else {
1056         context.getApplicationInfo(packageName)
1057     }
1058 }
1059 
Contextnull1060 private fun Context.shouldShowLogoWithOverrides(componentName: ComponentName): Boolean {
1061     return resources
1062         .getStringArray(R.array.biometric_dialog_package_names_for_logo_with_overrides)
1063         .find { componentName.packageName.contentEquals(it) } != null
1064 }
1065 
Contextnull1066 private fun Context.getActivityInfo(componentName: ComponentName): ActivityInfo? =
1067     try {
1068         packageManager.getActivityInfo(componentName, 0)
1069     } catch (e: PackageManager.NameNotFoundException) {
1070         Log.w(PromptViewModel.TAG, "Cannot find activity info for $opPackageName", e)
1071         null
1072     }
1073 
Contextnull1074 private fun Context.getApplicationInfo(packageName: String): ApplicationInfo? =
1075     try {
1076         packageManager.getApplicationInfo(
1077             packageName,
1078             PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
1079         )
1080     } catch (e: PackageManager.NameNotFoundException) {
1081         Log.w(PromptViewModel.TAG, "Cannot find application info for $opPackageName", e)
1082         null
1083     }
1084 
1085 /** How the fingerprint sensor was started for the prompt. */
1086 enum class FingerprintStartMode {
1087     /** Fingerprint sensor has not started. */
1088     Pending,
1089 
1090     /** Fingerprint sensor started immediately when prompt was displayed. */
1091     Normal,
1092 
1093     /** Fingerprint sensor started after the first failure of another passive modality. */
1094     Delayed;
1095 
1096     /** If this is [Normal] or [Delayed]. */
1097     val isStarted: Boolean
1098         get() = this == Normal || this == Delayed
1099 }
1100 
1101 /**
1102  * The state of haptic feedback to play. It is composed by a [HapticFeedbackConstants] and a
1103  * [HapticFeedbackConstants] flag.
1104  */
1105 data class HapticsToPlay(val hapticFeedbackConstant: Int, val flag: Int?)
1106