• 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.ActivityManager.RunningTaskInfo
20 import android.content.ComponentName
21 import android.content.applicationContext
22 import android.content.packageManager
23 import android.content.pm.ActivityInfo
24 import android.content.pm.ApplicationInfo
25 import android.content.pm.PackageManager.NameNotFoundException
26 import android.graphics.Bitmap
27 import android.graphics.Point
28 import android.graphics.Rect
29 import android.graphics.drawable.BitmapDrawable
30 import android.hardware.biometrics.BiometricFingerprintConstants
31 import android.hardware.biometrics.BiometricPrompt
32 import android.hardware.biometrics.PromptContentItemBulletedText
33 import android.hardware.biometrics.PromptContentView
34 import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
35 import android.hardware.biometrics.PromptInfo
36 import android.hardware.biometrics.PromptVerticalListContentView
37 import android.hardware.face.FaceSensorPropertiesInternal
38 import android.hardware.fingerprint.FingerprintSensorProperties
39 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
40 import android.os.UserHandle
41 import android.platform.test.annotations.DisableFlags
42 import android.platform.test.annotations.EnableFlags
43 import android.view.HapticFeedbackConstants
44 import android.view.MotionEvent
45 import android.view.Surface
46 import android.view.accessibility.accessibilityManager
47 import androidx.test.filters.SmallTest
48 import com.android.app.activityTaskManager
49 import com.android.keyguard.AuthInteractionProperties
50 import com.android.systemui.Flags
51 import com.android.systemui.SysuiTestCase
52 import com.android.systemui.biometrics.AuthController
53 import com.android.systemui.biometrics.Utils.toBitmap
54 import com.android.systemui.biometrics.authController
55 import com.android.systemui.biometrics.data.repository.biometricStatusRepository
56 import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
57 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
58 import com.android.systemui.biometrics.domain.interactor.promptSelectorInteractor
59 import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
60 import com.android.systemui.biometrics.extractAuthenticatorTypes
61 import com.android.systemui.biometrics.faceSensorPropertiesInternal
62 import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
63 import com.android.systemui.biometrics.shared.model.AuthenticationReason
64 import com.android.systemui.biometrics.shared.model.BiometricModalities
65 import com.android.systemui.biometrics.shared.model.BiometricModality
66 import com.android.systemui.biometrics.shared.model.DisplayRotation
67 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
68 import com.android.systemui.biometrics.shared.model.toSensorStrength
69 import com.android.systemui.biometrics.shared.model.toSensorType
70 import com.android.systemui.biometrics.udfpsUtils
71 import com.android.systemui.concurrency.fakeExecutor
72 import com.android.systemui.coroutines.collectLastValue
73 import com.android.systemui.coroutines.collectValues
74 import com.android.systemui.display.data.repository.displayStateRepository
75 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
76 import com.android.systemui.kosmos.testScope
77 import com.android.systemui.res.R
78 import com.android.systemui.testKosmos
79 import com.android.systemui.util.mockito.withArgCaptor
80 import com.google.android.msdl.data.model.MSDLToken
81 import com.google.common.truth.Truth.assertThat
82 import kotlinx.coroutines.flow.first
83 import kotlinx.coroutines.launch
84 import kotlinx.coroutines.test.TestScope
85 import kotlinx.coroutines.test.runCurrent
86 import kotlinx.coroutines.test.runTest
87 import org.junit.Before
88 import org.junit.Rule
89 import org.junit.Test
90 import org.junit.runner.RunWith
91 import org.mockito.ArgumentMatchers.anyInt
92 import org.mockito.ArgumentMatchers.eq
93 import org.mockito.Mock
94 import org.mockito.Mockito
95 import org.mockito.junit.MockitoJUnit
96 import org.mockito.kotlin.any
97 import org.mockito.kotlin.whenever
98 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
99 import platform.test.runner.parameterized.Parameters
100 
101 private const val USER_ID = 4
102 private const val WORK_USER_ID = 100
103 private const val REQUEST_ID = 4L
104 private const val CHALLENGE = 2L
105 private const val DELAY = 1000L
106 private const val OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO = "should.use.activiy.logo"
107 private const val OP_PACKAGE_NAME_WITH_APP_LOGO = "biometric.testapp"
108 private const val OP_PACKAGE_NAME_NO_LOGO_INFO = "biometric.testapp.nologoinfo"
109 private const val OP_PACKAGE_NAME_CAN_NOT_BE_FOUND = "can.not.be.found"
110 
111 @SmallTest
112 @RunWith(ParameterizedAndroidJunit4::class)
113 internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCase() {
114 
115     @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
116 
117     @Mock private lateinit var authController: AuthController
118     @Mock private lateinit var applicationInfoWithIconAndDescription: ApplicationInfo
119     @Mock private lateinit var applicationInfoNoIconOrDescription: ApplicationInfo
120     @Mock private lateinit var activityInfo: ActivityInfo
121     @Mock private lateinit var runningTaskInfo: RunningTaskInfo
122 
123     private val defaultLogoIconFromAppInfo = context.getDrawable(R.drawable.ic_android)
124     private val defaultLogoIconFromActivityInfo = context.getDrawable(R.drawable.ic_add)
125     private val defaultLogoIconWithBadge = context.getDrawable(R.drawable.ic_alarm)
126     private val logoResFromApp = R.drawable.ic_cake
127     private val logoDrawableFromAppRes = context.getDrawable(logoResFromApp)
128     private val logoBitmapFromApp = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565)
129     private val defaultLogoDescriptionFromAppInfo = "Test Android App"
130     private val defaultLogoDescriptionFromActivityInfo = "Test Coke App"
131     private val defaultLogoDescriptionWithBadge = "Work app"
132     private val logoDescriptionFromApp = "Test Cake App"
133 
134     private val authInteractionProperties = AuthInteractionProperties()
135 
136     /** Prompt panel size padding */
137     private val smallHorizontalGuidelinePadding =
138         context.resources.getDimensionPixelSize(
139             R.dimen.biometric_prompt_land_small_horizontal_guideline_padding
140         )
141     private val udfpsHorizontalGuidelinePadding =
142         context.resources.getDimensionPixelSize(
143             R.dimen.biometric_prompt_two_pane_udfps_horizontal_guideline_padding
144         )
145     private val udfpsHorizontalShorterGuidelinePadding =
146         context.resources.getDimensionPixelSize(
147             R.dimen.biometric_prompt_two_pane_udfps_shorter_horizontal_guideline_padding
148         )
149     private val mediumTopGuidelinePadding =
150         context.resources.getDimensionPixelSize(
151             R.dimen.biometric_prompt_one_pane_medium_top_guideline_padding
152         )
153     private val mediumHorizontalGuidelinePadding =
154         context.resources.getDimensionPixelSize(
155             R.dimen.biometric_prompt_two_pane_medium_horizontal_guideline_padding
156         )
157     private val mockFaceIconSize = 200
158     private val mockFingerprintIconWidth = 300
159     private val mockFingerprintIconHeight = 300
160 
161     private val faceIconAuthingDescription =
162         R.string.biometric_dialog_face_icon_description_authenticating
163     private val faceIconAuthedDescription =
164         R.string.biometric_dialog_face_icon_description_authenticated
165     private val faceIconConfirmedDescription =
166         R.string.biometric_dialog_face_icon_description_confirmed
167     private val faceIconIdleDescription = R.string.biometric_dialog_face_icon_description_idle
168     private val sfpsFindSensorDescription =
169         R.string.security_settings_sfps_enroll_find_sensor_message
170     private val udfpsIconDescription = R.string.accessibility_fingerprint_label
171     private val faceFailedDescription = R.string.keyguard_face_failed
172     private val bpTryAgainDescription = R.string.biometric_dialog_try_again
173     private val bpConfirmDescription = R.string.biometric_dialog_confirm
174     private val fingerprintIconAuthenticatedDescription =
175         R.string.fingerprint_dialog_authenticated_confirmation
176 
177     /** Mock [UdfpsOverlayParams] for a test. */
178     private fun mockUdfpsOverlayParams(isLandscape: Boolean = false): UdfpsOverlayParams =
179         UdfpsOverlayParams(
180             sensorBounds = Rect(400, 1600, 600, 1800),
181             overlayBounds = Rect(0, 1200, 1000, 2400),
182             naturalDisplayWidth = 1000,
183             naturalDisplayHeight = 3000,
184             scaleFactor = 1f,
185             rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
186         )
187 
188     private lateinit var promptContentView: PromptContentView
189     private lateinit var promptContentViewWithMoreOptionsButton:
190         PromptContentViewWithMoreOptionsButton
191 
192     private val kosmos = testKosmos()
193 
194     @Before
195     fun setup() {
196         setupLogo()
197         overrideResource(R.dimen.biometric_dialog_fingerprint_icon_width, mockFingerprintIconWidth)
198         overrideResource(
199             R.dimen.biometric_dialog_fingerprint_icon_height,
200             mockFingerprintIconHeight,
201         )
202         overrideResource(R.dimen.biometric_dialog_face_icon_size, mockFaceIconSize)
203 
204         kosmos.applicationContext = context
205         whenever(kosmos.accessibilityManager.getRecommendedTimeoutMillis(anyInt(), anyInt()))
206             .thenReturn(BiometricPrompt.HIDE_DIALOG_DELAY)
207 
208         if (testCase.fingerprint?.isAnyUdfpsType == true) {
209             kosmos.authController = authController
210         }
211 
212         testCase.fingerprint?.let {
213             kosmos.fakeFingerprintPropertyRepository.setProperties(
214                 it.sensorId,
215                 it.sensorStrength.toSensorStrength(),
216                 it.sensorType.toSensorType(),
217                 it.allLocations.associateBy { sensorLocationInternal ->
218                     sensorLocationInternal.displayId
219                 },
220             )
221         }
222 
223         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
224         testCase.fingerprint?.isAnySidefpsType.let {
225             kosmos.displayStateRepository.setIsInRearDisplayMode(testCase.isInRearDisplayMode)
226         }
227 
228         promptContentView =
229             PromptVerticalListContentView.Builder()
230                 .addListItem(PromptContentItemBulletedText("content item 1"))
231                 .addListItem(PromptContentItemBulletedText("content item 2"), 1)
232                 .build()
233 
234         promptContentViewWithMoreOptionsButton =
235             PromptContentViewWithMoreOptionsButton.Builder()
236                 .setDescription("test")
237                 .setMoreOptionsButtonListener(kosmos.fakeExecutor) { _, _ -> }
238                 .build()
239     }
240 
241     private fun setupLogo() {
242         // Set up app customized logo
243         overrideResource(logoResFromApp, logoDrawableFromAppRes)
244 
245         // Set up when activity info should be used
246         overrideResource(
247             R.array.config_useActivityLogoForBiometricPrompt,
248             arrayOf(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO),
249         )
250         whenever(kosmos.packageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo)
251         whenever(kosmos.iconProvider.getIcon(activityInfo))
252             .thenReturn(defaultLogoIconFromActivityInfo)
253         whenever(activityInfo.loadLabel(kosmos.packageManager))
254             .thenReturn(defaultLogoDescriptionFromActivityInfo)
255 
256         // Set up when application info should be used for default logo
257         whenever(
258                 kosmos.packageManager.getApplicationInfo(
259                     eq(OP_PACKAGE_NAME_WITH_APP_LOGO),
260                     anyInt(),
261                 )
262             )
263             .thenReturn(applicationInfoWithIconAndDescription)
264         whenever(
265                 kosmos.packageManager.getApplicationInfo(
266                     eq(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO),
267                     anyInt(),
268                 )
269             )
270             .thenReturn(applicationInfoWithIconAndDescription)
271         whenever(kosmos.packageManager.getApplicationIcon(applicationInfoWithIconAndDescription))
272             .thenReturn(defaultLogoIconFromAppInfo)
273         whenever(kosmos.packageManager.getApplicationLabel(applicationInfoWithIconAndDescription))
274             .thenReturn(defaultLogoDescriptionFromAppInfo)
275 
276         // Set up when package name cannot but found
277         whenever(
278                 kosmos.packageManager.getApplicationInfo(
279                     eq(OP_PACKAGE_NAME_CAN_NOT_BE_FOUND),
280                     anyInt(),
281                 )
282             )
283             .thenThrow(NameNotFoundException())
284 
285         // Set up when no default logo from application info
286         whenever(
287                 kosmos.packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME_NO_LOGO_INFO), anyInt())
288             )
289             .thenReturn(applicationInfoNoIconOrDescription)
290         whenever(kosmos.packageManager.getApplicationIcon(applicationInfoNoIconOrDescription))
291             .thenReturn(null)
292         whenever(kosmos.packageManager.getApplicationLabel(applicationInfoNoIconOrDescription))
293             .thenReturn("")
294 
295         // Set up work badge
296         whenever(kosmos.packageManager.getUserBadgedIcon(any(), eq(UserHandle.of(USER_ID)))).then {
297             it.getArgument(0)
298         }
299         whenever(kosmos.packageManager.getUserBadgedLabel(any(), eq(UserHandle.of(USER_ID)))).then {
300             it.getArgument(0)
301         }
302         whenever(kosmos.packageManager.getUserBadgedIcon(any(), eq(UserHandle.of(WORK_USER_ID))))
303             .then { defaultLogoIconWithBadge }
304         whenever(kosmos.packageManager.getUserBadgedLabel(any(), eq(UserHandle.of(WORK_USER_ID))))
305             .then { defaultLogoDescriptionWithBadge }
306         context.setMockPackageManager(kosmos.packageManager)
307     }
308 
309     @Test
310     fun start_idle_and_show_authenticating() =
311         runGenericTest(doNotStart = true) {
312             var expectedPromptSize =
313                 if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM
314             val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
315             val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
316             val modalities by collectLastValue(kosmos.promptViewModel.modalities)
317             val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
318             val shouldAnimateIconView by
319                 collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
320             val iconContentDescriptionId by
321                 collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
322             val message by collectLastValue(kosmos.promptViewModel.message)
323             val size by collectLastValue(kosmos.promptViewModel.size)
324 
325             assertThat(authenticating).isFalse()
326             assertThat(authenticated?.isNotAuthenticated).isTrue()
327             with(modalities ?: throw Exception("missing modalities")) {
328                 assertThat(hasFace).isEqualTo(testCase.face != null)
329                 assertThat(hasFingerprint).isEqualTo(testCase.fingerprint != null)
330             }
331 
332             assertThat(message).isEqualTo(PromptMessage.Empty)
333             assertThat(size).isEqualTo(expectedPromptSize)
334 
335             val forceExplicitFlow =
336                 testCase.isCoex && testCase.confirmationRequested ||
337                     testCase.authenticatedByFingerprint
338 
339             if ((testCase.isCoex && !forceExplicitFlow) || testCase.isFaceOnly) {
340                 // Face-only or implicit co-ex auth
341                 assertThat(iconAsset).isEqualTo(R.raw.face_dialog_idle_static)
342                 assertThat(shouldAnimateIconView).isEqualTo(false)
343             }
344 
345             if (forceExplicitFlow) {
346                 expectedPromptSize = PromptSize.MEDIUM
347                 kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
348             }
349 
350             val startMessage = "here we go"
351             kosmos.promptViewModel.showAuthenticating(startMessage, isRetry = false)
352             verifyIconSize(forceExplicitFlow)
353 
354             // Icon asset assertions
355             if ((testCase.isCoex && !forceExplicitFlow) || testCase.isFaceOnly) {
356                 // Face-only or implicit co-ex auth
357                 assertThat(iconAsset).isEqualTo(R.raw.face_dialog_authenticating)
358                 assertThat(iconContentDescriptionId).isEqualTo(faceIconAuthingDescription)
359                 assertThat(shouldAnimateIconView).isEqualTo(true)
360             } else if ((testCase.isCoex && forceExplicitFlow) || testCase.isFingerprintOnly) {
361                 // Fingerprint-only or explicit co-ex auth
362                 if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
363                     assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintAuthenticating())
364                     assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
365                     assertThat(shouldAnimateIconView).isEqualTo(true)
366                 } else {
367                     assertThat(iconAsset)
368                         .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie)
369                     assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
370                     assertThat(shouldAnimateIconView).isEqualTo(false)
371                 }
372             }
373 
374             assertThat(message).isEqualTo(PromptMessage.Help(startMessage))
375             assertThat(authenticating).isTrue()
376             assertThat(authenticated?.isNotAuthenticated).isTrue()
377             assertThat(size).isEqualTo(expectedPromptSize)
378             assertButtonsVisible(negative = expectedPromptSize != PromptSize.SMALL)
379         }
380 
381     @Test
382     fun start_authenticating_show_and_clear_error() = runGenericTest {
383         val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
384         val iconContentDescriptionId by
385             collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
386         val shouldAnimateIconView by
387             collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
388         val message by collectLastValue(kosmos.promptViewModel.message)
389 
390         var forceExplicitFlow =
391             testCase.isCoex && testCase.confirmationRequested || testCase.authenticatedByFingerprint
392         if (forceExplicitFlow) {
393             kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
394         }
395         verifyIconSize(forceExplicitFlow)
396 
397         val errorJob = launch {
398             kosmos.promptViewModel.showTemporaryError(
399                 "so sad",
400                 messageAfterError = "",
401                 authenticateAfterError = testCase.isFingerprintOnly || testCase.isCoex,
402             )
403             forceExplicitFlow = true
404             // Usually done by binder
405             kosmos.promptViewModel.iconViewModel.setPreviousIconWasError(true)
406         }
407 
408         assertThat(message?.isError).isEqualTo(true)
409         assertThat(message?.message).isEqualTo("so sad")
410 
411         // Icon asset assertions
412         if (testCase.isFaceOnly) {
413             // Face-only auth
414             assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_error)
415             assertThat(iconContentDescriptionId).isEqualTo(faceFailedDescription)
416             assertThat(shouldAnimateIconView).isEqualTo(true)
417 
418             // Clear error, go to idle
419             errorJob.join()
420 
421             assertThat(iconAsset).isEqualTo(R.raw.face_dialog_error_to_idle)
422             assertThat(iconContentDescriptionId).isEqualTo(faceIconIdleDescription)
423             assertThat(shouldAnimateIconView).isEqualTo(true)
424         } else if ((testCase.isCoex && forceExplicitFlow) || testCase.isFingerprintOnly) {
425             // Fingerprint-only or explicit co-ex auth
426             if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
427                 assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToError())
428                 assertThat(iconContentDescriptionId).isEqualTo(bpTryAgainDescription)
429                 assertThat(shouldAnimateIconView).isEqualTo(true)
430             } else {
431                 assertThat(iconAsset)
432                     .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie)
433                 assertThat(iconContentDescriptionId).isEqualTo(bpTryAgainDescription)
434                 assertThat(shouldAnimateIconView).isEqualTo(true)
435             }
436 
437             // Clear error, restart authenticating
438             errorJob.join()
439 
440             if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
441                 assertThat(iconAsset).isEqualTo(getSfpsAsset_errorToFingerprint())
442                 assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
443                 assertThat(shouldAnimateIconView).isEqualTo(true)
444             } else {
445                 assertThat(iconAsset)
446                     .isEqualTo(R.raw.fingerprint_dialogue_error_to_fingerprint_lottie)
447                 assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
448                 assertThat(shouldAnimateIconView).isEqualTo(true)
449             }
450         }
451     }
452 
453     @Test
454     fun shows_error_to_unlock_or_success() {
455         // Face-only auth does not use error -> unlock or error -> success assets
456         if (testCase.isFingerprintOnly || testCase.isCoex) {
457             runGenericTest {
458                 // Distinct asset for error -> success only applicable for fingerprint-only /
459                 // explicit co-ex auth
460                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
461                 val iconContentDescriptionId by
462                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
463                 val shouldAnimateIconView by
464                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
465 
466                 var forceExplicitFlow =
467                     testCase.isCoex && testCase.confirmationRequested ||
468                         testCase.authenticatedByFingerprint
469                 if (forceExplicitFlow) {
470                     kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
471                 }
472                 verifyIconSize(forceExplicitFlow)
473 
474                 kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
475                 kosmos.promptViewModel.iconViewModel.setPreviousIconWasError(true)
476 
477                 kosmos.promptViewModel.showAuthenticated(
478                     modality = testCase.authenticatedModality,
479                     dismissAfterDelay = DELAY,
480                 )
481 
482                 // SFPS test cases
483                 if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
484                     // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint
485                     if (testCase.authenticatedByFingerprint) {
486                         assertThat(iconAsset).isEqualTo(R.raw.biometricprompt_sfps_error_to_success)
487                         assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
488                         assertThat(shouldAnimateIconView).isEqualTo(true)
489                     } else { // Covers co-ex, authenticated by face
490                         assertThat(iconAsset).isEqualTo(R.raw.biometricprompt_sfps_error_to_unlock)
491                         assertThat(iconContentDescriptionId)
492                             .isEqualTo(fingerprintIconAuthenticatedDescription)
493                         assertThat(shouldAnimateIconView).isEqualTo(true)
494 
495                         // Confirm authentication
496                         kosmos.promptViewModel.confirmAuthenticated()
497 
498                         assertThat(iconAsset)
499                             .isEqualTo(R.raw.biometricprompt_sfps_unlock_to_success)
500                         assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
501                         assertThat(shouldAnimateIconView).isEqualTo(true)
502                     }
503                 } else { // Non-SFPS (UDFPS / rear-FPS) test cases
504                     // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint
505                     if (testCase.authenticatedByFingerprint) {
506                         assertThat(iconAsset)
507                             .isEqualTo(R.raw.fingerprint_dialogue_error_to_success_lottie)
508                         assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
509                         assertThat(shouldAnimateIconView).isEqualTo(true)
510                     } else { //  co-ex, authenticated by face
511                         assertThat(iconAsset)
512                             .isEqualTo(R.raw.fingerprint_dialogue_error_to_unlock_lottie)
513                         assertThat(iconContentDescriptionId).isEqualTo(bpConfirmDescription)
514                         assertThat(shouldAnimateIconView).isEqualTo(true)
515 
516                         // Confirm authentication
517                         kosmos.promptViewModel.confirmAuthenticated()
518 
519                         assertThat(iconAsset)
520                             .isEqualTo(
521                                 R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
522                             )
523                         assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
524                         assertThat(shouldAnimateIconView).isEqualTo(true)
525                     }
526                 }
527             }
528         }
529     }
530 
531     @Test
532     fun shows_authenticated_no_errors_no_confirmation_required() {
533         if (!testCase.confirmationRequested) {
534             runGenericTest {
535                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
536                 val iconContentDescriptionId by
537                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
538                 val shouldAnimateIconView by
539                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
540                 val message by collectLastValue(kosmos.promptViewModel.message)
541                 verifyIconSize()
542 
543                 kosmos.promptViewModel.showAuthenticated(
544                     modality = testCase.authenticatedModality,
545                     dismissAfterDelay = DELAY,
546                     "TEST",
547                 )
548 
549                 if (testCase.isFingerprintOnly) {
550                     // Fingerprint icon asset assertions
551                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
552                         assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToSuccess())
553                         assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
554                         assertThat(shouldAnimateIconView).isEqualTo(true)
555                     } else {
556                         assertThat(iconAsset)
557                             .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie)
558                         assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
559                         assertThat(shouldAnimateIconView).isEqualTo(true)
560                     }
561                 } else if (testCase.isFaceOnly || testCase.isCoex) {
562                     // Face icon asset assertions
563                     // If co-ex, use implicit flow (explicit flow always requires confirmation)
564                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
565                     assertThat(iconContentDescriptionId).isEqualTo(faceIconAuthedDescription)
566                     assertThat(shouldAnimateIconView).isEqualTo(true)
567                     assertThat(message).isEqualTo(PromptMessage.Empty)
568                 }
569             }
570         }
571     }
572 
573     @Test
574     fun shows_pending_confirmation() {
575         if (testCase.authenticatedByFace && testCase.confirmationRequested) {
576             runGenericTest {
577                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
578                 val iconContentDescriptionId by
579                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
580                 val shouldAnimateIconView by
581                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
582 
583                 val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
584                 verifyIconSize(forceExplicitFlow = forceExplicitFlow)
585 
586                 kosmos.promptViewModel.showAuthenticated(
587                     modality = testCase.authenticatedModality,
588                     dismissAfterDelay = DELAY,
589                 )
590 
591                 if (testCase.isFaceOnly) {
592                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark)
593                     assertThat(iconContentDescriptionId).isEqualTo(faceIconAuthedDescription)
594                     assertThat(shouldAnimateIconView).isEqualTo(true)
595                 } else if (testCase.isCoex) { // explicit flow, confirmation requested
596                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
597                         assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToUnlock())
598                         assertThat(iconContentDescriptionId)
599                             .isEqualTo(fingerprintIconAuthenticatedDescription)
600                         assertThat(shouldAnimateIconView).isEqualTo(true)
601                     } else {
602                         assertThat(iconAsset)
603                             .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie)
604                         assertThat(iconContentDescriptionId).isEqualTo(bpConfirmDescription)
605                         assertThat(shouldAnimateIconView).isEqualTo(true)
606                     }
607                 }
608             }
609         }
610     }
611 
612     @Test
613     fun shows_authenticated_explicitly_confirmed() {
614         if (testCase.authenticatedByFace && testCase.confirmationRequested) {
615             runGenericTest {
616                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
617                 val iconContentDescriptionId by
618                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
619                 val shouldAnimateIconView by
620                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
621                 val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
622                 verifyIconSize(forceExplicitFlow = forceExplicitFlow)
623 
624                 kosmos.promptViewModel.showAuthenticated(
625                     modality = testCase.authenticatedModality,
626                     dismissAfterDelay = DELAY,
627                 )
628 
629                 kosmos.promptViewModel.confirmAuthenticated()
630 
631                 if (testCase.isFaceOnly) {
632                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
633                     assertThat(iconContentDescriptionId).isEqualTo(faceIconConfirmedDescription)
634                     assertThat(shouldAnimateIconView).isEqualTo(true)
635                 }
636 
637                 // explicit flow because confirmation requested
638                 if (testCase.isCoex) {
639                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
640                         assertThat(iconAsset)
641                             .isEqualTo(R.raw.biometricprompt_sfps_unlock_to_success)
642                         assertThat(shouldAnimateIconView).isEqualTo(true)
643                     } else {
644                         assertThat(iconAsset)
645                             .isEqualTo(
646                                 R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
647                             )
648                         assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
649                         assertThat(shouldAnimateIconView).isEqualTo(true)
650                     }
651                 }
652             }
653         }
654     }
655 
656     private fun getSfpsAsset_fingerprintAuthenticating(): Int =
657         if (testCase.isInRearDisplayMode) {
658             R.raw.biometricprompt_sfps_rear_display_fingerprint_authenticating
659         } else {
660             R.raw.biometricprompt_sfps_fingerprint_authenticating
661         }
662 
663     private fun getSfpsAsset_fingerprintToError(): Int =
664         if (testCase.isInRearDisplayMode) {
665             R.raw.biometricprompt_sfps_rear_display_fingerprint_to_error
666         } else {
667             R.raw.biometricprompt_sfps_fingerprint_to_error
668         }
669 
670     private fun getSfpsAsset_fingerprintToUnlock(): Int =
671         if (testCase.isInRearDisplayMode) {
672             R.raw.biometricprompt_sfps_rear_display_fingerprint_to_unlock
673         } else {
674             R.raw.biometricprompt_sfps_fingerprint_to_unlock
675         }
676 
677     private fun getSfpsAsset_errorToFingerprint(): Int =
678         if (testCase.isInRearDisplayMode) {
679             R.raw.biometricprompt_sfps_rear_display_error_to_fingerprint
680         } else {
681             R.raw.biometricprompt_sfps_error_to_fingerprint
682         }
683 
684     private fun getSfpsAsset_fingerprintToSuccess(): Int =
685         if (testCase.isInRearDisplayMode) {
686             R.raw.biometricprompt_sfps_rear_display_fingerprint_to_success
687         } else {
688             R.raw.biometricprompt_sfps_fingerprint_to_success
689         }
690 
691     @Test
692     fun shows_authenticated_with_no_errors() = runGenericTest {
693         // this case can't happen until fingerprint is started
694         // trigger it now since no error has occurred in this test
695         val forceError = testCase.isCoex && testCase.authenticatedByFingerprint
696 
697         if (forceError) {
698             assertThat(kosmos.promptViewModel.fingerprintStartMode.first())
699                 .isEqualTo(FingerprintStartMode.Pending)
700             kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
701         }
702 
703         showAuthenticated(
704             testCase.authenticatedModality,
705             testCase.expectConfirmation(atLeastOneFailure = forceError),
706         )
707     }
708 
709     // Verifies expected icon sizes for all modalities
710     private fun TestScope.verifyIconSize(forceExplicitFlow: Boolean = false) {
711         val iconSize by collectLastValue(kosmos.promptViewModel.iconSize)
712         if ((testCase.isCoex && !forceExplicitFlow) || testCase.isFaceOnly) {
713             // Face-only or implicit co-ex auth
714             assertThat(iconSize).isEqualTo(Pair(mockFaceIconSize, mockFaceIconSize))
715         } else if ((testCase.isCoex && forceExplicitFlow) || testCase.isFingerprintOnly) {
716             // Fingerprint-only or explicit co-ex auth
717             if (testCase.fingerprint?.isAnyUdfpsType == true) {
718                 val udfpsOverlayParams by
719                     collectLastValue(kosmos.promptViewModel.udfpsOverlayParams)
720                 val expectedUdfpsOverlayParams = mockUdfpsOverlayParams()
721                 assertThat(udfpsOverlayParams).isEqualTo(expectedUdfpsOverlayParams)
722 
723                 assertThat(iconSize)
724                     .isEqualTo(
725                         Pair(
726                             expectedUdfpsOverlayParams.sensorBounds.width(),
727                             expectedUdfpsOverlayParams.sensorBounds.height(),
728                         )
729                     )
730             } else {
731                 assertThat(iconSize)
732                     .isEqualTo(Pair(mockFingerprintIconWidth, mockFingerprintIconHeight))
733             }
734         }
735     }
736 
737     @Test
738     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
739     fun set_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() =
740         runGenericTest {
741             val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
742 
743             kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
744 
745             val hapticsPreConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
746             if (expectConfirmation) {
747                 assertThat(hapticsPreConfirm).isEqualTo(PromptViewModel.HapticsToPlay.None)
748             } else {
749                 val confirmHaptics =
750                     hapticsPreConfirm as PromptViewModel.HapticsToPlay.HapticConstant
751                 assertThat(confirmHaptics.constant)
752                     .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
753                 assertThat(confirmHaptics.flag).isNull()
754             }
755 
756             if (expectConfirmation) {
757                 kosmos.promptViewModel.confirmAuthenticated()
758             }
759 
760             val hapticsPostConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
761             val confirmedHaptics =
762                 hapticsPostConfirm as PromptViewModel.HapticsToPlay.HapticConstant
763             assertThat(confirmedHaptics.constant)
764                 .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
765             assertThat(confirmedHaptics.flag).isNull()
766         }
767 
768     @Test
769     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
770     fun set_msdl_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() =
771         runGenericTest {
772             val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
773 
774             kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
775 
776             val hapticsPreConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
777 
778             if (expectConfirmation) {
779                 assertThat(hapticsPreConfirm).isEqualTo(PromptViewModel.HapticsToPlay.None)
780             } else {
781                 val confirmHaptics = hapticsPreConfirm as PromptViewModel.HapticsToPlay.MSDL
782                 assertThat(confirmHaptics.token).isEqualTo(MSDLToken.UNLOCK)
783                 assertThat(confirmHaptics.properties).isEqualTo(authInteractionProperties)
784             }
785 
786             if (expectConfirmation) {
787                 kosmos.promptViewModel.confirmAuthenticated()
788             }
789 
790             val hapticsPostConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
791             val confirmedHaptics = hapticsPostConfirm as PromptViewModel.HapticsToPlay.MSDL
792             assertThat(confirmedHaptics.token).isEqualTo(MSDLToken.UNLOCK)
793             assertThat(confirmedHaptics.properties).isEqualTo(authInteractionProperties)
794         }
795 
796     @Test
797     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
798     fun playSuccessHaptic_SetsConfirmConstant() = runGenericTest {
799         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
800         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
801 
802         if (expectConfirmation) {
803             kosmos.promptViewModel.confirmAuthenticated()
804         }
805 
806         val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
807         val currentHaptics = haptics as PromptViewModel.HapticsToPlay.HapticConstant
808         assertThat(currentHaptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
809         assertThat(currentHaptics.flag).isNull()
810     }
811 
812     @Test
813     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
814     fun playSuccessHaptic_SetsUnlockMSDLFeedback() = runGenericTest {
815         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
816         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
817 
818         if (expectConfirmation) {
819             kosmos.promptViewModel.confirmAuthenticated()
820         }
821 
822         val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
823         val currentHaptics = haptics as PromptViewModel.HapticsToPlay.MSDL
824         assertThat(currentHaptics.token).isEqualTo(MSDLToken.UNLOCK)
825         assertThat(currentHaptics.properties).isEqualTo(authInteractionProperties)
826     }
827 
828     @Test
829     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
830     fun playErrorHaptic_SetsRejectConstant() = runGenericTest {
831         kosmos.promptViewModel.showTemporaryError("test", "messageAfterError", false)
832 
833         val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
834         val currentHaptics = haptics as PromptViewModel.HapticsToPlay.HapticConstant
835         assertThat(currentHaptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
836         assertThat(currentHaptics.flag).isNull()
837     }
838 
839     @Test
840     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
841     fun playErrorHaptic_SetsFailureMSDLFeedback() = runGenericTest {
842         kosmos.promptViewModel.showTemporaryError("test", "messageAfterError", false)
843 
844         val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
845         val currentHaptics = haptics as PromptViewModel.HapticsToPlay.MSDL
846         assertThat(currentHaptics.token).isEqualTo(MSDLToken.FAILURE)
847         assertThat(currentHaptics.properties).isEqualTo(authInteractionProperties)
848     }
849 
850     // biometricprompt_sfps_fingerprint_authenticating reused across rotations
851     // Other SFPS assets change across rotations, testing authenticated asset
852     @Test
853     fun sfpsAuthenticatedIconUpdates_onRotation() {
854         if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
855             runGenericTest {
856                 val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
857 
858                 kosmos.promptViewModel.showAuthenticated(
859                     modality = testCase.authenticatedModality,
860                     dismissAfterDelay = DELAY,
861                 )
862 
863                 kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
864                 val iconRotation0 = currentIcon
865 
866                 kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
867                 val iconRotation90 = currentIcon
868 
869                 kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
870                 val iconRotation180 = currentIcon
871 
872                 kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
873                 val iconRotation270 = currentIcon
874 
875                 assertThat(iconRotation0).isNotEqualTo(iconRotation90)
876                 assertThat(iconRotation0).isNotEqualTo(iconRotation180)
877                 assertThat(iconRotation0).isNotEqualTo(iconRotation270)
878                 assertThat(iconRotation90).isNotEqualTo(iconRotation180)
879                 assertThat(iconRotation90).isNotEqualTo(iconRotation270)
880                 assertThat(iconRotation180).isNotEqualTo(iconRotation270)
881             }
882         }
883     }
884 
885     @Test
886     fun sfpsIconUpdates_onRearDisplayMode() {
887         if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
888             runGenericTest {
889                 val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
890 
891                 kosmos.displayStateRepository.setIsInRearDisplayMode(false)
892                 val iconNotRearDisplayMode = currentIcon
893 
894                 kosmos.displayStateRepository.setIsInRearDisplayMode(true)
895                 val iconRearDisplayMode = currentIcon
896 
897                 assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode)
898             }
899         }
900     }
901 
902     private suspend fun TestScope.showAuthenticated(
903         authenticatedModality: BiometricModality,
904         expectConfirmation: Boolean,
905     ) {
906         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
907         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
908         val fpStartMode by collectLastValue(kosmos.promptViewModel.fingerprintStartMode)
909         val size by collectLastValue(kosmos.promptViewModel.size)
910 
911         val authWithSmallPrompt =
912             testCase.shouldStartAsImplicitFlow &&
913                 (fpStartMode == FingerprintStartMode.Pending || testCase.isFaceOnly)
914         assertThat(authenticating).isTrue()
915         assertThat(authenticated?.isNotAuthenticated).isTrue()
916         assertThat(size).isEqualTo(if (authWithSmallPrompt) PromptSize.SMALL else PromptSize.MEDIUM)
917         assertButtonsVisible(negative = !authWithSmallPrompt)
918 
919         kosmos.promptViewModel.showAuthenticated(authenticatedModality, DELAY)
920 
921         assertThat(authenticated?.isAuthenticated).isTrue()
922         assertThat(authenticated?.delay).isEqualTo(DELAY)
923         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
924         assertThat(size)
925             .isEqualTo(
926                 if (authenticatedModality == BiometricModality.Fingerprint || expectConfirmation) {
927                     PromptSize.MEDIUM
928                 } else {
929                     PromptSize.SMALL
930                 }
931             )
932 
933         assertButtonsVisible(cancel = expectConfirmation, confirm = expectConfirmation)
934     }
935 
936     @Test
937     fun shows_temporary_errors() = runGenericTest {
938         val checkAtEnd = suspend { assertButtonsVisible(negative = true) }
939 
940         showTemporaryErrors(restart = false) { checkAtEnd() }
941         showTemporaryErrors(restart = false, helpAfterError = "foo") { checkAtEnd() }
942         showTemporaryErrors(restart = true) { checkAtEnd() }
943     }
944 
945     @Test
946     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
947     fun set_haptic_on_errors() = runGenericTest {
948         kosmos.promptViewModel.showTemporaryError(
949             "so sad",
950             messageAfterError = "",
951             authenticateAfterError = false,
952             hapticFeedback = true,
953         )
954 
955         val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
956         val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.HapticConstant
957         assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
958         assertThat(haptics.flag).isNull()
959     }
960 
961     @Test
962     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
963     fun set_msdl_haptic_on_errors() = runGenericTest {
964         kosmos.promptViewModel.showTemporaryError(
965             "so sad",
966             messageAfterError = "",
967             authenticateAfterError = false,
968             hapticFeedback = true,
969         )
970 
971         val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
972         val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.MSDL
973         assertThat(haptics.token).isEqualTo(MSDLToken.FAILURE)
974         assertThat(haptics.properties).isEqualTo(authInteractionProperties)
975     }
976 
977     @Test
978     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
979     fun plays_haptic_on_errors_unless_skipped() = runGenericTest {
980         kosmos.promptViewModel.showTemporaryError(
981             "still sad",
982             messageAfterError = "",
983             authenticateAfterError = false,
984             hapticFeedback = false,
985         )
986 
987         val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
988         assertThat(hapticsToPlay).isEqualTo(PromptViewModel.HapticsToPlay.None)
989     }
990 
991     @Test
992     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
993     fun plays_msdl_haptic_on_errors_unless_skipped() = runGenericTest {
994         kosmos.promptViewModel.showTemporaryError(
995             "still sad",
996             messageAfterError = "",
997             authenticateAfterError = false,
998             hapticFeedback = false,
999         )
1000 
1001         val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
1002         assertThat(hapticsToPlay).isEqualTo(PromptViewModel.HapticsToPlay.None)
1003     }
1004 
1005     @Test
1006     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
1007     fun plays_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest {
1008         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
1009         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1010 
1011         kosmos.promptViewModel.showTemporaryError(
1012             "still sad",
1013             messageAfterError = "",
1014             authenticateAfterError = false,
1015             hapticFeedback = true,
1016         )
1017 
1018         val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
1019         val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.HapticConstant
1020         if (expectConfirmation) {
1021             assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
1022             assertThat(haptics.flag).isNull()
1023         } else {
1024             assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
1025         }
1026     }
1027 
1028     @Test
1029     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
1030     fun plays_msdl_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest {
1031         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
1032         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1033 
1034         kosmos.promptViewModel.showTemporaryError(
1035             "still sad",
1036             messageAfterError = "",
1037             authenticateAfterError = false,
1038             hapticFeedback = true,
1039         )
1040 
1041         val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
1042         val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.MSDL
1043         if (expectConfirmation) {
1044             assertThat(haptics.token).isEqualTo(MSDLToken.FAILURE)
1045         } else {
1046             assertThat(haptics.token).isEqualTo(MSDLToken.UNLOCK)
1047         }
1048         assertThat(haptics.properties).isEqualTo(authInteractionProperties)
1049     }
1050 
1051     private suspend fun TestScope.showTemporaryErrors(
1052         restart: Boolean,
1053         helpAfterError: String = "",
1054         block: suspend TestScope.() -> Unit = {},
1055     ) {
1056         val errorMessage = "oh no!"
1057         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1058         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1059         val message by collectLastValue(kosmos.promptViewModel.message)
1060         val messageVisible by collectLastValue(kosmos.promptViewModel.isIndicatorMessageVisible)
1061         val size by collectLastValue(kosmos.promptViewModel.size)
1062         val canTryAgainNow by collectLastValue(kosmos.promptViewModel.canTryAgainNow)
1063 
1064         val errorJob = launch {
1065             kosmos.promptViewModel.showTemporaryError(
1066                 errorMessage,
1067                 authenticateAfterError = restart,
1068                 messageAfterError = helpAfterError,
1069             )
1070         }
1071 
1072         assertThat(size).isEqualTo(PromptSize.MEDIUM)
1073         assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
1074         assertThat(messageVisible).isTrue()
1075 
1076         // temporary error should disappear after a delay
1077         errorJob.join()
1078         if (helpAfterError.isNotBlank()) {
1079             assertThat(message).isEqualTo(PromptMessage.Help(helpAfterError))
1080             assertThat(messageVisible).isTrue()
1081         } else {
1082             assertThat(message).isEqualTo(PromptMessage.Empty)
1083             assertThat(messageVisible).isFalse()
1084         }
1085 
1086         assertThat(authenticating).isEqualTo(restart)
1087         assertThat(authenticated?.isNotAuthenticated).isTrue()
1088         assertThat(canTryAgainNow).isFalse()
1089 
1090         block()
1091     }
1092 
1093     @Test
1094     fun no_errors_or_temporary_help_after_authenticated() = runGenericTest {
1095         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1096         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1097         val message by collectLastValue(kosmos.promptViewModel.message)
1098         val messageIsShowing by collectLastValue(kosmos.promptViewModel.isIndicatorMessageVisible)
1099         val canTryAgain by collectLastValue(kosmos.promptViewModel.canTryAgainNow)
1100 
1101         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1102 
1103         val verifyNoError = {
1104             assertThat(authenticating).isFalse()
1105             assertThat(authenticated?.isAuthenticated).isTrue()
1106             assertThat(message).isEqualTo(PromptMessage.Empty)
1107             assertThat(canTryAgain).isFalse()
1108         }
1109 
1110         val errorJob = launch {
1111             kosmos.promptViewModel.showTemporaryError(
1112                 "error",
1113                 messageAfterError = "",
1114                 authenticateAfterError = false,
1115             )
1116         }
1117         verifyNoError()
1118         errorJob.join()
1119         verifyNoError()
1120 
1121         val helpJob = launch { kosmos.promptViewModel.showTemporaryHelp("hi") }
1122         verifyNoError()
1123         helpJob.join()
1124         verifyNoError()
1125 
1126         // persistent help is allowed
1127         val stickyHelpMessage = "blah"
1128         kosmos.promptViewModel.showHelp(stickyHelpMessage)
1129         assertThat(authenticating).isFalse()
1130         assertThat(authenticated?.isAuthenticated).isTrue()
1131         assertThat(message).isEqualTo(PromptMessage.Help(stickyHelpMessage))
1132         assertThat(messageIsShowing).isTrue()
1133     }
1134 
1135     @Test
1136     fun suppress_temporary_error() = runGenericTest {
1137         val messages by collectValues(kosmos.promptViewModel.message)
1138 
1139         for (error in listOf("never", "see", "me")) {
1140             launch {
1141                 kosmos.promptViewModel.showTemporaryError(
1142                     error,
1143                     messageAfterError = "or me",
1144                     authenticateAfterError = false,
1145                     suppressIf = { _, _ -> true },
1146                 )
1147             }
1148         }
1149 
1150         testScheduler.advanceUntilIdle()
1151         assertThat(messages).containsExactly(PromptMessage.Empty)
1152     }
1153 
1154     @Test
1155     fun suppress_temporary_error_when_already_showing_when_requested() =
1156         suppress_temporary_error_when_already_showing(suppress = true)
1157 
1158     @Test
1159     fun do_not_suppress_temporary_error_when_already_showing_when_not_requested() =
1160         suppress_temporary_error_when_already_showing(suppress = false)
1161 
1162     private fun suppress_temporary_error_when_already_showing(suppress: Boolean) = runGenericTest {
1163         val errors = listOf("woot", "oh yeah", "nope")
1164         val afterSuffix = "(after)"
1165         val expectedErrorMessage = if (suppress) errors.first() else errors.last()
1166         val messages by collectValues(kosmos.promptViewModel.message)
1167 
1168         for (error in errors) {
1169             launch {
1170                 kosmos.promptViewModel.showTemporaryError(
1171                     error,
1172                     messageAfterError = "$error $afterSuffix",
1173                     authenticateAfterError = false,
1174                     suppressIf = { currentMessage, _ -> suppress && currentMessage.isError },
1175                 )
1176             }
1177         }
1178 
1179         testScheduler.runCurrent()
1180         assertThat(messages)
1181             .containsExactly(PromptMessage.Empty, PromptMessage.Error(expectedErrorMessage))
1182             .inOrder()
1183 
1184         testScheduler.advanceUntilIdle()
1185         assertThat(messages)
1186             .containsExactly(
1187                 PromptMessage.Empty,
1188                 PromptMessage.Error(expectedErrorMessage),
1189                 PromptMessage.Help("$expectedErrorMessage $afterSuffix"),
1190             )
1191             .inOrder()
1192     }
1193 
1194     @Test
1195     fun authenticated_at_most_once_same_modality() = runGenericTest {
1196         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1197         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1198 
1199         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1200 
1201         assertThat(authenticating).isFalse()
1202         assertThat(authenticated?.isAuthenticated).isTrue()
1203 
1204         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1205 
1206         assertThat(authenticating).isFalse()
1207         assertThat(authenticated?.isAuthenticated).isTrue()
1208     }
1209 
1210     @Test
1211     fun authenticating_cannot_restart_after_authenticated() = runGenericTest {
1212         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1213         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1214 
1215         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1216 
1217         assertThat(authenticating).isFalse()
1218         assertThat(authenticated?.isAuthenticated).isTrue()
1219 
1220         kosmos.promptViewModel.showAuthenticating("again!")
1221 
1222         assertThat(authenticating).isFalse()
1223         assertThat(authenticated?.isAuthenticated).isTrue()
1224     }
1225 
1226     @Test
1227     fun confirm_authentication() = runGenericTest {
1228         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
1229 
1230         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1231 
1232         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1233         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1234         val message by collectLastValue(kosmos.promptViewModel.message)
1235         val size by collectLastValue(kosmos.promptViewModel.size)
1236         val canTryAgain by collectLastValue(kosmos.promptViewModel.canTryAgainNow)
1237 
1238         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
1239         if (expectConfirmation) {
1240             assertThat(size).isEqualTo(PromptSize.MEDIUM)
1241             assertButtonsVisible(cancel = true, confirm = true)
1242 
1243             kosmos.promptViewModel.confirmAuthenticated()
1244             assertThat(message).isEqualTo(PromptMessage.Empty)
1245             assertButtonsVisible()
1246         }
1247 
1248         assertThat(authenticating).isFalse()
1249         assertThat(authenticated?.isAuthenticated).isTrue()
1250         assertThat(canTryAgain).isFalse()
1251     }
1252 
1253     @Test
1254     fun second_authentication_acts_as_confirmation() = runGenericTest {
1255         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
1256 
1257         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1258 
1259         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1260         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1261         val message by collectLastValue(kosmos.promptViewModel.message)
1262         val size by collectLastValue(kosmos.promptViewModel.size)
1263         val canTryAgain by collectLastValue(kosmos.promptViewModel.canTryAgainNow)
1264 
1265         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
1266         if (expectConfirmation) {
1267             assertThat(size).isEqualTo(PromptSize.MEDIUM)
1268             assertButtonsVisible(cancel = true, confirm = true)
1269 
1270             if (testCase.modalities.hasSfps) {
1271                 kosmos.promptViewModel.showAuthenticated(BiometricModality.Fingerprint, 0)
1272                 assertThat(message).isEqualTo(PromptMessage.Empty)
1273                 assertButtonsVisible()
1274             }
1275         }
1276 
1277         assertThat(authenticating).isFalse()
1278         assertThat(authenticated?.isAuthenticated).isTrue()
1279         assertThat(canTryAgain).isFalse()
1280     }
1281 
1282     @Test
1283     fun auto_confirm_authentication_when_finger_down() = runGenericTest {
1284         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
1285 
1286         if (testCase.isCoex) {
1287             kosmos.promptViewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_DOWN))
1288         }
1289         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1290 
1291         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1292         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1293         val message by collectLastValue(kosmos.promptViewModel.message)
1294         val size by collectLastValue(kosmos.promptViewModel.size)
1295         val canTryAgain by collectLastValue(kosmos.promptViewModel.canTryAgainNow)
1296 
1297         assertThat(authenticating).isFalse()
1298         assertThat(canTryAgain).isFalse()
1299         assertThat(authenticated?.isAuthenticated).isTrue()
1300 
1301         if (expectConfirmation) {
1302             if (testCase.isFaceOnly) {
1303                 assertThat(size).isEqualTo(PromptSize.MEDIUM)
1304                 assertButtonsVisible(cancel = true, confirm = true)
1305 
1306                 kosmos.promptViewModel.confirmAuthenticated()
1307             } else if (testCase.isCoex) {
1308                 assertThat(authenticated?.isAuthenticatedAndConfirmed).isTrue()
1309             }
1310             assertThat(message).isEqualTo(PromptMessage.Empty)
1311             assertButtonsVisible()
1312         }
1313     }
1314 
1315     @Test
1316     fun cannot_auto_confirm_authentication_when_finger_up() = runGenericTest {
1317         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
1318 
1319         if (testCase.isCoex) {
1320             kosmos.promptViewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_DOWN))
1321             kosmos.promptViewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_UP))
1322         }
1323         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1324 
1325         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1326         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1327         val message by collectLastValue(kosmos.promptViewModel.message)
1328         val size by collectLastValue(kosmos.promptViewModel.size)
1329         val canTryAgain by collectLastValue(kosmos.promptViewModel.canTryAgainNow)
1330 
1331         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
1332         if (expectConfirmation) {
1333             assertThat(size).isEqualTo(PromptSize.MEDIUM)
1334             assertButtonsVisible(cancel = true, confirm = true)
1335 
1336             kosmos.promptViewModel.confirmAuthenticated()
1337             assertThat(message).isEqualTo(PromptMessage.Empty)
1338             assertButtonsVisible()
1339         }
1340 
1341         assertThat(authenticating).isFalse()
1342         assertThat(authenticated?.isAuthenticated).isTrue()
1343         assertThat(canTryAgain).isFalse()
1344     }
1345 
1346     @Test
1347     fun cannot_confirm_unless_authenticated() = runGenericTest {
1348         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1349         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1350 
1351         kosmos.promptViewModel.confirmAuthenticated()
1352         assertThat(authenticating).isTrue()
1353         assertThat(authenticated?.isNotAuthenticated).isTrue()
1354 
1355         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1356 
1357         // reconfirm should be a no-op
1358         kosmos.promptViewModel.confirmAuthenticated()
1359         kosmos.promptViewModel.confirmAuthenticated()
1360 
1361         assertThat(authenticating).isFalse()
1362         assertThat(authenticated?.isNotAuthenticated).isFalse()
1363     }
1364 
1365     @Test
1366     fun shows_help_before_authenticated() = runGenericTest {
1367         val helpMessage = "please help yourself to some cookies"
1368         val message by collectLastValue(kosmos.promptViewModel.message)
1369         val messageVisible by collectLastValue(kosmos.promptViewModel.isIndicatorMessageVisible)
1370         val size by collectLastValue(kosmos.promptViewModel.size)
1371 
1372         kosmos.promptViewModel.showHelp(helpMessage)
1373 
1374         assertThat(size).isEqualTo(PromptSize.MEDIUM)
1375         assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
1376         assertThat(messageVisible).isTrue()
1377 
1378         assertThat(kosmos.promptViewModel.isAuthenticating.first()).isFalse()
1379         assertThat(kosmos.promptViewModel.isAuthenticated.first().isNotAuthenticated).isTrue()
1380     }
1381 
1382     @Test
1383     fun shows_help_after_authenticated() = runGenericTest {
1384         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
1385         val helpMessage = "more cookies please"
1386         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1387         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1388         val message by collectLastValue(kosmos.promptViewModel.message)
1389         val messageVisible by collectLastValue(kosmos.promptViewModel.isIndicatorMessageVisible)
1390         val size by collectLastValue(kosmos.promptViewModel.size)
1391         val confirmationRequired by collectLastValue(kosmos.promptViewModel.isConfirmationRequired)
1392 
1393         if (testCase.isCoex && testCase.authenticatedByFingerprint) {
1394             kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
1395         }
1396         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1397         kosmos.promptViewModel.showHelp(helpMessage)
1398 
1399         assertThat(size).isEqualTo(PromptSize.MEDIUM)
1400 
1401         assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
1402         assertThat(messageVisible).isTrue()
1403         assertThat(authenticating).isFalse()
1404         assertThat(authenticated?.isAuthenticated).isTrue()
1405         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
1406         assertButtonsVisible(cancel = expectConfirmation, confirm = expectConfirmation)
1407     }
1408 
1409     @Test
1410     fun retries_after_failure() = runGenericTest {
1411         val errorMessage = "bad"
1412         val helpMessage = "again?"
1413         val expectTryAgainButton = testCase.isFaceOnly
1414         val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
1415         val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
1416         val message by collectLastValue(kosmos.promptViewModel.message)
1417         val messageVisible by collectLastValue(kosmos.promptViewModel.isIndicatorMessageVisible)
1418         val canTryAgain by collectLastValue(kosmos.promptViewModel.canTryAgainNow)
1419 
1420         kosmos.promptViewModel.showAuthenticating("go")
1421         val errorJob = launch {
1422             kosmos.promptViewModel.showTemporaryError(
1423                 errorMessage,
1424                 messageAfterError = helpMessage,
1425                 authenticateAfterError = false,
1426                 failedModality = testCase.authenticatedModality,
1427             )
1428         }
1429 
1430         assertThat(authenticating).isFalse()
1431         assertThat(authenticated?.isAuthenticated).isFalse()
1432         assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
1433         assertThat(messageVisible).isTrue()
1434         assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
1435         assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
1436 
1437         errorJob.join()
1438 
1439         assertThat(authenticating).isFalse()
1440         assertThat(authenticated?.isAuthenticated).isFalse()
1441         assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
1442         assertThat(messageVisible).isTrue()
1443         assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
1444         assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
1445 
1446         val helpMessage2 = "foo"
1447         kosmos.promptViewModel.showAuthenticating(helpMessage2, isRetry = true)
1448         assertThat(authenticating).isTrue()
1449         assertThat(authenticated?.isAuthenticated).isFalse()
1450         assertThat(message).isEqualTo(PromptMessage.Help(helpMessage2))
1451         assertThat(messageVisible).isTrue()
1452         assertButtonsVisible(negative = true)
1453     }
1454 
1455     @Test
1456     fun switch_to_credential_fallback() = runGenericTest {
1457         val size by collectLastValue(kosmos.promptViewModel.size)
1458 
1459         // TODO(b/251476085): remove Spaghetti, migrate logic, and update this test
1460         kosmos.promptViewModel.onSwitchToCredential()
1461 
1462         assertThat(size).isEqualTo(PromptSize.LARGE)
1463     }
1464 
1465     @Test
1466     fun hint_for_talkback_guidance() = runGenericTest {
1467         val hint by collectLastValue(kosmos.promptViewModel.accessibilityHint)
1468 
1469         // Touches should fall outside of sensor area
1470         whenever(kosmos.udfpsUtils.getTouchInNativeCoordinates(any(), any(), any()))
1471             .thenReturn(Point(0, 0))
1472         whenever(kosmos.udfpsUtils.onTouchOutsideOfSensorArea(any(), any(), any(), any(), any()))
1473             .thenReturn("Direction")
1474 
1475         kosmos.promptViewModel.onAnnounceAccessibilityHint(
1476             obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER),
1477             true,
1478         )
1479 
1480         if (testCase.modalities.hasUdfps) {
1481             assertThat(hint?.isNotBlank()).isTrue()
1482         } else {
1483             assertThat(hint.isNullOrBlank()).isTrue()
1484         }
1485     }
1486 
1487     @Test
1488     fun no_hint_for_talkback_guidance_after_auth() = runGenericTest {
1489         val hint by collectLastValue(kosmos.promptViewModel.accessibilityHint)
1490 
1491         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
1492         kosmos.promptViewModel.confirmAuthenticated()
1493 
1494         // Touches should fall outside of sensor area
1495         whenever(kosmos.udfpsUtils.getTouchInNativeCoordinates(any(), any(), any()))
1496             .thenReturn(Point(0, 0))
1497         whenever(kosmos.udfpsUtils.onTouchOutsideOfSensorArea(any(), any(), any(), any(), any()))
1498             .thenReturn("Direction")
1499 
1500         kosmos.promptViewModel.onAnnounceAccessibilityHint(
1501             obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER),
1502             true,
1503         )
1504 
1505         assertThat(hint.isNullOrBlank()).isTrue()
1506     }
1507 
1508     @Test
1509     fun descriptionOverriddenByVerticalListContentView() =
1510         runGenericTest(description = "test description", contentView = promptContentView) {
1511             val contentView by collectLastValue(kosmos.promptViewModel.contentView)
1512             val description by collectLastValue(kosmos.promptViewModel.description)
1513 
1514             assertThat(description).isEqualTo("")
1515             assertThat(contentView).isEqualTo(promptContentView)
1516         }
1517 
1518     @Test
1519     fun descriptionOverriddenByContentViewWithMoreOptionsButton() =
1520         runGenericTest(
1521             description = "test description",
1522             contentView = promptContentViewWithMoreOptionsButton,
1523         ) {
1524             val contentView by collectLastValue(kosmos.promptViewModel.contentView)
1525             val description by collectLastValue(kosmos.promptViewModel.description)
1526 
1527             assertThat(description).isEqualTo("")
1528             assertThat(contentView).isEqualTo(promptContentViewWithMoreOptionsButton)
1529         }
1530 
1531     @Test
1532     fun descriptionWithoutContentView() =
1533         runGenericTest(description = "test description") {
1534             val contentView by collectLastValue(kosmos.promptViewModel.contentView)
1535             val description by collectLastValue(kosmos.promptViewModel.description)
1536 
1537             assertThat(description).isEqualTo("test description")
1538             assertThat(contentView).isNull()
1539         }
1540 
1541     @Test
1542     fun logo_nullIfPkgNameNotFound() =
1543         runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) {
1544             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1545             assertThat(logoInfo).isNotNull()
1546             assertThat(logoInfo!!.first).isNull()
1547             assertThat(logoInfo!!.second).isEqualTo("")
1548         }
1549 
1550     @Test
1551     fun logo_defaultIsNull() =
1552         runGenericTest(packageName = OP_PACKAGE_NAME_NO_LOGO_INFO) {
1553             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1554             assertThat(logoInfo).isNotNull()
1555             assertThat(logoInfo!!.first).isNull()
1556             assertThat(logoInfo!!.second).isEqualTo("")
1557         }
1558 
1559     @Test
1560     fun logo_defaultFromActivityInfo() =
1561         runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) {
1562             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1563 
1564             assertThat(logoInfo).isNotNull()
1565             // 1. PM.getApplicationInfo(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) is set to return
1566             // applicationInfoWithIconAndDescription with "defaultLogoIconFromAppInfo",
1567             // 2. iconProvider.getIcon(activityInfo) is set to return
1568             // "defaultLogoIconFromActivityInfo"
1569             // For the apps with OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO, 2 should be called instead of 1
1570             assertThat(logoInfo!!.first).isEqualTo(defaultLogoIconFromActivityInfo)
1571             // 1. PM.getApplicationInfo(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) is set to return
1572             // applicationInfoWithIconAndDescription with "defaultLogoDescriptionFromAppInfo",
1573             // 2. activityInfo.loadLabel() is set to return defaultLogoDescriptionFromActivityInfo
1574             // For the apps with OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO, 2 should be called instead of 1
1575             assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromActivityInfo)
1576         }
1577 
1578     @Test
1579     fun logo_defaultFromApplicationInfo() = runGenericTest {
1580         val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1581         assertThat(logoInfo).isNotNull()
1582         assertThat(logoInfo!!.first).isEqualTo(defaultLogoIconFromAppInfo)
1583         assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo)
1584     }
1585 
1586     @Test
1587     fun logo_defaultWithWorkBadge() =
1588         runGenericTest(userId = WORK_USER_ID) {
1589             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1590             assertThat(logoInfo).isNotNull()
1591             assertThat(logoInfo!!.first).isEqualTo(defaultLogoIconWithBadge)
1592             // Logo label does not use badge info.
1593             assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo)
1594         }
1595 
1596     @Test
1597     fun logoRes_setByApp() =
1598         runGenericTest(logoRes = logoResFromApp) {
1599             val expectedBitmap = context.getDrawable(logoResFromApp).toBitmap()
1600             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1601             assertThat(logoInfo).isNotNull()
1602             assertThat((logoInfo!!.first as BitmapDrawable).bitmap.sameAs(expectedBitmap)).isTrue()
1603         }
1604 
1605     @Test
1606     fun logoBitmap_setByApp() =
1607         runGenericTest(logoBitmap = logoBitmapFromApp) {
1608             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1609             assertThat((logoInfo!!.first as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp)
1610         }
1611 
1612     @Test
1613     fun logoDescription_setByApp() =
1614         runGenericTest(logoDescription = logoDescriptionFromApp) {
1615             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
1616             assertThat(logoInfo!!.second).isEqualTo(logoDescriptionFromApp)
1617         }
1618 
1619     @Test
1620     fun position_bottom_rotation0() = runGenericTest {
1621         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
1622         val position by collectLastValue(kosmos.promptViewModel.position)
1623         assertThat(position).isEqualTo(PromptPosition.Bottom)
1624     } // TODO(b/335278136): Add test for no sensor landscape
1625 
1626     @Test
1627     fun position_bottom_forceLarge() = runGenericTest {
1628         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
1629         kosmos.promptViewModel.onSwitchToCredential()
1630         val position by collectLastValue(kosmos.promptViewModel.position)
1631         assertThat(position).isEqualTo(PromptPosition.Bottom)
1632     }
1633 
1634     @Test
1635     fun position_bottom_largeScreen() = runGenericTest {
1636         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
1637         kosmos.displayStateRepository.setIsLargeScreen(true)
1638         val position by collectLastValue(kosmos.promptViewModel.position)
1639         assertThat(position).isEqualTo(PromptPosition.Bottom)
1640     }
1641 
1642     @Test
1643     fun position_right_rotation90() = runGenericTest {
1644         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
1645         val position by collectLastValue(kosmos.promptViewModel.position)
1646         assertThat(position).isEqualTo(PromptPosition.Right)
1647     }
1648 
1649     @Test
1650     fun position_left_rotation270() = runGenericTest {
1651         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
1652         val position by collectLastValue(kosmos.promptViewModel.position)
1653         assertThat(position).isEqualTo(PromptPosition.Left)
1654     }
1655 
1656     @Test
1657     fun position_top_rotation180() = runGenericTest {
1658         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
1659         val position by collectLastValue(kosmos.promptViewModel.position)
1660         if (testCase.modalities.hasUdfps) {
1661             assertThat(position).isEqualTo(PromptPosition.Top)
1662         } else {
1663             assertThat(position).isEqualTo(PromptPosition.Bottom)
1664         }
1665     }
1666 
1667     @Test
1668     fun guideline_bottom() = runGenericTest {
1669         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
1670         val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
1671         assertThat(guidelineBounds).isEqualTo(Rect(0, mediumTopGuidelinePadding, 0, 0))
1672     } // TODO(b/335278136): Add test for no sensor landscape
1673 
1674     @Test
1675     fun guideline_right() = runGenericTest {
1676         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
1677 
1678         val isSmall = testCase.shouldStartAsImplicitFlow
1679         val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
1680 
1681         if (isSmall) {
1682             assertThat(guidelineBounds).isEqualTo(Rect(-smallHorizontalGuidelinePadding, 0, 0, 0))
1683         } else if (testCase.modalities.hasUdfps) {
1684             assertThat(guidelineBounds).isEqualTo(Rect(udfpsHorizontalGuidelinePadding, 0, 0, 0))
1685         } else {
1686             assertThat(guidelineBounds).isEqualTo(Rect(-mediumHorizontalGuidelinePadding, 0, 0, 0))
1687         }
1688     }
1689 
1690     @Test
1691     fun guideline_right_onlyShortTitle() =
1692         runGenericTest(subtitle = "") {
1693             kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
1694 
1695             val isSmall = testCase.shouldStartAsImplicitFlow
1696             val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
1697 
1698             if (!isSmall && testCase.modalities.hasUdfps) {
1699                 assertThat(guidelineBounds)
1700                     .isEqualTo(Rect(-udfpsHorizontalShorterGuidelinePadding, 0, 0, 0))
1701             }
1702         }
1703 
1704     @Test
1705     fun guideline_left() = runGenericTest {
1706         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
1707 
1708         val isSmall = testCase.shouldStartAsImplicitFlow
1709         val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
1710 
1711         if (isSmall) {
1712             assertThat(guidelineBounds).isEqualTo(Rect(0, 0, -smallHorizontalGuidelinePadding, 0))
1713         } else if (testCase.modalities.hasUdfps) {
1714             assertThat(guidelineBounds).isEqualTo(Rect(0, 0, udfpsHorizontalGuidelinePadding, 0))
1715         } else {
1716             assertThat(guidelineBounds).isEqualTo(Rect(0, 0, -mediumHorizontalGuidelinePadding, 0))
1717         }
1718     }
1719 
1720     @Test
1721     fun guideline_left_onlyShortTitle() =
1722         runGenericTest(subtitle = "") {
1723             kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
1724 
1725             val isSmall = testCase.shouldStartAsImplicitFlow
1726             val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
1727 
1728             if (!isSmall && testCase.modalities.hasUdfps) {
1729                 assertThat(guidelineBounds)
1730                     .isEqualTo(Rect(0, 0, -udfpsHorizontalShorterGuidelinePadding, 0))
1731             }
1732         }
1733 
1734     @Test
1735     fun guideline_top() = runGenericTest {
1736         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
1737         val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
1738         if (testCase.modalities.hasUdfps) {
1739             assertThat(guidelineBounds).isEqualTo(Rect(0, 0, 0, 0))
1740         }
1741     }
1742 
1743     @Test
1744     fun iconViewLoaded() = runGenericTest {
1745         val isIconViewLoaded by collectLastValue(kosmos.promptViewModel.isIconViewLoaded)
1746         // TODO(b/328677869): Add test for noIcon logic.
1747         assertThat(isIconViewLoaded).isFalse()
1748 
1749         kosmos.promptViewModel.setIsIconViewLoaded(true)
1750 
1751         assertThat(isIconViewLoaded).isTrue()
1752     }
1753 
1754     /** Asserts that the selected buttons are visible now. */
1755     private suspend fun TestScope.assertButtonsVisible(
1756         tryAgain: Boolean = false,
1757         confirm: Boolean = false,
1758         cancel: Boolean = false,
1759         negative: Boolean = false,
1760         credential: Boolean = false,
1761     ) {
1762         runCurrent()
1763         assertThat(kosmos.promptViewModel.isTryAgainButtonVisible.first()).isEqualTo(tryAgain)
1764         assertThat(kosmos.promptViewModel.isConfirmButtonVisible.first()).isEqualTo(confirm)
1765         assertThat(kosmos.promptViewModel.isCancelButtonVisible.first()).isEqualTo(cancel)
1766         assertThat(kosmos.promptViewModel.isNegativeButtonVisible.first()).isEqualTo(negative)
1767         assertThat(kosmos.promptViewModel.isCredentialButtonVisible.first()).isEqualTo(credential)
1768     }
1769 
1770     private fun runGenericTest(
1771         doNotStart: Boolean = false,
1772         allowCredentialFallback: Boolean = false,
1773         subtitle: String? = "s",
1774         description: String? = null,
1775         contentView: PromptContentView? = null,
1776         logoRes: Int = 0,
1777         logoBitmap: Bitmap? = null,
1778         logoDescription: String? = null,
1779         packageName: String = OP_PACKAGE_NAME_WITH_APP_LOGO,
1780         userId: Int = USER_ID,
1781         block: suspend TestScope.() -> Unit,
1782     ) {
1783         val topActivity = ComponentName(packageName, "test app")
1784         runningTaskInfo.topActivity = topActivity
1785         whenever(kosmos.activityTaskManager.getTasks(1)).thenReturn(listOf(runningTaskInfo))
1786         kosmos.promptSelectorInteractor.resetPrompt(REQUEST_ID)
1787 
1788         kosmos.promptSelectorInteractor.initializePrompt(
1789             requireConfirmation = testCase.confirmationRequested,
1790             allowCredentialFallback = allowCredentialFallback,
1791             fingerprint = testCase.fingerprint,
1792             face = testCase.face,
1793             subtitleFromApp = subtitle,
1794             descriptionFromApp = description,
1795             contentViewFromApp = contentView,
1796             logoResFromApp = logoRes,
1797             logoBitmapFromApp = if (logoRes != 0) logoDrawableFromAppRes.toBitmap() else logoBitmap,
1798             logoDescriptionFromApp = logoDescription,
1799             packageName = packageName,
1800             userId = userId,
1801         )
1802 
1803         kosmos.biometricStatusRepository.setFingerprintAcquiredStatus(
1804             AcquiredFingerprintAuthenticationStatus(
1805                 AuthenticationReason.BiometricPromptAuthentication,
1806                 BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_UNKNOWN,
1807             )
1808         )
1809 
1810         // put the view model in the initial authenticating state, unless explicitly skipped
1811         val startMode =
1812             when {
1813                 doNotStart -> null
1814                 testCase.isCoex -> FingerprintStartMode.Delayed
1815                 else -> FingerprintStartMode.Normal
1816             }
1817         when (startMode) {
1818             FingerprintStartMode.Normal -> {
1819                 kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = false)
1820                 kosmos.promptViewModel.showAuthenticating()
1821             }
1822             FingerprintStartMode.Delayed -> {
1823                 kosmos.promptViewModel.showAuthenticating()
1824             }
1825             else -> {
1826                 /* skip */
1827             }
1828         }
1829 
1830         if (testCase.fingerprint?.isAnyUdfpsType == true) {
1831             kosmos.testScope.collectLastValue(kosmos.udfpsOverlayInteractor.udfpsOverlayParams)
1832             kosmos.testScope.runCurrent()
1833             overrideUdfpsOverlayParams()
1834         }
1835 
1836         kosmos.testScope.runTest { block() }
1837     }
1838 
1839     private fun overrideUdfpsOverlayParams(isLandscape: Boolean = false) {
1840         val authControllerCallback = authController.captureCallback()
1841         authControllerCallback.onUdfpsLocationChanged(
1842             mockUdfpsOverlayParams(isLandscape = isLandscape)
1843         )
1844     }
1845 
1846     /** Obtain a MotionEvent with the specified MotionEvent action constant */
1847     private fun obtainMotionEvent(action: Int): MotionEvent =
1848         MotionEvent.obtain(0, 0, action, 0f, 0f, 0)
1849 
1850     companion object {
1851         @JvmStatic
1852         @Parameters(name = "{0}")
1853         fun data(): Collection<TestCase> = singleModalityTestCases + coexTestCases
1854 
1855         private val singleModalityTestCases =
1856             listOf(
1857                 TestCase(
1858                     face = faceSensorPropertiesInternal(strong = true).first(),
1859                     authenticatedModality = BiometricModality.Face,
1860                 ),
1861                 TestCase(
1862                     fingerprint =
1863                         fingerprintSensorPropertiesInternal(
1864                                 sensorType = FingerprintSensorProperties.TYPE_REAR
1865                             )
1866                             .first(),
1867                     authenticatedModality = BiometricModality.Fingerprint,
1868                 ),
1869                 TestCase(
1870                     fingerprint =
1871                         fingerprintSensorPropertiesInternal(
1872                                 strong = true,
1873                                 sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
1874                             )
1875                             .first(),
1876                     authenticatedModality = BiometricModality.Fingerprint,
1877                     isInRearDisplayMode = false,
1878                 ),
1879                 TestCase(
1880                     fingerprint =
1881                         fingerprintSensorPropertiesInternal(
1882                                 strong = true,
1883                                 sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
1884                             )
1885                             .first(),
1886                     authenticatedModality = BiometricModality.Fingerprint,
1887                     isInRearDisplayMode = true,
1888                 ),
1889                 TestCase(
1890                     fingerprint =
1891                         fingerprintSensorPropertiesInternal(
1892                                 strong = true,
1893                                 sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
1894                             )
1895                             .first(),
1896                     authenticatedModality = BiometricModality.Fingerprint,
1897                 ),
1898                 TestCase(
1899                     face = faceSensorPropertiesInternal(strong = true).first(),
1900                     authenticatedModality = BiometricModality.Face,
1901                     confirmationRequested = true,
1902                 ),
1903                 TestCase(
1904                     fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
1905                     authenticatedModality = BiometricModality.Fingerprint,
1906                     confirmationRequested = true,
1907                 ),
1908                 TestCase(
1909                     fingerprint =
1910                         fingerprintSensorPropertiesInternal(
1911                                 strong = true,
1912                                 sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
1913                             )
1914                             .first(),
1915                     authenticatedModality = BiometricModality.Fingerprint,
1916                     confirmationRequested = true,
1917                 ),
1918             )
1919 
1920         private val coexTestCases =
1921             listOf(
1922                 TestCase(
1923                     face = faceSensorPropertiesInternal(strong = true).first(),
1924                     fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
1925                     authenticatedModality = BiometricModality.Face,
1926                 ),
1927                 TestCase(
1928                     face = faceSensorPropertiesInternal(strong = true).first(),
1929                     fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
1930                     authenticatedModality = BiometricModality.Face,
1931                     confirmationRequested = true,
1932                 ),
1933                 TestCase(
1934                     face = faceSensorPropertiesInternal(strong = true).first(),
1935                     fingerprint =
1936                         fingerprintSensorPropertiesInternal(
1937                                 strong = true,
1938                                 sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
1939                             )
1940                             .first(),
1941                     authenticatedModality = BiometricModality.Fingerprint,
1942                     confirmationRequested = true,
1943                 ),
1944                 TestCase(
1945                     face = faceSensorPropertiesInternal(strong = true).first(),
1946                     fingerprint =
1947                         fingerprintSensorPropertiesInternal(
1948                                 strong = true,
1949                                 sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
1950                             )
1951                             .first(),
1952                     authenticatedModality = BiometricModality.Fingerprint,
1953                 ),
1954             )
1955     }
1956 }
1957 
1958 internal data class TestCase(
1959     val fingerprint: FingerprintSensorPropertiesInternal? = null,
1960     val face: FaceSensorPropertiesInternal? = null,
1961     val isInRearDisplayMode: Boolean = false,
1962     val authenticatedModality: BiometricModality,
1963     val confirmationRequested: Boolean = false,
1964 ) {
toStringnull1965     override fun toString(): String {
1966         val modality =
1967             when {
1968                 fingerprint != null && face != null -> "coex"
1969                 fingerprint != null && fingerprint.isAnySidefpsType -> "fingerprint only, sideFps"
1970                 fingerprint != null && fingerprint.isAnyUdfpsType -> "fingerprint only, udfps"
1971                 fingerprint != null &&
1972                     fingerprint.sensorType == FingerprintSensorProperties.TYPE_REAR ->
1973                     "fingerprint only, rearFps"
1974                 face != null -> "face only"
1975                 else -> "?"
1976             }
1977         return "[$modality, isInRearDisplayMode: $isInRearDisplayMode, by: " +
1978             "$authenticatedModality, confirm: $confirmationRequested]"
1979     }
1980 
expectConfirmationnull1981     fun expectConfirmation(atLeastOneFailure: Boolean): Boolean =
1982         when {
1983             isCoex && authenticatedModality == BiometricModality.Face ->
1984                 atLeastOneFailure || confirmationRequested
1985             isFaceOnly -> confirmationRequested
1986             else -> false
1987         }
1988 
1989     val modalities: BiometricModalities
1990         get() = BiometricModalities(fingerprint, face)
1991 
1992     val authenticatedByFingerprint: Boolean
1993         get() = authenticatedModality == BiometricModality.Fingerprint
1994 
1995     val authenticatedByFace: Boolean
1996         get() = authenticatedModality == BiometricModality.Face
1997 
1998     val isFaceOnly: Boolean
1999         get() = face != null && fingerprint == null
2000 
2001     val isFingerprintOnly: Boolean
2002         get() = face == null && fingerprint != null
2003 
2004     val isCoex: Boolean
2005         get() = face != null && fingerprint != null
2006 
2007     @FingerprintSensorProperties.SensorType val sensorType: Int? = fingerprint?.sensorType
2008 
2009     val shouldStartAsImplicitFlow: Boolean
2010         get() = (isFaceOnly || isCoex) && !confirmationRequested
2011 }
2012 
2013 /** Initialize the test by selecting the give [fingerprint] or [face] configuration(s). */
initializePromptnull2014 private fun PromptSelectorInteractor.initializePrompt(
2015     fingerprint: FingerprintSensorPropertiesInternal? = null,
2016     face: FaceSensorPropertiesInternal? = null,
2017     requireConfirmation: Boolean = false,
2018     allowCredentialFallback: Boolean = false,
2019     subtitleFromApp: String? = "s",
2020     descriptionFromApp: String? = null,
2021     contentViewFromApp: PromptContentView? = null,
2022     logoResFromApp: Int = 0,
2023     logoBitmapFromApp: Bitmap? = null,
2024     logoDescriptionFromApp: String? = null,
2025     packageName: String = OP_PACKAGE_NAME_WITH_APP_LOGO,
2026     userId: Int = USER_ID,
2027 ) {
2028     val info =
2029         PromptInfo().apply {
2030             logoDescription = logoDescriptionFromApp
2031             title = "t"
2032             subtitle = subtitleFromApp
2033             description = descriptionFromApp
2034             contentView = contentViewFromApp
2035             authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
2036             isDeviceCredentialAllowed = allowCredentialFallback
2037             isConfirmationRequested = requireConfirmation
2038         }
2039     if (logoBitmapFromApp != null) {
2040         info.setLogo(logoResFromApp, logoBitmapFromApp)
2041     }
2042 
2043     setPrompt(
2044         info,
2045         userId,
2046         REQUEST_ID,
2047         BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),
2048         CHALLENGE,
2049         packageName,
2050         onSwitchToCredential = false,
2051         isLandscape = false,
2052     )
2053 }
2054 
AuthControllernull2055 private fun AuthController.captureCallback() =
2056     withArgCaptor<AuthController.Callback> {
2057         Mockito.verify(this@captureCallback).addCallback(capture())
2058     }
2059