Merge "Propogate face unlock always require confirmation setting to biometric prompt." into udc-d1-dev am: 27443a9c09
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23668394 Change-Id: Ia2263435e31c9a87ac204c9e9c6cd3c423ef9441 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -1216,8 +1216,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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<Boolean>
|
||||
val isConfirmationRequired: Flow<Boolean>
|
||||
|
||||
/** 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<Boolean> = conflatedCallbackFlow {
|
||||
val callback =
|
||||
@@ -85,21 +108,30 @@ class PromptRepositoryImpl @Inject constructor(private val authController: AuthC
|
||||
private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric())
|
||||
override val kind = _kind.asStateFlow()
|
||||
|
||||
private val _isConfirmationRequired: MutableStateFlow<Boolean> = 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 {
|
||||
|
||||
@@ -59,13 +59,15 @@ interface PromptSelectorInteractor {
|
||||
*/
|
||||
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. */
|
||||
fun useBiometricsForAuthentication(
|
||||
promptInfo: PromptInfo,
|
||||
requireConfirmation: Boolean,
|
||||
userId: Int,
|
||||
challenge: Long,
|
||||
modalities: BiometricModalities,
|
||||
@@ -114,10 +116,8 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override val isConfirmationRequested: Flow<Boolean> =
|
||||
promptRepository.promptInfo
|
||||
.map { info -> info?.isConfirmationRequested ?: false }
|
||||
.distinctUntilChanged()
|
||||
override val isConfirmationRequired: Flow<Boolean> =
|
||||
promptRepository.isConfirmationRequired.distinctUntilChanged()
|
||||
|
||||
override val isCredentialAllowed: Flow<Boolean> =
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,8 +61,11 @@ constructor(
|
||||
/** If the user has successfully authenticated and confirmed (when explicitly required). */
|
||||
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. */
|
||||
val credentialKind: Flow<PromptKind> = 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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean>()
|
||||
val job = launch { repository.isShowing.toList(values) }
|
||||
assertThat(values).containsExactly(true)
|
||||
val values = mutableListOf<Boolean>()
|
||||
val job = launch { repository.isShowing.toList(values) }
|
||||
runCurrent()
|
||||
|
||||
withArgCaptor<AuthController.Callback> {
|
||||
verify(authController).addCallback(capture())
|
||||
assertThat(values).containsExactly(true)
|
||||
|
||||
value.onBiometricPromptShown()
|
||||
assertThat(values).containsExactly(true, true)
|
||||
withArgCaptor<AuthController.Callback> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -631,7 +631,6 @@ private fun PromptSelectorInteractor.initializePrompt(
|
||||
}
|
||||
useBiometricsForAuthentication(
|
||||
info,
|
||||
requireConfirmation,
|
||||
USER_ID,
|
||||
CHALLENGE,
|
||||
BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user