1 /* 2 * Copyright (C) 2022 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 package com.android.systemui.biometrics 17 18 import android.app.admin.DevicePolicyManager 19 import android.hardware.biometrics.BiometricAuthenticator 20 import android.hardware.biometrics.BiometricConstants 21 import android.hardware.biometrics.BiometricManager 22 import android.hardware.biometrics.PromptInfo 23 import android.hardware.face.FaceSensorPropertiesInternal 24 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal 25 import android.os.Handler 26 import android.os.IBinder 27 import android.os.UserManager 28 import android.testing.TestableLooper 29 import android.testing.TestableLooper.RunWithLooper 30 import android.testing.ViewUtils 31 import android.view.KeyEvent 32 import android.view.View 33 import android.view.WindowInsets 34 import android.view.WindowManager 35 import android.widget.ScrollView 36 import androidx.test.ext.junit.runners.AndroidJUnit4 37 import androidx.test.filters.SmallTest 38 import com.android.internal.jank.InteractionJankMonitor 39 import com.android.internal.widget.LockPatternUtils 40 import com.android.systemui.R 41 import com.android.systemui.SysuiTestCase 42 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository 43 import com.android.systemui.biometrics.data.repository.FakePromptRepository 44 import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository 45 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl 46 import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor 47 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor 48 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl 49 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel 50 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel 51 import com.android.systemui.flags.FakeFeatureFlags 52 import com.android.systemui.flags.Flags 53 import com.android.systemui.keyguard.WakefulnessLifecycle 54 import com.android.systemui.statusbar.VibratorHelper 55 import com.android.systemui.util.concurrency.FakeExecutor 56 import com.android.systemui.util.time.FakeSystemClock 57 import com.google.common.truth.Truth.assertThat 58 import kotlinx.coroutines.Dispatchers 59 import kotlinx.coroutines.test.StandardTestDispatcher 60 import kotlinx.coroutines.test.TestScope 61 import kotlinx.coroutines.test.runCurrent 62 import org.junit.After 63 import org.junit.Before 64 import org.junit.Ignore 65 import org.junit.Rule 66 import org.junit.Test 67 import org.junit.runner.RunWith 68 import org.mockito.Mock 69 import org.mockito.Mockito.anyBoolean 70 import org.mockito.Mockito.anyInt 71 import org.mockito.Mockito.anyLong 72 import org.mockito.Mockito.eq 73 import org.mockito.Mockito.never 74 import org.mockito.Mockito.times 75 import org.mockito.Mockito.verify 76 import org.mockito.junit.MockitoJUnit 77 import org.mockito.Mockito.`when` as whenever 78 79 @RunWith(AndroidJUnit4::class) 80 @RunWithLooper(setAsMainLooper = true) 81 @SmallTest 82 open class AuthContainerViewTest : SysuiTestCase() { 83 84 @JvmField @Rule 85 var mockitoRule = MockitoJUnit.rule() 86 87 private val featureFlags = FakeFeatureFlags() 88 89 @Mock 90 lateinit var callback: AuthDialogCallback 91 @Mock 92 lateinit var userManager: UserManager 93 @Mock 94 lateinit var lockPatternUtils: LockPatternUtils 95 @Mock 96 lateinit var wakefulnessLifecycle: WakefulnessLifecycle 97 @Mock 98 lateinit var panelInteractionDetector: AuthDialogPanelInteractionDetector 99 @Mock 100 lateinit var windowToken: IBinder 101 @Mock 102 lateinit var interactionJankMonitor: InteractionJankMonitor 103 @Mock 104 lateinit var vibrator: VibratorHelper 105 106 // TODO(b/278622168): remove with flag 107 open val useNewBiometricPrompt = false 108 109 private val testScope = TestScope(StandardTestDispatcher()) 110 private val fakeExecutor = FakeExecutor(FakeSystemClock()) 111 private val biometricPromptRepository = FakePromptRepository() 112 private val fingerprintRepository = FakeFingerprintPropertyRepository() 113 private val displayStateRepository = FakeDisplayStateRepository() 114 private val credentialInteractor = FakeCredentialInteractor() 115 private val bpCredentialInteractor = PromptCredentialInteractor( 116 Dispatchers.Main.immediate, 117 biometricPromptRepository, 118 credentialInteractor, 119 ) 120 private val promptSelectorInteractor by lazy { 121 PromptSelectorInteractorImpl( 122 fingerprintRepository, 123 biometricPromptRepository, 124 lockPatternUtils, 125 ) 126 } 127 128 private val displayStateInteractor = DisplayStateInteractorImpl( 129 testScope.backgroundScope, 130 mContext, 131 fakeExecutor, 132 displayStateRepository 133 ) 134 135 136 private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) 137 138 private var authContainer: TestAuthContainerView? = null 139 140 @Before 141 fun setup() { 142 featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt) 143 featureFlags.set(Flags.ONE_WAY_HAPTICS_API_MIGRATION, false) 144 } 145 146 @After 147 fun tearDown() { 148 if (authContainer?.isAttachedToWindow == true) { 149 ViewUtils.detachView(authContainer) 150 } 151 } 152 153 @Test 154 fun testNotifiesAnimatedIn() { 155 initializeFingerprintContainer() 156 verify(callback).onDialogAnimatedIn( 157 authContainer?.requestId ?: 0L, 158 true /* startFingerprintNow */ 159 ) 160 } 161 162 @Test 163 fun testDismissesOnBack() { 164 val container = initializeFingerprintContainer(addToView = true) 165 assertThat(container.parent).isNotNull() 166 val root = container.rootView 167 168 // Simulate back invocation 169 container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)) 170 container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)) 171 waitForIdleSync() 172 173 assertThat(container.parent).isNull() 174 assertThat(root.isAttachedToWindow).isFalse() 175 } 176 177 @Test 178 fun testCredentialPasswordDismissesOnBack() { 179 val container = initializeCredentialPasswordContainer(addToView = true) 180 assertThat(container.parent).isNotNull() 181 val root = container.rootView 182 183 // Simulate back invocation 184 container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)) 185 container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)) 186 waitForIdleSync() 187 188 assertThat(container.parent).isNull() 189 assertThat(root.isAttachedToWindow).isFalse() 190 } 191 192 @Test 193 fun testIgnoresAnimatedInWhenDismissed() { 194 val container = initializeFingerprintContainer(addToView = false) 195 container.dismissFromSystemServer() 196 waitForIdleSync() 197 198 verify(callback, never()).onDialogAnimatedIn(anyLong(), anyBoolean()) 199 200 container.addToView() 201 waitForIdleSync() 202 203 // attaching the view resets the state and allows this to happen again 204 verify(callback).onDialogAnimatedIn( 205 authContainer?.requestId ?: 0L, 206 true /* startFingerprintNow */ 207 ) 208 } 209 210 @Test 211 fun testDismissBeforeIntroEnd() { 212 val container = initializeFingerprintContainer() 213 waitForIdleSync() 214 215 // STATE_ANIMATING_IN = 1 216 container?.mContainerState = 1 217 218 container.dismissWithoutCallback(false) 219 220 // the first time is triggered by initializeFingerprintContainer() 221 // the second time was triggered by dismissWithoutCallback() 222 verify(callback, times(2)).onDialogAnimatedIn( 223 authContainer?.requestId ?: 0L, 224 true /* startFingerprintNow */ 225 ) 226 } 227 228 @Test 229 fun testActionCancel_panelInteractionDetectorDisable() { 230 val container = initializeFingerprintContainer() 231 container.mBiometricCallback.onAction( 232 AuthBiometricView.Callback.ACTION_USER_CANCELED 233 ) 234 waitForIdleSync() 235 verify(panelInteractionDetector).disable() 236 } 237 238 239 @Test 240 fun testActionAuthenticated_sendsDismissedAuthenticated() { 241 val container = initializeFingerprintContainer() 242 container.mBiometricCallback.onAction( 243 AuthBiometricView.Callback.ACTION_AUTHENTICATED 244 ) 245 waitForIdleSync() 246 247 verify(callback).onDismissed( 248 eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED), 249 eq<ByteArray?>(null), /* credentialAttestation */ 250 eq(authContainer?.requestId ?: 0L) 251 ) 252 assertThat(container.parent).isNull() 253 } 254 255 @Test 256 fun testActionUserCanceled_sendsDismissedUserCanceled() { 257 val container = initializeFingerprintContainer() 258 container.mBiometricCallback.onAction( 259 AuthBiometricView.Callback.ACTION_USER_CANCELED 260 ) 261 waitForIdleSync() 262 263 verify(callback).onSystemEvent( 264 eq(BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL), 265 eq(authContainer?.requestId ?: 0L) 266 ) 267 verify(callback).onDismissed( 268 eq(AuthDialogCallback.DISMISSED_USER_CANCELED), 269 eq<ByteArray?>(null), /* credentialAttestation */ 270 eq(authContainer?.requestId ?: 0L) 271 ) 272 assertThat(container.parent).isNull() 273 } 274 275 @Test 276 fun testActionButtonNegative_sendsDismissedButtonNegative() { 277 val container = initializeFingerprintContainer() 278 container.mBiometricCallback.onAction( 279 AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE 280 ) 281 waitForIdleSync() 282 283 verify(callback).onDismissed( 284 eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE), 285 eq<ByteArray?>(null), /* credentialAttestation */ 286 eq(authContainer?.requestId ?: 0L) 287 ) 288 assertThat(container.parent).isNull() 289 } 290 291 @Test 292 fun testActionTryAgain_sendsTryAgain() { 293 val container = initializeFingerprintContainer( 294 authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK 295 ) 296 container.mBiometricCallback.onAction( 297 AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN 298 ) 299 waitForIdleSync() 300 301 verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L) 302 } 303 304 @Test 305 fun testActionError_sendsDismissedError() { 306 val container = initializeFingerprintContainer() 307 container.mBiometricCallback.onAction( 308 AuthBiometricView.Callback.ACTION_ERROR 309 ) 310 waitForIdleSync() 311 312 verify(callback).onDismissed( 313 eq(AuthDialogCallback.DISMISSED_ERROR), 314 eq<ByteArray?>(null), /* credentialAttestation */ 315 eq(authContainer?.requestId ?: 0L) 316 ) 317 assertThat(authContainer!!.parent).isNull() 318 } 319 320 @Ignore("b/279650412") 321 @Test 322 fun testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() { 323 val container = initializeFingerprintContainer( 324 authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or 325 BiometricManager.Authenticators.DEVICE_CREDENTIAL 326 ) 327 container.mBiometricCallback.onAction( 328 AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL 329 ) 330 waitForIdleSync() 331 332 verify(callback).onDeviceCredentialPressed(authContainer?.requestId ?: 0L) 333 assertThat(container.hasCredentialView()).isTrue() 334 } 335 336 @Test 337 fun testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() { 338 val container = initializeFingerprintContainer( 339 authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or 340 BiometricManager.Authenticators.DEVICE_CREDENTIAL 341 ) 342 container.animateToCredentialUI(false) 343 waitForIdleSync() 344 345 assertThat(container.hasCredentialView()).isTrue() 346 } 347 348 @Test 349 fun testShowBiometricUI() { 350 val container = initializeFingerprintContainer() 351 352 waitForIdleSync() 353 354 assertThat(container.hasCredentialView()).isFalse() 355 assertThat(container.hasBiometricPrompt()).isTrue() 356 } 357 358 @Test 359 fun testShowCredentialUI() { 360 val container = initializeFingerprintContainer( 361 authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL 362 ) 363 waitForIdleSync() 364 365 assertThat(container.hasCredentialView()).isTrue() 366 assertThat(container.hasBiometricPrompt()).isFalse() 367 } 368 369 @Test 370 fun testCredentialViewUsesEffectiveUserId() { 371 whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200) 372 whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200))).thenReturn( 373 DevicePolicyManager.PASSWORD_QUALITY_SOMETHING 374 ) 375 376 val container = initializeFingerprintContainer( 377 authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL 378 ) 379 waitForIdleSync() 380 381 assertThat(container.hasCredentialPatternView()).isTrue() 382 assertThat(container.hasBiometricPrompt()).isFalse() 383 } 384 385 @Test 386 fun testCredentialUI_disablesClickingOnBackground() { 387 val container = initializeCredentialPasswordContainer() 388 assertThat(container.hasBiometricPrompt()).isFalse() 389 assertThat( 390 container.findViewById<View>(R.id.background)?.isImportantForAccessibility 391 ).isFalse() 392 393 container.findViewById<View>(R.id.background)?.performClick() 394 waitForIdleSync() 395 396 assertThat(container.hasCredentialPasswordView()).isTrue() 397 assertThat(container.hasBiometricPrompt()).isFalse() 398 } 399 400 @Test 401 fun testLayoutParams_hasSecureWindowFlag() { 402 val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") 403 assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0).isTrue() 404 } 405 406 @Test 407 fun testLayoutParams_hasShowWhenLockedFlag() { 408 val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") 409 assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) != 0) 410 .isTrue() 411 } 412 413 @Test 414 fun testLayoutParams_hasDimbehindWindowFlag() { 415 val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") 416 val lpFlags = layoutParams.flags 417 val lpDimAmount = layoutParams.dimAmount 418 419 assertThat((lpFlags and WindowManager.LayoutParams.FLAG_DIM_BEHIND) != 0).isTrue() 420 assertThat(lpDimAmount).isGreaterThan(0f) 421 } 422 423 @Test 424 fun testLayoutParams_excludesImeInsets() { 425 val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") 426 assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.ime()) == 0).isTrue() 427 } 428 429 @Test 430 fun coexFaceRestartsOnTouch() { 431 val container = initializeCoexContainer() 432 433 container.onPointerDown() 434 waitForIdleSync() 435 436 container.onAuthenticationFailed(BiometricAuthenticator.TYPE_FACE, "failed") 437 waitForIdleSync() 438 439 verify(callback, never()).onTryAgainPressed(anyLong()) 440 441 container.onPointerDown() 442 waitForIdleSync() 443 444 verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L) 445 } 446 447 private fun initializeCredentialPasswordContainer( 448 addToView: Boolean = true, 449 ): TestAuthContainerView { 450 whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20) 451 whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn( 452 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC 453 ) 454 455 // In the credential view, clicking on the background (to cancel authentication) is not 456 // valid. Thus, the listener should be null, and it should not be in the accessibility 457 // hierarchy. 458 val container = initializeFingerprintContainer( 459 authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, 460 addToView = addToView, 461 ) 462 waitForIdleSync() 463 464 assertThat(container.hasCredentialPasswordView()).isTrue() 465 return container 466 } 467 468 private fun initializeFingerprintContainer( 469 authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, 470 addToView: Boolean = true 471 ) = initializeContainer( 472 TestAuthContainerView( 473 authenticators = authenticators, 474 fingerprintProps = fingerprintSensorPropertiesInternal() 475 ), 476 addToView 477 ) 478 479 private fun initializeCoexContainer( 480 authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, 481 addToView: Boolean = true 482 ) = initializeContainer( 483 TestAuthContainerView( 484 authenticators = authenticators, 485 fingerprintProps = fingerprintSensorPropertiesInternal(), 486 faceProps = faceSensorPropertiesInternal() 487 ), 488 addToView 489 ) 490 491 private fun initializeContainer( 492 view: TestAuthContainerView, 493 addToView: Boolean 494 ): TestAuthContainerView { 495 authContainer = view 496 497 if (addToView) { 498 authContainer!!.addToView() 499 } 500 501 return authContainer!! 502 } 503 504 private inner class TestAuthContainerView( 505 authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, 506 fingerprintProps: List<FingerprintSensorPropertiesInternal> = listOf(), 507 faceProps: List<FaceSensorPropertiesInternal> = listOf() 508 ) : AuthContainerView( 509 Config().apply { 510 mContext = this@AuthContainerViewTest.context 511 mCallback = callback 512 mSensorIds = (fingerprintProps.map { it.sensorId } + 513 faceProps.map { it.sensorId }).toIntArray() 514 mSkipAnimation = true 515 mPromptInfo = PromptInfo().apply { 516 this.authenticators = authenticators 517 } 518 }, 519 featureFlags, 520 testScope.backgroundScope, 521 fingerprintProps, 522 faceProps, 523 wakefulnessLifecycle, 524 panelInteractionDetector, 525 userManager, 526 lockPatternUtils, 527 interactionJankMonitor, 528 { promptSelectorInteractor }, 529 { bpCredentialInteractor }, 530 PromptViewModel( 531 displayStateInteractor, 532 promptSelectorInteractor, 533 vibrator, 534 context, 535 featureFlags 536 ), 537 { credentialViewModel }, 538 Handler(TestableLooper.get(this).looper), 539 fakeExecutor, 540 vibrator 541 ) { 542 override fun postOnAnimation(runnable: Runnable) { 543 runnable.run() 544 } 545 } 546 547 override fun waitForIdleSync() { 548 testScope.runCurrent() 549 TestableLooper.get(this).processAllMessages() 550 } 551 552 private fun AuthContainerView.addToView() { 553 ViewUtils.attachView(this) 554 waitForIdleSync() 555 assertThat(isAttachedToWindow()).isTrue() 556 } 557 558 @Test 559 fun testLayoutParams_hasCutoutModeAlwaysFlag() { 560 val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") 561 val lpFlags = layoutParams.flags 562 563 assertThat((lpFlags and WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) 564 != 0).isTrue() 565 } 566 567 @Test 568 fun testLayoutParams_excludesSystemBarInsets() { 569 val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") 570 assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.systemBars()) == 0).isTrue() 571 } 572 } 573 574 private fun AuthContainerView.hasBiometricPrompt() = 575 (findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0 576 577 private fun AuthContainerView.hasCredentialView() = 578 hasCredentialPatternView() || hasCredentialPasswordView() 579 580 private fun AuthContainerView.hasCredentialPatternView() = 581 findViewById<View>(R.id.lockPattern) != null 582 583 private fun AuthContainerView.hasCredentialPasswordView() = 584 findViewById<View>(R.id.lockPassword) != null 585