feat: An option to not require biometric confirmation tap
This commit is contained in:
parent
75d7eb96a0
commit
7670a040e3
|
@ -50,6 +50,7 @@ import com.artemchep.keyguard.common.model.getOrNull
|
|||
import com.artemchep.keyguard.common.usecase.AddCipherUsedPasskeyHistory
|
||||
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
||||
import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.GetCiphers
|
||||
import com.artemchep.keyguard.common.usecase.GetVaultSession
|
||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||
|
@ -536,6 +537,7 @@ fun produceUserVerificationState(
|
|||
produceUserVerificationState(
|
||||
onAuthenticated = onAuthenticated,
|
||||
biometricStatusUseCase = instance(),
|
||||
getBiometricRequireConfirmation = instance(),
|
||||
confirmAccessByPasswordUseCase = instance(),
|
||||
windowCoroutineScope = instance(),
|
||||
)
|
||||
|
@ -545,6 +547,7 @@ fun produceUserVerificationState(
|
|||
fun produceUserVerificationState(
|
||||
onAuthenticated: () -> Unit,
|
||||
biometricStatusUseCase: BiometricStatusUseCase,
|
||||
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
): UserVerificationState = produceScreenState(
|
||||
|
@ -567,8 +570,11 @@ fun produceUserVerificationState(
|
|||
.getOrNull()
|
||||
when (biometricStatus) {
|
||||
is BiometricStatus.Available -> {
|
||||
val requireConfirmation = getBiometricRequireConfirmation()
|
||||
.first()
|
||||
createPromptOrNull(
|
||||
executor = executor,
|
||||
requireConfirmation = requireConfirmation,
|
||||
fn = {
|
||||
onAuthenticated()
|
||||
},
|
||||
|
@ -652,11 +658,13 @@ fun produceUserVerificationState(
|
|||
|
||||
private fun createPromptOrNull(
|
||||
executor: LoadingTask,
|
||||
requireConfirmation: Boolean,
|
||||
fn: () -> Unit,
|
||||
): PureBiometricAuthPrompt = run {
|
||||
BiometricAuthPromptSimple(
|
||||
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
|
||||
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
|
||||
requireConfirmation = requireConfirmation,
|
||||
onComplete = { result ->
|
||||
result.fold(
|
||||
ifLeft = { exception ->
|
||||
|
|
|
@ -51,6 +51,7 @@ private fun FragmentActivity.launchPrompt(
|
|||
?.also(::setDescription)
|
||||
}
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.setConfirmationRequired(event.requireConfirmation)
|
||||
.build()
|
||||
val prompt = BiometricPrompt(
|
||||
this,
|
||||
|
@ -89,6 +90,7 @@ private fun FragmentActivity.launchPrompt(
|
|||
?.also(::setDescription)
|
||||
}
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.setConfirmationRequired(event.requireConfirmation)
|
||||
.build()
|
||||
val prompt = BiometricPrompt(
|
||||
this,
|
||||
|
|
|
@ -10,6 +10,7 @@ class BiometricAuthPrompt(
|
|||
val title: TextHolder,
|
||||
val text: TextHolder? = null,
|
||||
val cipher: LeCipher,
|
||||
val requireConfirmation: Boolean,
|
||||
/**
|
||||
* Called when the user either failed the authentication or
|
||||
* successfully passed it.
|
||||
|
@ -22,6 +23,7 @@ class BiometricAuthPrompt(
|
|||
class BiometricAuthPromptSimple(
|
||||
val title: TextHolder,
|
||||
val text: TextHolder? = null,
|
||||
val requireConfirmation: Boolean,
|
||||
/**
|
||||
* Called when the user either failed the authentication or
|
||||
* successfully passed it.
|
||||
|
|
|
@ -17,6 +17,7 @@ sealed interface VaultState {
|
|||
class WithBiometric(
|
||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||
val getCreateIo: (String) -> IO<Unit>,
|
||||
val requireConfirmation: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -32,6 +33,7 @@ sealed interface VaultState {
|
|||
class WithBiometric(
|
||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||
val getCreateIo: () -> IO<Unit>,
|
||||
val requireConfirmation: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ interface SettingsReadRepository {
|
|||
|
||||
fun getBiometricTimeout(): Flow<Duration?>
|
||||
|
||||
fun getBiometricRequireConfirmation(): Flow<Boolean>
|
||||
|
||||
fun getClipboardClearDelay(): Flow<Duration?>
|
||||
|
||||
fun getClipboardUpdateDuration(): Flow<Duration?>
|
||||
|
|
|
@ -57,6 +57,10 @@ interface SettingsReadWriteRepository : SettingsReadRepository {
|
|||
duration: Duration?,
|
||||
): IO<Unit>
|
||||
|
||||
fun setBiometricRequireConfirmation(
|
||||
requireConfirmation: Boolean,
|
||||
): IO<Unit>
|
||||
|
||||
fun setClipboardClearDelay(
|
||||
duration: Duration?,
|
||||
): IO<Unit>
|
||||
|
|
|
@ -47,6 +47,7 @@ class SettingsRepositoryImpl(
|
|||
private const val KEY_VAULT_TIMEOUT = "vault_timeout"
|
||||
private const val KEY_VAULT_SCREEN_LOCK = "vault_screen_lock"
|
||||
private const val KEY_BIOMETRIC_TIMEOUT = "biometric_timeout"
|
||||
private const val KEY_BIOMETRIC_REQUIRE_CONFIRMATION = "biometric_require_confirmation"
|
||||
private const val KEY_CLIPBOARD_CLEAR_DELAY = "clipboard_clear_delay"
|
||||
private const val KEY_CLIPBOARD_UPDATE_DURATION = "clipboard_update_duration"
|
||||
private const val KEY_CONCEAL_FIELDS = "conceal_fields"
|
||||
|
@ -107,6 +108,8 @@ class SettingsRepositoryImpl(
|
|||
|
||||
private val biometricTimeoutPref = store.getLong(KEY_BIOMETRIC_TIMEOUT, NONE_DURATION)
|
||||
|
||||
private val biometricRequireConfirmationPref = store.getBoolean(KEY_BIOMETRIC_REQUIRE_CONFIRMATION, true)
|
||||
|
||||
private val clipboardClearDelayPref =
|
||||
store.getLong(KEY_CLIPBOARD_CLEAR_DELAY, NONE_DURATION)
|
||||
|
||||
|
@ -277,6 +280,13 @@ class SettingsRepositoryImpl(
|
|||
override fun getBiometricTimeout() = biometricTimeoutPref
|
||||
.asDuration()
|
||||
|
||||
override fun setBiometricRequireConfirmation(
|
||||
requireConfirmation: Boolean,
|
||||
) = biometricRequireConfirmationPref
|
||||
.setAndCommit(requireConfirmation)
|
||||
|
||||
override fun getBiometricRequireConfirmation() = biometricRequireConfirmationPref
|
||||
|
||||
override fun setClipboardClearDelay(duration: Duration?) = clipboardClearDelayPref
|
||||
.setAndCommit(duration)
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Duration
|
||||
|
||||
interface GetBiometricRequireConfirmation : () -> Flow<Boolean>
|
|
@ -0,0 +1,5 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
|
||||
interface PutBiometricRequireConfirmation : (Boolean) -> IO<Unit>
|
|
@ -0,0 +1,19 @@
|
|||
package com.artemchep.keyguard.common.usecase.impl
|
||||
|
||||
import com.artemchep.keyguard.common.service.settings.SettingsReadRepository
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class GetBiometricRequireConfirmationImpl(
|
||||
settingsReadRepository: SettingsReadRepository,
|
||||
) : GetBiometricRequireConfirmation {
|
||||
private val sharedFlow = settingsReadRepository.getBiometricRequireConfirmation()
|
||||
|
||||
constructor(directDI: DirectDI) : this(
|
||||
settingsReadRepository = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(): Flow<Boolean> = sharedFlow
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.artemchep.keyguard.common.usecase.impl
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.service.settings.SettingsReadWriteRepository
|
||||
import com.artemchep.keyguard.common.usecase.PutBiometricRequireConfirmation
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class PutBiometricRequireConfirmationImpl(
|
||||
private val settingsReadWriteRepository: SettingsReadWriteRepository,
|
||||
) : PutBiometricRequireConfirmation {
|
||||
constructor(directDI: DirectDI) : this(
|
||||
settingsReadWriteRepository = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(requireConfirmation: Boolean): IO<Unit> = settingsReadWriteRepository
|
||||
.setBiometricRequireConfirmation(requireConfirmation)
|
||||
}
|
|
@ -35,6 +35,7 @@ import com.artemchep.keyguard.common.usecase.BiometricKeyEncryptUseCase
|
|||
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
||||
import com.artemchep.keyguard.common.usecase.DisableBiometric
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRemainingDuration
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.GetVaultSession
|
||||
import com.artemchep.keyguard.common.usecase.PutVaultSession
|
||||
import com.artemchep.keyguard.common.usecase.UnlockUseCase
|
||||
|
@ -48,6 +49,7 @@ import kotlinx.coroutines.GlobalScope
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -69,6 +71,7 @@ class UnlockUseCaseImpl(
|
|||
private val disableBiometric: DisableBiometric,
|
||||
private val keyReadWriteRepository: FingerprintReadWriteRepository,
|
||||
private val sessionMetadataReadWriteRepository: SessionMetadataReadWriteRepository,
|
||||
private val getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
private val getBiometricRemainingDuration: GetBiometricRemainingDuration,
|
||||
private val biometricKeyEncryptUseCase: BiometricKeyEncryptUseCase,
|
||||
private val decryptBiometricKeyUseCase: BiometricKeyDecryptUseCase,
|
||||
|
@ -127,6 +130,7 @@ class UnlockUseCaseImpl(
|
|||
disableBiometric = directDI.instance(),
|
||||
keyReadWriteRepository = directDI.instance(),
|
||||
sessionMetadataReadWriteRepository = directDI.instance(),
|
||||
getBiometricRequireConfirmation = directDI.instance(),
|
||||
getBiometricRemainingDuration = directDI.instance(),
|
||||
biometricKeyEncryptUseCase = directDI.instance(),
|
||||
decryptBiometricKeyUseCase = directDI.instance(),
|
||||
|
@ -163,7 +167,7 @@ class UnlockUseCaseImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createCreateVaultState(
|
||||
private suspend fun createCreateVaultState(
|
||||
biometric: BiometricStatus,
|
||||
): VaultState {
|
||||
return VaultState.Create(
|
||||
|
@ -194,6 +198,8 @@ class UnlockUseCaseImpl(
|
|||
// the code in the block. This allows us to not
|
||||
// return the cipher from the action.
|
||||
.memoize()
|
||||
val requireConfirmation = getBiometricRequireConfirmation()
|
||||
.first()
|
||||
VaultState.Create.WithBiometric(
|
||||
getCipher = getCipherForEncryption,
|
||||
getCreateIo = { password ->
|
||||
|
@ -231,6 +237,7 @@ class UnlockUseCaseImpl(
|
|||
}
|
||||
.dispatchOn(Dispatchers.Default)
|
||||
},
|
||||
requireConfirmation = requireConfirmation,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
@ -238,7 +245,7 @@ class UnlockUseCaseImpl(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createUnlockVaultState(
|
||||
private suspend fun createUnlockVaultState(
|
||||
tokens: Fingerprint,
|
||||
biometric: BiometricStatus,
|
||||
lockReason: String?,
|
||||
|
@ -275,6 +282,8 @@ class UnlockUseCaseImpl(
|
|||
// the code in the block. This allows us to not
|
||||
// return the cipher from the action.
|
||||
.memoize()
|
||||
val requireConfirmation = getBiometricRequireConfirmation()
|
||||
.first()
|
||||
VaultState.Unlock.WithBiometric(
|
||||
getCipher = getCipherForDecryption,
|
||||
getCreateIo = {
|
||||
|
@ -296,6 +305,7 @@ class UnlockUseCaseImpl(
|
|||
.flatMap(::unlock)
|
||||
.dispatchOn(Dispatchers.Default)
|
||||
},
|
||||
requireConfirmation = requireConfirmation,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.artemchep.keyguard.common.model.BiometricAuthPrompt
|
|||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.model.ToastMessage
|
||||
import com.artemchep.keyguard.common.model.VaultState
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.UnlockUseCase
|
||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||
|
@ -21,6 +22,7 @@ import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
|||
import com.artemchep.keyguard.res.Res
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -37,6 +39,7 @@ private const val KEY_BIOMETRIC_ENABLED = "biometric.enabled"
|
|||
fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct) {
|
||||
changePasswordState(
|
||||
unlockUseCase = instance(),
|
||||
getBiometricRequireConfirmation = instance(),
|
||||
windowCoroutineScope = instance(),
|
||||
)
|
||||
}
|
||||
|
@ -44,6 +47,7 @@ fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct
|
|||
@Composable
|
||||
fun changePasswordState(
|
||||
unlockUseCase: UnlockUseCase,
|
||||
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
): Loadable<ChangePasswordState> = produceScreenState(
|
||||
key = "change_password",
|
||||
|
@ -63,11 +67,16 @@ fun changePasswordState(
|
|||
val fn = unlockUseCase()
|
||||
.map { vaultState ->
|
||||
when (vaultState) {
|
||||
is VaultState.Main -> ah(
|
||||
state = vaultState,
|
||||
biometricPromptSink = biometricPromptSink,
|
||||
windowCoroutineScope = windowCoroutineScope,
|
||||
)
|
||||
is VaultState.Main -> {
|
||||
val requireConfirmation = getBiometricRequireConfirmation()
|
||||
.first()
|
||||
ah(
|
||||
state = vaultState,
|
||||
requireConfirmation = requireConfirmation,
|
||||
biometricPromptSink = biometricPromptSink,
|
||||
windowCoroutineScope = windowCoroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
@ -154,6 +163,7 @@ fun changePasswordState(
|
|||
|
||||
private fun RememberStateFlowScope.ah(
|
||||
state: VaultState.Main,
|
||||
requireConfirmation: Boolean,
|
||||
biometricPromptSink: EventFlow<BiometricAuthPrompt>,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
) = Fn(
|
||||
|
@ -184,6 +194,7 @@ private fun RememberStateFlowScope.ah(
|
|||
val prompt = BiometricAuthPrompt(
|
||||
title = TextHolder.Res(Res.strings.changepassword_biometric_auth_confirm_title),
|
||||
cipher = cipher,
|
||||
requireConfirmation = requireConfirmation,
|
||||
onComplete = { result ->
|
||||
result.fold(
|
||||
ifLeft = { e ->
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.artemchep.keyguard.common.model.PureBiometricAuthPrompt
|
|||
import com.artemchep.keyguard.common.model.ToastMessage
|
||||
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
||||
import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
|
@ -28,6 +29,7 @@ import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
|||
import com.artemchep.keyguard.res.Res
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.kodein.di.compose.localDI
|
||||
|
@ -43,6 +45,7 @@ fun produceElevatedAccessState(
|
|||
produceElevatedAccessState(
|
||||
transmitter = transmitter,
|
||||
biometricStatusUseCase = instance(),
|
||||
getBiometricRequireConfirmation = instance(),
|
||||
confirmAccessByPasswordUseCase = instance(),
|
||||
windowCoroutineScope = instance(),
|
||||
)
|
||||
|
@ -52,6 +55,7 @@ fun produceElevatedAccessState(
|
|||
fun produceElevatedAccessState(
|
||||
transmitter: RouteResultTransmitter<ElevatedAccessResult>,
|
||||
biometricStatusUseCase: BiometricStatusUseCase,
|
||||
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
): ElevatedAccessState = produceScreenState(
|
||||
|
@ -74,8 +78,11 @@ fun produceElevatedAccessState(
|
|||
.getOrNull()
|
||||
when (biometricStatus) {
|
||||
is BiometricStatus.Available -> {
|
||||
val requireConfirmation = getBiometricRequireConfirmation()
|
||||
.first()
|
||||
createPromptOrNull(
|
||||
executor = executor,
|
||||
requireConfirmation = requireConfirmation,
|
||||
fn = {
|
||||
navigatePopSelf()
|
||||
transmitter(ElevatedAccessResult.Allow)
|
||||
|
@ -165,11 +172,13 @@ fun produceElevatedAccessState(
|
|||
|
||||
private fun createPromptOrNull(
|
||||
executor: LoadingTask,
|
||||
requireConfirmation: Boolean,
|
||||
fn: () -> Unit,
|
||||
): PureBiometricAuthPrompt = run {
|
||||
BiometricAuthPromptSimple(
|
||||
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
|
||||
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
|
||||
requireConfirmation = requireConfirmation,
|
||||
onComplete = { result ->
|
||||
result.fold(
|
||||
ifLeft = { exception ->
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.artemchep.keyguard.feature.home.settings.component.settingAutofillRes
|
|||
import com.artemchep.keyguard.feature.home.settings.component.settingAutofillSaveRequestProvider
|
||||
import com.artemchep.keyguard.feature.home.settings.component.settingAutofillSaveUriProvider
|
||||
import com.artemchep.keyguard.feature.home.settings.component.settingBiometricsProvider
|
||||
import com.artemchep.keyguard.feature.home.settings.component.settingBiometricsRequireConfirmationProvider
|
||||
import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedPasswordsProvider
|
||||
import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedServicesProvider
|
||||
import com.artemchep.keyguard.feature.home.settings.component.settingCheckTwoFAProvider
|
||||
|
@ -130,6 +131,7 @@ object Setting {
|
|||
const val PERMISSION_WRITE_EXTERNAL_STORAGE = "permission_write_external_storage"
|
||||
const val PERMISSION_POST_NOTIFICATION = "permission_post_notification"
|
||||
const val BIOMETRIC = "biometric"
|
||||
const val BIOMETRIC_REQUIRE_CONFIRMATION = "biometric_require_confirmation"
|
||||
const val VAULT_PERSIST = "vault_persist"
|
||||
const val VAULT_LOCK = "vault_lock"
|
||||
const val VAULT_LOCK_AFTER_REBOOT = "vault_lock_after_reboot"
|
||||
|
@ -211,6 +213,7 @@ val hub = mapOf<String, (DirectDI) -> SettingComponent>(
|
|||
Setting.PERMISSION_POST_NOTIFICATION to ::settingPermissionPostNotificationsProvider,
|
||||
Setting.PERMISSION_WRITE_EXTERNAL_STORAGE to ::settingPermissionWriteExternalStorageProvider,
|
||||
Setting.BIOMETRIC to ::settingBiometricsProvider,
|
||||
Setting.BIOMETRIC_REQUIRE_CONFIRMATION to ::settingBiometricsRequireConfirmationProvider,
|
||||
Setting.VAULT_PERSIST to ::settingVaultPersistProvider,
|
||||
Setting.VAULT_LOCK to ::settingVaultLockProvider,
|
||||
Setting.VAULT_LOCK_AFTER_REBOOT to ::settingVaultLockAfterRebootProvider,
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.artemchep.keyguard.common.service.vault.FingerprintReadRepository
|
|||
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
||||
import com.artemchep.keyguard.common.usecase.DisableBiometric
|
||||
import com.artemchep.keyguard.common.usecase.EnableBiometric
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||
import com.artemchep.keyguard.feature.biometric.BiometricPromptEffect
|
||||
|
@ -26,6 +27,7 @@ import com.artemchep.keyguard.res.Res
|
|||
import com.artemchep.keyguard.ui.FlatItem
|
||||
import com.artemchep.keyguard.ui.icons.icon
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
@ -38,6 +40,7 @@ fun settingBiometricsProvider(
|
|||
) = settingBiometricsProvider(
|
||||
fingerprintReadRepository = directDI.instance(),
|
||||
biometricStatusUseCase = directDI.instance(),
|
||||
getBiometricRequireConfirmation = directDI.instance(),
|
||||
enableBiometric = directDI.instance(),
|
||||
disableBiometric = directDI.instance(),
|
||||
windowCoroutineScope = directDI.instance(),
|
||||
|
@ -46,6 +49,7 @@ fun settingBiometricsProvider(
|
|||
fun settingBiometricsProvider(
|
||||
fingerprintReadRepository: FingerprintReadRepository,
|
||||
biometricStatusUseCase: BiometricStatusUseCase,
|
||||
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
enableBiometric: EnableBiometric,
|
||||
disableBiometric: DisableBiometric,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
|
@ -54,64 +58,82 @@ fun settingBiometricsProvider(
|
|||
.distinctUntilChanged()
|
||||
.flatMapLatest { hasBiometrics ->
|
||||
if (hasBiometrics) {
|
||||
fingerprintReadRepository.get()
|
||||
.map { tokens ->
|
||||
val hasBiometricEnabled = tokens?.biometric != null
|
||||
|
||||
SettingIi(
|
||||
search = SettingIi.Search(
|
||||
group = "biometric",
|
||||
tokens = listOf(
|
||||
"biometric",
|
||||
),
|
||||
),
|
||||
) {
|
||||
val promptSink = remember {
|
||||
EventFlow<BiometricAuthPrompt>()
|
||||
}
|
||||
|
||||
SettingBiometrics(
|
||||
checked = hasBiometricEnabled,
|
||||
onCheckedChange = { shouldBeChecked ->
|
||||
if (shouldBeChecked) {
|
||||
enableBiometric(null) // use global session
|
||||
.map { d ->
|
||||
val cipher = d.getCipher()
|
||||
val prompt = BiometricAuthPrompt(
|
||||
title = TextHolder.Res(Res.strings.pref_item_biometric_unlock_confirm_title),
|
||||
cipher = cipher,
|
||||
onComplete = { result ->
|
||||
result.fold(
|
||||
ifLeft = { exception ->
|
||||
val message = exception.message
|
||||
// biometricPromptErrorSink.emit(message)
|
||||
},
|
||||
ifRight = {
|
||||
d.getCreateIo()
|
||||
.launchIn(windowCoroutineScope)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
promptSink.emit(prompt)
|
||||
}
|
||||
.launchIn(windowCoroutineScope)
|
||||
} else {
|
||||
disableBiometric()
|
||||
.launchIn(windowCoroutineScope)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
BiometricPromptEffect(promptSink)
|
||||
}
|
||||
}
|
||||
createSettingComponentFlow(
|
||||
fingerprintReadRepository = fingerprintReadRepository,
|
||||
getBiometricRequireConfirmation = getBiometricRequireConfirmation,
|
||||
enableBiometric = enableBiometric,
|
||||
disableBiometric = disableBiometric,
|
||||
windowCoroutineScope = windowCoroutineScope,
|
||||
)
|
||||
} else {
|
||||
// hide option if the device does not have biometrics
|
||||
flowOf(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSettingComponentFlow(
|
||||
fingerprintReadRepository: FingerprintReadRepository,
|
||||
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
enableBiometric: EnableBiometric,
|
||||
disableBiometric: DisableBiometric,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
) = combine(
|
||||
getBiometricRequireConfirmation(),
|
||||
fingerprintReadRepository.get()
|
||||
.map { tokens ->
|
||||
tokens?.biometric != null
|
||||
},
|
||||
) { requireConfirmation, biometrics ->
|
||||
SettingIi(
|
||||
search = SettingIi.Search(
|
||||
group = "biometric",
|
||||
tokens = listOf(
|
||||
"biometric",
|
||||
),
|
||||
),
|
||||
) {
|
||||
val promptSink = remember {
|
||||
EventFlow<BiometricAuthPrompt>()
|
||||
}
|
||||
|
||||
SettingBiometrics(
|
||||
checked = biometrics,
|
||||
onCheckedChange = { shouldBeChecked ->
|
||||
if (shouldBeChecked) {
|
||||
enableBiometric(null) // use global session
|
||||
.map { d ->
|
||||
val cipher = d.getCipher()
|
||||
val prompt = BiometricAuthPrompt(
|
||||
title = TextHolder.Res(Res.strings.pref_item_biometric_unlock_confirm_title),
|
||||
cipher = cipher,
|
||||
requireConfirmation = requireConfirmation,
|
||||
onComplete = { result ->
|
||||
result.fold(
|
||||
ifLeft = { exception ->
|
||||
val message = exception.message
|
||||
// biometricPromptErrorSink.emit(message)
|
||||
},
|
||||
ifRight = {
|
||||
d.getCreateIo()
|
||||
.launchIn(windowCoroutineScope)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
promptSink.emit(prompt)
|
||||
}
|
||||
.launchIn(windowCoroutineScope)
|
||||
} else {
|
||||
disableBiometric()
|
||||
.launchIn(windowCoroutineScope)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
BiometricPromptEffect(promptSink)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingBiometrics(
|
||||
checked: Boolean,
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package com.artemchep.keyguard.feature.home.settings.component
|
||||
|
||||
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import arrow.core.partially1
|
||||
import com.artemchep.keyguard.common.io.launchIn
|
||||
import com.artemchep.keyguard.common.model.BiometricStatus
|
||||
import com.artemchep.keyguard.common.service.vault.FingerprintReadRepository
|
||||
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.PutBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItem
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
fun settingBiometricsRequireConfirmationProvider(
|
||||
directDI: DirectDI,
|
||||
) = settingBiometricsRequireConfirmationProvider(
|
||||
fingerprintReadRepository = directDI.instance(),
|
||||
biometricStatusUseCase = directDI.instance(),
|
||||
getBiometricRequireConfirmation = directDI.instance(),
|
||||
putBiometricRequireConfirmation = directDI.instance(),
|
||||
windowCoroutineScope = directDI.instance(),
|
||||
)
|
||||
|
||||
fun settingBiometricsRequireConfirmationProvider(
|
||||
fingerprintReadRepository: FingerprintReadRepository,
|
||||
biometricStatusUseCase: BiometricStatusUseCase,
|
||||
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
putBiometricRequireConfirmation: PutBiometricRequireConfirmation,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
): SettingComponent = biometricStatusUseCase()
|
||||
.map { it is BiometricStatus.Available }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { hasBiometrics ->
|
||||
if (hasBiometrics) {
|
||||
createSettingComponentFlow(
|
||||
fingerprintReadRepository = fingerprintReadRepository,
|
||||
getBiometricRequireConfirmation = getBiometricRequireConfirmation,
|
||||
putBiometricRequireConfirmation = putBiometricRequireConfirmation,
|
||||
windowCoroutineScope = windowCoroutineScope,
|
||||
)
|
||||
} else {
|
||||
// hide option if the device does not have biometrics
|
||||
flowOf(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSettingComponentFlow(
|
||||
fingerprintReadRepository: FingerprintReadRepository,
|
||||
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||
putBiometricRequireConfirmation: PutBiometricRequireConfirmation,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
) = combine(
|
||||
getBiometricRequireConfirmation(),
|
||||
fingerprintReadRepository.get()
|
||||
.map { tokens ->
|
||||
tokens?.biometric != null
|
||||
},
|
||||
) { requireConfirmation, biometrics ->
|
||||
val onCheckedChange = { shouldRequireConfirmation: Boolean ->
|
||||
putBiometricRequireConfirmation(shouldRequireConfirmation)
|
||||
.launchIn(windowCoroutineScope)
|
||||
Unit
|
||||
}
|
||||
|
||||
SettingIi(
|
||||
search = SettingIi.Search(
|
||||
group = "biometric",
|
||||
tokens = listOf(
|
||||
"biometric",
|
||||
"confirmation",
|
||||
),
|
||||
),
|
||||
) {
|
||||
SettingBiometricsRequireConfirmation(
|
||||
checked = requireConfirmation,
|
||||
onCheckedChange = onCheckedChange.takeIf { biometrics },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingBiometricsRequireConfirmation(
|
||||
checked: Boolean,
|
||||
onCheckedChange: ((Boolean) -> Unit)?,
|
||||
) {
|
||||
FlatItem(
|
||||
trailing = {
|
||||
CompositionLocalProvider(
|
||||
LocalMinimumInteractiveComponentEnforcement provides false,
|
||||
) {
|
||||
Switch(
|
||||
checked = checked,
|
||||
enabled = onCheckedChange != null,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(Res.strings.pref_item_biometric_unlock_require_confirmation_title),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(Res.strings.pref_item_biometric_unlock_require_confirmation_text),
|
||||
)
|
||||
},
|
||||
onClick = onCheckedChange?.partially1(!checked),
|
||||
)
|
||||
}
|
|
@ -36,6 +36,7 @@ fun SecuritySettingsScreen() {
|
|||
key = "biometric",
|
||||
list = listOf(
|
||||
SettingPaneItem.Item(Setting.BIOMETRIC),
|
||||
SettingPaneItem.Item(Setting.BIOMETRIC_REQUIRE_CONFIRMATION),
|
||||
SettingPaneItem.Item(Setting.REQUIRE_MASTER_PASSWORD),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -144,6 +144,7 @@ private fun createPromptOrNull(
|
|||
BiometricAuthPrompt(
|
||||
title = TextHolder.Res(Res.strings.setup_biometric_auth_confirm_title),
|
||||
cipher = cipher,
|
||||
requireConfirmation = createVaultWithMasterPasswordAndBiometricFn.requireConfirmation,
|
||||
onComplete = { result ->
|
||||
result.fold(
|
||||
ifLeft = { exception ->
|
||||
|
@ -185,6 +186,7 @@ private class CreateVaultWithBiometric(
|
|||
*/
|
||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||
getCreateIo: (String) -> IO<Unit>,
|
||||
val requireConfirmation: Boolean,
|
||||
) : CreateVaultWithPassword(executor, getCreateIo) {
|
||||
// Create from vault state options
|
||||
constructor(
|
||||
|
@ -194,6 +196,7 @@ private class CreateVaultWithBiometric(
|
|||
executor = executor,
|
||||
getCipher = options.getCipher,
|
||||
getCreateIo = options.getCreateIo,
|
||||
requireConfirmation = options.requireConfirmation,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -201,6 +201,7 @@ private fun createPromptOrNull(
|
|||
title = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_title),
|
||||
text = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_text),
|
||||
cipher = cipher,
|
||||
requireConfirmation = fn.requireConfirmation,
|
||||
onComplete = { result ->
|
||||
result.fold(
|
||||
ifLeft = { exception ->
|
||||
|
@ -249,6 +250,7 @@ private class UnlockVaultWithBiometric(
|
|||
*/
|
||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||
private val getCreateIo: () -> IO<Unit>,
|
||||
val requireConfirmation: Boolean,
|
||||
) : () -> Unit {
|
||||
// Create from vault state options
|
||||
constructor(
|
||||
|
@ -258,6 +260,7 @@ private class UnlockVaultWithBiometric(
|
|||
executor = executor,
|
||||
getCipher = options.getCipher,
|
||||
getCreateIo = options.getCreateIo,
|
||||
requireConfirmation = options.requireConfirmation,
|
||||
)
|
||||
|
||||
override fun invoke() {
|
||||
|
|
|
@ -991,6 +991,8 @@
|
|||
<string name="pref_item_features_overview_title">Features overview</string>
|
||||
<string name="pref_item_url_override_title">URL overrides</string>
|
||||
<string name="pref_item_biometric_unlock_title">Biometric unlock</string>
|
||||
<string name="pref_item_biometric_unlock_require_confirmation_title">Require confirmation</string>
|
||||
<string name="pref_item_biometric_unlock_require_confirmation_text">Require an additional confirmation button tap after a successful biometric authentication</string>
|
||||
<!--
|
||||
A title of the system popup that asks a user to use his biometric to later
|
||||
be able to use it to unlock the vault. -->
|
||||
|
|
|
@ -100,6 +100,7 @@ import com.artemchep.keyguard.common.usecase.GetAutofillManualSelection
|
|||
import com.artemchep.keyguard.common.usecase.GetAutofillRespectAutofillOff
|
||||
import com.artemchep.keyguard.common.usecase.GetAutofillSaveRequest
|
||||
import com.artemchep.keyguard.common.usecase.GetAutofillSaveUri
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricTimeout
|
||||
import com.artemchep.keyguard.common.usecase.GetBiometricTimeoutVariants
|
||||
import com.artemchep.keyguard.common.usecase.GetCachePremium
|
||||
|
@ -162,6 +163,7 @@ import com.artemchep.keyguard.common.usecase.PutAutofillManualSelection
|
|||
import com.artemchep.keyguard.common.usecase.PutAutofillRespectAutofillOff
|
||||
import com.artemchep.keyguard.common.usecase.PutAutofillSaveRequest
|
||||
import com.artemchep.keyguard.common.usecase.PutAutofillSaveUri
|
||||
import com.artemchep.keyguard.common.usecase.PutBiometricRequireConfirmation
|
||||
import com.artemchep.keyguard.common.usecase.PutBiometricTimeout
|
||||
import com.artemchep.keyguard.common.usecase.PutCachePremium
|
||||
import com.artemchep.keyguard.common.usecase.PutCheckPwnedPasswords
|
||||
|
@ -222,6 +224,7 @@ import com.artemchep.keyguard.common.usecase.impl.GetAutofillManualSelectionImpl
|
|||
import com.artemchep.keyguard.common.usecase.impl.GetAutofillRespectAutofillOffImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveRequestImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveUriImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetBiometricRequireConfirmationImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutVariantsImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetCachePremiumImpl
|
||||
|
@ -281,6 +284,7 @@ import com.artemchep.keyguard.common.usecase.impl.PutAutofillManualSelectionImpl
|
|||
import com.artemchep.keyguard.common.usecase.impl.PutAutofillRespectAutofillOffImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveRequestImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveUriImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.PutBiometricRequireConfirmationImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.PutBiometricTimeoutImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.PutCachePremiumImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.PutCheckPwnedPasswordsImpl
|
||||
|
@ -477,6 +481,16 @@ fun globalModuleJvm() = DI.Module(
|
|||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<GetBiometricRequireConfirmation> {
|
||||
GetBiometricRequireConfirmationImpl(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<PutBiometricRequireConfirmation> {
|
||||
PutBiometricRequireConfirmationImpl(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<GetCheckPwnedPasswords> {
|
||||
GetCheckPwnedPasswordsImpl(
|
||||
directDI = this,
|
||||
|
|
Loading…
Reference in New Issue