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.AddCipherUsedPasskeyHistory
|
||||||
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
||||||
import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase
|
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.GetCiphers
|
||||||
import com.artemchep.keyguard.common.usecase.GetVaultSession
|
import com.artemchep.keyguard.common.usecase.GetVaultSession
|
||||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||||
@ -536,6 +537,7 @@ fun produceUserVerificationState(
|
|||||||
produceUserVerificationState(
|
produceUserVerificationState(
|
||||||
onAuthenticated = onAuthenticated,
|
onAuthenticated = onAuthenticated,
|
||||||
biometricStatusUseCase = instance(),
|
biometricStatusUseCase = instance(),
|
||||||
|
getBiometricRequireConfirmation = instance(),
|
||||||
confirmAccessByPasswordUseCase = instance(),
|
confirmAccessByPasswordUseCase = instance(),
|
||||||
windowCoroutineScope = instance(),
|
windowCoroutineScope = instance(),
|
||||||
)
|
)
|
||||||
@ -545,6 +547,7 @@ fun produceUserVerificationState(
|
|||||||
fun produceUserVerificationState(
|
fun produceUserVerificationState(
|
||||||
onAuthenticated: () -> Unit,
|
onAuthenticated: () -> Unit,
|
||||||
biometricStatusUseCase: BiometricStatusUseCase,
|
biometricStatusUseCase: BiometricStatusUseCase,
|
||||||
|
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||||
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
|
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
|
||||||
windowCoroutineScope: WindowCoroutineScope,
|
windowCoroutineScope: WindowCoroutineScope,
|
||||||
): UserVerificationState = produceScreenState(
|
): UserVerificationState = produceScreenState(
|
||||||
@ -567,8 +570,11 @@ fun produceUserVerificationState(
|
|||||||
.getOrNull()
|
.getOrNull()
|
||||||
when (biometricStatus) {
|
when (biometricStatus) {
|
||||||
is BiometricStatus.Available -> {
|
is BiometricStatus.Available -> {
|
||||||
|
val requireConfirmation = getBiometricRequireConfirmation()
|
||||||
|
.first()
|
||||||
createPromptOrNull(
|
createPromptOrNull(
|
||||||
executor = executor,
|
executor = executor,
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
fn = {
|
fn = {
|
||||||
onAuthenticated()
|
onAuthenticated()
|
||||||
},
|
},
|
||||||
@ -652,11 +658,13 @@ fun produceUserVerificationState(
|
|||||||
|
|
||||||
private fun createPromptOrNull(
|
private fun createPromptOrNull(
|
||||||
executor: LoadingTask,
|
executor: LoadingTask,
|
||||||
|
requireConfirmation: Boolean,
|
||||||
fn: () -> Unit,
|
fn: () -> Unit,
|
||||||
): PureBiometricAuthPrompt = run {
|
): PureBiometricAuthPrompt = run {
|
||||||
BiometricAuthPromptSimple(
|
BiometricAuthPromptSimple(
|
||||||
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
|
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
|
||||||
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
|
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
onComplete = { result ->
|
onComplete = { result ->
|
||||||
result.fold(
|
result.fold(
|
||||||
ifLeft = { exception ->
|
ifLeft = { exception ->
|
||||||
|
@ -51,6 +51,7 @@ private fun FragmentActivity.launchPrompt(
|
|||||||
?.also(::setDescription)
|
?.also(::setDescription)
|
||||||
}
|
}
|
||||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||||
|
.setConfirmationRequired(event.requireConfirmation)
|
||||||
.build()
|
.build()
|
||||||
val prompt = BiometricPrompt(
|
val prompt = BiometricPrompt(
|
||||||
this,
|
this,
|
||||||
@ -89,6 +90,7 @@ private fun FragmentActivity.launchPrompt(
|
|||||||
?.also(::setDescription)
|
?.also(::setDescription)
|
||||||
}
|
}
|
||||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||||
|
.setConfirmationRequired(event.requireConfirmation)
|
||||||
.build()
|
.build()
|
||||||
val prompt = BiometricPrompt(
|
val prompt = BiometricPrompt(
|
||||||
this,
|
this,
|
||||||
|
@ -10,6 +10,7 @@ class BiometricAuthPrompt(
|
|||||||
val title: TextHolder,
|
val title: TextHolder,
|
||||||
val text: TextHolder? = null,
|
val text: TextHolder? = null,
|
||||||
val cipher: LeCipher,
|
val cipher: LeCipher,
|
||||||
|
val requireConfirmation: Boolean,
|
||||||
/**
|
/**
|
||||||
* Called when the user either failed the authentication or
|
* Called when the user either failed the authentication or
|
||||||
* successfully passed it.
|
* successfully passed it.
|
||||||
@ -22,6 +23,7 @@ class BiometricAuthPrompt(
|
|||||||
class BiometricAuthPromptSimple(
|
class BiometricAuthPromptSimple(
|
||||||
val title: TextHolder,
|
val title: TextHolder,
|
||||||
val text: TextHolder? = null,
|
val text: TextHolder? = null,
|
||||||
|
val requireConfirmation: Boolean,
|
||||||
/**
|
/**
|
||||||
* Called when the user either failed the authentication or
|
* Called when the user either failed the authentication or
|
||||||
* successfully passed it.
|
* successfully passed it.
|
||||||
|
@ -17,6 +17,7 @@ sealed interface VaultState {
|
|||||||
class WithBiometric(
|
class WithBiometric(
|
||||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||||
val getCreateIo: (String) -> IO<Unit>,
|
val getCreateIo: (String) -> IO<Unit>,
|
||||||
|
val requireConfirmation: Boolean,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ sealed interface VaultState {
|
|||||||
class WithBiometric(
|
class WithBiometric(
|
||||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||||
val getCreateIo: () -> IO<Unit>,
|
val getCreateIo: () -> IO<Unit>,
|
||||||
|
val requireConfirmation: Boolean,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ interface SettingsReadRepository {
|
|||||||
|
|
||||||
fun getBiometricTimeout(): Flow<Duration?>
|
fun getBiometricTimeout(): Flow<Duration?>
|
||||||
|
|
||||||
|
fun getBiometricRequireConfirmation(): Flow<Boolean>
|
||||||
|
|
||||||
fun getClipboardClearDelay(): Flow<Duration?>
|
fun getClipboardClearDelay(): Flow<Duration?>
|
||||||
|
|
||||||
fun getClipboardUpdateDuration(): Flow<Duration?>
|
fun getClipboardUpdateDuration(): Flow<Duration?>
|
||||||
|
@ -57,6 +57,10 @@ interface SettingsReadWriteRepository : SettingsReadRepository {
|
|||||||
duration: Duration?,
|
duration: Duration?,
|
||||||
): IO<Unit>
|
): IO<Unit>
|
||||||
|
|
||||||
|
fun setBiometricRequireConfirmation(
|
||||||
|
requireConfirmation: Boolean,
|
||||||
|
): IO<Unit>
|
||||||
|
|
||||||
fun setClipboardClearDelay(
|
fun setClipboardClearDelay(
|
||||||
duration: Duration?,
|
duration: Duration?,
|
||||||
): IO<Unit>
|
): IO<Unit>
|
||||||
|
@ -47,6 +47,7 @@ class SettingsRepositoryImpl(
|
|||||||
private const val KEY_VAULT_TIMEOUT = "vault_timeout"
|
private const val KEY_VAULT_TIMEOUT = "vault_timeout"
|
||||||
private const val KEY_VAULT_SCREEN_LOCK = "vault_screen_lock"
|
private const val KEY_VAULT_SCREEN_LOCK = "vault_screen_lock"
|
||||||
private const val KEY_BIOMETRIC_TIMEOUT = "biometric_timeout"
|
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_CLEAR_DELAY = "clipboard_clear_delay"
|
||||||
private const val KEY_CLIPBOARD_UPDATE_DURATION = "clipboard_update_duration"
|
private const val KEY_CLIPBOARD_UPDATE_DURATION = "clipboard_update_duration"
|
||||||
private const val KEY_CONCEAL_FIELDS = "conceal_fields"
|
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 biometricTimeoutPref = store.getLong(KEY_BIOMETRIC_TIMEOUT, NONE_DURATION)
|
||||||
|
|
||||||
|
private val biometricRequireConfirmationPref = store.getBoolean(KEY_BIOMETRIC_REQUIRE_CONFIRMATION, true)
|
||||||
|
|
||||||
private val clipboardClearDelayPref =
|
private val clipboardClearDelayPref =
|
||||||
store.getLong(KEY_CLIPBOARD_CLEAR_DELAY, NONE_DURATION)
|
store.getLong(KEY_CLIPBOARD_CLEAR_DELAY, NONE_DURATION)
|
||||||
|
|
||||||
@ -277,6 +280,13 @@ class SettingsRepositoryImpl(
|
|||||||
override fun getBiometricTimeout() = biometricTimeoutPref
|
override fun getBiometricTimeout() = biometricTimeoutPref
|
||||||
.asDuration()
|
.asDuration()
|
||||||
|
|
||||||
|
override fun setBiometricRequireConfirmation(
|
||||||
|
requireConfirmation: Boolean,
|
||||||
|
) = biometricRequireConfirmationPref
|
||||||
|
.setAndCommit(requireConfirmation)
|
||||||
|
|
||||||
|
override fun getBiometricRequireConfirmation() = biometricRequireConfirmationPref
|
||||||
|
|
||||||
override fun setClipboardClearDelay(duration: Duration?) = clipboardClearDelayPref
|
override fun setClipboardClearDelay(duration: Duration?) = clipboardClearDelayPref
|
||||||
.setAndCommit(duration)
|
.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.BiometricStatusUseCase
|
||||||
import com.artemchep.keyguard.common.usecase.DisableBiometric
|
import com.artemchep.keyguard.common.usecase.DisableBiometric
|
||||||
import com.artemchep.keyguard.common.usecase.GetBiometricRemainingDuration
|
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.GetVaultSession
|
||||||
import com.artemchep.keyguard.common.usecase.PutVaultSession
|
import com.artemchep.keyguard.common.usecase.PutVaultSession
|
||||||
import com.artemchep.keyguard.common.usecase.UnlockUseCase
|
import com.artemchep.keyguard.common.usecase.UnlockUseCase
|
||||||
@ -48,6 +49,7 @@ import kotlinx.coroutines.GlobalScope
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -69,6 +71,7 @@ class UnlockUseCaseImpl(
|
|||||||
private val disableBiometric: DisableBiometric,
|
private val disableBiometric: DisableBiometric,
|
||||||
private val keyReadWriteRepository: FingerprintReadWriteRepository,
|
private val keyReadWriteRepository: FingerprintReadWriteRepository,
|
||||||
private val sessionMetadataReadWriteRepository: SessionMetadataReadWriteRepository,
|
private val sessionMetadataReadWriteRepository: SessionMetadataReadWriteRepository,
|
||||||
|
private val getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||||
private val getBiometricRemainingDuration: GetBiometricRemainingDuration,
|
private val getBiometricRemainingDuration: GetBiometricRemainingDuration,
|
||||||
private val biometricKeyEncryptUseCase: BiometricKeyEncryptUseCase,
|
private val biometricKeyEncryptUseCase: BiometricKeyEncryptUseCase,
|
||||||
private val decryptBiometricKeyUseCase: BiometricKeyDecryptUseCase,
|
private val decryptBiometricKeyUseCase: BiometricKeyDecryptUseCase,
|
||||||
@ -127,6 +130,7 @@ class UnlockUseCaseImpl(
|
|||||||
disableBiometric = directDI.instance(),
|
disableBiometric = directDI.instance(),
|
||||||
keyReadWriteRepository = directDI.instance(),
|
keyReadWriteRepository = directDI.instance(),
|
||||||
sessionMetadataReadWriteRepository = directDI.instance(),
|
sessionMetadataReadWriteRepository = directDI.instance(),
|
||||||
|
getBiometricRequireConfirmation = directDI.instance(),
|
||||||
getBiometricRemainingDuration = directDI.instance(),
|
getBiometricRemainingDuration = directDI.instance(),
|
||||||
biometricKeyEncryptUseCase = directDI.instance(),
|
biometricKeyEncryptUseCase = directDI.instance(),
|
||||||
decryptBiometricKeyUseCase = directDI.instance(),
|
decryptBiometricKeyUseCase = directDI.instance(),
|
||||||
@ -163,7 +167,7 @@ class UnlockUseCaseImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCreateVaultState(
|
private suspend fun createCreateVaultState(
|
||||||
biometric: BiometricStatus,
|
biometric: BiometricStatus,
|
||||||
): VaultState {
|
): VaultState {
|
||||||
return VaultState.Create(
|
return VaultState.Create(
|
||||||
@ -194,6 +198,8 @@ class UnlockUseCaseImpl(
|
|||||||
// the code in the block. This allows us to not
|
// the code in the block. This allows us to not
|
||||||
// return the cipher from the action.
|
// return the cipher from the action.
|
||||||
.memoize()
|
.memoize()
|
||||||
|
val requireConfirmation = getBiometricRequireConfirmation()
|
||||||
|
.first()
|
||||||
VaultState.Create.WithBiometric(
|
VaultState.Create.WithBiometric(
|
||||||
getCipher = getCipherForEncryption,
|
getCipher = getCipherForEncryption,
|
||||||
getCreateIo = { password ->
|
getCreateIo = { password ->
|
||||||
@ -231,6 +237,7 @@ class UnlockUseCaseImpl(
|
|||||||
}
|
}
|
||||||
.dispatchOn(Dispatchers.Default)
|
.dispatchOn(Dispatchers.Default)
|
||||||
},
|
},
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@ -238,7 +245,7 @@ class UnlockUseCaseImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUnlockVaultState(
|
private suspend fun createUnlockVaultState(
|
||||||
tokens: Fingerprint,
|
tokens: Fingerprint,
|
||||||
biometric: BiometricStatus,
|
biometric: BiometricStatus,
|
||||||
lockReason: String?,
|
lockReason: String?,
|
||||||
@ -275,6 +282,8 @@ class UnlockUseCaseImpl(
|
|||||||
// the code in the block. This allows us to not
|
// the code in the block. This allows us to not
|
||||||
// return the cipher from the action.
|
// return the cipher from the action.
|
||||||
.memoize()
|
.memoize()
|
||||||
|
val requireConfirmation = getBiometricRequireConfirmation()
|
||||||
|
.first()
|
||||||
VaultState.Unlock.WithBiometric(
|
VaultState.Unlock.WithBiometric(
|
||||||
getCipher = getCipherForDecryption,
|
getCipher = getCipherForDecryption,
|
||||||
getCreateIo = {
|
getCreateIo = {
|
||||||
@ -296,6 +305,7 @@ class UnlockUseCaseImpl(
|
|||||||
.flatMap(::unlock)
|
.flatMap(::unlock)
|
||||||
.dispatchOn(Dispatchers.Default)
|
.dispatchOn(Dispatchers.Default)
|
||||||
},
|
},
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
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.Loadable
|
||||||
import com.artemchep.keyguard.common.model.ToastMessage
|
import com.artemchep.keyguard.common.model.ToastMessage
|
||||||
import com.artemchep.keyguard.common.model.VaultState
|
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.UnlockUseCase
|
||||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
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 com.artemchep.keyguard.res.Res
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -37,6 +39,7 @@ private const val KEY_BIOMETRIC_ENABLED = "biometric.enabled"
|
|||||||
fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct) {
|
fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct) {
|
||||||
changePasswordState(
|
changePasswordState(
|
||||||
unlockUseCase = instance(),
|
unlockUseCase = instance(),
|
||||||
|
getBiometricRequireConfirmation = instance(),
|
||||||
windowCoroutineScope = instance(),
|
windowCoroutineScope = instance(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -44,6 +47,7 @@ fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct
|
|||||||
@Composable
|
@Composable
|
||||||
fun changePasswordState(
|
fun changePasswordState(
|
||||||
unlockUseCase: UnlockUseCase,
|
unlockUseCase: UnlockUseCase,
|
||||||
|
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||||
windowCoroutineScope: WindowCoroutineScope,
|
windowCoroutineScope: WindowCoroutineScope,
|
||||||
): Loadable<ChangePasswordState> = produceScreenState(
|
): Loadable<ChangePasswordState> = produceScreenState(
|
||||||
key = "change_password",
|
key = "change_password",
|
||||||
@ -63,11 +67,16 @@ fun changePasswordState(
|
|||||||
val fn = unlockUseCase()
|
val fn = unlockUseCase()
|
||||||
.map { vaultState ->
|
.map { vaultState ->
|
||||||
when (vaultState) {
|
when (vaultState) {
|
||||||
is VaultState.Main -> ah(
|
is VaultState.Main -> {
|
||||||
state = vaultState,
|
val requireConfirmation = getBiometricRequireConfirmation()
|
||||||
biometricPromptSink = biometricPromptSink,
|
.first()
|
||||||
windowCoroutineScope = windowCoroutineScope,
|
ah(
|
||||||
)
|
state = vaultState,
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
|
biometricPromptSink = biometricPromptSink,
|
||||||
|
windowCoroutineScope = windowCoroutineScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -154,6 +163,7 @@ fun changePasswordState(
|
|||||||
|
|
||||||
private fun RememberStateFlowScope.ah(
|
private fun RememberStateFlowScope.ah(
|
||||||
state: VaultState.Main,
|
state: VaultState.Main,
|
||||||
|
requireConfirmation: Boolean,
|
||||||
biometricPromptSink: EventFlow<BiometricAuthPrompt>,
|
biometricPromptSink: EventFlow<BiometricAuthPrompt>,
|
||||||
windowCoroutineScope: WindowCoroutineScope,
|
windowCoroutineScope: WindowCoroutineScope,
|
||||||
) = Fn(
|
) = Fn(
|
||||||
@ -184,6 +194,7 @@ private fun RememberStateFlowScope.ah(
|
|||||||
val prompt = BiometricAuthPrompt(
|
val prompt = BiometricAuthPrompt(
|
||||||
title = TextHolder.Res(Res.strings.changepassword_biometric_auth_confirm_title),
|
title = TextHolder.Res(Res.strings.changepassword_biometric_auth_confirm_title),
|
||||||
cipher = cipher,
|
cipher = cipher,
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
onComplete = { result ->
|
onComplete = { result ->
|
||||||
result.fold(
|
result.fold(
|
||||||
ifLeft = { e ->
|
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.model.ToastMessage
|
||||||
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
|
||||||
import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase
|
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.usecase.WindowCoroutineScope
|
||||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
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 com.artemchep.keyguard.res.Res
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import org.kodein.di.compose.localDI
|
import org.kodein.di.compose.localDI
|
||||||
@ -43,6 +45,7 @@ fun produceElevatedAccessState(
|
|||||||
produceElevatedAccessState(
|
produceElevatedAccessState(
|
||||||
transmitter = transmitter,
|
transmitter = transmitter,
|
||||||
biometricStatusUseCase = instance(),
|
biometricStatusUseCase = instance(),
|
||||||
|
getBiometricRequireConfirmation = instance(),
|
||||||
confirmAccessByPasswordUseCase = instance(),
|
confirmAccessByPasswordUseCase = instance(),
|
||||||
windowCoroutineScope = instance(),
|
windowCoroutineScope = instance(),
|
||||||
)
|
)
|
||||||
@ -52,6 +55,7 @@ fun produceElevatedAccessState(
|
|||||||
fun produceElevatedAccessState(
|
fun produceElevatedAccessState(
|
||||||
transmitter: RouteResultTransmitter<ElevatedAccessResult>,
|
transmitter: RouteResultTransmitter<ElevatedAccessResult>,
|
||||||
biometricStatusUseCase: BiometricStatusUseCase,
|
biometricStatusUseCase: BiometricStatusUseCase,
|
||||||
|
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||||
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
|
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
|
||||||
windowCoroutineScope: WindowCoroutineScope,
|
windowCoroutineScope: WindowCoroutineScope,
|
||||||
): ElevatedAccessState = produceScreenState(
|
): ElevatedAccessState = produceScreenState(
|
||||||
@ -74,8 +78,11 @@ fun produceElevatedAccessState(
|
|||||||
.getOrNull()
|
.getOrNull()
|
||||||
when (biometricStatus) {
|
when (biometricStatus) {
|
||||||
is BiometricStatus.Available -> {
|
is BiometricStatus.Available -> {
|
||||||
|
val requireConfirmation = getBiometricRequireConfirmation()
|
||||||
|
.first()
|
||||||
createPromptOrNull(
|
createPromptOrNull(
|
||||||
executor = executor,
|
executor = executor,
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
fn = {
|
fn = {
|
||||||
navigatePopSelf()
|
navigatePopSelf()
|
||||||
transmitter(ElevatedAccessResult.Allow)
|
transmitter(ElevatedAccessResult.Allow)
|
||||||
@ -165,11 +172,13 @@ fun produceElevatedAccessState(
|
|||||||
|
|
||||||
private fun createPromptOrNull(
|
private fun createPromptOrNull(
|
||||||
executor: LoadingTask,
|
executor: LoadingTask,
|
||||||
|
requireConfirmation: Boolean,
|
||||||
fn: () -> Unit,
|
fn: () -> Unit,
|
||||||
): PureBiometricAuthPrompt = run {
|
): PureBiometricAuthPrompt = run {
|
||||||
BiometricAuthPromptSimple(
|
BiometricAuthPromptSimple(
|
||||||
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
|
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
|
||||||
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
|
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
|
||||||
|
requireConfirmation = requireConfirmation,
|
||||||
onComplete = { result ->
|
onComplete = { result ->
|
||||||
result.fold(
|
result.fold(
|
||||||
ifLeft = { exception ->
|
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.settingAutofillSaveRequestProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingAutofillSaveUriProvider
|
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.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.settingCheckPwnedPasswordsProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedServicesProvider
|
import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedServicesProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingCheckTwoFAProvider
|
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_WRITE_EXTERNAL_STORAGE = "permission_write_external_storage"
|
||||||
const val PERMISSION_POST_NOTIFICATION = "permission_post_notification"
|
const val PERMISSION_POST_NOTIFICATION = "permission_post_notification"
|
||||||
const val BIOMETRIC = "biometric"
|
const val BIOMETRIC = "biometric"
|
||||||
|
const val BIOMETRIC_REQUIRE_CONFIRMATION = "biometric_require_confirmation"
|
||||||
const val VAULT_PERSIST = "vault_persist"
|
const val VAULT_PERSIST = "vault_persist"
|
||||||
const val VAULT_LOCK = "vault_lock"
|
const val VAULT_LOCK = "vault_lock"
|
||||||
const val VAULT_LOCK_AFTER_REBOOT = "vault_lock_after_reboot"
|
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_POST_NOTIFICATION to ::settingPermissionPostNotificationsProvider,
|
||||||
Setting.PERMISSION_WRITE_EXTERNAL_STORAGE to ::settingPermissionWriteExternalStorageProvider,
|
Setting.PERMISSION_WRITE_EXTERNAL_STORAGE to ::settingPermissionWriteExternalStorageProvider,
|
||||||
Setting.BIOMETRIC to ::settingBiometricsProvider,
|
Setting.BIOMETRIC to ::settingBiometricsProvider,
|
||||||
|
Setting.BIOMETRIC_REQUIRE_CONFIRMATION to ::settingBiometricsRequireConfirmationProvider,
|
||||||
Setting.VAULT_PERSIST to ::settingVaultPersistProvider,
|
Setting.VAULT_PERSIST to ::settingVaultPersistProvider,
|
||||||
Setting.VAULT_LOCK to ::settingVaultLockProvider,
|
Setting.VAULT_LOCK to ::settingVaultLockProvider,
|
||||||
Setting.VAULT_LOCK_AFTER_REBOOT to ::settingVaultLockAfterRebootProvider,
|
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.BiometricStatusUseCase
|
||||||
import com.artemchep.keyguard.common.usecase.DisableBiometric
|
import com.artemchep.keyguard.common.usecase.DisableBiometric
|
||||||
import com.artemchep.keyguard.common.usecase.EnableBiometric
|
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.usecase.WindowCoroutineScope
|
||||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||||
import com.artemchep.keyguard.feature.biometric.BiometricPromptEffect
|
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.FlatItem
|
||||||
import com.artemchep.keyguard.ui.icons.icon
|
import com.artemchep.keyguard.ui.icons.icon
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
@ -38,6 +40,7 @@ fun settingBiometricsProvider(
|
|||||||
) = settingBiometricsProvider(
|
) = settingBiometricsProvider(
|
||||||
fingerprintReadRepository = directDI.instance(),
|
fingerprintReadRepository = directDI.instance(),
|
||||||
biometricStatusUseCase = directDI.instance(),
|
biometricStatusUseCase = directDI.instance(),
|
||||||
|
getBiometricRequireConfirmation = directDI.instance(),
|
||||||
enableBiometric = directDI.instance(),
|
enableBiometric = directDI.instance(),
|
||||||
disableBiometric = directDI.instance(),
|
disableBiometric = directDI.instance(),
|
||||||
windowCoroutineScope = directDI.instance(),
|
windowCoroutineScope = directDI.instance(),
|
||||||
@ -46,6 +49,7 @@ fun settingBiometricsProvider(
|
|||||||
fun settingBiometricsProvider(
|
fun settingBiometricsProvider(
|
||||||
fingerprintReadRepository: FingerprintReadRepository,
|
fingerprintReadRepository: FingerprintReadRepository,
|
||||||
biometricStatusUseCase: BiometricStatusUseCase,
|
biometricStatusUseCase: BiometricStatusUseCase,
|
||||||
|
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
|
||||||
enableBiometric: EnableBiometric,
|
enableBiometric: EnableBiometric,
|
||||||
disableBiometric: DisableBiometric,
|
disableBiometric: DisableBiometric,
|
||||||
windowCoroutineScope: WindowCoroutineScope,
|
windowCoroutineScope: WindowCoroutineScope,
|
||||||
@ -54,64 +58,82 @@ fun settingBiometricsProvider(
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.flatMapLatest { hasBiometrics ->
|
.flatMapLatest { hasBiometrics ->
|
||||||
if (hasBiometrics) {
|
if (hasBiometrics) {
|
||||||
fingerprintReadRepository.get()
|
createSettingComponentFlow(
|
||||||
.map { tokens ->
|
fingerprintReadRepository = fingerprintReadRepository,
|
||||||
val hasBiometricEnabled = tokens?.biometric != null
|
getBiometricRequireConfirmation = getBiometricRequireConfirmation,
|
||||||
|
enableBiometric = enableBiometric,
|
||||||
SettingIi(
|
disableBiometric = disableBiometric,
|
||||||
search = SettingIi.Search(
|
windowCoroutineScope = windowCoroutineScope,
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// hide option if the device does not have biometrics
|
// hide option if the device does not have biometrics
|
||||||
flowOf(null)
|
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
|
@Composable
|
||||||
private fun SettingBiometrics(
|
private fun SettingBiometrics(
|
||||||
checked: Boolean,
|
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",
|
key = "biometric",
|
||||||
list = listOf(
|
list = listOf(
|
||||||
SettingPaneItem.Item(Setting.BIOMETRIC),
|
SettingPaneItem.Item(Setting.BIOMETRIC),
|
||||||
|
SettingPaneItem.Item(Setting.BIOMETRIC_REQUIRE_CONFIRMATION),
|
||||||
SettingPaneItem.Item(Setting.REQUIRE_MASTER_PASSWORD),
|
SettingPaneItem.Item(Setting.REQUIRE_MASTER_PASSWORD),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -144,6 +144,7 @@ private fun createPromptOrNull(
|
|||||||
BiometricAuthPrompt(
|
BiometricAuthPrompt(
|
||||||
title = TextHolder.Res(Res.strings.setup_biometric_auth_confirm_title),
|
title = TextHolder.Res(Res.strings.setup_biometric_auth_confirm_title),
|
||||||
cipher = cipher,
|
cipher = cipher,
|
||||||
|
requireConfirmation = createVaultWithMasterPasswordAndBiometricFn.requireConfirmation,
|
||||||
onComplete = { result ->
|
onComplete = { result ->
|
||||||
result.fold(
|
result.fold(
|
||||||
ifLeft = { exception ->
|
ifLeft = { exception ->
|
||||||
@ -185,6 +186,7 @@ private class CreateVaultWithBiometric(
|
|||||||
*/
|
*/
|
||||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||||
getCreateIo: (String) -> IO<Unit>,
|
getCreateIo: (String) -> IO<Unit>,
|
||||||
|
val requireConfirmation: Boolean,
|
||||||
) : CreateVaultWithPassword(executor, getCreateIo) {
|
) : CreateVaultWithPassword(executor, getCreateIo) {
|
||||||
// Create from vault state options
|
// Create from vault state options
|
||||||
constructor(
|
constructor(
|
||||||
@ -194,6 +196,7 @@ private class CreateVaultWithBiometric(
|
|||||||
executor = executor,
|
executor = executor,
|
||||||
getCipher = options.getCipher,
|
getCipher = options.getCipher,
|
||||||
getCreateIo = options.getCreateIo,
|
getCreateIo = options.getCreateIo,
|
||||||
|
requireConfirmation = options.requireConfirmation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,6 +201,7 @@ private fun createPromptOrNull(
|
|||||||
title = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_title),
|
title = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_title),
|
||||||
text = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_text),
|
text = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_text),
|
||||||
cipher = cipher,
|
cipher = cipher,
|
||||||
|
requireConfirmation = fn.requireConfirmation,
|
||||||
onComplete = { result ->
|
onComplete = { result ->
|
||||||
result.fold(
|
result.fold(
|
||||||
ifLeft = { exception ->
|
ifLeft = { exception ->
|
||||||
@ -249,6 +250,7 @@ private class UnlockVaultWithBiometric(
|
|||||||
*/
|
*/
|
||||||
val getCipher: () -> Either<Throwable, LeCipher>,
|
val getCipher: () -> Either<Throwable, LeCipher>,
|
||||||
private val getCreateIo: () -> IO<Unit>,
|
private val getCreateIo: () -> IO<Unit>,
|
||||||
|
val requireConfirmation: Boolean,
|
||||||
) : () -> Unit {
|
) : () -> Unit {
|
||||||
// Create from vault state options
|
// Create from vault state options
|
||||||
constructor(
|
constructor(
|
||||||
@ -258,6 +260,7 @@ private class UnlockVaultWithBiometric(
|
|||||||
executor = executor,
|
executor = executor,
|
||||||
getCipher = options.getCipher,
|
getCipher = options.getCipher,
|
||||||
getCreateIo = options.getCreateIo,
|
getCreateIo = options.getCreateIo,
|
||||||
|
requireConfirmation = options.requireConfirmation,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun invoke() {
|
override fun invoke() {
|
||||||
|
@ -991,6 +991,8 @@
|
|||||||
<string name="pref_item_features_overview_title">Features overview</string>
|
<string name="pref_item_features_overview_title">Features overview</string>
|
||||||
<string name="pref_item_url_override_title">URL overrides</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_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
|
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. -->
|
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.GetAutofillRespectAutofillOff
|
||||||
import com.artemchep.keyguard.common.usecase.GetAutofillSaveRequest
|
import com.artemchep.keyguard.common.usecase.GetAutofillSaveRequest
|
||||||
import com.artemchep.keyguard.common.usecase.GetAutofillSaveUri
|
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.GetBiometricTimeout
|
||||||
import com.artemchep.keyguard.common.usecase.GetBiometricTimeoutVariants
|
import com.artemchep.keyguard.common.usecase.GetBiometricTimeoutVariants
|
||||||
import com.artemchep.keyguard.common.usecase.GetCachePremium
|
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.PutAutofillRespectAutofillOff
|
||||||
import com.artemchep.keyguard.common.usecase.PutAutofillSaveRequest
|
import com.artemchep.keyguard.common.usecase.PutAutofillSaveRequest
|
||||||
import com.artemchep.keyguard.common.usecase.PutAutofillSaveUri
|
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.PutBiometricTimeout
|
||||||
import com.artemchep.keyguard.common.usecase.PutCachePremium
|
import com.artemchep.keyguard.common.usecase.PutCachePremium
|
||||||
import com.artemchep.keyguard.common.usecase.PutCheckPwnedPasswords
|
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.GetAutofillRespectAutofillOffImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveRequestImpl
|
import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveRequestImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveUriImpl
|
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.GetBiometricTimeoutImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutVariantsImpl
|
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutVariantsImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.GetCachePremiumImpl
|
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.PutAutofillRespectAutofillOffImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveRequestImpl
|
import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveRequestImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveUriImpl
|
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.PutBiometricTimeoutImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.PutCachePremiumImpl
|
import com.artemchep.keyguard.common.usecase.impl.PutCachePremiumImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.PutCheckPwnedPasswordsImpl
|
import com.artemchep.keyguard.common.usecase.impl.PutCheckPwnedPasswordsImpl
|
||||||
@ -477,6 +481,16 @@ fun globalModuleJvm() = DI.Module(
|
|||||||
directDI = this,
|
directDI = this,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
bindSingleton<GetBiometricRequireConfirmation> {
|
||||||
|
GetBiometricRequireConfirmationImpl(
|
||||||
|
directDI = this,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
bindSingleton<PutBiometricRequireConfirmation> {
|
||||||
|
PutBiometricRequireConfirmationImpl(
|
||||||
|
directDI = this,
|
||||||
|
)
|
||||||
|
}
|
||||||
bindSingleton<GetCheckPwnedPasswords> {
|
bindSingleton<GetCheckPwnedPasswords> {
|
||||||
GetCheckPwnedPasswordsImpl(
|
GetCheckPwnedPasswordsImpl(
|
||||||
directDI = this,
|
directDI = this,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user