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