Merge "Propogate face unlock always require confirmation setting to biometric prompt." into udc-d1-dev

This commit is contained in:
Joe Bolinger
2023-06-13 21:05:45 +00:00
committed by Android (Google) Code Review
15 changed files with 480 additions and 66 deletions

View File

@@ -374,7 +374,6 @@ public class AuthContainerView extends LinearLayout
if (Utils.isBiometricAllowed(config.mPromptInfo)) { if (Utils.isBiometricAllowed(config.mPromptInfo)) {
mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication( mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication(
config.mPromptInfo, config.mPromptInfo,
config.mRequireConfirmation,
config.mUserId, config.mUserId,
config.mOperationId, config.mOperationId,
new BiometricModalities(fpProps, faceProps)); new BiometricModalities(fpProps, faceProps));

View File

@@ -1208,8 +1208,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
final PromptInfo promptInfo = (PromptInfo) args.arg1; final PromptInfo promptInfo = (PromptInfo) args.arg1;
final int[] sensorIds = (int[]) args.arg3; final int[] sensorIds = (int[]) args.arg3;
// TODO(b/251476085): remove these unused parameters (replaced with SSOT elsewhere)
final boolean credentialAllowed = (boolean) args.arg4; final boolean credentialAllowed = (boolean) args.arg4;
final boolean requireConfirmation = (boolean) args.arg5; final boolean requireConfirmation = (boolean) args.arg5;
final int userId = args.argi1; final int userId = args.argi1;
final String opPackageName = (String) args.arg6; final String opPackageName = (String) args.arg6;
final long operationId = args.argl1; final long operationId = args.argl1;

View File

@@ -17,6 +17,8 @@
package com.android.systemui.biometrics.dagger package com.android.systemui.biometrics.dagger
import com.android.settingslib.udfps.UdfpsUtils import com.android.settingslib.udfps.UdfpsUtils
import com.android.systemui.biometrics.data.repository.FaceSettingsRepository
import com.android.systemui.biometrics.data.repository.FaceSettingsRepositoryImpl
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepositoryImpl import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepositoryImpl
import com.android.systemui.biometrics.data.repository.PromptRepository import com.android.systemui.biometrics.data.repository.PromptRepository
@@ -45,6 +47,10 @@ import javax.inject.Qualifier
@Module @Module
interface BiometricsModule { interface BiometricsModule {
@Binds
@SysUISingleton
fun faceSettings(impl: FaceSettingsRepositoryImpl): FaceSettingsRepository
@Binds @Binds
@SysUISingleton @SysUISingleton
fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository

View File

@@ -0,0 +1,57 @@
/*
* 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.systemui.biometrics.data.repository
import android.os.Handler
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.settings.SecureSettings
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
/**
* Repository for the global state of users Face Unlock preferences.
*
* Largely a wrapper around [SecureSettings]'s proxy to Settings.Secure.
*/
interface FaceSettingsRepository {
/** Get Settings for the given user [id]. */
fun forUser(id: Int?): FaceUserSettingsRepository
}
@SysUISingleton
class FaceSettingsRepositoryImpl
@Inject
constructor(
@Main private val mainHandler: Handler,
private val secureSettings: SecureSettings,
) : FaceSettingsRepository {
private val userSettings = ConcurrentHashMap<Int, FaceUserSettingsRepository>()
override fun forUser(id: Int?): FaceUserSettingsRepository =
if (id != null) {
userSettings.computeIfAbsent(id) { _ ->
FaceUserSettingsRepositoryImpl(id, mainHandler, secureSettings).also { repo ->
repo.start()
}
}
} else {
FaceUserSettingsRepositoryImpl.Empty
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.systemui.biometrics.data.repository
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.settings.SecureSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf
/** Settings for a user. */
interface FaceUserSettingsRepository {
/** The user's id. */
val userId: Int
/** If BiometricPrompt should always require confirmation (overrides app's preference). */
val alwaysRequireConfirmationInApps: Flow<Boolean>
}
class FaceUserSettingsRepositoryImpl(
override val userId: Int,
@Main private val mainHandler: Handler,
private val secureSettings: SecureSettings,
) : FaceUserSettingsRepository {
/** Indefinitely subscribe to user preference changes. */
fun start() {
watch(
FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION,
_alwaysRequireConfirmationInApps,
)
}
private var _alwaysRequireConfirmationInApps = MutableStateFlow(false)
override val alwaysRequireConfirmationInApps: Flow<Boolean> =
_alwaysRequireConfirmationInApps.asStateFlow()
/** Defaults to use when no user is specified. */
object Empty : FaceUserSettingsRepository {
override val userId = -1
override val alwaysRequireConfirmationInApps = flowOf(false)
}
private fun watch(
key: String,
toUpdate: MutableStateFlow<Boolean>,
defaultValue: Boolean = false,
) = secureSettings.watch(userId, mainHandler, key, defaultValue) { v -> toUpdate.value = v }
}
private fun SecureSettings.watch(
userId: Int,
handler: Handler,
key: String,
defaultValue: Boolean = false,
onChange: (Boolean) -> Unit,
) {
fun fetch(): Boolean = getIntForUser(key, if (defaultValue) 1 else 0, userId) > 0
registerContentObserverForUser(
key,
false /* notifyForDescendants */,
object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) = onChange(fetch())
},
userId
)
onChange(fetch())
}

View File

@@ -1,3 +1,19 @@
/*
* 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.systemui.biometrics.data.repository package com.android.systemui.biometrics.data.repository
import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptInfo
@@ -12,6 +28,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
/** /**
* A repository for the global state of BiometricPrompt. * A repository for the global state of BiometricPrompt.
@@ -40,7 +60,7 @@ interface PromptRepository {
* *
* Note: overlaps/conflicts with [PromptInfo.isConfirmationRequested], which needs clean up. * Note: overlaps/conflicts with [PromptInfo.isConfirmationRequested], which needs clean up.
*/ */
val isConfirmationRequired: StateFlow<Boolean> val isConfirmationRequired: Flow<Boolean>
/** Update the prompt configuration, which should be set before [isShowing]. */ /** Update the prompt configuration, which should be set before [isShowing]. */
fun setPrompt( fun setPrompt(
@@ -48,7 +68,6 @@ interface PromptRepository {
userId: Int, userId: Int,
gatekeeperChallenge: Long?, gatekeeperChallenge: Long?,
kind: PromptKind, kind: PromptKind,
requireConfirmation: Boolean = false,
) )
/** Unset the prompt info. */ /** Unset the prompt info. */
@@ -56,8 +75,12 @@ interface PromptRepository {
} }
@SysUISingleton @SysUISingleton
class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) : class PromptRepositoryImpl
PromptRepository { @Inject
constructor(
private val faceSettings: FaceSettingsRepository,
private val authController: AuthController,
) : PromptRepository {
override val isShowing: Flow<Boolean> = conflatedCallbackFlow { override val isShowing: Flow<Boolean> = conflatedCallbackFlow {
val callback = val callback =
@@ -85,21 +108,30 @@ class PromptRepositoryImpl @Inject constructor(private val authController: AuthC
private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric()) private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric())
override val kind = _kind.asStateFlow() override val kind = _kind.asStateFlow()
private val _isConfirmationRequired: MutableStateFlow<Boolean> = MutableStateFlow(false) private val _faceSettings =
override val isConfirmationRequired = _isConfirmationRequired.asStateFlow() _userId.map { id -> faceSettings.forUser(id) }.distinctUntilChanged()
private val _faceSettingAlwaysRequireConfirmation =
_faceSettings.flatMapLatest { it.alwaysRequireConfirmationInApps }.distinctUntilChanged()
private val _isConfirmationRequired = _promptInfo.map { it?.isConfirmationRequested ?: false }
override val isConfirmationRequired =
combine(_isConfirmationRequired, _faceSettingAlwaysRequireConfirmation) {
appRequiresConfirmation,
forceRequireConfirmation ->
forceRequireConfirmation || appRequiresConfirmation
}
.distinctUntilChanged()
override fun setPrompt( override fun setPrompt(
promptInfo: PromptInfo, promptInfo: PromptInfo,
userId: Int, userId: Int,
gatekeeperChallenge: Long?, gatekeeperChallenge: Long?,
kind: PromptKind, kind: PromptKind,
requireConfirmation: Boolean,
) { ) {
_kind.value = kind _kind.value = kind
_userId.value = userId _userId.value = userId
_challenge.value = gatekeeperChallenge _challenge.value = gatekeeperChallenge
_promptInfo.value = promptInfo _promptInfo.value = promptInfo
_isConfirmationRequired.value = requireConfirmation
} }
override fun unsetPrompt() { override fun unsetPrompt() {
@@ -107,7 +139,6 @@ class PromptRepositoryImpl @Inject constructor(private val authController: AuthC
_userId.value = null _userId.value = null
_challenge.value = null _challenge.value = null
_kind.value = PromptKind.Biometric() _kind.value = PromptKind.Biometric()
_isConfirmationRequired.value = false
} }
companion object { companion object {

View File

@@ -59,13 +59,15 @@ interface PromptSelectorInteractor {
*/ */
val credentialKind: Flow<PromptKind> val credentialKind: Flow<PromptKind>
/** If the API caller requested explicit confirmation after successful authentication. */ /**
val isConfirmationRequested: Flow<Boolean> * If the API caller or the user's personal preferences require explicit confirmation after
* successful authentication.
*/
val isConfirmationRequired: Flow<Boolean>
/** Use biometrics for authentication. */ /** Use biometrics for authentication. */
fun useBiometricsForAuthentication( fun useBiometricsForAuthentication(
promptInfo: PromptInfo, promptInfo: PromptInfo,
requireConfirmation: Boolean,
userId: Int, userId: Int,
challenge: Long, challenge: Long,
modalities: BiometricModalities, modalities: BiometricModalities,
@@ -114,10 +116,8 @@ constructor(
} }
} }
override val isConfirmationRequested: Flow<Boolean> = override val isConfirmationRequired: Flow<Boolean> =
promptRepository.promptInfo promptRepository.isConfirmationRequired.distinctUntilChanged()
.map { info -> info?.isConfirmationRequested ?: false }
.distinctUntilChanged()
override val isCredentialAllowed: Flow<Boolean> = override val isCredentialAllowed: Flow<Boolean> =
promptRepository.promptInfo promptRepository.promptInfo
@@ -142,7 +142,6 @@ constructor(
override fun useBiometricsForAuthentication( override fun useBiometricsForAuthentication(
promptInfo: PromptInfo, promptInfo: PromptInfo,
requireConfirmation: Boolean,
userId: Int, userId: Int,
challenge: Long, challenge: Long,
modalities: BiometricModalities modalities: BiometricModalities
@@ -152,7 +151,6 @@ constructor(
userId = userId, userId = userId,
gatekeeperChallenge = challenge, gatekeeperChallenge = challenge,
kind = PromptKind.Biometric(modalities), kind = PromptKind.Biometric(modalities),
requireConfirmation = requireConfirmation,
) )
} }

View File

@@ -158,7 +158,7 @@ object BiometricViewBinder {
view.updateFingerprintAffordanceSize(iconController) view.updateFingerprintAffordanceSize(iconController)
} }
if (iconController is HackyCoexIconController) { if (iconController is HackyCoexIconController) {
iconController.faceMode = !viewModel.isConfirmationRequested.first() iconController.faceMode = !viewModel.isConfirmationRequired.first()
} }
// the icon controller must be created before this happens for the legacy // the icon controller must be created before this happens for the legacy

View File

@@ -61,8 +61,11 @@ constructor(
/** If the user has successfully authenticated and confirmed (when explicitly required). */ /** If the user has successfully authenticated and confirmed (when explicitly required). */
val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow()
/** If the API caller requested explicit confirmation after successful authentication. */ /**
val isConfirmationRequested: Flow<Boolean> = interactor.isConfirmationRequested * If the API caller or the user's personal preferences require explicit confirmation after
* successful authentication.
*/
val isConfirmationRequired: Flow<Boolean> = interactor.isConfirmationRequired
/** The kind of credential the user has. */ /** The kind of credential the user has. */
val credentialKind: Flow<PromptKind> = interactor.credentialKind val credentialKind: Flow<PromptKind> = interactor.credentialKind
@@ -91,7 +94,7 @@ constructor(
_forceLargeSize, _forceLargeSize,
_forceMediumSize, _forceMediumSize,
modalities, modalities,
interactor.isConfirmationRequested, interactor.isConfirmationRequired,
fingerprintStartMode, fingerprintStartMode,
) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode -> ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode ->
when { when {
@@ -383,7 +386,7 @@ constructor(
private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
val availableModalities = modalities.first() val availableModalities = modalities.first()
val confirmationRequested = interactor.isConfirmationRequested.first() val confirmationRequested = interactor.isConfirmationRequired.first()
if (availableModalities.hasFaceAndFingerprint) { if (availableModalities.hasFaceAndFingerprint) {
// coex only needs confirmation when face is successful, unless it happens on the // coex only needs confirmation when face is successful, unless it happens on the

View File

@@ -0,0 +1,124 @@
/*
* 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.systemui.biometrics.data.repository
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.util.mockito.captureMany
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
private const val USER_ID = 8
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class FaceSettingsRepositoryImplTest : SysuiTestCase() {
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
private val testScope = TestScope()
@Mock private lateinit var mainHandler: Handler
@Mock private lateinit var secureSettings: SecureSettings
private lateinit var repository: FaceSettingsRepositoryImpl
@Before
fun setup() {
repository = FaceSettingsRepositoryImpl(mainHandler, secureSettings)
}
@Test
fun createsOneRepositoryPerUser() =
testScope.runTest {
val userRepo = repository.forUser(USER_ID)
assertThat(userRepo.userId).isEqualTo(USER_ID)
assertThat(repository.forUser(USER_ID)).isSameInstanceAs(userRepo)
assertThat(repository.forUser(USER_ID + 1)).isNotSameInstanceAs(userRepo)
}
@Test
fun startsRepoImmediatelyWithAllSettingKeys() =
testScope.runTest {
val userRepo = repository.forUser(USER_ID)
val keys =
captureMany<String> {
verify(secureSettings)
.registerContentObserverForUser(capture(), anyBoolean(), any(), eq(USER_ID))
}
assertThat(keys).containsExactly(FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION)
}
@Test
fun forwardsSettingsValues() = runTest {
val userRepo = repository.forUser(USER_ID)
val intAsBooleanSettings =
listOf(
FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION to
collectLastValue(userRepo.alwaysRequireConfirmationInApps)
)
for ((setting, accessor) in intAsBooleanSettings) {
val observer =
withArgCaptor<ContentObserver> {
verify(secureSettings)
.registerContentObserverForUser(
eq(setting),
anyBoolean(),
capture(),
eq(USER_ID)
)
}
for (value in listOf(true, false)) {
secureSettings.mockIntSetting(setting, if (value) 1 else 0)
observer.onChange(false)
assertThat(accessor()).isEqualTo(value)
}
}
}
private fun SecureSettings.mockIntSetting(key: String, value: Int) {
whenever(getIntForUser(eq(key), anyInt(), eq(USER_ID))).thenReturn(value)
}
}

View File

@@ -1,3 +1,19 @@
/*
* 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.systemui.biometrics.data.repository package com.android.systemui.biometrics.data.repository
import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptInfo
@@ -5,12 +21,16 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -21,61 +41,109 @@ import org.mockito.Mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
private const val USER_ID = 9
private const val CHALLENGE = 90L
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
class PromptRepositoryImplTest : SysuiTestCase() { class PromptRepositoryImplTest : SysuiTestCase() {
@JvmField @Rule var mockitoRule = MockitoJUnit.rule() @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
private val testScope = TestScope()
private val faceSettings = FakeFaceSettingsRepository()
@Mock private lateinit var authController: AuthController @Mock private lateinit var authController: AuthController
private lateinit var repository: PromptRepositoryImpl private lateinit var repository: PromptRepositoryImpl
@Before @Before
fun setup() { fun setup() {
repository = PromptRepositoryImpl(authController) repository = PromptRepositoryImpl(faceSettings, authController)
} }
@Test @Test
fun isShowing() = runBlockingTest { fun isShowing() =
whenever(authController.isShowing).thenReturn(true) testScope.runTest {
whenever(authController.isShowing).thenReturn(true)
val values = mutableListOf<Boolean>() val values = mutableListOf<Boolean>()
val job = launch { repository.isShowing.toList(values) } val job = launch { repository.isShowing.toList(values) }
assertThat(values).containsExactly(true) runCurrent()
withArgCaptor<AuthController.Callback> { assertThat(values).containsExactly(true)
verify(authController).addCallback(capture())
value.onBiometricPromptShown() withArgCaptor<AuthController.Callback> {
assertThat(values).containsExactly(true, true) verify(authController).addCallback(capture())
value.onBiometricPromptDismissed() value.onBiometricPromptShown()
assertThat(values).containsExactly(true, true, false).inOrder() runCurrent()
assertThat(values).containsExactly(true, true)
job.cancel() value.onBiometricPromptDismissed()
verify(authController).removeCallback(eq(value)) runCurrent()
assertThat(values).containsExactly(true, true, false).inOrder()
job.cancel()
runCurrent()
verify(authController).removeCallback(eq(value))
}
} }
}
@Test @Test
fun setsAndUnsetsPrompt() = runBlockingTest { fun isConfirmationRequired_whenNotForced() =
val kind = PromptKind.Pin testScope.runTest {
val uid = 8 faceSettings.setUserSettings(USER_ID, alwaysRequireConfirmationInApps = false)
val challenge = 90L val isConfirmationRequired by collectLastValue(repository.isConfirmationRequired)
val promptInfo = PromptInfo()
repository.setPrompt(promptInfo, uid, challenge, kind) for (case in listOf(true, false)) {
repository.setPrompt(
PromptInfo().apply { isConfirmationRequested = case },
USER_ID,
CHALLENGE,
PromptKind.Biometric()
)
assertThat(repository.kind.value).isEqualTo(kind) assertThat(isConfirmationRequired).isEqualTo(case)
assertThat(repository.userId.value).isEqualTo(uid) }
assertThat(repository.challenge.value).isEqualTo(challenge) }
assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo)
repository.unsetPrompt() @Test
fun isConfirmationRequired_whenForced() =
testScope.runTest {
faceSettings.setUserSettings(USER_ID, alwaysRequireConfirmationInApps = true)
val isConfirmationRequired by collectLastValue(repository.isConfirmationRequired)
assertThat(repository.promptInfo.value).isNull() for (case in listOf(true, false)) {
assertThat(repository.userId.value).isNull() repository.setPrompt(
assertThat(repository.challenge.value).isNull() PromptInfo().apply { isConfirmationRequested = case },
} USER_ID,
CHALLENGE,
PromptKind.Biometric()
)
assertThat(isConfirmationRequired).isTrue()
}
}
@Test
fun setsAndUnsetsPrompt() =
testScope.runTest {
val kind = PromptKind.Pin
val promptInfo = PromptInfo()
repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind)
assertThat(repository.kind.value).isEqualTo(kind)
assertThat(repository.userId.value).isEqualTo(USER_ID)
assertThat(repository.challenge.value).isEqualTo(CHALLENGE)
assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo)
repository.unsetPrompt()
assertThat(repository.promptInfo.value).isNull()
assertThat(repository.userId.value).isNull()
assertThat(repository.challenge.value).isNull()
}
} }

View File

@@ -106,17 +106,11 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
val currentPrompt by collectLastValue(interactor.prompt) val currentPrompt by collectLastValue(interactor.prompt)
val credentialKind by collectLastValue(interactor.credentialKind) val credentialKind by collectLastValue(interactor.credentialKind)
val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed) val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed)
val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequested) val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequired)
assertThat(currentPrompt).isNull() assertThat(currentPrompt).isNull()
interactor.useBiometricsForAuthentication( interactor.useBiometricsForAuthentication(info, USER_ID, CHALLENGE, modalities)
info,
confirmationRequired,
USER_ID,
CHALLENGE,
modalities
)
assertThat(currentPrompt).isNotNull() assertThat(currentPrompt).isNotNull()
assertThat(currentPrompt?.title).isEqualTo(TITLE) assertThat(currentPrompt?.title).isEqualTo(TITLE)

View File

@@ -631,7 +631,6 @@ private fun PromptSelectorInteractor.initializePrompt(
} }
useBiometricsForAuthentication( useBiometricsForAuthentication(
info, info,
requireConfirmation,
USER_ID, USER_ID,
CHALLENGE, CHALLENGE,
BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),

View File

@@ -0,0 +1,37 @@
/*
* 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.systemui.biometrics.data.repository
import kotlinx.coroutines.flow.flowOf
/** Fake settings for tests. */
class FakeFaceSettingsRepository : FaceSettingsRepository {
private val userRepositories = mutableMapOf<Int, FaceUserSettingsRepository>()
/** Add fixed settings for a user. */
fun setUserSettings(userId: Int, alwaysRequireConfirmationInApps: Boolean = false) {
userRepositories[userId] =
object : FaceUserSettingsRepository {
override val userId = userId
override val alwaysRequireConfirmationInApps =
flowOf(alwaysRequireConfirmationInApps)
}
}
override fun forUser(id: Int?) = userRepositories[id] ?: FaceUserSettingsRepositoryImpl.Empty
}

View File

@@ -31,13 +31,20 @@ class FakePromptRepository : PromptRepository {
userId: Int, userId: Int,
gatekeeperChallenge: Long?, gatekeeperChallenge: Long?,
kind: PromptKind, kind: PromptKind,
requireConfirmation: Boolean, ) = setPrompt(promptInfo, userId, gatekeeperChallenge, kind, forceConfirmation = false)
fun setPrompt(
promptInfo: PromptInfo,
userId: Int,
gatekeeperChallenge: Long?,
kind: PromptKind,
forceConfirmation: Boolean = false,
) { ) {
_promptInfo.value = promptInfo _promptInfo.value = promptInfo
_userId.value = userId _userId.value = userId
_challenge.value = gatekeeperChallenge _challenge.value = gatekeeperChallenge
_kind.value = kind _kind.value = kind
_isConfirmationRequired.value = requireConfirmation _isConfirmationRequired.value = promptInfo.isConfirmationRequested || forceConfirmation
} }
override fun unsetPrompt() { override fun unsetPrompt() {