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.PromptContentItemBulletedText
32 import android.hardware.biometrics.PromptContentView
33 import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
34 import android.hardware.biometrics.PromptInfo
35 import android.hardware.biometrics.PromptVerticalListContentView
36 import android.hardware.face.FaceSensorPropertiesInternal
37 import android.hardware.fingerprint.FingerprintSensorProperties
38 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
39 import android.os.UserHandle
40 import android.platform.test.annotations.DisableFlags
41 import android.platform.test.annotations.EnableFlags
42 import android.view.HapticFeedbackConstants
43 import android.view.MotionEvent
44 import android.view.Surface
45 import androidx.test.filters.SmallTest
46 import com.android.app.activityTaskManager
47 import com.android.keyguard.AuthInteractionProperties
48 import com.android.systemui.Flags
49 import com.android.systemui.SysuiTestCase
50 import com.android.systemui.biometrics.AuthController
51 import com.android.systemui.biometrics.Utils.toBitmap
52 import com.android.systemui.biometrics.authController
53 import com.android.systemui.biometrics.data.repository.biometricStatusRepository
54 import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
55 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
56 import com.android.systemui.biometrics.domain.interactor.promptSelectorInteractor
57 import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
58 import com.android.systemui.biometrics.extractAuthenticatorTypes
59 import com.android.systemui.biometrics.faceSensorPropertiesInternal
60 import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
61 import com.android.systemui.biometrics.shared.model.AuthenticationReason
62 import com.android.systemui.biometrics.shared.model.BiometricModalities
63 import com.android.systemui.biometrics.shared.model.BiometricModality
64 import com.android.systemui.biometrics.shared.model.DisplayRotation
65 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
66 import com.android.systemui.biometrics.shared.model.toSensorStrength
67 import com.android.systemui.biometrics.shared.model.toSensorType
68 import com.android.systemui.biometrics.udfpsUtils
69 import com.android.systemui.concurrency.fakeExecutor
70 import com.android.systemui.coroutines.collectLastValue
71 import com.android.systemui.coroutines.collectValues
72 import com.android.systemui.display.data.repository.displayStateRepository
73 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
74 import com.android.systemui.kosmos.Kosmos
75 import com.android.systemui.kosmos.testScope
76 import com.android.systemui.res.R
77 import com.android.systemui.util.mockito.withArgCaptor
78 import com.google.android.msdl.data.model.MSDLToken
79 import com.google.common.truth.Truth.assertThat
80 import kotlinx.coroutines.ExperimentalCoroutinesApi
81 import kotlinx.coroutines.flow.first
82 import kotlinx.coroutines.launch
83 import kotlinx.coroutines.test.TestScope
84 import kotlinx.coroutines.test.runCurrent
85 import kotlinx.coroutines.test.runTest
86 import org.junit.Before
87 import org.junit.Rule
88 import org.junit.Test
89 import org.junit.runner.RunWith
90 import org.mockito.ArgumentMatchers.anyInt
91 import org.mockito.ArgumentMatchers.eq
92 import org.mockito.Mock
93 import org.mockito.Mockito
94 import org.mockito.junit.MockitoJUnit
95 import org.mockito.kotlin.any
96 import org.mockito.kotlin.whenever
97 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
98 import platform.test.runner.parameterized.Parameters
99 
100 private const val USER_ID = 4
101 private const val WORK_USER_ID = 100
102 private const val REQUEST_ID = 4L
103 private const val CHALLENGE = 2L
104 private const val DELAY = 1000L
105 private const val OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO = "should.use.activiy.logo"
106 private const val OP_PACKAGE_NAME_WITH_APP_LOGO = "biometric.testapp"
107 private const val OP_PACKAGE_NAME_NO_LOGO_INFO = "biometric.testapp.nologoinfo"
108 private const val OP_PACKAGE_NAME_CAN_NOT_BE_FOUND = "can.not.be.found"
109 
110 @OptIn(ExperimentalCoroutinesApi::class)
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     /** Mock [UdfpsOverlayParams] for a test. */
162     private fun mockUdfpsOverlayParams(isLandscape: Boolean = false): UdfpsOverlayParams =
163         UdfpsOverlayParams(
164             sensorBounds = Rect(400, 1600, 600, 1800),
165             overlayBounds = Rect(0, 1200, 1000, 2400),
166             naturalDisplayWidth = 1000,
167             naturalDisplayHeight = 3000,
168             scaleFactor = 1f,
169             rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
170         )
171 
172     private lateinit var promptContentView: PromptContentView
173     private lateinit var promptContentViewWithMoreOptionsButton:
174         PromptContentViewWithMoreOptionsButton
175 
176     private val kosmos = Kosmos()
177 
178     @Before
179     fun setup() {
180         setupLogo()
181         overrideResource(R.dimen.biometric_dialog_fingerprint_icon_width, mockFingerprintIconWidth)
182         overrideResource(
183             R.dimen.biometric_dialog_fingerprint_icon_height,
184             mockFingerprintIconHeight,
185         )
186         overrideResource(R.dimen.biometric_dialog_face_icon_size, mockFaceIconSize)
187 
188         kosmos.applicationContext = context
189 
190         if (testCase.fingerprint?.isAnyUdfpsType == true) {
191             kosmos.authController = authController
192         }
193 
194         testCase.fingerprint?.let {
195             kosmos.fakeFingerprintPropertyRepository.setProperties(
196                 it.sensorId,
197                 it.sensorStrength.toSensorStrength(),
198                 it.sensorType.toSensorType(),
199                 it.allLocations.associateBy { sensorLocationInternal ->
200                     sensorLocationInternal.displayId
201                 },
202             )
203         }
204 
205         kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
206         testCase.fingerprint?.isAnySidefpsType.let {
207             kosmos.displayStateRepository.setIsInRearDisplayMode(testCase.isInRearDisplayMode)
208         }
209 
210         promptContentView =
211             PromptVerticalListContentView.Builder()
212                 .addListItem(PromptContentItemBulletedText("content item 1"))
213                 .addListItem(PromptContentItemBulletedText("content item 2"), 1)
214                 .build()
215 
216         promptContentViewWithMoreOptionsButton =
217             PromptContentViewWithMoreOptionsButton.Builder()
218                 .setDescription("test")
219                 .setMoreOptionsButtonListener(kosmos.fakeExecutor) { _, _ -> }
220                 .build()
221     }
222 
223     private fun setupLogo() {
224         // Set up app customized logo
225         overrideResource(logoResFromApp, logoDrawableFromAppRes)
226 
227         // Set up when activity info should be used
228         overrideResource(
229             R.array.config_useActivityLogoForBiometricPrompt,
230             arrayOf(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO),
231         )
232         whenever(kosmos.packageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo)
233         whenever(kosmos.iconProvider.getIcon(activityInfo))
234             .thenReturn(defaultLogoIconFromActivityInfo)
235         whenever(activityInfo.loadLabel(kosmos.packageManager))
236             .thenReturn(defaultLogoDescriptionFromActivityInfo)
237 
238         // Set up when application info should be used for default logo
239         whenever(
240                 kosmos.packageManager.getApplicationInfo(
241                     eq(OP_PACKAGE_NAME_WITH_APP_LOGO),
242                     anyInt(),
243                 )
244             )
245             .thenReturn(applicationInfoWithIconAndDescription)
246         whenever(
247                 kosmos.packageManager.getApplicationInfo(
248                     eq(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO),
249                     anyInt(),
250                 )
251             )
252             .thenReturn(applicationInfoWithIconAndDescription)
253         whenever(kosmos.packageManager.getApplicationIcon(applicationInfoWithIconAndDescription))
254             .thenReturn(defaultLogoIconFromAppInfo)
255         whenever(kosmos.packageManager.getApplicationLabel(applicationInfoWithIconAndDescription))
256             .thenReturn(defaultLogoDescriptionFromAppInfo)
257 
258         // Set up when package name cannot but found
259         whenever(
260                 kosmos.packageManager.getApplicationInfo(
261                     eq(OP_PACKAGE_NAME_CAN_NOT_BE_FOUND),
262                     anyInt(),
263                 )
264             )
265             .thenThrow(NameNotFoundException())
266 
267         // Set up when no default logo from application info
268         whenever(
269                 kosmos.packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME_NO_LOGO_INFO), anyInt())
270             )
271             .thenReturn(applicationInfoNoIconOrDescription)
272         whenever(kosmos.packageManager.getApplicationIcon(applicationInfoNoIconOrDescription))
273             .thenReturn(null)
274         whenever(kosmos.packageManager.getApplicationLabel(applicationInfoNoIconOrDescription))
275             .thenReturn("")
276 
277         // Set up work badge
278         whenever(kosmos.packageManager.getUserBadgedIcon(any(), eq(UserHandle.of(USER_ID)))).then {
279             it.getArgument(0)
280         }
281         whenever(kosmos.packageManager.getUserBadgedLabel(any(), eq(UserHandle.of(USER_ID)))).then {
282             it.getArgument(0)
283         }
284         whenever(kosmos.packageManager.getUserBadgedIcon(any(), eq(UserHandle.of(WORK_USER_ID))))
285             .then { defaultLogoIconWithBadge }
286         whenever(kosmos.packageManager.getUserBadgedLabel(any(), eq(UserHandle.of(WORK_USER_ID))))
287             .then { defaultLogoDescriptionWithBadge }
288         context.setMockPackageManager(kosmos.packageManager)
289     }
290 
291     @Test
292     fun start_idle_and_show_authenticating() =
293         runGenericTest(doNotStart = true) {
294             var expectedPromptSize =
295                 if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM
296             val authenticating by collectLastValue(kosmos.promptViewModel.isAuthenticating)
297             val authenticated by collectLastValue(kosmos.promptViewModel.isAuthenticated)
298             val modalities by collectLastValue(kosmos.promptViewModel.modalities)
299             val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
300             val shouldAnimateIconView by
301                 collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
302             val iconContentDescriptionId by
303                 collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
304             val message by collectLastValue(kosmos.promptViewModel.message)
305             val size by collectLastValue(kosmos.promptViewModel.size)
306 
307             assertThat(authenticating).isFalse()
308             assertThat(authenticated?.isNotAuthenticated).isTrue()
309             with(modalities ?: throw Exception("missing modalities")) {
310                 assertThat(hasFace).isEqualTo(testCase.face != null)
311                 assertThat(hasFingerprint).isEqualTo(testCase.fingerprint != null)
312             }
313 
314             assertThat(message).isEqualTo(PromptMessage.Empty)
315             assertThat(size).isEqualTo(expectedPromptSize)
316 
317             val forceExplicitFlow =
318                 testCase.isCoex && testCase.confirmationRequested ||
319                     testCase.authenticatedByFingerprint
320 
321             if ((testCase.isCoex && !forceExplicitFlow) || testCase.isFaceOnly) {
322                 // Face-only or implicit co-ex auth
323                 assertThat(iconAsset).isEqualTo(R.raw.face_dialog_idle_static)
324                 assertThat(shouldAnimateIconView).isEqualTo(false)
325             }
326 
327             if (forceExplicitFlow) {
328                 expectedPromptSize = PromptSize.MEDIUM
329                 kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
330             }
331 
332             val startMessage = "here we go"
333             kosmos.promptViewModel.showAuthenticating(startMessage, isRetry = false)
334             verifyIconSize(forceExplicitFlow)
335 
336             // Icon asset assertions
337             if ((testCase.isCoex && !forceExplicitFlow) || testCase.isFaceOnly) {
338                 // Face-only or implicit co-ex auth
339                 assertThat(iconAsset).isEqualTo(R.raw.face_dialog_authenticating)
340                 assertThat(iconContentDescriptionId)
341                     .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating)
342                 assertThat(shouldAnimateIconView).isEqualTo(true)
343             } else if ((testCase.isCoex && forceExplicitFlow) || testCase.isFingerprintOnly) {
344                 // Fingerprint-only or explicit co-ex auth
345                 if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
346                     assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintAuthenticating())
347                     assertThat(iconContentDescriptionId)
348                         .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
349                     assertThat(shouldAnimateIconView).isEqualTo(true)
350                 } else {
351                     assertThat(iconAsset)
352                         .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie)
353                     assertThat(iconContentDescriptionId)
354                         .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
355                     assertThat(shouldAnimateIconView).isEqualTo(false)
356                 }
357             }
358 
359             assertThat(message).isEqualTo(PromptMessage.Help(startMessage))
360             assertThat(authenticating).isTrue()
361             assertThat(authenticated?.isNotAuthenticated).isTrue()
362             assertThat(size).isEqualTo(expectedPromptSize)
363             assertButtonsVisible(negative = expectedPromptSize != PromptSize.SMALL)
364         }
365 
366     @Test
367     fun start_authenticating_show_and_clear_error() = runGenericTest {
368         val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
369         val iconContentDescriptionId by
370             collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
371         val shouldAnimateIconView by
372             collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
373         val message by collectLastValue(kosmos.promptViewModel.message)
374 
375         var forceExplicitFlow =
376             testCase.isCoex && testCase.confirmationRequested || testCase.authenticatedByFingerprint
377         if (forceExplicitFlow) {
378             kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
379         }
380         verifyIconSize(forceExplicitFlow)
381 
382         val errorJob = launch {
383             kosmos.promptViewModel.showTemporaryError(
384                 "so sad",
385                 messageAfterError = "",
386                 authenticateAfterError = testCase.isFingerprintOnly || testCase.isCoex,
387             )
388             forceExplicitFlow = true
389             // Usually done by binder
390             kosmos.promptViewModel.iconViewModel.setPreviousIconWasError(true)
391         }
392 
393         assertThat(message?.isError).isEqualTo(true)
394         assertThat(message?.message).isEqualTo("so sad")
395 
396         // Icon asset assertions
397         if (testCase.isFaceOnly) {
398             // Face-only auth
399             assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_error)
400             assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed)
401             assertThat(shouldAnimateIconView).isEqualTo(true)
402 
403             // Clear error, go to idle
404             errorJob.join()
405 
406             assertThat(iconAsset).isEqualTo(R.raw.face_dialog_error_to_idle)
407             assertThat(iconContentDescriptionId)
408                 .isEqualTo(R.string.biometric_dialog_face_icon_description_idle)
409             assertThat(shouldAnimateIconView).isEqualTo(true)
410         } else if ((testCase.isCoex && forceExplicitFlow) || testCase.isFingerprintOnly) {
411             // Fingerprint-only or explicit co-ex auth
412             if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
413                 assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToError())
414                 assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again)
415                 assertThat(shouldAnimateIconView).isEqualTo(true)
416             } else {
417                 assertThat(iconAsset)
418                     .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie)
419                 assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again)
420                 assertThat(shouldAnimateIconView).isEqualTo(true)
421             }
422 
423             // Clear error, restart authenticating
424             errorJob.join()
425 
426             if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
427                 assertThat(iconAsset).isEqualTo(getSfpsAsset_errorToFingerprint())
428                 assertThat(iconContentDescriptionId)
429                     .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
430                 assertThat(shouldAnimateIconView).isEqualTo(true)
431             } else {
432                 assertThat(iconAsset)
433                     .isEqualTo(R.raw.fingerprint_dialogue_error_to_fingerprint_lottie)
434                 assertThat(iconContentDescriptionId)
435                     .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
436                 assertThat(shouldAnimateIconView).isEqualTo(true)
437             }
438         }
439     }
440 
441     @Test
442     fun shows_error_to_unlock_or_success() {
443         // Face-only auth does not use error -> unlock or error -> success assets
444         if (testCase.isFingerprintOnly || testCase.isCoex) {
445             runGenericTest {
446                 // Distinct asset for error -> success only applicable for fingerprint-only /
447                 // explicit co-ex auth
448                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
449                 val iconContentDescriptionId by
450                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
451                 val shouldAnimateIconView by
452                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
453 
454                 var forceExplicitFlow =
455                     testCase.isCoex && testCase.confirmationRequested ||
456                         testCase.authenticatedByFingerprint
457                 if (forceExplicitFlow) {
458                     kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
459                 }
460                 verifyIconSize(forceExplicitFlow)
461 
462                 kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
463                 kosmos.promptViewModel.iconViewModel.setPreviousIconWasError(true)
464 
465                 kosmos.promptViewModel.showAuthenticated(
466                     modality = testCase.authenticatedModality,
467                     dismissAfterDelay = DELAY,
468                 )
469 
470                 // SFPS test cases
471                 if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
472                     // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint
473                     if (testCase.authenticatedByFingerprint) {
474                         assertThat(iconAsset).isEqualTo(R.raw.biometricprompt_sfps_error_to_success)
475                         assertThat(iconContentDescriptionId)
476                             .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
477                         assertThat(shouldAnimateIconView).isEqualTo(true)
478                     } else { // Covers co-ex, authenticated by face
479                         assertThat(iconAsset).isEqualTo(R.raw.biometricprompt_sfps_error_to_unlock)
480                         assertThat(iconContentDescriptionId)
481                             .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
482                         assertThat(shouldAnimateIconView).isEqualTo(true)
483 
484                         // Confirm authentication
485                         kosmos.promptViewModel.confirmAuthenticated()
486 
487                         assertThat(iconAsset)
488                             .isEqualTo(R.raw.biometricprompt_sfps_unlock_to_success)
489                         assertThat(iconContentDescriptionId)
490                             .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
491                         assertThat(shouldAnimateIconView).isEqualTo(true)
492                     }
493                 } else { // Non-SFPS (UDFPS / rear-FPS) test cases
494                     // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint
495                     if (testCase.authenticatedByFingerprint) {
496                         assertThat(iconAsset)
497                             .isEqualTo(R.raw.fingerprint_dialogue_error_to_success_lottie)
498                         assertThat(iconContentDescriptionId)
499                             .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
500                         assertThat(shouldAnimateIconView).isEqualTo(true)
501                     } else { //  co-ex, authenticated by face
502                         assertThat(iconAsset)
503                             .isEqualTo(R.raw.fingerprint_dialogue_error_to_unlock_lottie)
504                         assertThat(iconContentDescriptionId)
505                             .isEqualTo(R.string.biometric_dialog_confirm)
506                         assertThat(shouldAnimateIconView).isEqualTo(true)
507 
508                         // Confirm authentication
509                         kosmos.promptViewModel.confirmAuthenticated()
510 
511                         assertThat(iconAsset)
512                             .isEqualTo(
513                                 R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
514                             )
515                         assertThat(iconContentDescriptionId)
516                             .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
517                         assertThat(shouldAnimateIconView).isEqualTo(true)
518                     }
519                 }
520             }
521         }
522     }
523 
524     @Test
525     fun shows_authenticated_no_errors_no_confirmation_required() {
526         if (!testCase.confirmationRequested) {
527             runGenericTest {
528                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
529                 val iconContentDescriptionId by
530                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
531                 val shouldAnimateIconView by
532                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
533                 val message by collectLastValue(kosmos.promptViewModel.message)
534                 verifyIconSize()
535 
536                 kosmos.promptViewModel.showAuthenticated(
537                     modality = testCase.authenticatedModality,
538                     dismissAfterDelay = DELAY,
539                     "TEST",
540                 )
541 
542                 if (testCase.isFingerprintOnly) {
543                     // Fingerprint icon asset assertions
544                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
545                         assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToSuccess())
546                         assertThat(iconContentDescriptionId)
547                             .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
548                         assertThat(shouldAnimateIconView).isEqualTo(true)
549                     } else {
550                         assertThat(iconAsset)
551                             .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie)
552                         assertThat(iconContentDescriptionId)
553                             .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
554                         assertThat(shouldAnimateIconView).isEqualTo(true)
555                     }
556                 } else if (testCase.isFaceOnly || testCase.isCoex) {
557                     // Face icon asset assertions
558                     // If co-ex, use implicit flow (explicit flow always requires confirmation)
559                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
560                     assertThat(iconContentDescriptionId)
561                         .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
562                     assertThat(shouldAnimateIconView).isEqualTo(true)
563                     assertThat(message).isEqualTo(PromptMessage.Empty)
564                 }
565             }
566         }
567     }
568 
569     @Test
570     fun shows_pending_confirmation() {
571         if (testCase.authenticatedByFace && testCase.confirmationRequested) {
572             runGenericTest {
573                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
574                 val iconContentDescriptionId by
575                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
576                 val shouldAnimateIconView by
577                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
578 
579                 val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
580                 verifyIconSize(forceExplicitFlow = forceExplicitFlow)
581 
582                 kosmos.promptViewModel.showAuthenticated(
583                     modality = testCase.authenticatedModality,
584                     dismissAfterDelay = DELAY,
585                 )
586 
587                 if (testCase.isFaceOnly) {
588                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark)
589                     assertThat(iconContentDescriptionId)
590                         .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
591                     assertThat(shouldAnimateIconView).isEqualTo(true)
592                 } else if (testCase.isCoex) { // explicit flow, confirmation requested
593                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
594                         assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToUnlock())
595                         assertThat(iconContentDescriptionId)
596                             .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
597                         assertThat(shouldAnimateIconView).isEqualTo(true)
598                     } else {
599                         assertThat(iconAsset)
600                             .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie)
601                         assertThat(iconContentDescriptionId)
602                             .isEqualTo(R.string.biometric_dialog_confirm)
603                         assertThat(shouldAnimateIconView).isEqualTo(true)
604                     }
605                 }
606             }
607         }
608     }
609 
610     @Test
611     fun shows_authenticated_explicitly_confirmed() {
612         if (testCase.authenticatedByFace && testCase.confirmationRequested) {
613             runGenericTest {
614                 val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
615                 val iconContentDescriptionId by
616                     collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
617                 val shouldAnimateIconView by
618                     collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
619                 val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
620                 verifyIconSize(forceExplicitFlow = forceExplicitFlow)
621 
622                 kosmos.promptViewModel.showAuthenticated(
623                     modality = testCase.authenticatedModality,
624                     dismissAfterDelay = DELAY,
625                 )
626 
627                 kosmos.promptViewModel.confirmAuthenticated()
628 
629                 if (testCase.isFaceOnly) {
630                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
631                     assertThat(iconContentDescriptionId)
632                         .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed)
633                     assertThat(shouldAnimateIconView).isEqualTo(true)
634                 }
635 
636                 // explicit flow because confirmation requested
637                 if (testCase.isCoex) {
638                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
639                         assertThat(iconAsset)
640                             .isEqualTo(R.raw.biometricprompt_sfps_unlock_to_success)
641                         assertThat(shouldAnimateIconView).isEqualTo(true)
642                     } else {
643                         assertThat(iconAsset)
644                             .isEqualTo(
645                                 R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
646                             )
647                         assertThat(iconContentDescriptionId)
648                             .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
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