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