Merge "Propogate face unlock always require confirmation setting to biometric prompt." into udc-d1-dev
This commit is contained in:
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
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 {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user