change : confirm biometric before allowing biometric unlock.

This commit is contained in:
ganfra 2024-11-22 17:13:08 +01:00
parent 2895d0263c
commit 5fcf8a6cb4
19 changed files with 321 additions and 117 deletions

View file

@ -10,7 +10,7 @@ package io.element.android.features.lockscreen.impl
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService 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.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback 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.pin.PinCodeManager
@ -45,7 +45,7 @@ class DefaultLockScreenService @Inject constructor(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val sessionObserver: SessionObserver, private val sessionObserver: SessionObserver,
private val appForegroundStateService: AppForegroundStateService, private val appForegroundStateService: AppForegroundStateService,
biometricUnlockManager: BiometricUnlockManager, biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : LockScreenService { ) : LockScreenService {
private val _lockState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked) private val _lockState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked)
override val lockState: StateFlow<LockScreenLockState> = _lockState override val lockState: StateFlow<LockScreenLockState> = _lockState
@ -62,8 +62,8 @@ class DefaultLockScreenService @Inject constructor(
_lockState.value = LockScreenLockState.Unlocked _lockState.value = LockScreenLockState.Unlocked
} }
}) })
biometricUnlockManager.addCallback(object : DefaultBiometricUnlockCallback() { biometricAuthenticatorManager.addCallback(object : DefaultBiometricUnlockCallback() {
override fun onBiometricUnlockSuccess() { override fun onBiometricAuthenticationSuccess() {
_lockState.value = LockScreenLockState.Unlocked _lockState.value = LockScreenLockState.Unlocked
coroutineScope.launch { coroutineScope.launch {
lockScreenStore.resetCounter() lockScreenStore.resetCounter()

View file

@ -21,11 +21,11 @@ import timber.log.Timber
import java.security.InvalidKeyException import java.security.InvalidKeyException
import javax.crypto.Cipher import javax.crypto.Cipher
interface BiometricUnlock { interface BiometricAuthenticator {
interface Callback { interface Callback {
fun onBiometricSetupError() fun onBiometricSetupError()
fun onBiometricUnlockSuccess() fun onBiometricAuthenticationSuccess()
fun onBiometricUnlockFailed(error: Exception?) fun onBiometricAuthenticationFailed(error: Exception?)
} }
sealed interface AuthenticationResult { sealed interface AuthenticationResult {
@ -38,23 +38,23 @@ interface BiometricUnlock {
suspend fun authenticate(): AuthenticationResult suspend fun authenticate(): AuthenticationResult
} }
class NoopBiometricUnlock : BiometricUnlock { class NoopBiometricAuthentication : BiometricAuthenticator {
override val isActive: Boolean = false override val isActive: Boolean = false
override fun setup() = Unit 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 activity: FragmentActivity,
private val promptInfo: PromptInfo, private val promptInfo: PromptInfo,
private val secretKeyRepository: SecretKeyRepository, private val secretKeyRepository: SecretKeyRepository,
private val encryptionDecryptionService: EncryptionDecryptionService, private val encryptionDecryptionService: EncryptionDecryptionService,
private val keyAlias: String, private val keyAlias: String,
private val callbacks: List<BiometricUnlock.Callback> private val callbacks: List<BiometricAuthenticator.Callback>
) : BiometricUnlock { ) : BiometricAuthenticator {
override val isActive: Boolean = true override val isActive: Boolean = true
private lateinit var cryptoObject: CryptoObject private var cryptoObject: CryptoObject? = null
override fun setup() { override fun setup() {
try { try {
@ -67,11 +67,10 @@ class DefaultBiometricUnlock(
} }
} }
override suspend fun authenticate(): BiometricUnlock.AuthenticationResult { override suspend fun authenticate(): BiometricAuthenticator.AuthenticationResult {
if (!this::cryptoObject.isInitialized) { val cryptoObject = cryptoObject ?: return BiometricAuthenticator.AuthenticationResult.Failure()
return BiometricUnlock.AuthenticationResult.Failure()
} val deferredAuthenticationResult = CompletableDeferred<BiometricAuthenticator.AuthenticationResult>()
val deferredAuthenticationResult = CompletableDeferred<BiometricUnlock.AuthenticationResult>()
val executor = ContextCompat.getMainExecutor(activity.baseContext) val executor = ContextCompat.getMainExecutor(activity.baseContext)
val callback = AuthenticationCallback(callbacks, deferredAuthenticationResult) val callback = AuthenticationCallback(callbacks, deferredAuthenticationResult)
val prompt = BiometricPrompt(activity, executor, callback) val prompt = BiometricPrompt(activity, executor, callback)
@ -80,7 +79,7 @@ class DefaultBiometricUnlock(
deferredAuthenticationResult.await() deferredAuthenticationResult.await()
} catch (cancellation: CancellationException) { } catch (cancellation: CancellationException) {
prompt.cancelAuthentication() prompt.cancelAuthentication()
BiometricUnlock.AuthenticationResult.Failure(cancellation) BiometricAuthenticator.AuthenticationResult.Failure(cancellation)
} }
} }
@ -91,30 +90,30 @@ class DefaultBiometricUnlock(
} }
private class AuthenticationCallback( private class AuthenticationCallback(
private val callbacks: List<BiometricUnlock.Callback>, private val callbacks: List<BiometricAuthenticator.Callback>,
private val deferredAuthenticationResult: CompletableDeferred<BiometricUnlock.AuthenticationResult>, private val deferredAuthenticationResult: CompletableDeferred<BiometricAuthenticator.AuthenticationResult>,
) : BiometricPrompt.AuthenticationCallback() { ) : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
val biometricUnlockError = BiometricUnlockError(errorCode, errString.toString()) val biometricUnlockError = BiometricUnlockError(errorCode, errString.toString())
callbacks.forEach { it.onBiometricUnlockFailed(biometricUnlockError) } callbacks.forEach { it.onBiometricAuthenticationFailed(biometricUnlockError) }
deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure(biometricUnlockError)) deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Failure(biometricUnlockError))
} }
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
callbacks.forEach { it.onBiometricUnlockFailed(null) } callbacks.forEach { it.onBiometricAuthenticationFailed(null) }
} }
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result) super.onAuthenticationSucceeded(result)
if (result.cryptoObject?.cipher.isValid()) { if (result.cryptoObject?.cipher.isValid()) {
callbacks.forEach { it.onBiometricUnlockSuccess() } callbacks.forEach { it.onBiometricAuthenticationSuccess() }
deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Success) deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Success)
} else { } else {
val error = IllegalStateException("Invalid cipher") val error = IllegalStateException("Invalid cipher")
callbacks.forEach { it.onBiometricUnlockFailed(error) } callbacks.forEach { it.onBiometricAuthenticationFailed(error) }
deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure()) deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Failure())
} }
} }

View file

@ -9,7 +9,7 @@ package io.element.android.features.lockscreen.impl.biometric
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
interface BiometricUnlockManager { interface BiometricAuthenticatorManager {
/** /**
* If the device is secured for example with a pin, pattern or password. * If the device is secured for example with a pin, pattern or password.
*/ */
@ -20,9 +20,18 @@ interface BiometricUnlockManager {
*/ */
val hasAvailableAuthenticator: Boolean val hasAvailableAuthenticator: Boolean
fun addCallback(callback: BiometricUnlock.Callback) fun addCallback(callback: BiometricAuthenticator.Callback)
fun removeCallback(callback: BiometricUnlock.Callback) fun removeCallback(callback: BiometricAuthenticator.Callback)
/**
* Remember a biometric authenticator ready for unlocking the app.
*/
@Composable @Composable
fun rememberBiometricUnlock(): BiometricUnlock fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator
/**
* Remember a biometric authenticator ready for confirmation.
*/
@Composable
fun rememberConfirmBiometricAuthenticator(): BiometricAuthenticator
} }

View file

@ -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.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@ -40,15 +41,15 @@ private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_BIOMETRIC"
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class DefaultBiometricUnlockManager @Inject constructor( class DefaultBiometricAuthenticatorManager @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val lockScreenStore: LockScreenStore, private val lockScreenStore: LockScreenStore,
private val lockScreenConfig: LockScreenConfig, private val lockScreenConfig: LockScreenConfig,
private val encryptionDecryptionService: EncryptionDecryptionService, private val encryptionDecryptionService: EncryptionDecryptionService,
private val secretKeyRepository: SecretKeyRepository, private val secretKeyRepository: SecretKeyRepository,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
) : BiometricUnlockManager { ) : BiometricAuthenticatorManager {
private val callbacks = CopyOnWriteArrayList<BiometricUnlock.Callback>() private val callbacks = CopyOnWriteArrayList<BiometricAuthenticator.Callback>()
private val biometricManager = BiometricManager.from(context) private val biometricManager = BiometricManager.from(context)
private val keyguardManager: KeyguardManager = context.getSystemService()!! private val keyguardManager: KeyguardManager = context.getSystemService()!!
@ -85,16 +86,42 @@ class DefaultBiometricUnlockManager @Inject constructor(
} }
@Composable @Composable
override fun rememberBiometricUnlock(): BiometricUnlock { override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator {
val isBiometricAllowed by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false) val isBiometricAllowed by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false)
val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState() val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
val isAvailable by remember(lifecycleState) { val isAvailable by remember(lifecycleState) {
derivedStateOf { derivedStateOf { isBiometricAllowed && hasAvailableAuthenticator }
isBiometricAllowed && hasAvailableAuthenticator
}
} }
val promptTitle = stringResource(id = R.string.screen_app_lock_biometric_unlock_title_android) 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) 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() val activity = LocalContext.current.findFragmentActivity()
return remember(isAvailable) { return remember(isAvailable) {
if (isAvailable && activity != null) { if (isAvailable && activity != null) {
@ -108,7 +135,7 @@ class DefaultBiometricUnlockManager @Inject constructor(
setNegativeButtonText(promptNegative) setNegativeButtonText(promptNegative)
setAllowedAuthenticators(authenticators) setAllowedAuthenticators(authenticators)
}.build() }.build()
DefaultBiometricUnlock( DefaultBiometricAuthentication(
activity = activity, activity = activity,
promptInfo = promptInfo, promptInfo = promptInfo,
secretKeyRepository = secretKeyRepository, secretKeyRepository = secretKeyRepository,
@ -117,16 +144,16 @@ class DefaultBiometricUnlockManager @Inject constructor(
callbacks = callbacks + internalCallback callbacks = callbacks + internalCallback
) )
} else { } else {
NoopBiometricUnlock() NoopBiometricAuthentication()
} }
} }
} }
override fun addCallback(callback: BiometricUnlock.Callback) { override fun addCallback(callback: BiometricAuthenticator.Callback) {
callbacks.add(callback) callbacks.add(callback)
} }
override fun removeCallback(callback: BiometricUnlock.Callback) { override fun removeCallback(callback: BiometricAuthenticator.Callback) {
callbacks.remove(callback) callbacks.remove(callback)
} }

View file

@ -7,8 +7,8 @@
package io.element.android.features.lockscreen.impl.biometric package io.element.android.features.lockscreen.impl.biometric
open class DefaultBiometricUnlockCallback : BiometricUnlock.Callback { open class DefaultBiometricUnlockCallback : BiometricAuthenticator.Callback {
override fun onBiometricSetupError() = Unit override fun onBiometricSetupError() = Unit
override fun onBiometricUnlockSuccess() = Unit override fun onBiometricAuthenticationSuccess() = Unit
override fun onBiometricUnlockFailed(error: Exception?) = Unit override fun onBiometricAuthenticationFailed(error: Exception?) = Unit
} }

View file

@ -15,7 +15,8 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.features.lockscreen.impl.LockScreenConfig 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.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -27,7 +28,7 @@ class LockScreenSettingsPresenter @Inject constructor(
private val lockScreenConfig: LockScreenConfig, private val lockScreenConfig: LockScreenConfig,
private val pinCodeManager: PinCodeManager, private val pinCodeManager: PinCodeManager,
private val lockScreenStore: LockScreenStore, private val lockScreenStore: LockScreenStore,
private val biometricUnlockManager: BiometricUnlockManager, private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
) : Presenter<LockScreenSettingsState> { ) : Presenter<LockScreenSettingsState> {
@Composable @Composable
@ -42,6 +43,8 @@ class LockScreenSettingsPresenter @Inject constructor(
mutableStateOf(false) mutableStateOf(false)
} }
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
fun handleEvents(event: LockScreenSettingsEvents) { fun handleEvents(event: LockScreenSettingsEvents) {
when (event) { when (event) {
LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false
@ -56,7 +59,14 @@ class LockScreenSettingsPresenter @Inject constructor(
LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true
LockScreenSettingsEvents.ToggleBiometricAllowed -> { LockScreenSettingsEvents.ToggleBiometricAllowed -> {
coroutineScope.launch { 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, showRemovePinOption = showRemovePinOption,
isBiometricEnabled = isBiometricEnabled, isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation, showRemovePinConfirmation = showRemovePinConfirmation,
showToggleBiometric = biometricUnlockManager.isDeviceSecured, showToggleBiometric = biometricAuthenticatorManager.isDeviceSecured,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -20,6 +20,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode 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.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.setup.biometric.SetupBiometricNode import io.element.android.features.lockscreen.impl.setup.biometric.SetupBiometricNode
@ -35,6 +36,7 @@ class LockScreenSetupFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val pinCodeManager: PinCodeManager, private val pinCodeManager: PinCodeManager,
val biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : BaseFlowNode<LockScreenSetupFlowNode.NavTarget>( ) : BaseFlowNode<LockScreenSetupFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Pin, initialElement = NavTarget.Pin,
@ -61,7 +63,11 @@ class LockScreenSetupFlowNode @AssistedInject constructor(
private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() {
override fun onPinCodeCreated() { override fun onPinCodeCreated() {
backstack.newRoot(NavTarget.Biometric) if (biometricAuthenticatorManager.hasAvailableAuthenticator) {
backstack.newRoot(NavTarget.Biometric)
} else {
onSetupDone()
}
} }
} }

View file

@ -13,6 +13,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,6 +22,7 @@ import javax.inject.Inject
class SetupBiometricPresenter @Inject constructor( class SetupBiometricPresenter @Inject constructor(
private val lockScreenStore: LockScreenStore, private val lockScreenStore: LockScreenStore,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : Presenter<SetupBiometricState> { ) : Presenter<SetupBiometricState> {
@Composable @Composable
override fun present(): SetupBiometricState { override fun present(): SetupBiometricState {
@ -28,12 +31,16 @@ class SetupBiometricPresenter @Inject constructor(
} }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
fun handleEvents(event: SetupBiometricEvents) { fun handleEvents(event: SetupBiometricEvents) {
when (event) { when (event) {
SetupBiometricEvents.AllowBiometric -> coroutineScope.launch { SetupBiometricEvents.AllowBiometric -> coroutineScope.launch {
lockScreenStore.setIsBiometricUnlockAllowed(true) biometricUnlock.setup()
isBiometricSetupDone = true if (biometricUnlock.authenticate() == BiometricAuthenticator.AuthenticationResult.Success) {
lockScreenStore.setIsBiometricUnlockAllowed(true)
isBiometricSetupDone = true
}
} }
SetupBiometricEvents.UsePin -> coroutineScope.launch { SetupBiometricEvents.UsePin -> coroutineScope.launch {
lockScreenStore.setIsBiometricUnlockAllowed(false) lockScreenStore.setIsBiometricUnlockAllowed(false)

View file

@ -11,14 +11,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState 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.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback 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.pin.PinCodeManager
import javax.inject.Inject import javax.inject.Inject
class PinUnlockHelper @Inject constructor( class PinUnlockHelper @Inject constructor(
private val biometricUnlockManager: BiometricUnlockManager, private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val pinCodeManager: PinCodeManager private val pinCodeManager: PinCodeManager
) { ) {
@Composable @Composable
@ -26,7 +26,7 @@ class PinUnlockHelper @Inject constructor(
val latestOnUnlock by rememberUpdatedState(onUnlock) val latestOnUnlock by rememberUpdatedState(onUnlock)
DisposableEffect(Unit) { DisposableEffect(Unit) {
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() { val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
override fun onBiometricUnlockSuccess() { override fun onBiometricAuthenticationSuccess() {
latestOnUnlock() latestOnUnlock()
} }
} }
@ -35,10 +35,10 @@ class PinUnlockHelper @Inject constructor(
latestOnUnlock() latestOnUnlock()
} }
} }
biometricUnlockManager.addCallback(biometricUnlockCallback) biometricAuthenticatorManager.addCallback(biometricUnlockCallback)
pinCodeManager.addCallback(pinCodeVerifiedCallback) pinCodeManager.addCallback(pinCodeVerifiedCallback)
onDispose { onDispose {
biometricUnlockManager.removeCallback(biometricUnlockCallback) biometricAuthenticatorManager.removeCallback(biometricUnlockCallback)
pinCodeManager.removeCallback(pinCodeVerifiedCallback) pinCodeManager.removeCallback(pinCodeVerifiedCallback)
} }
} }

View file

@ -15,8 +15,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
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.BiometricUnlockManager 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.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
@ -32,7 +32,7 @@ import javax.inject.Inject
class PinUnlockPresenter @Inject constructor( class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager, private val pinCodeManager: PinCodeManager,
private val biometricUnlockManager: BiometricUnlockManager, private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val logoutUseCase: LogoutUseCase, private val logoutUseCase: LogoutUseCase,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val pinUnlockHelper: PinUnlockHelper, private val pinUnlockHelper: PinUnlockHelper,
@ -56,12 +56,12 @@ class PinUnlockPresenter @Inject constructor(
mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized) mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized)
} }
var biometricUnlockResult by remember { var biometricUnlockResult by remember {
mutableStateOf<BiometricUnlock.AuthenticationResult?>(null) mutableStateOf<BiometricAuthenticator.AuthenticationResult?>(null)
} }
val isUnlocked = remember { val isUnlocked = remember {
mutableStateOf(false) mutableStateOf(false)
} }
val biometricUnlock = biometricUnlockManager.rememberBiometricUnlock() val biometricUnlock = biometricAuthenticatorManager.rememberUnlockBiometricAuthenticator()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
suspend { suspend {
val pinCodeSize = pinCodeManager.getPinCodeSize() val pinCodeSize = pinCodeManager.getPinCodeSize()

View file

@ -7,7 +7,7 @@
package io.element.android.features.lockscreen.impl.unlock 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.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -21,7 +21,7 @@ data class PinUnlockState(
val signOutAction: AsyncAction<String?>, val signOutAction: AsyncAction<String?>,
val showBiometricUnlock: Boolean, val showBiometricUnlock: Boolean,
val isUnlocked: Boolean, val isUnlocked: Boolean,
val biometricUnlockResult: BiometricUnlock.AuthenticationResult?, val biometricUnlockResult: BiometricAuthenticator.AuthenticationResult?,
val eventSink: (PinUnlockEvents) -> Unit val eventSink: (PinUnlockEvents) -> Unit
) { ) {
val isSignOutPromptCancellable = when (remainingAttempts) { val isSignOutPromptCancellable = when (remainingAttempts) {
@ -30,7 +30,7 @@ data class PinUnlockState(
} }
val biometricUnlockErrorMessage = when { val biometricUnlockErrorMessage = when {
biometricUnlockResult is BiometricUnlock.AuthenticationResult.Failure && biometricUnlockResult is BiometricAuthenticator.AuthenticationResult.Failure &&
biometricUnlockResult.error is BiometricUnlockError && biometricUnlockResult.error is BiometricUnlockError &&
biometricUnlockResult.error.isAuthDisabledError -> { biometricUnlockResult.error.isAuthDisabledError -> {
biometricUnlockResult.error.message biometricUnlockResult.error.message

View file

@ -9,7 +9,7 @@ package io.element.android.features.lockscreen.impl.unlock
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -25,7 +25,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
aPinUnlockState(showBiometricUnlock = false), aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = AsyncAction.Loading), aPinUnlockState(signOutAction = AsyncAction.Loading),
aPinUnlockState(biometricUnlockResult = BiometricUnlock.AuthenticationResult.Failure( aPinUnlockState(biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure(
BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled") BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled")
)), )),
) )
@ -37,7 +37,7 @@ fun aPinUnlockState(
showWrongPinTitle: Boolean = false, showWrongPinTitle: Boolean = false,
showSignOutPrompt: Boolean = false, showSignOutPrompt: Boolean = false,
showBiometricUnlock: Boolean = true, showBiometricUnlock: Boolean = true,
biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null, biometricUnlockResult: BiometricAuthenticator.AuthenticationResult? = null,
isUnlocked: Boolean = false, isUnlocked: Boolean = false,
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized, signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
) = PinUnlockState( ) = PinUnlockState(

View file

@ -3,6 +3,7 @@
<string name="screen_app_lock_biometric_authentication">"biometric authentication"</string> <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">"biometric unlock"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Unlock with biometric"</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_forgot_pin">"Forgot PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Change PIN code"</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> <string name="screen_app_lock_settings_enable_biometric_unlock">"Allow biometric unlock"</string>

View file

@ -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()
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -7,28 +7,38 @@
package io.element.android.features.lockscreen.impl.settings 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 com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.LockScreenConfig 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.aLockScreenConfig
import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager 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.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.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.test
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class LockScreenSettingsPresenterTest { 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 @Test
fun `present - remove pin flow`() = runTest { fun `present - remove pin flow`() = runTest {
val presenter = createLockScreenSettingsPresenter(this) val presenter = createLockScreenSettingsPresenter(this)
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
consumeItemsUntilPredicate { state -> consumeItemsUntilPredicate { state ->
state.showRemovePinOption state.showRemovePinOption
}.last().also { state -> }.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( private suspend fun createLockScreenSettingsPresenter(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
lockScreenConfig: LockScreenConfig = aLockScreenConfig(), lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
): LockScreenSettingsPresenter { ): LockScreenSettingsPresenter {
val lockScreenStore = InMemoryLockScreenStore()
val pinCodeManager = aPinCodeManager(lockScreenStore = lockScreenStore).apply { val pinCodeManager = aPinCodeManager(lockScreenStore = lockScreenStore).apply {
createPinCode("1234") createPinCode("1234")
} }
@ -68,7 +162,7 @@ class LockScreenSettingsPresenterTest {
pinCodeManager = pinCodeManager, pinCodeManager = pinCodeManager,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
lockScreenConfig = lockScreenConfig, lockScreenConfig = lockScreenConfig,
biometricUnlockManager = FakeBiometricUnlockManager(), biometricAuthenticatorManager = biometricAuthenticatorManager,
) )
} }
} }

View file

@ -11,6 +11,10 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat 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.pin.storage.InMemoryLockScreenStore
import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -19,9 +23,12 @@ import org.junit.Test
class SetupBiometricPresenterTest { class SetupBiometricPresenterTest {
@Test @Test
fun `present - allow flow`() = runTest { fun `present - allow flow with biometric authentication success`() = runTest {
val lockScreenStore = InMemoryLockScreenStore() val lockScreenStore = InMemoryLockScreenStore()
val presenter = createSetupBiometricPresenter(lockScreenStore) val fakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(createBiometricAuthenticator = {
FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Success })
})
val presenter = createSetupBiometricPresenter(lockScreenStore, fakeBiometricAuthenticatorManager)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -36,6 +43,24 @@ class SetupBiometricPresenterTest {
assertThat(lockScreenStore.isBiometricUnlockAllowed().first()).isTrue() 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 @Test
fun `present - skip flow`() = runTest { fun `present - skip flow`() = runTest {
val lockScreenStore = InMemoryLockScreenStore() val lockScreenStore = InMemoryLockScreenStore()
@ -55,10 +80,12 @@ class SetupBiometricPresenterTest {
} }
private fun createSetupBiometricPresenter( private fun createSetupBiometricPresenter(
lockScreenStore: LockScreenStore = InMemoryLockScreenStore() lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
): SetupBiometricPresenter { ): SetupBiometricPresenter {
return SetupBiometricPresenter( return SetupBiometricPresenter(
lockScreenStore = lockScreenStore, lockScreenStore = lockScreenStore,
biometricAuthenticatorManager = biometricAuthenticatorManager
) )
} }
} }

View file

@ -11,8 +11,8 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
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.FakeBiometricUnlockManager 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.fixtures.aPinCodeManager
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback 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.pin.PinCodeManager
@ -137,7 +137,7 @@ class PinUnlockPresenterTest {
private suspend fun createPinUnlockPresenter( private suspend fun createPinUnlockPresenter(
scope: CoroutineScope, scope: CoroutineScope,
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(), biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }), logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
): PinUnlockPresenter { ): PinUnlockPresenter {
@ -147,10 +147,10 @@ class PinUnlockPresenterTest {
} }
return PinUnlockPresenter( return PinUnlockPresenter(
pinCodeManager = pinCodeManager, pinCodeManager = pinCodeManager,
biometricUnlockManager = biometricUnlockManager, biometricAuthenticatorManager = biometricAuthenticatorManager,
logoutUseCase = logoutUseCase, logoutUseCase = logoutUseCase,
coroutineScope = scope, coroutineScope = scope,
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager), pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
) )
} }
} }