diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 7f706859abb3c..d8348eda3d97a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -374,7 +374,6 @@ public class AuthContainerView extends LinearLayout if (Utils.isBiometricAllowed(config.mPromptInfo)) { mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication( config.mPromptInfo, - config.mRequireConfirmation, config.mUserId, config.mOperationId, new BiometricModalities(fpProps, faceProps)); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 8d51375ed2cea..6b861ce235d73 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -1208,8 +1208,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, final PromptInfo promptInfo = (PromptInfo) args.arg1; 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 requireConfirmation = (boolean) args.arg5; + final int userId = args.argi1; final String opPackageName = (String) args.arg6; final long operationId = args.argl1; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index ddf1457e385c4..a5e846ad61ca7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -17,6 +17,8 @@ package com.android.systemui.biometrics.dagger 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.FingerprintPropertyRepositoryImpl import com.android.systemui.biometrics.data.repository.PromptRepository @@ -45,6 +47,10 @@ import javax.inject.Qualifier @Module interface BiometricsModule { + @Binds + @SysUISingleton + fun faceSettings(impl: FaceSettingsRepositoryImpl): FaceSettingsRepository + @Binds @SysUISingleton fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepository.kt new file mode 100644 index 0000000000000..3d5ed823f7716 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepository.kt @@ -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() + + override fun forUser(id: Int?): FaceUserSettingsRepository = + if (id != null) { + userSettings.computeIfAbsent(id) { _ -> + FaceUserSettingsRepositoryImpl(id, mainHandler, secureSettings).also { repo -> + repo.start() + } + } + } else { + FaceUserSettingsRepositoryImpl.Empty + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceUserSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceUserSettingsRepository.kt new file mode 100644 index 0000000000000..68c4a10fcfad6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceUserSettingsRepository.kt @@ -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 +} + +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 = + _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, + 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()) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index b4dc272b71da2..b35fbbc7bb320 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -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 import android.hardware.biometrics.PromptInfo @@ -12,6 +28,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow 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. @@ -40,7 +60,7 @@ interface PromptRepository { * * Note: overlaps/conflicts with [PromptInfo.isConfirmationRequested], which needs clean up. */ - val isConfirmationRequired: StateFlow + val isConfirmationRequired: Flow /** Update the prompt configuration, which should be set before [isShowing]. */ fun setPrompt( @@ -48,7 +68,6 @@ interface PromptRepository { userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, - requireConfirmation: Boolean = false, ) /** Unset the prompt info. */ @@ -56,8 +75,12 @@ interface PromptRepository { } @SysUISingleton -class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) : - PromptRepository { +class PromptRepositoryImpl +@Inject +constructor( + private val faceSettings: FaceSettingsRepository, + private val authController: AuthController, +) : PromptRepository { override val isShowing: Flow = conflatedCallbackFlow { val callback = @@ -85,21 +108,30 @@ class PromptRepositoryImpl @Inject constructor(private val authController: AuthC private val _kind: MutableStateFlow = MutableStateFlow(PromptKind.Biometric()) override val kind = _kind.asStateFlow() - private val _isConfirmationRequired: MutableStateFlow = MutableStateFlow(false) - override val isConfirmationRequired = _isConfirmationRequired.asStateFlow() + private val _faceSettings = + _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( promptInfo: PromptInfo, userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, - requireConfirmation: Boolean, ) { _kind.value = kind _userId.value = userId _challenge.value = gatekeeperChallenge _promptInfo.value = promptInfo - _isConfirmationRequired.value = requireConfirmation } override fun unsetPrompt() { @@ -107,7 +139,6 @@ class PromptRepositoryImpl @Inject constructor(private val authController: AuthC _userId.value = null _challenge.value = null _kind.value = PromptKind.Biometric() - _isConfirmationRequired.value = false } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index e6e07f9d77941..be99dd92f1bdd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -59,13 +59,15 @@ interface PromptSelectorInteractor { */ val credentialKind: Flow - /** If the API caller requested explicit confirmation after successful authentication. */ - val isConfirmationRequested: Flow + /** + * If the API caller or the user's personal preferences require explicit confirmation after + * successful authentication. + */ + val isConfirmationRequired: Flow /** Use biometrics for authentication. */ fun useBiometricsForAuthentication( promptInfo: PromptInfo, - requireConfirmation: Boolean, userId: Int, challenge: Long, modalities: BiometricModalities, @@ -114,10 +116,8 @@ constructor( } } - override val isConfirmationRequested: Flow = - promptRepository.promptInfo - .map { info -> info?.isConfirmationRequested ?: false } - .distinctUntilChanged() + override val isConfirmationRequired: Flow = + promptRepository.isConfirmationRequired.distinctUntilChanged() override val isCredentialAllowed: Flow = promptRepository.promptInfo @@ -142,7 +142,6 @@ constructor( override fun useBiometricsForAuthentication( promptInfo: PromptInfo, - requireConfirmation: Boolean, userId: Int, challenge: Long, modalities: BiometricModalities @@ -152,7 +151,6 @@ constructor( userId = userId, gatekeeperChallenge = challenge, kind = PromptKind.Biometric(modalities), - requireConfirmation = requireConfirmation, ) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 34c8d9f6e894f..6a7431e540341 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -158,7 +158,7 @@ object BiometricViewBinder { view.updateFingerprintAffordanceSize(iconController) } 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 diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 0fffee61318ab..05a536236de55 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -61,8 +61,11 @@ constructor( /** If the user has successfully authenticated and confirmed (when explicitly required). */ val isAuthenticated: Flow = _isAuthenticated.asStateFlow() - /** If the API caller requested explicit confirmation after successful authentication. */ - val isConfirmationRequested: Flow = interactor.isConfirmationRequested + /** + * If the API caller or the user's personal preferences require explicit confirmation after + * successful authentication. + */ + val isConfirmationRequired: Flow = interactor.isConfirmationRequired /** The kind of credential the user has. */ val credentialKind: Flow = interactor.credentialKind @@ -91,7 +94,7 @@ constructor( _forceLargeSize, _forceMediumSize, modalities, - interactor.isConfirmationRequested, + interactor.isConfirmationRequired, fingerprintStartMode, ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode -> when { @@ -383,7 +386,7 @@ constructor( private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { val availableModalities = modalities.first() - val confirmationRequested = interactor.isConfirmationRequested.first() + val confirmationRequested = interactor.isConfirmationRequired.first() if (availableModalities.hasFaceAndFingerprint) { // coex only needs confirmation when face is successful, unless it happens on the diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt new file mode 100644 index 0000000000000..0df4fbf86d98b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt @@ -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 { + 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 { + 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) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index 4836af635aed5..ec7ce634fd781 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -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 import android.hardware.biometrics.PromptInfo @@ -5,12 +21,16 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController 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.withArgCaptor import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList 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.Rule import org.junit.Test @@ -21,61 +41,109 @@ import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit +private const val USER_ID = 9 +private const val CHALLENGE = 90L + +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class PromptRepositoryImplTest : SysuiTestCase() { @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + private val testScope = TestScope() + private val faceSettings = FakeFaceSettingsRepository() + @Mock private lateinit var authController: AuthController private lateinit var repository: PromptRepositoryImpl @Before fun setup() { - repository = PromptRepositoryImpl(authController) + repository = PromptRepositoryImpl(faceSettings, authController) } @Test - fun isShowing() = runBlockingTest { - whenever(authController.isShowing).thenReturn(true) + fun isShowing() = + testScope.runTest { + whenever(authController.isShowing).thenReturn(true) - val values = mutableListOf() - val job = launch { repository.isShowing.toList(values) } - assertThat(values).containsExactly(true) + val values = mutableListOf() + val job = launch { repository.isShowing.toList(values) } + runCurrent() - withArgCaptor { - verify(authController).addCallback(capture()) + assertThat(values).containsExactly(true) - value.onBiometricPromptShown() - assertThat(values).containsExactly(true, true) + withArgCaptor { + verify(authController).addCallback(capture()) - value.onBiometricPromptDismissed() - assertThat(values).containsExactly(true, true, false).inOrder() + value.onBiometricPromptShown() + runCurrent() + assertThat(values).containsExactly(true, true) - job.cancel() - verify(authController).removeCallback(eq(value)) + value.onBiometricPromptDismissed() + runCurrent() + assertThat(values).containsExactly(true, true, false).inOrder() + + job.cancel() + runCurrent() + verify(authController).removeCallback(eq(value)) + } } - } @Test - fun setsAndUnsetsPrompt() = runBlockingTest { - val kind = PromptKind.Pin - val uid = 8 - val challenge = 90L - val promptInfo = PromptInfo() + fun isConfirmationRequired_whenNotForced() = + testScope.runTest { + faceSettings.setUserSettings(USER_ID, alwaysRequireConfirmationInApps = false) + val isConfirmationRequired by collectLastValue(repository.isConfirmationRequired) - 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(repository.userId.value).isEqualTo(uid) - assertThat(repository.challenge.value).isEqualTo(challenge) - assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) + assertThat(isConfirmationRequired).isEqualTo(case) + } + } - 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() - assertThat(repository.userId.value).isNull() - assertThat(repository.challenge.value).isNull() - } + for (case in listOf(true, false)) { + repository.setPrompt( + 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() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index a62ea3b77fc23..81cbaeab2a328 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -106,17 +106,11 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { val currentPrompt by collectLastValue(interactor.prompt) val credentialKind by collectLastValue(interactor.credentialKind) val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed) - val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequested) + val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequired) assertThat(currentPrompt).isNull() - interactor.useBiometricsForAuthentication( - info, - confirmationRequired, - USER_ID, - CHALLENGE, - modalities - ) + interactor.useBiometricsForAuthentication(info, USER_ID, CHALLENGE, modalities) assertThat(currentPrompt).isNotNull() assertThat(currentPrompt?.title).isEqualTo(TITLE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 3ba6004e4532d..5b3edaba8bc03 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -631,7 +631,6 @@ private fun PromptSelectorInteractor.initializePrompt( } useBiometricsForAuthentication( info, - requireConfirmation, USER_ID, CHALLENGE, BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFaceSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFaceSettingsRepository.kt new file mode 100644 index 0000000000000..af2706e6b2878 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFaceSettingsRepository.kt @@ -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() + + /** 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 +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt index d270700aa8566..42ec8fed0127d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -31,13 +31,20 @@ class FakePromptRepository : PromptRepository { userId: Int, gatekeeperChallenge: Long?, 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 _userId.value = userId _challenge.value = gatekeeperChallenge _kind.value = kind - _isConfirmationRequired.value = requireConfirmation + _isConfirmationRequired.value = promptInfo.isConfirmationRequested || forceConfirmation } override fun unsetPrompt() {