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