change : confirm biometric before allowing biometric unlock.
This commit is contained in:
@@ -10,7 +10,7 @@ package io.element.android.features.lockscreen.impl
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
@@ -45,7 +45,7 @@ class DefaultLockScreenService @Inject constructor(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val sessionObserver: SessionObserver,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
biometricUnlockManager: BiometricUnlockManager,
|
||||
biometricAuthenticatorManager: BiometricAuthenticatorManager,
|
||||
) : LockScreenService {
|
||||
private val _lockState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked)
|
||||
override val lockState: StateFlow<LockScreenLockState> = _lockState
|
||||
@@ -62,8 +62,8 @@ class DefaultLockScreenService @Inject constructor(
|
||||
_lockState.value = LockScreenLockState.Unlocked
|
||||
}
|
||||
})
|
||||
biometricUnlockManager.addCallback(object : DefaultBiometricUnlockCallback() {
|
||||
override fun onBiometricUnlockSuccess() {
|
||||
biometricAuthenticatorManager.addCallback(object : DefaultBiometricUnlockCallback() {
|
||||
override fun onBiometricAuthenticationSuccess() {
|
||||
_lockState.value = LockScreenLockState.Unlocked
|
||||
coroutineScope.launch {
|
||||
lockScreenStore.resetCounter()
|
||||
|
||||
@@ -21,11 +21,11 @@ import timber.log.Timber
|
||||
import java.security.InvalidKeyException
|
||||
import javax.crypto.Cipher
|
||||
|
||||
interface BiometricUnlock {
|
||||
interface BiometricAuthenticator {
|
||||
interface Callback {
|
||||
fun onBiometricSetupError()
|
||||
fun onBiometricUnlockSuccess()
|
||||
fun onBiometricUnlockFailed(error: Exception?)
|
||||
fun onBiometricAuthenticationSuccess()
|
||||
fun onBiometricAuthenticationFailed(error: Exception?)
|
||||
}
|
||||
|
||||
sealed interface AuthenticationResult {
|
||||
@@ -38,23 +38,23 @@ interface BiometricUnlock {
|
||||
suspend fun authenticate(): AuthenticationResult
|
||||
}
|
||||
|
||||
class NoopBiometricUnlock : BiometricUnlock {
|
||||
class NoopBiometricAuthentication : BiometricAuthenticator {
|
||||
override val isActive: Boolean = false
|
||||
override fun setup() = Unit
|
||||
override suspend fun authenticate() = BiometricUnlock.AuthenticationResult.Failure()
|
||||
override suspend fun authenticate() = BiometricAuthenticator.AuthenticationResult.Failure()
|
||||
}
|
||||
|
||||
class DefaultBiometricUnlock(
|
||||
class DefaultBiometricAuthentication(
|
||||
private val activity: FragmentActivity,
|
||||
private val promptInfo: PromptInfo,
|
||||
private val secretKeyRepository: SecretKeyRepository,
|
||||
private val encryptionDecryptionService: EncryptionDecryptionService,
|
||||
private val keyAlias: String,
|
||||
private val callbacks: List<BiometricUnlock.Callback>
|
||||
) : BiometricUnlock {
|
||||
private val callbacks: List<BiometricAuthenticator.Callback>
|
||||
) : BiometricAuthenticator {
|
||||
override val isActive: Boolean = true
|
||||
|
||||
private lateinit var cryptoObject: CryptoObject
|
||||
private var cryptoObject: CryptoObject? = null
|
||||
|
||||
override fun setup() {
|
||||
try {
|
||||
@@ -67,11 +67,10 @@ class DefaultBiometricUnlock(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun authenticate(): BiometricUnlock.AuthenticationResult {
|
||||
if (!this::cryptoObject.isInitialized) {
|
||||
return BiometricUnlock.AuthenticationResult.Failure()
|
||||
}
|
||||
val deferredAuthenticationResult = CompletableDeferred<BiometricUnlock.AuthenticationResult>()
|
||||
override suspend fun authenticate(): BiometricAuthenticator.AuthenticationResult {
|
||||
val cryptoObject = cryptoObject ?: return BiometricAuthenticator.AuthenticationResult.Failure()
|
||||
|
||||
val deferredAuthenticationResult = CompletableDeferred<BiometricAuthenticator.AuthenticationResult>()
|
||||
val executor = ContextCompat.getMainExecutor(activity.baseContext)
|
||||
val callback = AuthenticationCallback(callbacks, deferredAuthenticationResult)
|
||||
val prompt = BiometricPrompt(activity, executor, callback)
|
||||
@@ -80,7 +79,7 @@ class DefaultBiometricUnlock(
|
||||
deferredAuthenticationResult.await()
|
||||
} catch (cancellation: CancellationException) {
|
||||
prompt.cancelAuthentication()
|
||||
BiometricUnlock.AuthenticationResult.Failure(cancellation)
|
||||
BiometricAuthenticator.AuthenticationResult.Failure(cancellation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,30 +90,30 @@ class DefaultBiometricUnlock(
|
||||
}
|
||||
|
||||
private class AuthenticationCallback(
|
||||
private val callbacks: List<BiometricUnlock.Callback>,
|
||||
private val deferredAuthenticationResult: CompletableDeferred<BiometricUnlock.AuthenticationResult>,
|
||||
private val callbacks: List<BiometricAuthenticator.Callback>,
|
||||
private val deferredAuthenticationResult: CompletableDeferred<BiometricAuthenticator.AuthenticationResult>,
|
||||
) : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
val biometricUnlockError = BiometricUnlockError(errorCode, errString.toString())
|
||||
callbacks.forEach { it.onBiometricUnlockFailed(biometricUnlockError) }
|
||||
deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure(biometricUnlockError))
|
||||
callbacks.forEach { it.onBiometricAuthenticationFailed(biometricUnlockError) }
|
||||
deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Failure(biometricUnlockError))
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
callbacks.forEach { it.onBiometricUnlockFailed(null) }
|
||||
callbacks.forEach { it.onBiometricAuthenticationFailed(null) }
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
if (result.cryptoObject?.cipher.isValid()) {
|
||||
callbacks.forEach { it.onBiometricUnlockSuccess() }
|
||||
deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Success)
|
||||
callbacks.forEach { it.onBiometricAuthenticationSuccess() }
|
||||
deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Success)
|
||||
} else {
|
||||
val error = IllegalStateException("Invalid cipher")
|
||||
callbacks.forEach { it.onBiometricUnlockFailed(error) }
|
||||
deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure())
|
||||
callbacks.forEach { it.onBiometricAuthenticationFailed(error) }
|
||||
deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Failure())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ package io.element.android.features.lockscreen.impl.biometric
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
interface BiometricUnlockManager {
|
||||
interface BiometricAuthenticatorManager {
|
||||
/**
|
||||
* If the device is secured for example with a pin, pattern or password.
|
||||
*/
|
||||
@@ -20,9 +20,18 @@ interface BiometricUnlockManager {
|
||||
*/
|
||||
val hasAvailableAuthenticator: Boolean
|
||||
|
||||
fun addCallback(callback: BiometricUnlock.Callback)
|
||||
fun removeCallback(callback: BiometricUnlock.Callback)
|
||||
fun addCallback(callback: BiometricAuthenticator.Callback)
|
||||
fun removeCallback(callback: BiometricAuthenticator.Callback)
|
||||
|
||||
/**
|
||||
* Remember a biometric authenticator ready for unlocking the app.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberBiometricUnlock(): BiometricUnlock
|
||||
fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator
|
||||
|
||||
/**
|
||||
* Remember a biometric authenticator ready for confirmation.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberConfirmBiometricAuthenticator(): BiometricAuthenticator
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import io.element.android.libraries.cryptography.api.SecretKeyRepository
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
@@ -40,15 +41,15 @@ private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_BIOMETRIC"
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultBiometricUnlockManager @Inject constructor(
|
||||
class DefaultBiometricAuthenticatorManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val lockScreenStore: LockScreenStore,
|
||||
private val lockScreenConfig: LockScreenConfig,
|
||||
private val encryptionDecryptionService: EncryptionDecryptionService,
|
||||
private val secretKeyRepository: SecretKeyRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : BiometricUnlockManager {
|
||||
private val callbacks = CopyOnWriteArrayList<BiometricUnlock.Callback>()
|
||||
) : BiometricAuthenticatorManager {
|
||||
private val callbacks = CopyOnWriteArrayList<BiometricAuthenticator.Callback>()
|
||||
private val biometricManager = BiometricManager.from(context)
|
||||
private val keyguardManager: KeyguardManager = context.getSystemService()!!
|
||||
|
||||
@@ -85,16 +86,42 @@ class DefaultBiometricUnlockManager @Inject constructor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun rememberBiometricUnlock(): BiometricUnlock {
|
||||
override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator {
|
||||
val isBiometricAllowed by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false)
|
||||
val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
|
||||
val isAvailable by remember(lifecycleState) {
|
||||
derivedStateOf {
|
||||
isBiometricAllowed && hasAvailableAuthenticator
|
||||
}
|
||||
derivedStateOf { isBiometricAllowed && hasAvailableAuthenticator }
|
||||
}
|
||||
val promptTitle = stringResource(id = R.string.screen_app_lock_biometric_unlock_title_android)
|
||||
val promptNegative = stringResource(id = R.string.screen_app_lock_use_pin_android)
|
||||
return rememberBiometricAuthenticator(
|
||||
isAvailable = isAvailable,
|
||||
promptTitle = promptTitle,
|
||||
promptNegative = promptNegative,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun rememberConfirmBiometricAuthenticator(): BiometricAuthenticator {
|
||||
val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
|
||||
val isAvailable by remember(lifecycleState) {
|
||||
derivedStateOf { hasAvailableAuthenticator }
|
||||
}
|
||||
val promptTitle = stringResource(id = R.string.screen_app_lock_confirm_biometric_authentication_android)
|
||||
val promptNegative = stringResource(id = CommonStrings.action_cancel)
|
||||
return rememberBiometricAuthenticator(
|
||||
isAvailable = isAvailable,
|
||||
promptTitle = promptTitle,
|
||||
promptNegative = promptNegative,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberBiometricAuthenticator(
|
||||
isAvailable: Boolean,
|
||||
promptTitle: String,
|
||||
promptNegative: String,
|
||||
): BiometricAuthenticator {
|
||||
val activity = LocalContext.current.findFragmentActivity()
|
||||
return remember(isAvailable) {
|
||||
if (isAvailable && activity != null) {
|
||||
@@ -108,7 +135,7 @@ class DefaultBiometricUnlockManager @Inject constructor(
|
||||
setNegativeButtonText(promptNegative)
|
||||
setAllowedAuthenticators(authenticators)
|
||||
}.build()
|
||||
DefaultBiometricUnlock(
|
||||
DefaultBiometricAuthentication(
|
||||
activity = activity,
|
||||
promptInfo = promptInfo,
|
||||
secretKeyRepository = secretKeyRepository,
|
||||
@@ -117,16 +144,16 @@ class DefaultBiometricUnlockManager @Inject constructor(
|
||||
callbacks = callbacks + internalCallback
|
||||
)
|
||||
} else {
|
||||
NoopBiometricUnlock()
|
||||
NoopBiometricAuthentication()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCallback(callback: BiometricUnlock.Callback) {
|
||||
override fun addCallback(callback: BiometricAuthenticator.Callback) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
override fun removeCallback(callback: BiometricUnlock.Callback) {
|
||||
override fun removeCallback(callback: BiometricAuthenticator.Callback) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
package io.element.android.features.lockscreen.impl.biometric
|
||||
|
||||
open class DefaultBiometricUnlockCallback : BiometricUnlock.Callback {
|
||||
open class DefaultBiometricUnlockCallback : BiometricAuthenticator.Callback {
|
||||
override fun onBiometricSetupError() = Unit
|
||||
override fun onBiometricUnlockSuccess() = Unit
|
||||
override fun onBiometricUnlockFailed(error: Exception?) = Unit
|
||||
override fun onBiometricAuthenticationSuccess() = Unit
|
||||
override fun onBiometricAuthenticationFailed(error: Exception?) = Unit
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.lockscreen.impl.LockScreenConfig
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
@@ -27,7 +28,7 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||
private val lockScreenConfig: LockScreenConfig,
|
||||
private val pinCodeManager: PinCodeManager,
|
||||
private val lockScreenStore: LockScreenStore,
|
||||
private val biometricUnlockManager: BiometricUnlockManager,
|
||||
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : Presenter<LockScreenSettingsState> {
|
||||
@Composable
|
||||
@@ -42,6 +43,8 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
|
||||
|
||||
fun handleEvents(event: LockScreenSettingsEvents) {
|
||||
when (event) {
|
||||
LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false
|
||||
@@ -56,7 +59,14 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||
LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true
|
||||
LockScreenSettingsEvents.ToggleBiometricAllowed -> {
|
||||
coroutineScope.launch {
|
||||
lockScreenStore.setIsBiometricUnlockAllowed(!isBiometricEnabled)
|
||||
if (!isBiometricEnabled) {
|
||||
biometricUnlock.setup()
|
||||
if (biometricUnlock.authenticate() == BiometricAuthenticator.AuthenticationResult.Success) {
|
||||
lockScreenStore.setIsBiometricUnlockAllowed(true)
|
||||
}
|
||||
} else {
|
||||
lockScreenStore.setIsBiometricUnlockAllowed(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +76,7 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||
showRemovePinOption = showRemovePinOption,
|
||||
isBiometricEnabled = isBiometricEnabled,
|
||||
showRemovePinConfirmation = showRemovePinConfirmation,
|
||||
showToggleBiometric = biometricUnlockManager.isDeviceSecured,
|
||||
showToggleBiometric = biometricAuthenticatorManager.isDeviceSecured,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.setup.biometric.SetupBiometricNode
|
||||
@@ -35,6 +36,7 @@ class LockScreenSetupFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val pinCodeManager: PinCodeManager,
|
||||
val biometricAuthenticatorManager: BiometricAuthenticatorManager,
|
||||
) : BaseFlowNode<LockScreenSetupFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Pin,
|
||||
@@ -61,7 +63,11 @@ class LockScreenSetupFlowNode @AssistedInject constructor(
|
||||
|
||||
private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() {
|
||||
override fun onPinCodeCreated() {
|
||||
backstack.newRoot(NavTarget.Biometric)
|
||||
if (biometricAuthenticatorManager.hasAvailableAuthenticator) {
|
||||
backstack.newRoot(NavTarget.Biometric)
|
||||
} else {
|
||||
onSetupDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -20,6 +22,7 @@ import javax.inject.Inject
|
||||
|
||||
class SetupBiometricPresenter @Inject constructor(
|
||||
private val lockScreenStore: LockScreenStore,
|
||||
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
|
||||
) : Presenter<SetupBiometricState> {
|
||||
@Composable
|
||||
override fun present(): SetupBiometricState {
|
||||
@@ -28,12 +31,16 @@ class SetupBiometricPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
|
||||
|
||||
fun handleEvents(event: SetupBiometricEvents) {
|
||||
when (event) {
|
||||
SetupBiometricEvents.AllowBiometric -> coroutineScope.launch {
|
||||
lockScreenStore.setIsBiometricUnlockAllowed(true)
|
||||
isBiometricSetupDone = true
|
||||
biometricUnlock.setup()
|
||||
if (biometricUnlock.authenticate() == BiometricAuthenticator.AuthenticationResult.Success) {
|
||||
lockScreenStore.setIsBiometricUnlockAllowed(true)
|
||||
isBiometricSetupDone = true
|
||||
}
|
||||
}
|
||||
SetupBiometricEvents.UsePin -> coroutineScope.launch {
|
||||
lockScreenStore.setIsBiometricUnlockAllowed(false)
|
||||
|
||||
@@ -11,14 +11,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class PinUnlockHelper @Inject constructor(
|
||||
private val biometricUnlockManager: BiometricUnlockManager,
|
||||
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
|
||||
private val pinCodeManager: PinCodeManager
|
||||
) {
|
||||
@Composable
|
||||
@@ -26,7 +26,7 @@ class PinUnlockHelper @Inject constructor(
|
||||
val latestOnUnlock by rememberUpdatedState(onUnlock)
|
||||
DisposableEffect(Unit) {
|
||||
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
|
||||
override fun onBiometricUnlockSuccess() {
|
||||
override fun onBiometricAuthenticationSuccess() {
|
||||
latestOnUnlock()
|
||||
}
|
||||
}
|
||||
@@ -35,10 +35,10 @@ class PinUnlockHelper @Inject constructor(
|
||||
latestOnUnlock()
|
||||
}
|
||||
}
|
||||
biometricUnlockManager.addCallback(biometricUnlockCallback)
|
||||
biometricAuthenticatorManager.addCallback(biometricUnlockCallback)
|
||||
pinCodeManager.addCallback(pinCodeVerifiedCallback)
|
||||
onDispose {
|
||||
biometricUnlockManager.removeCallback(biometricUnlockCallback)
|
||||
biometricAuthenticatorManager.removeCallback(biometricUnlockCallback)
|
||||
pinCodeManager.removeCallback(pinCodeVerifiedCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
@@ -32,7 +32,7 @@ import javax.inject.Inject
|
||||
|
||||
class PinUnlockPresenter @Inject constructor(
|
||||
private val pinCodeManager: PinCodeManager,
|
||||
private val biometricUnlockManager: BiometricUnlockManager,
|
||||
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
|
||||
private val logoutUseCase: LogoutUseCase,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val pinUnlockHelper: PinUnlockHelper,
|
||||
@@ -56,12 +56,12 @@ class PinUnlockPresenter @Inject constructor(
|
||||
mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
var biometricUnlockResult by remember {
|
||||
mutableStateOf<BiometricUnlock.AuthenticationResult?>(null)
|
||||
mutableStateOf<BiometricAuthenticator.AuthenticationResult?>(null)
|
||||
}
|
||||
val isUnlocked = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val biometricUnlock = biometricUnlockManager.rememberBiometricUnlock()
|
||||
val biometricUnlock = biometricAuthenticatorManager.rememberUnlockBiometricAuthenticator()
|
||||
LaunchedEffect(Unit) {
|
||||
suspend {
|
||||
val pinCodeSize = pinCodeManager.getPinCodeSize()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock
|
||||
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -21,7 +21,7 @@ data class PinUnlockState(
|
||||
val signOutAction: AsyncAction<String?>,
|
||||
val showBiometricUnlock: Boolean,
|
||||
val isUnlocked: Boolean,
|
||||
val biometricUnlockResult: BiometricUnlock.AuthenticationResult?,
|
||||
val biometricUnlockResult: BiometricAuthenticator.AuthenticationResult?,
|
||||
val eventSink: (PinUnlockEvents) -> Unit
|
||||
) {
|
||||
val isSignOutPromptCancellable = when (remainingAttempts) {
|
||||
@@ -30,7 +30,7 @@ data class PinUnlockState(
|
||||
}
|
||||
|
||||
val biometricUnlockErrorMessage = when {
|
||||
biometricUnlockResult is BiometricUnlock.AuthenticationResult.Failure &&
|
||||
biometricUnlockResult is BiometricAuthenticator.AuthenticationResult.Failure &&
|
||||
biometricUnlockResult.error is BiometricUnlockError &&
|
||||
biometricUnlockResult.error.isAuthDisabledError -> {
|
||||
biometricUnlockResult.error.message
|
||||
|
||||
@@ -9,7 +9,7 @@ package io.element.android.features.lockscreen.impl.unlock
|
||||
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -25,7 +25,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
||||
aPinUnlockState(showBiometricUnlock = false),
|
||||
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
|
||||
aPinUnlockState(signOutAction = AsyncAction.Loading),
|
||||
aPinUnlockState(biometricUnlockResult = BiometricUnlock.AuthenticationResult.Failure(
|
||||
aPinUnlockState(biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure(
|
||||
BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled")
|
||||
)),
|
||||
)
|
||||
@@ -37,7 +37,7 @@ fun aPinUnlockState(
|
||||
showWrongPinTitle: Boolean = false,
|
||||
showSignOutPrompt: Boolean = false,
|
||||
showBiometricUnlock: Boolean = true,
|
||||
biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null,
|
||||
biometricUnlockResult: BiometricAuthenticator.AuthenticationResult? = null,
|
||||
isUnlocked: Boolean = false,
|
||||
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
) = PinUnlockState(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="screen_app_lock_biometric_authentication">"biometric authentication"</string>
|
||||
<string name="screen_app_lock_biometric_unlock">"biometric unlock"</string>
|
||||
<string name="screen_app_lock_biometric_unlock_title_android">"Unlock with biometric"</string>
|
||||
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirm biometric"</string>
|
||||
<string name="screen_app_lock_forgot_pin">"Forgot PIN?"</string>
|
||||
<string name="screen_app_lock_settings_change_pin">"Change PIN code"</string>
|
||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Allow biometric unlock"</string>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.biometric
|
||||
|
||||
class FakeBiometricAuthenticator(
|
||||
override val isActive: Boolean = false,
|
||||
private val authenticateLambda: suspend () -> BiometricAuthenticator.AuthenticationResult = { BiometricAuthenticator.AuthenticationResult.Success },
|
||||
) : BiometricAuthenticator {
|
||||
override fun setup() = Unit
|
||||
override suspend fun authenticate() = authenticateLambda()
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.biometric
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
class FakeBiometricAuthenticatorManager(
|
||||
override var isDeviceSecured: Boolean = true,
|
||||
override var hasAvailableAuthenticator: Boolean = false,
|
||||
private val createBiometricAuthenticator: () -> BiometricAuthenticator = { FakeBiometricAuthenticator() },
|
||||
) : BiometricAuthenticatorManager {
|
||||
override fun addCallback(callback: BiometricAuthenticator.Callback) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun removeCallback(callback: BiometricAuthenticator.Callback) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator {
|
||||
return remember {
|
||||
createBiometricAuthenticator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun rememberConfirmBiometricAuthenticator(): BiometricAuthenticator {
|
||||
return remember {
|
||||
createBiometricAuthenticator()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.biometric
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
class FakeBiometricUnlockManager : BiometricUnlockManager {
|
||||
override var isDeviceSecured: Boolean = true
|
||||
override var hasAvailableAuthenticator: Boolean = false
|
||||
|
||||
override fun addCallback(callback: BiometricUnlock.Callback) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun removeCallback(callback: BiometricUnlock.Callback) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun rememberBiometricUnlock(): BiometricUnlock {
|
||||
return remember {
|
||||
NoopBiometricUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,38 @@
|
||||
|
||||
package io.element.android.features.lockscreen.impl.settings
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.impl.LockScreenConfig
|
||||
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricUnlockManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig
|
||||
import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
|
||||
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LockScreenSettingsPresenterTest {
|
||||
@Test
|
||||
fun `present - remove pin option is hidden when mandatory`() = runTest {
|
||||
val presenter = createLockScreenSettingsPresenter(this, lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
|
||||
presenter.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.showRemovePinOption).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - remove pin flow`() = runTest {
|
||||
val presenter = createLockScreenSettingsPresenter(this)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
consumeItemsUntilPredicate { state ->
|
||||
state.showRemovePinOption
|
||||
}.last().also { state ->
|
||||
@@ -55,11 +65,95 @@ class LockScreenSettingsPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show toggle biometric if device is secured`() = runTest {
|
||||
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
|
||||
isDeviceSecured = true,
|
||||
)
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().showToggleBiometric).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enable biometric unlock success`() = runTest {
|
||||
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
|
||||
createBiometricAuthenticator = {
|
||||
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Success })
|
||||
}
|
||||
)
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isBiometricEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enable biometric unlock failure`() = runTest {
|
||||
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
|
||||
createBiometricAuthenticator = {
|
||||
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Failure() })
|
||||
}
|
||||
)
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - disable biometric unlock`() = runTest {
|
||||
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(
|
||||
createBiometricAuthenticator = {
|
||||
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Failure() })
|
||||
}
|
||||
)
|
||||
val lockScreenStore = InMemoryLockScreenStore()
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
lockScreenStore = lockScreenStore,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
lockScreenStore.setIsBiometricUnlockAllowed(true)
|
||||
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isBiometricEnabled).isTrue()
|
||||
state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isBiometricEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createLockScreenSettingsPresenter(
|
||||
coroutineScope: CoroutineScope,
|
||||
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
||||
): LockScreenSettingsPresenter {
|
||||
val lockScreenStore = InMemoryLockScreenStore()
|
||||
val pinCodeManager = aPinCodeManager(lockScreenStore = lockScreenStore).apply {
|
||||
createPinCode("1234")
|
||||
}
|
||||
@@ -68,7 +162,7 @@ class LockScreenSettingsPresenterTest {
|
||||
pinCodeManager = pinCodeManager,
|
||||
coroutineScope = coroutineScope,
|
||||
lockScreenConfig = lockScreenConfig,
|
||||
biometricUnlockManager = FakeBiometricUnlockManager(),
|
||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticator
|
||||
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
|
||||
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -19,9 +23,12 @@ import org.junit.Test
|
||||
|
||||
class SetupBiometricPresenterTest {
|
||||
@Test
|
||||
fun `present - allow flow`() = runTest {
|
||||
fun `present - allow flow with biometric authentication success`() = runTest {
|
||||
val lockScreenStore = InMemoryLockScreenStore()
|
||||
val presenter = createSetupBiometricPresenter(lockScreenStore)
|
||||
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(createBiometricAuthenticator = {
|
||||
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Success })
|
||||
})
|
||||
val presenter = createSetupBiometricPresenter(lockScreenStore, fakeBiometricAuthenticatorManager)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -36,6 +43,24 @@ class SetupBiometricPresenterTest {
|
||||
assertThat(lockScreenStore.isBiometricUnlockAllowed().first()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - allow flow with biometric authentication failure`() = runTest {
|
||||
val lockScreenStore = InMemoryLockScreenStore()
|
||||
val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(createBiometricAuthenticator = {
|
||||
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Failure() })
|
||||
})
|
||||
val presenter = createSetupBiometricPresenter(lockScreenStore, fakeBiometricAuthenticatorManager)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isBiometricSetupDone).isFalse()
|
||||
state.eventSink(SetupBiometricEvents.AllowBiometric)
|
||||
}
|
||||
}
|
||||
assertThat(lockScreenStore.isBiometricUnlockAllowed().first()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - skip flow`() = runTest {
|
||||
val lockScreenStore = InMemoryLockScreenStore()
|
||||
@@ -55,10 +80,12 @@ class SetupBiometricPresenterTest {
|
||||
}
|
||||
|
||||
private fun createSetupBiometricPresenter(
|
||||
lockScreenStore: LockScreenStore = InMemoryLockScreenStore()
|
||||
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||
): SetupBiometricPresenter {
|
||||
return SetupBiometricPresenter(
|
||||
lockScreenStore = lockScreenStore,
|
||||
biometricAuthenticatorManager = biometricAuthenticatorManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricUnlockManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
@@ -137,7 +137,7 @@ class PinUnlockPresenterTest {
|
||||
|
||||
private suspend fun createPinUnlockPresenter(
|
||||
scope: CoroutineScope,
|
||||
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
|
||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
|
||||
): PinUnlockPresenter {
|
||||
@@ -147,10 +147,10 @@ class PinUnlockPresenterTest {
|
||||
}
|
||||
return PinUnlockPresenter(
|
||||
pinCodeManager = pinCodeManager,
|
||||
biometricUnlockManager = biometricUnlockManager,
|
||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||
logoutUseCase = logoutUseCase,
|
||||
coroutineScope = scope,
|
||||
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
|
||||
pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user