Merge "Implement basic Fingerprint functionality." into udc-qpr-dev am: 1412f391d5 am: 22b67802f5

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Settings/+/23480676

Change-Id: I981accee2b7f0f3742c98d1b352d3076d421dede
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Joshua Mccloskey
2023-07-18 17:28:29 +00:00
committed by Automerger Merge Worker
18 changed files with 2658 additions and 737 deletions

View File

@@ -21,6 +21,7 @@ android_test {
],
static_libs: [
"androidx.arch.core_core-testing",
"androidx.test.core",
"androidx.test.rules",
"androidx.test.espresso.core",

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fingerprint2.domain.interactor
import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/** Fake to be used by other classes to easily fake the FingerprintManager implementation. */
class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
var enrollableFingerprints: Int = 5
var enrolledFingerprintsInternal: MutableList<FingerprintViewModel> = mutableListOf()
var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1)
var pressToAuthEnabled = true
var sensorProps =
listOf(
FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
emptyList() /* ComponentInfoInternal */,
TYPE_POWER_BUTTON,
true /* resetLockoutRequiresHardwareAuthToken */
)
)
override suspend fun authenticate(): FingerprintAuthAttemptViewModel {
return authenticateAttempt
}
override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> {
return challengeToGenerate
}
override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
emit(enrolledFingerprintsInternal)
}
override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
emit(numFingerprints < enrollableFingerprints)
}
override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean {
return enrolledFingerprintsInternal.remove(fp)
}
override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {}
override suspend fun hasSideFps(): Boolean {
return sensorProps.any { it.isAnySidefpsType }
}
override suspend fun pressToAuthEnabled(): Boolean {
return pressToAuthEnabled
}
override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
sensorProps
}

View File

@@ -0,0 +1,287 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fingerprint2.domain.interactor
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.hardware.fingerprint.Fingerprint
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintManager.CryptoObject
import android.hardware.fingerprint.FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT
import android.os.CancellationSignal
import android.os.Handler
import androidx.test.core.app.ApplicationProvider
import com.android.settings.biometrics.GatekeeperPasswordProvider
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
import com.android.settings.password.ChooseLockSettingsHelper
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.eq
import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
class FingerprintManagerInteractorTest {
@JvmField @Rule var rule = MockitoJUnit.rule()
private lateinit var underTest: FingerprintManagerInteractor
private var context: Context = ApplicationProvider.getApplicationContext()
private var backgroundDispatcher = StandardTestDispatcher()
@Mock private lateinit var fingerprintManager: FingerprintManager
@Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider
private var testScope = TestScope(backgroundDispatcher)
private var pressToAuthProvider = { true }
@Before
fun setup() {
underTest =
FingerprintManagerInteractorImpl(
context,
backgroundDispatcher,
fingerprintManager,
gateKeeperPasswordProvider,
pressToAuthProvider,
)
}
@Test
fun testEmptyFingerprints() =
testScope.runTest {
Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
.thenReturn(emptyList())
val emptyFingerprintList: List<Fingerprint> = emptyList()
assertThat(underTest.enrolledFingerprints.last()).isEqualTo(emptyFingerprintList)
}
@Test
fun testOneFingerprint() =
testScope.runTest {
val expected = Fingerprint("Finger 1,", 2, 3L)
val fingerprintList: List<Fingerprint> = listOf(expected)
Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
.thenReturn(fingerprintList)
val list = underTest.enrolledFingerprints.last()
assertThat(list.size).isEqualTo(fingerprintList.size)
val actual = list[0]
assertThat(actual.name).isEqualTo(expected.name)
assertThat(actual.fingerId).isEqualTo(expected.biometricId)
assertThat(actual.deviceId).isEqualTo(expected.deviceId)
}
@Test
fun testCanEnrollFingerprint() =
testScope.runTest {
val mockContext = Mockito.mock(Context::class.java)
val resources = Mockito.mock(Resources::class.java)
Mockito.`when`(mockContext.resources).thenReturn(resources)
Mockito.`when`(resources.getInteger(anyInt())).thenReturn(3)
underTest =
FingerprintManagerInteractorImpl(
mockContext,
backgroundDispatcher,
fingerprintManager,
gateKeeperPasswordProvider,
pressToAuthProvider,
)
assertThat(underTest.canEnrollFingerprints(2).last()).isTrue()
assertThat(underTest.canEnrollFingerprints(3).last()).isFalse()
}
@Test
fun testGenerateChallenge() =
testScope.runTest {
val byteArray = byteArrayOf(5, 3, 2)
val challenge = 100L
val intent = Intent()
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, challenge)
Mockito.`when`(
gateKeeperPasswordProvider.requestGatekeeperHat(
any(Intent::class.java),
anyLong(),
anyInt()
)
)
.thenReturn(byteArray)
val generateChallengeCallback: ArgumentCaptor<FingerprintManager.GenerateChallengeCallback> =
ArgumentCaptor.forClass(FingerprintManager.GenerateChallengeCallback::class.java)
var result: Pair<Long, ByteArray?>? = null
val job = testScope.launch { result = underTest.generateChallenge(1L) }
runCurrent()
Mockito.verify(fingerprintManager)
.generateChallenge(anyInt(), capture(generateChallengeCallback))
generateChallengeCallback.value.onChallengeGenerated(1, 2, challenge)
runCurrent()
job.cancelAndJoin()
assertThat(result?.first).isEqualTo(challenge)
assertThat(result?.second).isEqualTo(byteArray)
}
@Test
fun testRemoveFingerprint_succeeds() =
testScope.runTest {
val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
var result: Boolean? = null
val job =
testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
runCurrent()
Mockito.verify(fingerprintManager)
.remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
removalCallback.value.onRemovalSucceeded(fingerprintToRemove, 1)
runCurrent()
job.cancelAndJoin()
assertThat(result).isTrue()
}
@Test
fun testRemoveFingerprint_fails() =
testScope.runTest {
val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
var result: Boolean? = null
val job =
testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
runCurrent()
Mockito.verify(fingerprintManager)
.remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
removalCallback.value.onRemovalError(
fingerprintToRemove,
100,
"Oh no, we couldn't find that one"
)
runCurrent()
job.cancelAndJoin()
assertThat(result).isFalse()
}
@Test
fun testRenameFingerprint_succeeds() =
testScope.runTest {
val fingerprintToRename = FingerprintViewModel("Finger 2", 1, 2L)
underTest.renameFingerprint(fingerprintToRename, "Woo")
Mockito.verify(fingerprintManager)
.rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo"))
}
@Test
fun testAuth_succeeds() =
testScope.runTest {
val fingerprint = Fingerprint("Woooo", 100, 101L)
var result: FingerprintAuthAttemptViewModel? = null
val job = launch { result = underTest.authenticate() }
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
runCurrent()
Mockito.verify(fingerprintManager)
.authenticate(
nullable(CryptoObject::class.java),
any(CancellationSignal::class.java),
capture(authCallback),
nullable(Handler::class.java),
anyInt()
)
authCallback.value.onAuthenticationSucceeded(
FingerprintManager.AuthenticationResult(null, fingerprint, 1, false)
)
runCurrent()
job.cancelAndJoin()
assertThat(result).isEqualTo(FingerprintAuthAttemptViewModel.Success(fingerprint.biometricId))
}
@Test
fun testAuth_lockout() =
testScope.runTest {
var result: FingerprintAuthAttemptViewModel? = null
val job = launch { result = underTest.authenticate() }
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
runCurrent()
Mockito.verify(fingerprintManager)
.authenticate(
nullable(CryptoObject::class.java),
any(CancellationSignal::class.java),
capture(authCallback),
nullable(Handler::class.java),
anyInt()
)
authCallback.value.onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
runCurrent()
job.cancelAndJoin()
assertThat(result)
.isEqualTo(
FingerprintAuthAttemptViewModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
)
}
private fun <T : Any> safeEq(value: T): T = eq(value) ?: value
private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
}

View File

@@ -0,0 +1,275 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fingerprint2.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
import com.android.settings.fingerprint2.domain.interactor.FakeFingerprintManagerInteractor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
class FingerprintSettingsNavigationViewModelTest {
@JvmField @Rule var rule = MockitoJUnit.rule()
@get:Rule val instantTaskRule = InstantTaskExecutorRule()
private lateinit var underTest: FingerprintSettingsNavigationViewModel
private val defaultUserId = 0
private var backgroundDispatcher = StandardTestDispatcher()
private var testScope = TestScope(backgroundDispatcher)
private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
@Before
fun setup() {
fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
backgroundDispatcher = StandardTestDispatcher()
testScope = TestScope(backgroundDispatcher)
Dispatchers.setMain(backgroundDispatcher)
underTest =
FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
defaultUserId,
fakeFingerprintManagerInteractor,
backgroundDispatcher,
null,
null,
)
.create(FingerprintSettingsNavigationViewModel::class.java)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun testNoGateKeeper_launchesConfirmDeviceCredential() =
testScope.runTest {
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
runCurrent()
assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
job.cancel()
}
@Test
fun testConfirmDevice_fails() =
testScope.runTest {
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(false, null)
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun confirmDeviceSuccess_noGateKeeper() =
testScope.runTest {
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(true, null)
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(true, 10L)
runCurrent()
assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
job.cancel()
}
@Test
fun firstEnrollment_fails() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirstFailure("We failed!!")
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun firstEnrollment_failsWithReason() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
val failStr = "We failed!!"
val failReason = 101
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirstFailure(failStr, failReason)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
job.cancel()
}
@Test
fun firstEnrollmentSucceeds_noToken() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(null, null)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
job.cancel()
}
@Test
fun firstEnrollmentSucceeds_noKeyChallenge() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
val byteArray = ByteArray(1) { 3 }
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(byteArray, null)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
job.cancel()
}
@Test
fun firstEnrollment_succeeds() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
var nextStep: NextStepViewModel? = null
val job = testScope.launch { underTest.nextStep.collect { nextStep = it } }
val byteArray = ByteArray(1) { 3 }
val keyChallenge = 89L
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(byteArray, keyChallenge)
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings)
job.cancel()
}
@Test
fun enrollAdditionalFingerprints_fails() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(FingerprintViewModel("a", 1, 3L))
fakeFingerprintManagerInteractor.challengeToGenerate = Pair(4L, byteArrayOf(3, 3, 1))
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(true, 10L)
runCurrent()
underTest.onEnrollAdditionalFailure()
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun enrollAdditional_success() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(FingerprintViewModel("a", 1, 3L))
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollSuccess()
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings)
job.cancel()
}
@Test
fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() =
testScope.runTest {
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(FingerprintViewModel("a", 1, 3L))
fakeFingerprintManagerInteractor.challengeToGenerate = Pair(10L, byteArrayOf(1, 2, 3))
var nextStep: NextStepViewModel? = null
val job = launch { underTest.nextStep.collect { nextStep = it } }
underTest.onConfirmDevice(true, 10L)
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings)
job.cancel()
}
}

View File

@@ -16,317 +16,232 @@
package com.android.settings.fingerprint2.viewmodel
import android.hardware.fingerprint.Fingerprint
import android.hardware.fingerprint.FingerprintManager
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
import com.android.settings.fingerprint2.domain.interactor.FakeFingerprintManagerInteractor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.Mockito.`when` as whenever
@RunWith(MockitoJUnitRunner::class)
class FingerprintSettingsViewModelTest {
@JvmField
@Rule
var rule = MockitoJUnit.rule()
@JvmField @Rule var rule = MockitoJUnit.rule()
@Mock
private lateinit var fingerprintManager: FingerprintManager
private lateinit var underTest: FingerprintSettingsViewModel
private val defaultUserId = 0
@get:Rule val instantTaskRule = InstantTaskExecutorRule()
@Before
fun setup() {
// @formatter:off
underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
private lateinit var underTest: FingerprintSettingsViewModel
private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
private val defaultUserId = 0
private var backgroundDispatcher = StandardTestDispatcher()
private var testScope = TestScope(backgroundDispatcher)
private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
@Before
fun setup() {
fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
backgroundDispatcher = StandardTestDispatcher()
testScope = TestScope(backgroundDispatcher)
Dispatchers.setMain(backgroundDispatcher)
navigationViewModel =
FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
defaultUserId,
fakeFingerprintManagerInteractor,
backgroundDispatcher,
null,
null,
)
.create(FingerprintSettingsNavigationViewModel::class.java)
underTest =
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
defaultUserId,
fakeFingerprintManagerInteractor,
backgroundDispatcher,
navigationViewModel,
)
.create(FingerprintSettingsViewModel::class.java)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun authenticate_DoesNotRun_ifOptical() =
testScope.runTest {
fakeFingerprintManagerInteractor.sensorProps =
listOf(
FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
emptyList() /* ComponentInfoInternal */,
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
true /* resetLockoutRequiresHardwareAuthToken */
)
)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(FingerprintViewModel("a", 1, 3L))
underTest =
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
defaultUserId,
fingerprintManager,
).create(FingerprintSettingsViewModel::class.java)
// @formatter:on
fakeFingerprintManagerInteractor,
backgroundDispatcher,
navigationViewModel,
)
.create(FingerprintSettingsViewModel::class.java)
var authAttempt: FingerprintAuthAttemptViewModel? = null
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
underTest.shouldAuthenticate(true)
// Ensure we are showing settings
navigationViewModel.onConfirmDevice(true, 10L)
runCurrent()
advanceTimeBy(400)
assertThat(authAttempt).isNull()
job.cancel()
}
@Test
fun testNoGateKeeper_launchesConfirmDeviceCredential() = runTest {
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
runCurrent()
assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
job.cancel()
}
@Test
fun testConfirmDevice_fails() = runTest {
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(false, null)
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun confirmDeviceSuccess_noGateKeeper() = runTest {
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, null)
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
runCurrent()
assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
job.cancel()
}
@Test
fun firstEnrollment_fails() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirstFailure("We failed!!")
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
}
@Test
fun firstEnrollment_failsWithReason() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
val failStr = "We failed!!"
val failReason = 101
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirstFailure(failStr, failReason)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
job.cancel()
}
@Test
fun firstEnrollmentSucceeds_noToken() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(null, null)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
job.cancel()
}
@Test
fun firstEnrollmentSucceeds_noKeyChallenge() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
val byteArray = ByteArray(1) {
3
}
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(byteArray, null)
runCurrent()
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
job.cancel()
}
@Test
fun firstEnrollment_succeeds() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
val byteArray = ByteArray(1) {
3
}
val keyChallenge = 89L
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollFirst(byteArray, keyChallenge)
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
job.cancel()
}
@Test
fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
listOf(
Fingerprint(
"a", 1, 2, 3L
)
)
@Test
fun authenticate_DoesNotRun_ifUltrasonic() =
testScope.runTest {
fakeFingerprintManagerInteractor.sensorProps =
listOf(
FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
emptyList() /* ComponentInfoInternal */,
FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
true /* resetLockoutRequiresHardwareAuthToken */
)
)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(FingerprintViewModel("a", 1, 3L))
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest =
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
defaultUserId,
fakeFingerprintManagerInteractor,
backgroundDispatcher,
navigationViewModel,
)
.create(FingerprintSettingsViewModel::class.java)
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
var authAttempt: FingerprintAuthAttemptViewModel? = null
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
runCurrent()
underTest.shouldAuthenticate(true)
navigationViewModel.onConfirmDevice(true, 10L)
advanceTimeBy(400)
runCurrent()
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
job.cancel()
assertThat(authAttempt).isNull()
job.cancel()
}
@Test
fun enrollAdditionalFingerprints_fails() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
listOf(
Fingerprint(
"a", 1, 2, 3L
)
)
@Test
fun authenticate_DoesRun_ifNotUdfps() =
testScope.runTest {
fakeFingerprintManagerInteractor.sensorProps =
listOf(
FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
emptyList() /* ComponentInfoInternal */,
FingerprintSensorProperties.TYPE_POWER_BUTTON,
true /* resetLockoutRequiresHardwareAuthToken */
)
)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
mutableListOf(FingerprintViewModel("a", 1, 3L))
val success = FingerprintAuthAttemptViewModel.Success(1)
fakeFingerprintManagerInteractor.authenticateAttempt = success
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
underTest =
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
defaultUserId,
fakeFingerprintManagerInteractor,
backgroundDispatcher,
navigationViewModel,
)
.create(FingerprintSettingsViewModel::class.java)
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollAdditionalFailure()
var authAttempt: FingerprintAuthAttemptViewModel? = null
runCurrent()
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
underTest.shouldAuthenticate(true)
navigationViewModel.onConfirmDevice(true, 10L)
advanceTimeBy(400)
runCurrent()
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
job.cancel()
assertThat(authAttempt).isEqualTo(success)
job.cancel()
}
@Test
fun enrollAdditional_success() = runTest {
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
listOf(
Fingerprint(
"a", 1, 2, 3L
)
)
@Test
fun deleteDialog_showAndDismiss() = runTest {
val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete)
underTest =
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
defaultUserId,
fakeFingerprintManagerInteractor,
backgroundDispatcher,
navigationViewModel,
)
.create(FingerprintSettingsViewModel::class.java)
var nextStep: NextStepViewModel? = null
val job = launch {
underTest.nextStep.collect {
nextStep = it
}
}
var dialog: PreferenceViewModel? = null
val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } }
underTest.updateTokenAndChallenge(null, null)
underTest.onConfirmDevice(true, 10L)
underTest.onEnrollSuccess()
// Move to the ShowSettings state
navigationViewModel.onConfirmDevice(true, 10L)
runCurrent()
underTest.onDeleteClicked(fingerprintToDelete)
runCurrent()
runCurrent()
assertThat(dialog is PreferenceViewModel.DeleteDialog)
assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete))
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
job.cancel()
}
}
underTest.deleteFingerprint(fingerprintToDelete)
underTest.onDeleteDialogFinished()
runCurrent()
assertThat(dialog).isNull()
dialogJob.cancel()
}
}