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