diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index ab5d163e60..0b77d5e176 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -146,7 +146,7 @@ class FtueFlowNode @AssistedInject constructor( } NavTarget.LockScreenSetup -> { val callback = object : LockScreenEntryPoint.Callback { - override fun onSetupCompleted() { + override fun onSetupDone() { lifecycleScope.launch { moveToNextStep() } } } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index f63757717e..fb96895721 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -31,8 +31,8 @@ interface LockScreenEntryPoint : FeatureEntryPoint { fun build(): Node } - interface Callback: Plugin { - fun onSetupCompleted() + interface Callback : Plugin { + fun onSetupDone() } enum class Target { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index 48bfa465ee..4818a51bdc 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -20,7 +20,6 @@ import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.composable.Children -import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -30,8 +29,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.lockscreen.api.LockScreenEntryPoint -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.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode @@ -46,7 +43,6 @@ import kotlinx.parcelize.Parcelize class LockScreenFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val pinCodeManager: PinCodeManager, ) : BackstackNode( backstack = BackStack( initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget, @@ -71,26 +67,14 @@ class LockScreenFlowNode @AssistedInject constructor( data object Settings : NavTarget } - private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { - override fun onPinCodeCreated() { - plugins().forEach { - it.onSetupCompleted() + private class OnSetupDoneCallback(private val plugins: List) : LockScreenSetupFlowNode.Callback { + override fun onSetupDone() { + plugins.forEach { + it.onSetupDone() } } } - override fun onBuilt() { - super.onBuilt() - lifecycle.subscribe( - onCreate = { - pinCodeManager.addCallback(pinCodeManagerCallback) - }, - onDestroy = { - pinCodeManager.removeCallback(pinCodeManagerCallback) - } - ) - } - override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Unlock -> { @@ -98,7 +82,8 @@ class LockScreenFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(inputs)) } NavTarget.Setup -> { - createNode(buildContext) + val callback = OnSetupDoneCallback(plugins()) + createNode(buildContext, plugins = listOf(callback)) } NavTarget.Settings -> { createNode(buildContext) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt index e21b8e235c..c7029421e8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt @@ -24,6 +24,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.SecretKeyRepository +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import timber.log.Timber import java.security.InvalidKeyException @@ -86,7 +87,12 @@ class DefaultBiometricUnlock( val callback = AuthenticationCallback(callbacks, deferredAuthenticationResult) val prompt = BiometricPrompt(activity, executor, callback) prompt.authenticate(promptInfo, cryptoObject) - return deferredAuthenticationResult.await() + return try { + deferredAuthenticationResult.await() + } catch (cancellation: CancellationException) { + prompt.cancelAuthentication() + BiometricUnlock.AuthenticationResult.Failure(cancellation) + } } @Throws(KeyPermanentlyInvalidatedException::class) @@ -110,7 +116,6 @@ private class AuthenticationCallback( override fun onAuthenticationFailed() { super.onAuthenticationFailed() callbacks.forEach { it.onBiometricUnlockFailed(null) } - deferredAuthenticationResult.complete(BiometricUnlock.AuthenticationResult.Failure(null)) } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index a052ba65a2..ac87529e93 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -33,7 +33,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager -import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.setup.pin.SetupPinNode @@ -76,9 +75,6 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( } private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { - override fun onPinCodeVerified() { - backstack.newRoot(NavTarget.Settings) - } override fun onPinCodeRemoved() { navigateUp() @@ -89,12 +85,6 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( } } - private val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() { - override fun onBiometricUnlockSuccess() { - backstack.newRoot(NavTarget.Settings) - } - } - override fun onBuilt() { super.onBuilt() lifecycleScope.launch { @@ -108,11 +98,9 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( lifecycle.subscribe( onCreate = { pinCodeManager.addCallback(pinCodeManagerCallback) - biometricUnlockManager.addCallback(biometricUnlockCallback) }, onDestroy = { pinCodeManager.removeCallback(pinCodeManagerCallback) - biometricUnlockManager.removeCallback(biometricUnlockCallback) } ) } @@ -121,7 +109,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( return when (navTarget) { NavTarget.Unlock -> { val inputs = PinUnlockNode.Inputs(isInAppUnlock = true) - createNode(buildContext, plugins = listOf(inputs)) + val callback = object : PinUnlockNode.Callback { + override fun onUnlock() { + backstack.newRoot(NavTarget.Settings) + } + } + createNode(buildContext, plugins = listOf(inputs, callback)) } NavTarget.SetupPin -> { createNode(buildContext) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index a0818716b6..fbda73fd45 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -17,10 +17,12 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -35,15 +37,30 @@ class PinUnlockNode @AssistedInject constructor( private val presenter: PinUnlockPresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onUnlock() + } + data class Inputs( val isInAppUnlock: Boolean ) : NodeInputs private val inputs: Inputs = inputs() + private fun onUnlock() { + plugins().forEach { + it.onUnlock() + } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() + LaunchedEffect(state.isUnlocked) { + if (state.isUnlocked) { + onUnlock() + } + } PinUnlockView( state = state, isInAppUnlock = inputs.isInAppUnlock, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 872547dfdc..21f08a3829 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -17,6 +17,7 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -26,6 +27,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager +import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback +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.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel @@ -36,6 +39,7 @@ import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject class PinUnlockPresenter @Inject constructor( @@ -66,9 +70,10 @@ class PinUnlockPresenter @Inject constructor( var biometricUnlockResult by remember { mutableStateOf(null) } - + val isUnlocked = remember { + mutableStateOf(false) + } val biometricUnlock = biometricUnlockManager.rememberBiometricUnlock() - LaunchedEffect(Unit) { suspend { val pinCodeSize = pinCodeManager.getPinCodeSize() @@ -94,7 +99,7 @@ class PinUnlockPresenter @Inject constructor( showSignOutPrompt = true } } - + IsUnlockedEffect(isUnlocked) fun handleEvents(event: PinUnlockEvents) { when (event) { is PinUnlockEvents.OnPinKeypadPressed -> { @@ -129,10 +134,33 @@ class PinUnlockPresenter @Inject constructor( signOutAction = signOutAction.value, showBiometricUnlock = biometricUnlock.isActive, biometricUnlockResult = biometricUnlockResult, + isUnlocked = isUnlocked.value, eventSink = ::handleEvents ) } + @Composable + private fun IsUnlockedEffect(isUnlocked: MutableState) { + DisposableEffect(Unit) { + val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() { + override fun onBiometricUnlockSuccess() { + isUnlocked.value = true + } + } + val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() { + override fun onPinCodeVerified() { + isUnlocked.value = true + } + } + biometricUnlockManager.addCallback(biometricUnlockCallback) + pinCodeManager.addCallback(pinCodeVerifiedCallback) + onDispose { + biometricUnlockManager.removeCallback(biometricUnlockCallback) + pinCodeManager.removeCallback(pinCodeVerifiedCallback) + } + } + } + private fun Async.isComplete(): Boolean { return dataOrNull()?.isComplete().orFalse() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 667f7a825a..006234270c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -28,6 +28,7 @@ data class PinUnlockState( val showSignOutPrompt: Boolean, val signOutAction: Async, val showBiometricUnlock: Boolean, + val isUnlocked: Boolean, val biometricUnlockResult: BiometricUnlock.AuthenticationResult?, val eventSink: (PinUnlockEvents) -> Unit ) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index cbf6b2dee2..02da0e0350 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -41,6 +41,7 @@ fun aPinUnlockState( showSignOutPrompt: Boolean = false, showBiometricUnlock: Boolean = true, biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null, + isUnlocked: Boolean = false, signOutAction: Async = Async.Uninitialized, ) = PinUnlockState( pinEntry = Async.Success(pinEntry), @@ -50,5 +51,6 @@ fun aPinUnlockState( showBiometricUnlock = showBiometricUnlock, signOutAction = signOutAction, biometricUnlockResult = biometricUnlockResult, + isUnlocked = isUnlocked, eventSink = {} )