1 /* 2 * 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.settings.fingerprint2.domain.interactor 18 19 import android.content.Intent 20 import android.hardware.biometrics.ComponentInfoInternal 21 import android.hardware.biometrics.SensorLocationInternal 22 import android.hardware.biometrics.SensorProperties 23 import android.hardware.fingerprint.Fingerprint 24 import android.hardware.fingerprint.FingerprintEnrollOptions 25 import android.hardware.fingerprint.FingerprintManager 26 import android.hardware.fingerprint.FingerprintManager.CryptoObject 27 import android.hardware.fingerprint.FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT 28 import android.hardware.fingerprint.FingerprintSensorProperties 29 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal 30 import android.os.CancellationSignal 31 import android.os.Handler 32 import com.android.settings.biometrics.GatekeeperPasswordProvider 33 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintEnrollmentRepository 34 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintEnrollmentRepositoryImpl 35 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository 36 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSettingsRepositoryImpl 37 import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo 38 import com.android.settings.biometrics.fingerprint2.domain.interactor.AuthenticateInteractorImpl 39 import com.android.settings.biometrics.fingerprint2.domain.interactor.CanEnrollFingerprintsInteractorImpl 40 import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollFingerprintInteractorImpl 41 import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrolledFingerprintsInteractorImpl 42 import com.android.settings.biometrics.fingerprint2.domain.interactor.GenerateChallengeInteractorImpl 43 import com.android.settings.biometrics.fingerprint2.domain.interactor.RemoveFingerprintsInteractorImpl 44 import com.android.settings.biometrics.fingerprint2.domain.interactor.RenameFingerprintsInteractorImpl 45 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnrollFingerprintsInteractor 46 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrollFingerprintInteractor 47 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrolledFingerprintsInteractor 48 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor 49 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RemoveFingerprintInteractor 50 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RenameFingerprintInteractor 51 import com.android.settings.biometrics.fingerprint2.lib.model.Default 52 import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason 53 import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState 54 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel 55 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData 56 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow 57 import com.android.settings.password.ChooseLockSettingsHelper 58 import com.android.systemui.biometrics.shared.model.FingerprintSensor 59 import com.android.systemui.biometrics.shared.model.toFingerprintSensor 60 import com.google.common.truth.Truth.assertThat 61 import kotlinx.coroutines.cancelAndJoin 62 import kotlinx.coroutines.flow.Flow 63 import kotlinx.coroutines.flow.MutableStateFlow 64 import kotlinx.coroutines.flow.flowOf 65 import kotlinx.coroutines.flow.update 66 import kotlinx.coroutines.launch 67 import kotlinx.coroutines.test.StandardTestDispatcher 68 import kotlinx.coroutines.test.TestScope 69 import kotlinx.coroutines.test.runCurrent 70 import kotlinx.coroutines.test.runTest 71 import org.junit.Before 72 import org.junit.Rule 73 import org.junit.Test 74 import org.junit.runner.RunWith 75 import org.mockito.ArgumentCaptor 76 import org.mockito.ArgumentMatchers.anyInt 77 import org.mockito.ArgumentMatchers.anyLong 78 import org.mockito.ArgumentMatchers.eq 79 import org.mockito.ArgumentMatchers.nullable 80 import org.mockito.Mock 81 import org.mockito.Mockito 82 import org.mockito.Mockito.verify 83 import org.mockito.Mockito.`when` 84 import org.mockito.junit.MockitoJUnit 85 import org.mockito.junit.MockitoJUnitRunner 86 import org.mockito.stubbing.OngoingStubbing 87 88 @RunWith(MockitoJUnitRunner::class) 89 class FingerprintManagerInteractorTest { 90 91 @JvmField @Rule var rule = MockitoJUnit.rule() 92 private lateinit var enrolledFingerprintsInteractorUnderTest: EnrolledFingerprintsInteractor 93 private lateinit var generateChallengeInteractorUnderTest: GenerateChallengeInteractor 94 private lateinit var removeFingerprintsInteractorUnderTest: RemoveFingerprintInteractor 95 private lateinit var renameFingerprintsInteractorUnderTest: RenameFingerprintInteractor 96 private lateinit var authenticateInteractorImplUnderTest: AuthenticateInteractorImpl 97 private lateinit var canEnrollFingerprintsInteractorUnderTest: CanEnrollFingerprintsInteractor 98 private lateinit var enrollInteractorUnderTest: EnrollFingerprintInteractor 99 100 private val userId = 0 101 private var backgroundDispatcher = StandardTestDispatcher() 102 @Mock private lateinit var fingerprintManager: FingerprintManager 103 @Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider 104 105 private var testScope = TestScope(backgroundDispatcher) 106 private var backgroundScope = testScope.backgroundScope 107 private val flow: FingerprintFlow = Default 108 private val maxFingerprints = 5 109 private val currUser = MutableStateFlow(0) 110 private lateinit var fingerprintEnrollRepo: FingerprintEnrollmentRepository 111 private val userRepo = 112 object : UserRepo { 113 override val currentUser: Flow<Int> = currUser 114 updateUsernull115 override fun updateUser(user: Int) { 116 currUser.update { user } 117 } 118 } 119 120 @Before setupnull121 fun setup() { 122 val sensor = 123 FingerprintSensorPropertiesInternal( 124 0 /* sensorId */, 125 SensorProperties.STRENGTH_STRONG, 126 maxFingerprints, 127 listOf<ComponentInfoInternal>(), 128 FingerprintSensorProperties.TYPE_POWER_BUTTON, 129 false /* halControlsIllumination */, 130 true /* resetLockoutRequiresHardwareAuthToken */, 131 listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT), 132 ) 133 .toFingerprintSensor() 134 135 val fingerprintSensorRepository = 136 object : FingerprintSensorRepository { 137 override val fingerprintSensor: Flow<FingerprintSensor> = flowOf(sensor) 138 override val hasSideFps: Flow<Boolean> = flowOf(false) 139 } 140 141 val settingsRepository = FingerprintSettingsRepositoryImpl(maxFingerprints) 142 fingerprintEnrollRepo = 143 FingerprintEnrollmentRepositoryImpl( 144 fingerprintManager, 145 userRepo, 146 settingsRepository, 147 backgroundDispatcher, 148 backgroundScope, 149 fingerprintSensorRepository, 150 ) 151 152 enrolledFingerprintsInteractorUnderTest = 153 EnrolledFingerprintsInteractorImpl(fingerprintEnrollRepo) 154 generateChallengeInteractorUnderTest = 155 GenerateChallengeInteractorImpl(fingerprintManager, userRepo, gateKeeperPasswordProvider) 156 removeFingerprintsInteractorUnderTest = 157 RemoveFingerprintsInteractorImpl(fingerprintManager, userRepo) 158 renameFingerprintsInteractorUnderTest = 159 RenameFingerprintsInteractorImpl(fingerprintManager, userRepo, backgroundDispatcher) 160 authenticateInteractorImplUnderTest = AuthenticateInteractorImpl(fingerprintManager, userRepo) 161 162 canEnrollFingerprintsInteractorUnderTest = 163 CanEnrollFingerprintsInteractorImpl(fingerprintEnrollRepo) 164 165 enrollInteractorUnderTest = EnrollFingerprintInteractorImpl(userRepo, fingerprintManager, flow) 166 } 167 168 @Test testEmptyFingerprintsnull169 fun testEmptyFingerprints() = 170 testScope.runTest { 171 whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList()) 172 173 var list: List<FingerprintData>? = null 174 val job = 175 testScope.launch { 176 enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.collect { list = it } 177 } 178 179 runCurrent() 180 job.cancelAndJoin() 181 182 assertThat(list!!.isEmpty()) 183 } 184 185 @Test testOneFingerprintnull186 fun testOneFingerprint() = 187 testScope.runTest { 188 val expected = Fingerprint("Finger 1,", 2, 3L) 189 val fingerprintList: List<Fingerprint> = listOf(expected) 190 whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) 191 // This causes the enrolled fingerprints to be updated 192 193 var list: List<FingerprintData>? = null 194 val job = 195 testScope.launch { 196 enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.collect { list = it } 197 } 198 199 runCurrent() 200 job.cancelAndJoin() 201 202 assertThat(list!!.size).isEqualTo(fingerprintList.size) 203 val actual = list!![0] 204 assertThat(actual.name).isEqualTo(expected.name) 205 assertThat(actual.fingerId).isEqualTo(expected.biometricId) 206 assertThat(actual.deviceId).isEqualTo(expected.deviceId) 207 } 208 209 @Test testCanEnrollFingerprintSucceedsnull210 fun testCanEnrollFingerprintSucceeds() = 211 testScope.runTest { 212 val fingerprintList: List<Fingerprint> = 213 listOf( 214 Fingerprint("Finger 1", 2, 3L), 215 Fingerprint("Finger 2", 3, 3L), 216 Fingerprint("Finger 3", 4, 3L), 217 ) 218 whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) 219 220 var result: Boolean? = null 221 val job = 222 testScope.launch { 223 canEnrollFingerprintsInteractorUnderTest.canEnrollFingerprints.collect { result = it } 224 } 225 226 runCurrent() 227 job.cancelAndJoin() 228 229 assertThat(result).isTrue() 230 } 231 232 @Test testCanEnrollFingerprintFailsnull233 fun testCanEnrollFingerprintFails() = 234 testScope.runTest { 235 val fingerprintList: List<Fingerprint> = 236 listOf( 237 Fingerprint("Finger 1", 2, 3L), 238 Fingerprint("Finger 2", 3, 3L), 239 Fingerprint("Finger 3", 4, 3L), 240 Fingerprint("Finger 4", 5, 3L), 241 Fingerprint("Finger 5", 6, 3L), 242 ) 243 whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) 244 245 var result: Boolean? = null 246 val job = testScope.launch { fingerprintEnrollRepo.canEnrollUser.collect { result = it } } 247 runCurrent() 248 job.cancelAndJoin() 249 250 assertThat(result).isFalse() 251 } 252 253 @Test testGenerateChallengenull254 fun testGenerateChallenge() = 255 testScope.runTest { 256 val byteArray = byteArrayOf(5, 3, 2) 257 val challenge = 100L 258 val intent = Intent() 259 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, challenge) 260 whenever( 261 gateKeeperPasswordProvider.requestGatekeeperHat( 262 any(Intent::class.java), 263 anyLong(), 264 anyInt(), 265 ) 266 ) 267 .thenReturn(byteArray) 268 269 val generateChallengeCallback: ArgumentCaptor<FingerprintManager.GenerateChallengeCallback> = 270 argumentCaptor() 271 272 var result: Pair<Long, ByteArray?>? = null 273 val job = 274 testScope.launch { result = generateChallengeInteractorUnderTest.generateChallenge(1L) } 275 runCurrent() 276 277 verify(fingerprintManager).generateChallenge(anyInt(), capture(generateChallengeCallback)) 278 generateChallengeCallback.value.onChallengeGenerated(1, 2, challenge) 279 280 runCurrent() 281 job.cancelAndJoin() 282 283 assertThat(result?.first).isEqualTo(challenge) 284 assertThat(result?.second).isEqualTo(byteArray) 285 } 286 287 @Test testRemoveFingerprint_succeedsnull288 fun testRemoveFingerprint_succeeds() = 289 testScope.runTest { 290 val fingerprintViewModelToRemove = FingerprintData("Finger 2", 1, 2L) 291 val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) 292 293 val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = argumentCaptor() 294 295 var result: Boolean? = null 296 val job = 297 testScope.launch { 298 result = 299 removeFingerprintsInteractorUnderTest.removeFingerprint(fingerprintViewModelToRemove) 300 } 301 runCurrent() 302 303 verify(fingerprintManager) 304 .remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback)) 305 removalCallback.value.onRemovalSucceeded(fingerprintToRemove, 1) 306 307 runCurrent() 308 job.cancelAndJoin() 309 310 assertThat(result).isTrue() 311 } 312 313 @Test testRemoveFingerprint_failsnull314 fun testRemoveFingerprint_fails() = 315 testScope.runTest { 316 val fingerprintViewModelToRemove = FingerprintData("Finger 2", 1, 2L) 317 val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) 318 319 val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = argumentCaptor() 320 321 var result: Boolean? = null 322 val job = 323 testScope.launch { 324 result = 325 removeFingerprintsInteractorUnderTest.removeFingerprint(fingerprintViewModelToRemove) 326 } 327 runCurrent() 328 329 verify(fingerprintManager) 330 .remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback)) 331 removalCallback.value.onRemovalError( 332 fingerprintToRemove, 333 100, 334 "Oh no, we couldn't find that one", 335 ) 336 337 runCurrent() 338 job.cancelAndJoin() 339 340 assertThat(result).isFalse() 341 } 342 343 @Test testRenameFingerprint_succeedsnull344 fun testRenameFingerprint_succeeds() = 345 testScope.runTest { 346 val fingerprintToRename = FingerprintData("Finger 2", 1, 2L) 347 348 renameFingerprintsInteractorUnderTest.renameFingerprint(fingerprintToRename, "Woo") 349 350 verify(fingerprintManager).rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo")) 351 } 352 353 @Test testAuth_succeedsnull354 fun testAuth_succeeds() = 355 testScope.runTest { 356 val fingerprint = Fingerprint("Woooo", 100, 101L) 357 358 var result: FingerprintAuthAttemptModel? = null 359 val job = launch { result = authenticateInteractorImplUnderTest.authenticate() } 360 361 val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = argumentCaptor() 362 363 runCurrent() 364 365 verify(fingerprintManager) 366 .authenticate( 367 nullable(CryptoObject::class.java), 368 any(CancellationSignal::class.java), 369 capture(authCallback), 370 nullable(Handler::class.java), 371 anyInt(), 372 ) 373 authCallback.value.onAuthenticationSucceeded( 374 FingerprintManager.AuthenticationResult(null, fingerprint, 1, false) 375 ) 376 377 runCurrent() 378 job.cancelAndJoin() 379 assertThat(result).isEqualTo(FingerprintAuthAttemptModel.Success(fingerprint.biometricId)) 380 } 381 382 @Test testAuth_lockoutnull383 fun testAuth_lockout() = 384 testScope.runTest { 385 var result: FingerprintAuthAttemptModel? = null 386 val job = launch { result = authenticateInteractorImplUnderTest.authenticate() } 387 388 val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = argumentCaptor() 389 390 runCurrent() 391 392 verify(fingerprintManager) 393 .authenticate( 394 nullable(CryptoObject::class.java), 395 any(CancellationSignal::class.java), 396 capture(authCallback), 397 nullable(Handler::class.java), 398 anyInt(), 399 ) 400 authCallback.value.onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!") 401 402 runCurrent() 403 job.cancelAndJoin() 404 assertThat(result) 405 .isEqualTo( 406 FingerprintAuthAttemptModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!") 407 ) 408 } 409 410 @Test testEnroll_progressnull411 fun testEnroll_progress() = 412 testScope.runTest { 413 val token = byteArrayOf(5, 3, 2) 414 var result: FingerEnrollState? = null 415 val job = launch { 416 enrollInteractorUnderTest 417 .enroll(token, EnrollReason.FindSensor, FingerprintEnrollOptions.Builder().build()) 418 .collect { result = it } 419 } 420 val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor() 421 runCurrent() 422 423 verify(fingerprintManager) 424 .enroll( 425 eq(token), 426 any(CancellationSignal::class.java), 427 anyInt(), 428 capture(enrollCallback), 429 eq(FingerprintManager.ENROLL_FIND_SENSOR), 430 any(FingerprintEnrollOptions::class.java), 431 ) 432 enrollCallback.value.onEnrollmentProgress(1) 433 runCurrent() 434 job.cancelAndJoin() 435 436 assertThat(result).isEqualTo(FingerEnrollState.EnrollProgress(1, 2)) 437 } 438 439 @Test testEnroll_helpnull440 fun testEnroll_help() = 441 testScope.runTest { 442 val token = byteArrayOf(5, 3, 2) 443 var result: FingerEnrollState? = null 444 val job = launch { 445 enrollInteractorUnderTest 446 .enroll(token, EnrollReason.FindSensor, FingerprintEnrollOptions.Builder().build()) 447 .collect { result = it } 448 } 449 val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor() 450 runCurrent() 451 452 verify(fingerprintManager) 453 .enroll( 454 eq(token), 455 any(CancellationSignal::class.java), 456 anyInt(), 457 capture(enrollCallback), 458 eq(FingerprintManager.ENROLL_FIND_SENSOR), 459 any(FingerprintEnrollOptions::class.java), 460 ) 461 enrollCallback.value.onEnrollmentHelp(-1, "help") 462 runCurrent() 463 job.cancelAndJoin() 464 465 assertThat(result).isEqualTo(FingerEnrollState.EnrollHelp(-1, "help")) 466 } 467 468 @Test testEnroll_errornull469 fun testEnroll_error() = 470 testScope.runTest { 471 val token = byteArrayOf(5, 3, 2) 472 var result: FingerEnrollState? = null 473 val job = launch { 474 enrollInteractorUnderTest 475 .enroll(token, EnrollReason.FindSensor, FingerprintEnrollOptions.Builder().build()) 476 .collect { result = it } 477 } 478 val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor() 479 runCurrent() 480 481 verify(fingerprintManager) 482 .enroll( 483 eq(token), 484 any(CancellationSignal::class.java), 485 anyInt(), 486 capture(enrollCallback), 487 eq(FingerprintManager.ENROLL_FIND_SENSOR), 488 any(FingerprintEnrollOptions::class.java), 489 ) 490 enrollCallback.value.onEnrollmentError(-1, "error") 491 runCurrent() 492 job.cancelAndJoin() 493 assertThat(result).isInstanceOf(FingerEnrollState.EnrollError::class.java) 494 } 495 safeEqnull496 private fun <T : Any> safeEq(value: T): T = eq(value) ?: value 497 498 private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() 499 500 private fun <T> any(type: Class<T>): T = Mockito.any<T>(type) 501 502 private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) 503 504 inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = 505 ArgumentCaptor.forClass(T::class.java) 506 } 507