diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index 8a248127f2..965179ed28 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) - implementation(projects.libraries.statemachine) + api(libs.statemachine) api(projects.features.verifysession.api) testImplementation(libs.test.junit) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt index aa1fe59c32..689ff3a154 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt @@ -14,24 +14,31 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.verifysession.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import com.freeletics.flowredux.compose.rememberStateAndDispatch import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState class VerifySelfSessionPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, + private val stateMachine: VerifySelfSessionStateMachine, ) : Presenter { @Composable @@ -40,48 +47,36 @@ class VerifySelfSessionPresenter @Inject constructor( // Force reset, just in case the service was left in a broken state sessionVerificationService.reset() } - - val coroutineScope = rememberCoroutineScope() - val stateMachine = remember { VerifySelfSessionStateMachine(coroutineScope, sessionVerificationService) } - - // Create the new view state from the StateMachine state - val stateMachineCurrentState by stateMachine.state.collectAsState() - val verificationFlowState by remember { - derivedStateOf { stateMachineStateToViewState(stateMachineCurrentState) } + val stateAndDispatch = stateMachine.rememberStateAndDispatch() + val verificationFlowStep by remember { + derivedStateOf { stateAndDispatch.state.value.toVerificationStep() } + } + // Start this after observing state machine + LaunchedEffect(Unit) { + observeVerificationService() } fun handleEvents(event: VerifySelfSessionViewEvents) { when (event) { - VerifySelfSessionViewEvents.RequestVerification -> stateMachine.process(StateMachineEvent.RequestVerification) - VerifySelfSessionViewEvents.StartSasVerification -> stateMachine.process(StateMachineEvent.StartSasVerification) - VerifySelfSessionViewEvents.Restart -> stateMachine.process(StateMachineEvent.Restart) - VerifySelfSessionViewEvents.ConfirmVerification -> stateMachine.process(StateMachineEvent.AcceptChallenge) - VerifySelfSessionViewEvents.DeclineVerification -> stateMachine.process(StateMachineEvent.DeclineChallenge) - VerifySelfSessionViewEvents.CancelAndClose -> { - if (stateMachineCurrentState !in sequenceOf( - StateMachineState.Initial, - StateMachineState.Completed, - StateMachineState.Canceled - ) - ) { - stateMachine.process(StateMachineEvent.Cancel) - } - } + VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification) + VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification) + VerifySelfSessionViewEvents.Restart -> stateAndDispatch.dispatchAction(StateMachineEvent.Restart) + VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge) + VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) + VerifySelfSessionViewEvents.CancelAndClose -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) } } - return VerifySelfSessionState( - verificationFlowStep = verificationFlowState, + verificationFlowStep = verificationFlowStep, eventSink = ::handleEvents, ) } - private fun stateMachineStateToViewState(state: StateMachineState): VerifySelfSessionState.VerificationStep = - when (state) { - StateMachineState.Initial -> { + private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep = + when (val machineState = this) { + StateMachineState.Initial, null -> { VerifySelfSessionState.VerificationStep.Initial } - StateMachineState.RequestingVerification, StateMachineState.StartingSasVerification, StateMachineState.SasVerificationStarted, @@ -98,15 +93,41 @@ class VerifySelfSessionPresenter @Inject constructor( } is StateMachineState.Verifying -> { - val async = when (state) { + val async = when (machineState) { is StateMachineState.Verifying.Replying -> Async.Loading() else -> Async.Uninitialized } - VerifySelfSessionState.VerificationStep.Verifying(state.emojis, async) + VerifySelfSessionState.VerificationStep.Verifying(machineState.emojis, async) } StateMachineState.Completed -> { VerifySelfSessionState.VerificationStep.Completed } } + + private fun CoroutineScope.observeVerificationService() { + sessionVerificationService.verificationFlowState.onEach { verificationAttemptState -> + when (verificationAttemptState) { + VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Restart) + VerificationFlowState.AcceptedVerificationRequest -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest) + } + VerificationFlowState.StartedSasVerification -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification) + } + is VerificationFlowState.ReceivedVerificationData -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.emoji)) + } + VerificationFlowState.Finished -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge) + } + VerificationFlowState.Canceled -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel) + } + VerificationFlowState.Failed -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail) + } + } + }.launchIn(this) + } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt index 8006a0b24f..29818197e0 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt @@ -15,102 +15,114 @@ */ @file:Suppress("WildcardImport") +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.verifysession.impl -import io.element.android.libraries.statemachine.createStateMachine +import com.freeletics.flowredux.dsl.FlowReduxStateMachine import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationEmoji -import io.element.android.libraries.matrix.api.verification.VerificationFlowState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.ExperimentalCoroutinesApi +import javax.inject.Inject +import com.freeletics.flowredux.dsl.State as MachineState -class VerifySelfSessionStateMachine( - coroutineScope: CoroutineScope, +class VerifySelfSessionStateMachine @Inject constructor( private val sessionVerificationService: SessionVerificationService, +) : FlowReduxStateMachine( + initialState = State.Initial ) { - private val stateMachine = createStateMachine { - addInitialState(State.Initial) { - on(State.RequestingVerification) - on(State.StartingSasVerification) - } - addState { - onEnter { sessionVerificationService.requestVerification() } - - on(State.VerificationRequestAccepted) - on(State.Initial) - } - addState { - onEnter { sessionVerificationService.startVerification() } - } - addState { - on(State.StartingSasVerification) - } - addState { - on(State.RequestingVerification) - } - addState { - on { event, _ -> State.Verifying.ChallengeReceived(event.emojis) } - } - addState { - on { _, prevState -> State.Verifying.Replying(prevState.emojis, true) } - on { _, prevState -> State.Verifying.Replying(prevState.emojis, false) } - } - addState { - onEnter { state -> - if (state.accept) { - sessionVerificationService.approveVerification() - } else { - sessionVerificationService.declineVerification() + init { + spec { + inState { + on { _: Event.RequestVerification, state: MachineState -> + state.override { State.RequestingVerification } + } + on { _: Event.StartSasVerification, state: MachineState -> + state.override { State.StartingSasVerification } } } - on(State.Completed) - } - addState { - onEnter { sessionVerificationService.cancelVerification() } - } - on(State.SasVerificationStarted) - on(State.Canceling) - on(State.Canceled) - on(State.Canceled) - } - - init { - // Observe the verification service state, translate it to state machine input events - sessionVerificationService.verificationFlowState.onEach { verificationAttemptState -> - when (verificationAttemptState) { - VerificationFlowState.Initial -> stateMachine.restart() - VerificationFlowState.AcceptedVerificationRequest -> { - stateMachine.process(Event.DidAcceptVerificationRequest) + inState { + onEnterEffect { + sessionVerificationService.requestVerification() } - VerificationFlowState.StartedSasVerification -> { - stateMachine.process(Event.DidStartSasVerification) + on { _: Event.DidAcceptVerificationRequest, state: MachineState -> + state.override { State.VerificationRequestAccepted } } - is VerificationFlowState.ReceivedVerificationData -> { - // For some reason we receive this state twice, we need to discard the 2nd one - if (stateMachine.currentState == State.SasVerificationStarted) { - stateMachine.process(Event.DidReceiveChallenge(verificationAttemptState.emoji)) + on { _: Event.DidFail, state: MachineState -> + state.override { State.Initial } + } + } + inState { + onEnterEffect { + sessionVerificationService.startVerification() + } + } + inState { + on { _: Event.StartSasVerification, state: MachineState -> + state.override { State.StartingSasVerification } + } + } + inState { + on { _: Event.Restart, state: MachineState -> + state.override { State.RequestingVerification } + } + } + inState { + on { event: Event.DidReceiveChallenge, state: MachineState -> + state.override { State.Verifying.ChallengeReceived(event.emojis) } + } + } + inState { + on { _: Event.AcceptChallenge, state: MachineState -> + state.override { State.Verifying.Replying(state.snapshot.emojis, accept = true) } + } + on { _: Event.DeclineChallenge, state: MachineState -> + state.override { State.Verifying.Replying(state.snapshot.emojis, accept = false) } + } + } + inState { + onEnterEffect { state -> + if (state.accept) { + sessionVerificationService.approveVerification() + } else { + sessionVerificationService.declineVerification() } } - VerificationFlowState.Finished -> { - stateMachine.process(Event.DidAcceptChallenge) - } - VerificationFlowState.Canceled -> { - stateMachine.process(Event.DidCancel) - } - VerificationFlowState.Failed -> { - stateMachine.process(Event.DidFail) + on { _: Event.DidAcceptChallenge, state: MachineState -> + state.override { State.Completed } } } - }.launchIn(coroutineScope) + inState { + onEnterEffect { + sessionVerificationService.cancelVerification() + } + } + inState { + on { _: Event.DidStartSasVerification, state: MachineState -> + state.override { State.SasVerificationStarted } + } + on { _: Event.Cancel, state: MachineState -> + if (state.snapshot in sequenceOf( + State.Initial, + State.Completed, + State.Canceled + )) { + state.noChange() + } else { + state.override { State.Canceling } + } + } + on { _: Event.DidCancel, state: MachineState -> + state.override { State.Canceled } + } + on { _: Event.DidFail, state: MachineState -> + state.override { State.Canceled } + } + } + } } - val state: StateFlow = stateMachine.stateFlow - - fun process(event: Event) = stateMachine.process(event) - sealed interface State { /** The initial state, before verification started. */ object Initial : State @@ -134,10 +146,13 @@ class VerifySelfSessionStateMachine( /** Replying to a verification challenge. */ data class Replying(override val emojis: List, val accept: Boolean) : Verifying(emojis) } + /** The verification is being canceled. */ object Canceling : State + /** The verification has been canceled, remotely or locally. */ object Canceled : State + /** Verification successful. */ object Completed : State } @@ -145,26 +160,37 @@ class VerifySelfSessionStateMachine( sealed interface Event { /** Request verification. */ object RequestVerification : Event + /** The current verification request has been accepted. */ object DidAcceptVerificationRequest : Event + /** Start a SaS verification flow. */ object StartSasVerification : Event + /** Started a SaS verification flow. */ object DidStartSasVerification : Event + /** Has received emojis. */ data class DidReceiveChallenge(val emojis: List) : Event + /** Emojis match. */ object AcceptChallenge : Event + /** Emojis do not match. */ object DeclineChallenge : Event + /** Remote accepted challenge. */ object DidAcceptChallenge : Event + /** Request cancellation. */ object Cancel : Event + /** Verification cancelled. */ object DidCancel : Event + /** Request failed. */ object DidFail : Event + /** Restart the verification flow. */ object Restart : Event } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index 4007f4c8cf..0b58c125de 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -18,14 +18,13 @@ package io.element.android.features.verifysession.impl import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow -import app.cash.turbine.Event import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as VerificationStep +import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationEmoji +import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -36,8 +35,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Initial state is received`() = runTest { - val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -48,30 +46,18 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Handles requestVerification`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - // Await for other device response: - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) - // Await for the state to be Ready - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Ready) - // Await for other device response (again): - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) - // Finally, ChallengeReceived: - val verifyingState = awaitItem() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) + requestVerificationAndAwaitVerifyingState(service) } } @Test fun `present - Handles startSasVerification`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -82,6 +68,7 @@ class VerifySelfSessionPresenterTests { // Await for other device response: assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) // ChallengeReceived: + service.triggerReceiveVerificationData() val verifyingState = awaitItem() assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) } @@ -89,8 +76,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Cancelation on initial state does nothing`() = runTest { - val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -105,65 +91,43 @@ class VerifySelfSessionPresenterTests { @Test fun `present - A fail in the flow cancels it`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - val verifyingState = awaitChallengeReceivedState() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) - + val state = requestVerificationAndAwaitVerifyingState(service) service.shouldFail = true - eventSink(VerifySelfSessionViewEvents.ConfirmVerification) - - val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item)?.value } - assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled) + state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) + // Cancelling + assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) + // Cancelled + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) } } @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - val verifyingState = awaitChallengeReceivedState() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) - - eventSink(VerifySelfSessionViewEvents.CancelAndClose) - - val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item)?.value } - assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled) + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.CancelAndClose) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) } } @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - val verifyingState = awaitChallengeReceivedState() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) - + requestVerificationAndAwaitVerifyingState(service) service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList())) - ensureAllEventsConsumed() } } @@ -171,21 +135,14 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Restart after cancelation returns to requesting verification`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - - eventSink(VerifySelfSessionViewEvents.RequestVerification) - assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized)) - + val state = requestVerificationAndAwaitVerifyingState(service) service.givenVerificationFlowState(VerificationFlowState.Canceled) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) - - eventSink(VerifySelfSessionViewEvents.Restart) + state.eventSink(VerifySelfSessionViewEvents.Restart) // Went back to requesting verification assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) cancelAndIgnoreRemainingEvents() @@ -200,18 +157,12 @@ class VerifySelfSessionPresenterTests { val service = FakeSessionVerificationService().apply { givenEmojiList(emojis) } - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - - eventSink(VerifySelfSessionViewEvents.RequestVerification) - assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Uninitialized)) - - eventSink(VerifySelfSessionViewEvents.ConfirmVerification) + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Loading())) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed) } @@ -220,28 +171,41 @@ class VerifySelfSessionPresenterTests { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized)) - eventSink(VerifySelfSessionViewEvents.DeclineVerification) - + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Loading())) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) } } - private suspend fun ReceiveTurbine.awaitChallengeReceivedState(): VerifySelfSessionState { - // Skip 'waiting for response', 'ready' and 'starting verification' state - skipItems(3) - // Received challenge - return awaitItem() + private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( + fakeService: FakeSessionVerificationService + ): VerifySelfSessionState { + var state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial) + state.eventSink(VerifySelfSessionViewEvents.RequestVerification) + // Await for other device response: + state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) + // Await for the state to be Ready + state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Ready) + state.eventSink(VerifySelfSessionViewEvents.StartSasVerification) + // Await for other device response (again): + state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) + fakeService.triggerReceiveVerificationData() + // Finally, ChallengeReceived: + state = awaitItem() + assertThat(state.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) + return state + } + + private fun createPresenter(service: FakeSessionVerificationService = FakeSessionVerificationService()): VerifySelfSessionPresenter { + return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service)) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75772f75ec..b0638e6d09 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -139,7 +139,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.15" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.16" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } @@ -151,6 +151,7 @@ otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } +statemachine = "com.freeletics.flowredux:compose:1.1.0" # Analytics posthog = "com.posthog.android:posthog:2.0.3" diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index 82c90efae9..b2f79c0750 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -40,32 +40,32 @@ interface SessionVerificationService { /** * Request verification of the current session. */ - fun requestVerification() + suspend fun requestVerification() /** * Cancels the current verification attempt. */ - fun cancelVerification() + suspend fun cancelVerification() /** * Approves the current verification. This must happen on both devices to successfully verify a session. */ - fun approveVerification() + suspend fun approveVerification() /** * Declines the verification attempt because the user could not verify or does not trust the other side of the verification. */ - fun declineVerification() + suspend fun declineVerification() /** * Starts the verification of the unverified session from another device. */ - fun startVerification() + suspend fun startVerification() /** * Returns the verification service state to the initial step. */ - fun reset() + suspend fun reset() } /** Verification status of the current session. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 51fc903d8d..d8cd8c480f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -64,6 +64,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncList import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters +import org.matrix.rustcomponents.sdk.SlidingSyncSelectiveModeBuilder import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use import timber.log.Timber @@ -124,8 +125,7 @@ class RustMatrixClient constructor( ) ) .filters(visibleRoomsSlidingSyncFilters) - .syncModeSelective() - .addRange(0u, 20u) + .syncModeSelective(SlidingSyncSelectiveModeBuilder().addRange(0u, 20u)) .onceBuilt(object : SlidingSyncListOnceBuilt { override fun updateList(list: SlidingSyncList): SlidingSyncList { visibleRoomsSlidingSyncList.tryEmit(list) @@ -146,8 +146,7 @@ class RustMatrixClient constructor( ) ) .filters(invitesSlidingSyncFilters) - .syncModeSelective() - .addRange(0u, 20u) + .syncModeSelective(SlidingSyncSelectiveModeBuilder().addRange(0u, 20u)) .onceBuilt(object : SlidingSyncListOnceBuilt { override fun updateList(list: SlidingSyncList): SlidingSyncList { invitesSlidingSyncList.tryEmit(list) @@ -156,10 +155,9 @@ class RustMatrixClient constructor( }) private val slidingSync = client - .slidingSync() + .slidingSync("ElementX") // .homeserver("https://slidingsync.lab.matrix.org") .withCommonExtensions() - .storageKey("ElementX") .addList(visibleRoomsSlidingSyncListBuilder) .addList(invitesSlidingSyncListBuilder) .use { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index df620852cc..213cab2256 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -75,8 +75,7 @@ class RustMediaLoader( mediaSource = mediaSource, body = body, mimeType = mimeType ?: "application/octet-stream", - //TODO uncomment when rust api will be merged - //tempDir = cacheDirectory.path, + tempDir = cacheDirectory.path, ) RustMediaFile(mediaFile) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt index 2574e96d3b..ab94298418 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt @@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntry import org.matrix.rustcomponents.sdk.SlidingSync import org.matrix.rustcomponents.sdk.SlidingSyncList import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsListDiff +import org.matrix.rustcomponents.sdk.SlidingSyncSelectiveModeBuilder import org.matrix.rustcomponents.sdk.SlidingSyncState import org.matrix.rustcomponents.sdk.UpdateSummary import timber.log.Timber @@ -98,7 +99,9 @@ internal class RustRoomSummaryDataSource( override fun setSlidingSyncRange(range: IntRange) { Timber.v("setVisibleRange=$range") coroutineScope.launch { - slidingSyncListFlow.first().setRange(range.first.toUInt(), range.last.toUInt()) + val slidingSyncMode = SlidingSyncSelectiveModeBuilder() + .addRange(range.first.toUInt(), range.last.toUInt()) + slidingSyncListFlow.first().setSyncMode(slidingSyncMode) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 408861226b..af27aedc13 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -159,7 +159,7 @@ class RustMatrixTimeline( ), timelineLimit = null ) - listenerTokens += slidingSyncRoom.subscribeToRoom(settings) + slidingSyncRoom.subscribeToRoom(settings) val result = slidingSyncRoom.addTimelineListener(timelineListener) launch { fetchMembers() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 50fbf6f232..dc65bb74a6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -18,9 +18,9 @@ package io.element.android.libraries.matrix.impl.verification import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji +import io.element.android.libraries.matrix.api.verification.VerificationFlowState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -52,21 +52,21 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown) override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus.asStateFlow() - override fun requestVerification() = tryOrFail { + override suspend fun requestVerification() = tryOrFail { verificationController?.requestVerification() } - override fun cancelVerification() = tryOrFail { verificationController?.cancelVerification() } + override suspend fun cancelVerification() = tryOrFail { verificationController?.cancelVerification() } - override fun approveVerification() = tryOrFail { verificationController?.approveVerification() } + override suspend fun approveVerification() = tryOrFail { verificationController?.approveVerification() } - override fun declineVerification() = tryOrFail { verificationController?.declineVerification() } + override suspend fun declineVerification() = tryOrFail { verificationController?.declineVerification() } - override fun startVerification() = tryOrFail { + override suspend fun startVerification() = tryOrFail { verificationController?.startSasVerification() } - private fun tryOrFail(block: () -> Unit) { + private suspend fun tryOrFail(block: suspend () -> Unit) { runCatching { block() }.onFailure { didFail() } @@ -107,7 +107,7 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification // end-region - override fun reset() { + override suspend fun reset() { if (isReady.value) { // Cancel any pending verification attempt tryOrNull { verificationController?.cancelVerification() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt index 4b885cee8c..2f7887c537 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt @@ -37,17 +37,15 @@ class FakeSessionVerificationService : SessionVerificationService { override val isReady: StateFlow = _isReady - override fun requestVerification() { + override suspend fun requestVerification() { _verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest - _verificationFlowState.value = VerificationFlowState.StartedSasVerification - _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList) } - override fun cancelVerification() { + override suspend fun cancelVerification() { _verificationFlowState.value = VerificationFlowState.Canceled } - override fun approveVerification() { + override suspend fun approveVerification() { if (!shouldFail) { _verificationFlowState.value = VerificationFlowState.Finished } else { @@ -55,7 +53,7 @@ class FakeSessionVerificationService : SessionVerificationService { } } - override fun declineVerification() { + override suspend fun declineVerification() { if (!shouldFail) { _verificationFlowState.value = VerificationFlowState.Canceled } else { @@ -63,11 +61,14 @@ class FakeSessionVerificationService : SessionVerificationService { } } - override fun startVerification() { - _verificationFlowState.value = VerificationFlowState.StartedSasVerification + fun triggerReceiveVerificationData() { _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList) } + override suspend fun startVerification() { + _verificationFlowState.value = VerificationFlowState.StartedSasVerification + } + fun givenVerifiedStatus(status: SessionVerifiedStatus) { _sessionVerifiedStatus.value = status } @@ -84,7 +85,7 @@ class FakeSessionVerificationService : SessionVerificationService { this.emojiList = emojis } - override fun reset() { + override suspend fun reset() { _verificationFlowState.value = VerificationFlowState.Initial } } diff --git a/libraries/statemachine/build.gradle.kts b/libraries/statemachine/build.gradle.kts deleted file mode 100644 index ba6aef4027..0000000000 --- a/libraries/statemachine/build.gradle.kts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { - id("java-library") - id("com.android.lint") - alias(libs.plugins.kotlin.jvm) -} - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -dependencies { - implementation(libs.coroutines.core) - - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) -} diff --git a/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt b/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt deleted file mode 100644 index dd8ea7114d..0000000000 --- a/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.statemachine - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -fun createStateMachine( - config: StateMachineBuilder.() -> Unit -): StateMachine { - val builder = StateMachineBuilder() - config(builder) - return builder.build() -} - -class StateMachine( - val initialState: State, - private val stateConfigs: Map, StateConfig<*>>, - private val routes: List>, -) { - - private val _stateFlow = MutableStateFlow(initialState) - val stateFlow = _stateFlow.asStateFlow() - val currentState: State get() = stateFlow.value - - var transitionHandler: ((State, Event, State) -> Unit)? = null - - init { - @Suppress("UNCHECKED_CAST") - val initialStateConfig = stateConfigs[initialState::class.java] as StateConfig - initialStateConfig.onEnter?.invoke(initialState) - } - - @Suppress("UNCHECKED_CAST") - fun process(event: E) { - val route = findMatchingRoute(event) ?: error("No route found for state $currentState on event $event") - - val lastStateConfig: StateConfig? = stateConfigs[currentState::class.java] as? StateConfig - lastStateConfig?.onExit?.invoke(currentState) - - val nextState = route.toState(event, currentState) - transitionHandler?.invoke(currentState, event, nextState) - _stateFlow.value = nextState - - val currentStateConfig = stateConfigs[nextState::class.java] as? StateConfig - currentStateConfig?.onEnter?.invoke(nextState) - } - - @Suppress("UNCHECKED_CAST") - private fun findMatchingRoute(event: E): StateMachineRoute? { - val routesForEvent = routes.filter { it.eventType.isInstance(event) } - - return (routesForEvent.firstOrNull { it.fromState?.isInstance(currentState) == true } - ?: routesForEvent.firstOrNull { it.fromState == null }) as? StateMachineRoute - } - - fun restart() { - _stateFlow.value = initialState - } -} - -class StateMachineBuilder( - val routes: MutableList> = mutableListOf(), -) { - - lateinit var initialState: State - var stateConfigs = mutableMapOf, StateConfig>() - - inline fun addState(block: StateRegistrationBuilder.() -> Unit = {}) { - val config = StateConfig(S::class.java) - val registrationBuilder = StateRegistrationBuilder(config) - block(registrationBuilder) - - verifyRoutesAreUnique(S::class.java, routes, registrationBuilder.routes) - - if (stateConfigs.contains(S::class.java)) { - error("Duplicate registration for state ${S::class.java.name}") - } - stateConfigs[S::class.java] = config - routes.addAll(registrationBuilder.routes) - } - - inline fun addInitialState(state: S, config: StateRegistrationBuilder.() -> Unit = {}) { - initialState = state - addState(block = config) - } - - inline fun on(noinline configuration: (E, State) -> S) { - val builder = RouteBuilder(E::class.java, null) - builder.toState = configuration - val newRoute = builder.build() - verifyRoutesAreUnique(S::class.java, routes, listOf(newRoute)) - routes.add(newRoute) - } - - inline fun on(newState: State) { - val builder = RouteBuilder(E::class.java, null) - builder.toState = { _, _ -> newState } - val newRoute = builder.build() - verifyRoutesAreUnique(null, routes, listOf(newRoute)) - routes.add(newRoute) - } - - fun build(): StateMachine { - if (::initialState.isInitialized) { - return StateMachine(initialState, stateConfigs.toMap(), routes) - } else { - error("The state machine has no initial state") - } - } - - companion object { - fun verifyRoutesAreUnique( - state: Class<*>?, - oldRoutes: List>, - newRoutes: List>, - ) { - val oldEvents = oldRoutes.filter { it.fromState == state }.map { it.eventType } - val newEvents = newRoutes.filter { it.fromState == state }.map { it.eventType } - val intersection = oldEvents.intersect(newEvents) - if (intersection.isNotEmpty()) { - val duplicates = intersection.joinToString(", ") { it.name } - error("Duplicate registration in state ${state?.name} for events: $duplicates") - } - } - } -} - -class StateRegistrationBuilder( - val fromState: StateConfig, - val routes: MutableList> = mutableListOf(), -) { - - fun onEnter(enter: (State) -> Unit) { - fromState.onEnter = enter - } - - fun onExit(exit: (State) -> Unit) { - fromState.onExit = exit - } - - inline fun on(noinline configuration: (E, State) -> BaseState) { - val builder = RouteBuilder(E::class.java, fromState.state) - builder.toState = configuration - val newRoute = builder.build() - StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute)) - routes.add(newRoute) - } - - inline fun on(newState: BaseState) { - val builder = RouteBuilder(E::class.java, fromState.state) - builder.toState = { _, _ -> newState } - val newRoute = builder.build() - StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute)) - routes.add(newRoute) - } -} - -class RouteBuilder( - val eventType: Class, - val fromState: Class?, -) { - lateinit var toState: (Event, FromState) -> ToState - - fun build() = StateMachineRoute(eventType, fromState, toState) -} - -data class StateMachineRoute( - val eventType: Class, - val fromState: Class?, - val toState: (Event, FromState) -> ToState, -) - -data class StateConfig( - val state: Class, - var onEnter: ((State) -> Unit)? = null, - var onExit: ((State) -> Unit)? = null, -) diff --git a/libraries/statemachine/src/test/kotlin/io/element/android/libraries/statemachine/StateMachineTests.kt b/libraries/statemachine/src/test/kotlin/io/element/android/libraries/statemachine/StateMachineTests.kt deleted file mode 100644 index 722ed1c004..0000000000 --- a/libraries/statemachine/src/test/kotlin/io/element/android/libraries/statemachine/StateMachineTests.kt +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.statemachine - -import com.google.common.truth.Truth.assertThat -import org.junit.Assert.fail -import org.junit.Test - -class StateMachineTests { - - sealed interface Events { - data class GoToSecond(val string: String) : Events - - object GoToThird : Events - - object GoToFourth : Events - - object Cancel : Events - } - - sealed interface States { - object First : States - - data class Second(val string: String) : States - - object Third : States - - object Fourth : States - object Canceled : States - } - - private var enteredSecondState = false - private var exitedFirstState = false - private var transitionHandlerParams: Triple? = null - private fun aStateMachine() = createStateMachine { - addInitialState(States.First) { - onExit { exitedFirstState = true } - on { first, _ -> - States.Second(first.string) - } - } - addState { - onEnter { enteredSecondState = true } - on(States.Third) - } - - addState() - - on { _, _ -> States.Fourth } - on(States.Canceled) - } - - @Test - fun `process - moves to next state given an event if the route exists`() = aStateMachine().run { - process(Events.GoToSecond("Hello")) - assertThat(currentState).isEqualTo(States.Second("Hello")) - process(Events.GoToThird) - assertThat(currentState).isEqualTo(States.Third) - process(Events.GoToFourth) - assertThat(currentState).isEqualTo(States.Fourth) - } - - @Test - fun `process - throws exception if there is no route for an event in a state`() = aStateMachine().run { - runCatching { - process(Events.GoToThird) - }.onSuccess { - fail("It should have thrown an error") - }.onFailure { - assertThat(it.message).startsWith("No route found for state") - } - Unit - } - - @Test - fun `process - calls onEnter and onExit callbacks when moving through states`() = aStateMachine().run { - process(Events.GoToSecond("Hello")) - assertThat(currentState).isEqualTo(States.Second("Hello")) - - assertThat(exitedFirstState).isTrue() - assertThat(enteredSecondState).isTrue() - } - - @Test - fun `process - if an Event route is registered inside a state and outside it, the internal registration takes precedence`() { - val customStateMachine = createStateMachine { - addInitialState(States.First) { - on(States.Canceled) - } - on(States.Fourth) - } - customStateMachine.process(Events.Cancel) - assertThat(customStateMachine.currentState).isEqualTo(States.Canceled) - } - - @Test - fun `transitionHandler - is called when moving from a state to another`() = aStateMachine().run { - transitionHandler = { from, event, to -> - transitionHandlerParams = Triple(from, event, to) - } - - process(Events.GoToSecond("Hello")) - - assertThat(transitionHandlerParams).isEqualTo( - Triple( - States.First, - Events.GoToSecond("Hello"), - States.Second("Hello"), - ) - ) - } - - @Test - fun `restart - sets the state machine to its initial state`() { - val customStateMachine = createStateMachine { - addInitialState(States.First) - on(States.Fourth) - } - customStateMachine.process(Events.GoToFourth) - assertThat(customStateMachine.currentState).isEqualTo(States.Fourth) - - customStateMachine.restart() - assertThat(customStateMachine.currentState).isEqualTo(customStateMachine.initialState) - } - - @Test - fun `init - the state machine must have registered a initial state`() { - runCatching { - createStateMachine { - addState() - on(States.Canceled) - } - }.onSuccess { - fail("It should have thrown an error") - }.onFailure { error -> - assertThat(error.message).isEqualTo("The state machine has no initial state") - } - Unit - } - - @Test - fun `init - the state machine having duplicate registrations for a state throws an error`() { - runCatching { - createStateMachine { - addInitialState(States.First) - addState() - } - }.onSuccess { - fail("It should have thrown an error") - }.onFailure { error -> - assertThat(error.message).startsWith("Duplicate registration for state ") - } - Unit - } - - @Test - fun `init - the state machine having duplicate registrations for an event inside a state throws an error`() { - runCatching { - createStateMachine { - addInitialState(States.First) { - on(States.Third) - on { _, _ -> States.Third } - } - } - }.onSuccess { - fail("It should have thrown an error") - }.onFailure { error -> - assertThat(error.message).startsWith("Duplicate registration in state") - } - Unit - } - - @Test - fun `init - the state machine having duplicate registrations for an event at the root level throws an error`() { - runCatching { - createStateMachine { - addInitialState(States.First) - on(States.Third) - on(States.Third) - } - }.onSuccess { - fail("It should have thrown an error") - }.onFailure { error -> - assertThat(error.message).startsWith("Duplicate registration in state") - } - Unit - } -} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index fa601a998d..51c568960d 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -94,7 +94,6 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) implementation(project(":libraries:session-storage:impl")) - implementation(project(":libraries:statemachine")) implementation(project(":libraries:mediapickers:impl")) implementation(project(":libraries:mediaupload:impl")) implementation(project(":libraries:usersearch:impl"))