change : confirm biometric before allowing biometric unlock.
This commit is contained in:
parent
2895d0263c
commit
5fcf8a6cb4
19 changed files with 321 additions and 117 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue