Merge pull request #566 from vector-im/feature/fga/update-rust-sdk-0.1.16
Feature/fga/update rust sdk 0.1.16
This commit is contained in:
commit
53f234d135
16 changed files with 251 additions and 666 deletions
|
|
@ -39,7 +39,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.statemachine)
|
api(libs.statemachine)
|
||||||
api(projects.features.verifysession.api)
|
api(projects.features.verifysession.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,31 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.features.verifysession.impl
|
package io.element.android.features.verifysession.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
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.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
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 javax.inject.Inject
|
||||||
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
|
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
|
||||||
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
|
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
|
||||||
|
|
||||||
class VerifySelfSessionPresenter @Inject constructor(
|
class VerifySelfSessionPresenter @Inject constructor(
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val stateMachine: VerifySelfSessionStateMachine,
|
||||||
) : Presenter<VerifySelfSessionState> {
|
) : Presenter<VerifySelfSessionState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -40,48 +47,36 @@ class VerifySelfSessionPresenter @Inject constructor(
|
||||||
// Force reset, just in case the service was left in a broken state
|
// Force reset, just in case the service was left in a broken state
|
||||||
sessionVerificationService.reset()
|
sessionVerificationService.reset()
|
||||||
}
|
}
|
||||||
|
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val verificationFlowStep by remember {
|
||||||
val stateMachine = remember { VerifySelfSessionStateMachine(coroutineScope, sessionVerificationService) }
|
derivedStateOf { stateAndDispatch.state.value.toVerificationStep() }
|
||||||
|
}
|
||||||
// Create the new view state from the StateMachine state
|
// Start this after observing state machine
|
||||||
val stateMachineCurrentState by stateMachine.state.collectAsState()
|
LaunchedEffect(Unit) {
|
||||||
val verificationFlowState by remember {
|
observeVerificationService()
|
||||||
derivedStateOf { stateMachineStateToViewState(stateMachineCurrentState) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(event: VerifySelfSessionViewEvents) {
|
fun handleEvents(event: VerifySelfSessionViewEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
VerifySelfSessionViewEvents.RequestVerification -> stateMachine.process(StateMachineEvent.RequestVerification)
|
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
|
||||||
VerifySelfSessionViewEvents.StartSasVerification -> stateMachine.process(StateMachineEvent.StartSasVerification)
|
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
|
||||||
VerifySelfSessionViewEvents.Restart -> stateMachine.process(StateMachineEvent.Restart)
|
VerifySelfSessionViewEvents.Restart -> stateAndDispatch.dispatchAction(StateMachineEvent.Restart)
|
||||||
VerifySelfSessionViewEvents.ConfirmVerification -> stateMachine.process(StateMachineEvent.AcceptChallenge)
|
VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
|
||||||
VerifySelfSessionViewEvents.DeclineVerification -> stateMachine.process(StateMachineEvent.DeclineChallenge)
|
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
|
||||||
VerifySelfSessionViewEvents.CancelAndClose -> {
|
VerifySelfSessionViewEvents.CancelAndClose -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
|
||||||
if (stateMachineCurrentState !in sequenceOf(
|
|
||||||
StateMachineState.Initial,
|
|
||||||
StateMachineState.Completed,
|
|
||||||
StateMachineState.Canceled
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
stateMachine.process(StateMachineEvent.Cancel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VerifySelfSessionState(
|
return VerifySelfSessionState(
|
||||||
verificationFlowStep = verificationFlowState,
|
verificationFlowStep = verificationFlowStep,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stateMachineStateToViewState(state: StateMachineState): VerifySelfSessionState.VerificationStep =
|
private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep =
|
||||||
when (state) {
|
when (val machineState = this) {
|
||||||
StateMachineState.Initial -> {
|
StateMachineState.Initial, null -> {
|
||||||
VerifySelfSessionState.VerificationStep.Initial
|
VerifySelfSessionState.VerificationStep.Initial
|
||||||
}
|
}
|
||||||
|
|
||||||
StateMachineState.RequestingVerification,
|
StateMachineState.RequestingVerification,
|
||||||
StateMachineState.StartingSasVerification,
|
StateMachineState.StartingSasVerification,
|
||||||
StateMachineState.SasVerificationStarted,
|
StateMachineState.SasVerificationStarted,
|
||||||
|
|
@ -98,15 +93,41 @@ class VerifySelfSessionPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is StateMachineState.Verifying -> {
|
is StateMachineState.Verifying -> {
|
||||||
val async = when (state) {
|
val async = when (machineState) {
|
||||||
is StateMachineState.Verifying.Replying -> Async.Loading()
|
is StateMachineState.Verifying.Replying -> Async.Loading()
|
||||||
else -> Async.Uninitialized
|
else -> Async.Uninitialized
|
||||||
}
|
}
|
||||||
VerifySelfSessionState.VerificationStep.Verifying(state.emojis, async)
|
VerifySelfSessionState.VerificationStep.Verifying(machineState.emojis, async)
|
||||||
}
|
}
|
||||||
|
|
||||||
StateMachineState.Completed -> {
|
StateMachineState.Completed -> {
|
||||||
VerifySelfSessionState.VerificationStep.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,102 +15,114 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:Suppress("WildcardImport")
|
@file:Suppress("WildcardImport")
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.features.verifysession.impl
|
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.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import com.freeletics.flowredux.dsl.State as MachineState
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
class VerifySelfSessionStateMachine(
|
class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
) : FlowReduxStateMachine<VerifySelfSessionStateMachine.State, VerifySelfSessionStateMachine.Event>(
|
||||||
|
initialState = State.Initial
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val stateMachine = createStateMachine {
|
init {
|
||||||
addInitialState(State.Initial) {
|
spec {
|
||||||
on<Event.RequestVerification>(State.RequestingVerification)
|
inState<State.Initial> {
|
||||||
on<Event.StartSasVerification>(State.StartingSasVerification)
|
on { _: Event.RequestVerification, state: MachineState<State.Initial> ->
|
||||||
}
|
state.override { State.RequestingVerification }
|
||||||
addState<State.RequestingVerification> {
|
}
|
||||||
onEnter { sessionVerificationService.requestVerification() }
|
on { _: Event.StartSasVerification, state: MachineState<State.Initial> ->
|
||||||
|
state.override { State.StartingSasVerification }
|
||||||
on<Event.DidAcceptVerificationRequest>(State.VerificationRequestAccepted)
|
|
||||||
on<Event.DidFail>(State.Initial)
|
|
||||||
}
|
|
||||||
addState<State.StartingSasVerification> {
|
|
||||||
onEnter { sessionVerificationService.startVerification() }
|
|
||||||
}
|
|
||||||
addState<State.VerificationRequestAccepted> {
|
|
||||||
on<Event.StartSasVerification>(State.StartingSasVerification)
|
|
||||||
}
|
|
||||||
addState<State.Canceled> {
|
|
||||||
on<Event.Restart>(State.RequestingVerification)
|
|
||||||
}
|
|
||||||
addState<State.SasVerificationStarted> {
|
|
||||||
on<Event.DidReceiveChallenge> { event, _ -> State.Verifying.ChallengeReceived(event.emojis) }
|
|
||||||
}
|
|
||||||
addState<State.Verifying.ChallengeReceived> {
|
|
||||||
on<Event.AcceptChallenge> { _, prevState -> State.Verifying.Replying(prevState.emojis, true) }
|
|
||||||
on<Event.DeclineChallenge> { _, prevState -> State.Verifying.Replying(prevState.emojis, false) }
|
|
||||||
}
|
|
||||||
addState<State.Verifying.Replying> {
|
|
||||||
onEnter { state ->
|
|
||||||
if (state.accept) {
|
|
||||||
sessionVerificationService.approveVerification()
|
|
||||||
} else {
|
|
||||||
sessionVerificationService.declineVerification()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
on<Event.DidAcceptChallenge>(State.Completed)
|
inState<State.RequestingVerification> {
|
||||||
}
|
onEnterEffect {
|
||||||
addState<State.Canceling> {
|
sessionVerificationService.requestVerification()
|
||||||
onEnter { sessionVerificationService.cancelVerification() }
|
|
||||||
}
|
|
||||||
on<Event.DidStartSasVerification>(State.SasVerificationStarted)
|
|
||||||
on<Event.Cancel>(State.Canceling)
|
|
||||||
on<Event.DidCancel>(State.Canceled)
|
|
||||||
on<Event.DidFail>(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)
|
|
||||||
}
|
}
|
||||||
VerificationFlowState.StartedSasVerification -> {
|
on { _: Event.DidAcceptVerificationRequest, state: MachineState<State.RequestingVerification> ->
|
||||||
stateMachine.process(Event.DidStartSasVerification)
|
state.override { State.VerificationRequestAccepted }
|
||||||
}
|
}
|
||||||
is VerificationFlowState.ReceivedVerificationData -> {
|
on { _: Event.DidFail, state: MachineState<State.RequestingVerification> ->
|
||||||
// For some reason we receive this state twice, we need to discard the 2nd one
|
state.override { State.Initial }
|
||||||
if (stateMachine.currentState == State.SasVerificationStarted) {
|
}
|
||||||
stateMachine.process(Event.DidReceiveChallenge(verificationAttemptState.emoji))
|
}
|
||||||
|
inState<State.StartingSasVerification> {
|
||||||
|
onEnterEffect {
|
||||||
|
sessionVerificationService.startVerification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inState<State.VerificationRequestAccepted> {
|
||||||
|
on { _: Event.StartSasVerification, state: MachineState<State.VerificationRequestAccepted> ->
|
||||||
|
state.override { State.StartingSasVerification }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inState<State.Canceled> {
|
||||||
|
on { _: Event.Restart, state: MachineState<State.Canceled> ->
|
||||||
|
state.override { State.RequestingVerification }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inState<State.SasVerificationStarted> {
|
||||||
|
on { event: Event.DidReceiveChallenge, state: MachineState<State.SasVerificationStarted> ->
|
||||||
|
state.override { State.Verifying.ChallengeReceived(event.emojis) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inState<State.Verifying.ChallengeReceived> {
|
||||||
|
on { _: Event.AcceptChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
|
||||||
|
state.override { State.Verifying.Replying(state.snapshot.emojis, accept = true) }
|
||||||
|
}
|
||||||
|
on { _: Event.DeclineChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
|
||||||
|
state.override { State.Verifying.Replying(state.snapshot.emojis, accept = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inState<State.Verifying.Replying> {
|
||||||
|
onEnterEffect { state ->
|
||||||
|
if (state.accept) {
|
||||||
|
sessionVerificationService.approveVerification()
|
||||||
|
} else {
|
||||||
|
sessionVerificationService.declineVerification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VerificationFlowState.Finished -> {
|
on { _: Event.DidAcceptChallenge, state: MachineState<State.Verifying.Replying> ->
|
||||||
stateMachine.process(Event.DidAcceptChallenge)
|
state.override { State.Completed }
|
||||||
}
|
|
||||||
VerificationFlowState.Canceled -> {
|
|
||||||
stateMachine.process(Event.DidCancel)
|
|
||||||
}
|
|
||||||
VerificationFlowState.Failed -> {
|
|
||||||
stateMachine.process(Event.DidFail)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.launchIn(coroutineScope)
|
inState<State.Canceling> {
|
||||||
|
onEnterEffect {
|
||||||
|
sessionVerificationService.cancelVerification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inState {
|
||||||
|
on { _: Event.DidStartSasVerification, state: MachineState<State> ->
|
||||||
|
state.override { State.SasVerificationStarted }
|
||||||
|
}
|
||||||
|
on { _: Event.Cancel, state: MachineState<State> ->
|
||||||
|
if (state.snapshot in sequenceOf(
|
||||||
|
State.Initial,
|
||||||
|
State.Completed,
|
||||||
|
State.Canceled
|
||||||
|
)) {
|
||||||
|
state.noChange()
|
||||||
|
} else {
|
||||||
|
state.override { State.Canceling }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on { _: Event.DidCancel, state: MachineState<State> ->
|
||||||
|
state.override { State.Canceled }
|
||||||
|
}
|
||||||
|
on { _: Event.DidFail, state: MachineState<State> ->
|
||||||
|
state.override { State.Canceled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val state: StateFlow<State> = stateMachine.stateFlow
|
|
||||||
|
|
||||||
fun process(event: Event) = stateMachine.process(event)
|
|
||||||
|
|
||||||
sealed interface State {
|
sealed interface State {
|
||||||
/** The initial state, before verification started. */
|
/** The initial state, before verification started. */
|
||||||
object Initial : State
|
object Initial : State
|
||||||
|
|
@ -134,10 +146,13 @@ class VerifySelfSessionStateMachine(
|
||||||
/** Replying to a verification challenge. */
|
/** Replying to a verification challenge. */
|
||||||
data class Replying(override val emojis: List<VerificationEmoji>, val accept: Boolean) : Verifying(emojis)
|
data class Replying(override val emojis: List<VerificationEmoji>, val accept: Boolean) : Verifying(emojis)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The verification is being canceled. */
|
/** The verification is being canceled. */
|
||||||
object Canceling : State
|
object Canceling : State
|
||||||
|
|
||||||
/** The verification has been canceled, remotely or locally. */
|
/** The verification has been canceled, remotely or locally. */
|
||||||
object Canceled : State
|
object Canceled : State
|
||||||
|
|
||||||
/** Verification successful. */
|
/** Verification successful. */
|
||||||
object Completed : State
|
object Completed : State
|
||||||
}
|
}
|
||||||
|
|
@ -145,26 +160,37 @@ class VerifySelfSessionStateMachine(
|
||||||
sealed interface Event {
|
sealed interface Event {
|
||||||
/** Request verification. */
|
/** Request verification. */
|
||||||
object RequestVerification : Event
|
object RequestVerification : Event
|
||||||
|
|
||||||
/** The current verification request has been accepted. */
|
/** The current verification request has been accepted. */
|
||||||
object DidAcceptVerificationRequest : Event
|
object DidAcceptVerificationRequest : Event
|
||||||
|
|
||||||
/** Start a SaS verification flow. */
|
/** Start a SaS verification flow. */
|
||||||
object StartSasVerification : Event
|
object StartSasVerification : Event
|
||||||
|
|
||||||
/** Started a SaS verification flow. */
|
/** Started a SaS verification flow. */
|
||||||
object DidStartSasVerification : Event
|
object DidStartSasVerification : Event
|
||||||
|
|
||||||
/** Has received emojis. */
|
/** Has received emojis. */
|
||||||
data class DidReceiveChallenge(val emojis: List<VerificationEmoji>) : Event
|
data class DidReceiveChallenge(val emojis: List<VerificationEmoji>) : Event
|
||||||
|
|
||||||
/** Emojis match. */
|
/** Emojis match. */
|
||||||
object AcceptChallenge : Event
|
object AcceptChallenge : Event
|
||||||
|
|
||||||
/** Emojis do not match. */
|
/** Emojis do not match. */
|
||||||
object DeclineChallenge : Event
|
object DeclineChallenge : Event
|
||||||
|
|
||||||
/** Remote accepted challenge. */
|
/** Remote accepted challenge. */
|
||||||
object DidAcceptChallenge : Event
|
object DidAcceptChallenge : Event
|
||||||
|
|
||||||
/** Request cancellation. */
|
/** Request cancellation. */
|
||||||
object Cancel : Event
|
object Cancel : Event
|
||||||
|
|
||||||
/** Verification cancelled. */
|
/** Verification cancelled. */
|
||||||
object DidCancel : Event
|
object DidCancel : Event
|
||||||
|
|
||||||
/** Request failed. */
|
/** Request failed. */
|
||||||
object DidFail : Event
|
object DidFail : Event
|
||||||
|
|
||||||
/** Restart the verification flow. */
|
/** Restart the verification flow. */
|
||||||
object Restart : Event
|
object Restart : Event
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,13 @@ package io.element.android.features.verifysession.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.Event
|
|
||||||
import app.cash.turbine.ReceiveTurbine
|
import app.cash.turbine.ReceiveTurbine
|
||||||
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.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.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.VerificationEmoji
|
||||||
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -36,8 +35,7 @@ class VerifySelfSessionPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Initial state is received`() = runTest {
|
fun `present - Initial state is received`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val presenter = createPresenter()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -48,30 +46,18 @@ class VerifySelfSessionPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - Handles requestVerification`() = runTest {
|
fun `present - Handles requestVerification`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
requestVerificationAndAwaitVerifyingState(service)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Handles startSasVerification`() = runTest {
|
fun `present - Handles startSasVerification`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -82,6 +68,7 @@ class VerifySelfSessionPresenterTests {
|
||||||
// Await for other device response:
|
// Await for other device response:
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
||||||
// ChallengeReceived:
|
// ChallengeReceived:
|
||||||
|
service.triggerReceiveVerificationData()
|
||||||
val verifyingState = awaitItem()
|
val verifyingState = awaitItem()
|
||||||
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
|
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
|
||||||
}
|
}
|
||||||
|
|
@ -89,8 +76,7 @@ class VerifySelfSessionPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Cancelation on initial state does nothing`() = runTest {
|
fun `present - Cancelation on initial state does nothing`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val presenter = createPresenter()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -105,65 +91,43 @@ class VerifySelfSessionPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - A fail in the flow cancels it`() = runTest {
|
fun `present - A fail in the flow cancels it`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
|
||||||
val eventSink = initialState.eventSink
|
|
||||||
eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
|
||||||
|
|
||||||
val verifyingState = awaitChallengeReceivedState()
|
|
||||||
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
|
|
||||||
|
|
||||||
service.shouldFail = true
|
service.shouldFail = true
|
||||||
eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||||
|
// Cancelling
|
||||||
val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item<VerifySelfSessionState>)?.value }
|
assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
|
||||||
assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
// Cancelled
|
||||||
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
|
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
state.eventSink(VerifySelfSessionViewEvents.CancelAndClose)
|
||||||
val eventSink = initialState.eventSink
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
||||||
eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
||||||
|
|
||||||
val verifyingState = awaitChallengeReceivedState()
|
|
||||||
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
|
|
||||||
|
|
||||||
eventSink(VerifySelfSessionViewEvents.CancelAndClose)
|
|
||||||
|
|
||||||
val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item<VerifySelfSessionState>)?.value }
|
|
||||||
assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - When verifying, if we receive another challenge we ignore it`() = runTest {
|
fun `present - When verifying, if we receive another challenge we ignore it`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
requestVerificationAndAwaitVerifyingState(service)
|
||||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
|
||||||
val eventSink = initialState.eventSink
|
|
||||||
eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
|
||||||
|
|
||||||
val verifyingState = awaitChallengeReceivedState()
|
|
||||||
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
|
|
||||||
|
|
||||||
service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList()))
|
service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList()))
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,21 +135,14 @@ class VerifySelfSessionPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - Restart after cancelation returns to requesting verification`() = runTest {
|
fun `present - Restart after cancelation returns to requesting verification`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
|
||||||
val eventSink = initialState.eventSink
|
|
||||||
|
|
||||||
eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
|
||||||
assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized))
|
|
||||||
|
|
||||||
service.givenVerificationFlowState(VerificationFlowState.Canceled)
|
service.givenVerificationFlowState(VerificationFlowState.Canceled)
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
||||||
|
state.eventSink(VerifySelfSessionViewEvents.Restart)
|
||||||
eventSink(VerifySelfSessionViewEvents.Restart)
|
|
||||||
// Went back to requesting verification
|
// Went back to requesting verification
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -200,18 +157,12 @@ class VerifySelfSessionPresenterTests {
|
||||||
val service = FakeSessionVerificationService().apply {
|
val service = FakeSessionVerificationService().apply {
|
||||||
givenEmojiList(emojis)
|
givenEmojiList(emojis)
|
||||||
}
|
}
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||||
val eventSink = initialState.eventSink
|
|
||||||
|
|
||||||
eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
|
||||||
assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Uninitialized))
|
|
||||||
|
|
||||||
eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Loading()))
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Loading()))
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
|
||||||
}
|
}
|
||||||
|
|
@ -220,28 +171,41 @@ class VerifySelfSessionPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - When verification is declined, the flow is canceled`() = runTest {
|
fun `present - When verification is declined, the flow is canceled`() = runTest {
|
||||||
val service = FakeSessionVerificationService()
|
val service = FakeSessionVerificationService()
|
||||||
val presenter = VerifySelfSessionPresenter(service)
|
val presenter = createPresenter(service)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
||||||
val eventSink = initialState.eventSink
|
|
||||||
|
|
||||||
eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
|
||||||
|
|
||||||
assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized))
|
|
||||||
eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
|
||||||
|
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Loading()))
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Loading()))
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ReceiveTurbine<VerifySelfSessionState>.awaitChallengeReceivedState(): VerifySelfSessionState {
|
private suspend fun ReceiveTurbine<VerifySelfSessionState>.requestVerificationAndAwaitVerifyingState(
|
||||||
// Skip 'waiting for response', 'ready' and 'starting verification' state
|
fakeService: FakeSessionVerificationService
|
||||||
skipItems(3)
|
): VerifySelfSessionState {
|
||||||
// Received challenge
|
var state = awaitItem()
|
||||||
return 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
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-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-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" }
|
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_blurhash = "com.vanniktech:blurhash:0.1.0"
|
||||||
vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
|
vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
|
statemachine = "com.freeletics.flowredux:compose:1.1.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog.android:posthog:2.0.3"
|
posthog = "com.posthog.android:posthog:2.0.3"
|
||||||
|
|
|
||||||
|
|
@ -40,32 +40,32 @@ interface SessionVerificationService {
|
||||||
/**
|
/**
|
||||||
* Request verification of the current session.
|
* Request verification of the current session.
|
||||||
*/
|
*/
|
||||||
fun requestVerification()
|
suspend fun requestVerification()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the current verification attempt.
|
* 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.
|
* 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.
|
* 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.
|
* Starts the verification of the unverified session from another device.
|
||||||
*/
|
*/
|
||||||
fun startVerification()
|
suspend fun startVerification()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the verification service state to the initial step.
|
* Returns the verification service state to the initial step.
|
||||||
*/
|
*/
|
||||||
fun reset()
|
suspend fun reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verification status of the current session. */
|
/** Verification status of the current session. */
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncList
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder
|
import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt
|
import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
|
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
|
||||||
|
import org.matrix.rustcomponents.sdk.SlidingSyncSelectiveModeBuilder
|
||||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -124,8 +125,7 @@ class RustMatrixClient constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filters(visibleRoomsSlidingSyncFilters)
|
.filters(visibleRoomsSlidingSyncFilters)
|
||||||
.syncModeSelective()
|
.syncModeSelective(SlidingSyncSelectiveModeBuilder().addRange(0u, 20u))
|
||||||
.addRange(0u, 20u)
|
|
||||||
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
||||||
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
||||||
visibleRoomsSlidingSyncList.tryEmit(list)
|
visibleRoomsSlidingSyncList.tryEmit(list)
|
||||||
|
|
@ -146,8 +146,7 @@ class RustMatrixClient constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filters(invitesSlidingSyncFilters)
|
.filters(invitesSlidingSyncFilters)
|
||||||
.syncModeSelective()
|
.syncModeSelective(SlidingSyncSelectiveModeBuilder().addRange(0u, 20u))
|
||||||
.addRange(0u, 20u)
|
|
||||||
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
||||||
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
||||||
invitesSlidingSyncList.tryEmit(list)
|
invitesSlidingSyncList.tryEmit(list)
|
||||||
|
|
@ -156,10 +155,9 @@ class RustMatrixClient constructor(
|
||||||
})
|
})
|
||||||
|
|
||||||
private val slidingSync = client
|
private val slidingSync = client
|
||||||
.slidingSync()
|
.slidingSync("ElementX")
|
||||||
// .homeserver("https://slidingsync.lab.matrix.org")
|
// .homeserver("https://slidingsync.lab.matrix.org")
|
||||||
.withCommonExtensions()
|
.withCommonExtensions()
|
||||||
.storageKey("ElementX")
|
|
||||||
.addList(visibleRoomsSlidingSyncListBuilder)
|
.addList(visibleRoomsSlidingSyncListBuilder)
|
||||||
.addList(invitesSlidingSyncListBuilder)
|
.addList(invitesSlidingSyncListBuilder)
|
||||||
.use {
|
.use {
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,7 @@ class RustMediaLoader(
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
body = body,
|
body = body,
|
||||||
mimeType = mimeType ?: "application/octet-stream",
|
mimeType = mimeType ?: "application/octet-stream",
|
||||||
//TODO uncomment when rust api will be merged
|
tempDir = cacheDirectory.path,
|
||||||
//tempDir = cacheDirectory.path,
|
|
||||||
)
|
)
|
||||||
RustMediaFile(mediaFile)
|
RustMediaFile(mediaFile)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSync
|
import org.matrix.rustcomponents.sdk.SlidingSync
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncList
|
import org.matrix.rustcomponents.sdk.SlidingSyncList
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsListDiff
|
import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsListDiff
|
||||||
|
import org.matrix.rustcomponents.sdk.SlidingSyncSelectiveModeBuilder
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncState
|
import org.matrix.rustcomponents.sdk.SlidingSyncState
|
||||||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -98,7 +99,9 @@ internal class RustRoomSummaryDataSource(
|
||||||
override fun setSlidingSyncRange(range: IntRange) {
|
override fun setSlidingSyncRange(range: IntRange) {
|
||||||
Timber.v("setVisibleRange=$range")
|
Timber.v("setVisibleRange=$range")
|
||||||
coroutineScope.launch {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ class RustMatrixTimeline(
|
||||||
),
|
),
|
||||||
timelineLimit = null
|
timelineLimit = null
|
||||||
)
|
)
|
||||||
listenerTokens += slidingSyncRoom.subscribeToRoom(settings)
|
slidingSyncRoom.subscribeToRoom(settings)
|
||||||
val result = slidingSyncRoom.addTimelineListener(timelineListener)
|
val result = slidingSyncRoom.addTimelineListener(timelineListener)
|
||||||
launch {
|
launch {
|
||||||
fetchMembers()
|
fetchMembers()
|
||||||
|
|
|
||||||
|
|
@ -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.core.data.tryOrNull
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
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.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -52,21 +52,21 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification
|
||||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
|
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
|
||||||
|
|
||||||
override fun requestVerification() = tryOrFail {
|
override suspend fun requestVerification() = tryOrFail {
|
||||||
verificationController?.requestVerification()
|
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()
|
verificationController?.startSasVerification()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryOrFail(block: () -> Unit) {
|
private suspend fun tryOrFail(block: suspend () -> Unit) {
|
||||||
runCatching {
|
runCatching {
|
||||||
block()
|
block()
|
||||||
}.onFailure { didFail() }
|
}.onFailure { didFail() }
|
||||||
|
|
@ -107,7 +107,7 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification
|
||||||
|
|
||||||
// end-region
|
// end-region
|
||||||
|
|
||||||
override fun reset() {
|
override suspend fun reset() {
|
||||||
if (isReady.value) {
|
if (isReady.value) {
|
||||||
// Cancel any pending verification attempt
|
// Cancel any pending verification attempt
|
||||||
tryOrNull { verificationController?.cancelVerification() }
|
tryOrNull { verificationController?.cancelVerification() }
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,15 @@ class FakeSessionVerificationService : SessionVerificationService {
|
||||||
|
|
||||||
override val isReady: StateFlow<Boolean> = _isReady
|
override val isReady: StateFlow<Boolean> = _isReady
|
||||||
|
|
||||||
override fun requestVerification() {
|
override suspend fun requestVerification() {
|
||||||
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
|
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
|
||||||
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
|
|
||||||
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelVerification() {
|
override suspend fun cancelVerification() {
|
||||||
_verificationFlowState.value = VerificationFlowState.Canceled
|
_verificationFlowState.value = VerificationFlowState.Canceled
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun approveVerification() {
|
override suspend fun approveVerification() {
|
||||||
if (!shouldFail) {
|
if (!shouldFail) {
|
||||||
_verificationFlowState.value = VerificationFlowState.Finished
|
_verificationFlowState.value = VerificationFlowState.Finished
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -55,7 +53,7 @@ class FakeSessionVerificationService : SessionVerificationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun declineVerification() {
|
override suspend fun declineVerification() {
|
||||||
if (!shouldFail) {
|
if (!shouldFail) {
|
||||||
_verificationFlowState.value = VerificationFlowState.Canceled
|
_verificationFlowState.value = VerificationFlowState.Canceled
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -63,11 +61,14 @@ class FakeSessionVerificationService : SessionVerificationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startVerification() {
|
fun triggerReceiveVerificationData() {
|
||||||
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
|
|
||||||
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
|
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun startVerification() {
|
||||||
|
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
|
||||||
|
}
|
||||||
|
|
||||||
fun givenVerifiedStatus(status: SessionVerifiedStatus) {
|
fun givenVerifiedStatus(status: SessionVerifiedStatus) {
|
||||||
_sessionVerifiedStatus.value = status
|
_sessionVerifiedStatus.value = status
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +85,7 @@ class FakeSessionVerificationService : SessionVerificationService {
|
||||||
this.emojiList = emojis
|
this.emojiList = emojis
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reset() {
|
override suspend fun reset() {
|
||||||
_verificationFlowState.value = VerificationFlowState.Initial
|
_verificationFlowState.value = VerificationFlowState.Initial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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 <Event : Any, State : Any> createStateMachine(
|
|
||||||
config: StateMachineBuilder<Event, State>.() -> Unit
|
|
||||||
): StateMachine<Event, State> {
|
|
||||||
val builder = StateMachineBuilder<Event, State>()
|
|
||||||
config(builder)
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateMachine<Event : Any, State : Any>(
|
|
||||||
val initialState: State,
|
|
||||||
private val stateConfigs: Map<Class<*>, StateConfig<*>>,
|
|
||||||
private val routes: List<StateMachineRoute<*, *, *>>,
|
|
||||||
) {
|
|
||||||
|
|
||||||
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<State>
|
|
||||||
initialStateConfig.onEnter?.invoke(initialState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <E : Event> process(event: E) {
|
|
||||||
val route = findMatchingRoute(event) ?: error("No route found for state $currentState on event $event")
|
|
||||||
|
|
||||||
val lastStateConfig: StateConfig<State>? = stateConfigs[currentState::class.java] as? StateConfig<State>
|
|
||||||
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<State>
|
|
||||||
currentStateConfig?.onEnter?.invoke(nextState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun <E : Event> findMatchingRoute(event: E): StateMachineRoute<E, State, State>? {
|
|
||||||
val routesForEvent = routes.filter { it.eventType.isInstance(event) }
|
|
||||||
|
|
||||||
return (routesForEvent.firstOrNull { it.fromState?.isInstance(currentState) == true }
|
|
||||||
?: routesForEvent.firstOrNull { it.fromState == null }) as? StateMachineRoute<E, State, State>
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restart() {
|
|
||||||
_stateFlow.value = initialState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateMachineBuilder<Event : Any, State : Any>(
|
|
||||||
val routes: MutableList<StateMachineRoute<out Event, out State, out State>> = mutableListOf(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
lateinit var initialState: State
|
|
||||||
var stateConfigs = mutableMapOf<Class<out State>, StateConfig<out State>>()
|
|
||||||
|
|
||||||
inline fun <reified S : State> addState(block: StateRegistrationBuilder<Event, State, S>.() -> Unit = {}) {
|
|
||||||
val config = StateConfig(S::class.java)
|
|
||||||
val registrationBuilder = StateRegistrationBuilder<Event, State, S>(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 <reified S : State> addInitialState(state: S, config: StateRegistrationBuilder<Event, State, S>.() -> Unit = {}) {
|
|
||||||
initialState = state
|
|
||||||
addState(block = config)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event, reified S : State> on(noinline configuration: (E, State) -> S) {
|
|
||||||
val builder = RouteBuilder<E, State, S>(E::class.java, null)
|
|
||||||
builder.toState = configuration
|
|
||||||
val newRoute = builder.build()
|
|
||||||
verifyRoutesAreUnique(S::class.java, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event> on(newState: State) {
|
|
||||||
val builder = RouteBuilder<E, State, State>(E::class.java, null)
|
|
||||||
builder.toState = { _, _ -> newState }
|
|
||||||
val newRoute = builder.build()
|
|
||||||
verifyRoutesAreUnique(null, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(): StateMachine<Event, State> {
|
|
||||||
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<StateMachineRoute<*, *, *>>,
|
|
||||||
newRoutes: List<StateMachineRoute<*, *, *>>,
|
|
||||||
) {
|
|
||||||
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<Event : Any, BaseState : Any, State : BaseState>(
|
|
||||||
val fromState: StateConfig<State>,
|
|
||||||
val routes: MutableList<StateMachineRoute<out Event, out State, out BaseState>> = mutableListOf(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun onEnter(enter: (State) -> Unit) {
|
|
||||||
fromState.onEnter = enter
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onExit(exit: (State) -> Unit) {
|
|
||||||
fromState.onExit = exit
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event> on(noinline configuration: (E, State) -> BaseState) {
|
|
||||||
val builder = RouteBuilder<E, State, BaseState>(E::class.java, fromState.state)
|
|
||||||
builder.toState = configuration
|
|
||||||
val newRoute = builder.build()
|
|
||||||
StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event> on(newState: BaseState) {
|
|
||||||
val builder = RouteBuilder<E, State, BaseState>(E::class.java, fromState.state)
|
|
||||||
builder.toState = { _, _ -> newState }
|
|
||||||
val newRoute = builder.build()
|
|
||||||
StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RouteBuilder<Event : Any, FromState : Any, ToState : Any>(
|
|
||||||
val eventType: Class<out Event>,
|
|
||||||
val fromState: Class<out FromState>?,
|
|
||||||
) {
|
|
||||||
lateinit var toState: (Event, FromState) -> ToState
|
|
||||||
|
|
||||||
fun build() = StateMachineRoute(eventType, fromState, toState)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class StateMachineRoute<Event : Any, FromState : Any, ToState : Any>(
|
|
||||||
val eventType: Class<out Event>,
|
|
||||||
val fromState: Class<out FromState>?,
|
|
||||||
val toState: (Event, FromState) -> ToState,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class StateConfig<State : Any>(
|
|
||||||
val state: Class<State>,
|
|
||||||
var onEnter: ((State) -> Unit)? = null,
|
|
||||||
var onExit: ((State) -> Unit)? = 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<States, Events, States>? = null
|
|
||||||
private fun aStateMachine() = createStateMachine<Events, States> {
|
|
||||||
addInitialState(States.First) {
|
|
||||||
onExit { exitedFirstState = true }
|
|
||||||
on<Events.GoToSecond> { first, _ ->
|
|
||||||
States.Second(first.string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addState<States.Second> {
|
|
||||||
onEnter { enteredSecondState = true }
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
}
|
|
||||||
|
|
||||||
addState<States.Fourth>()
|
|
||||||
|
|
||||||
on<Events.GoToFourth, States.Fourth> { _, _ -> States.Fourth }
|
|
||||||
on<Events.Cancel>(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<Events.Cancel>(States.Canceled)
|
|
||||||
}
|
|
||||||
on<Events.Cancel>(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<Events.GoToFourth>(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<Events, States> {
|
|
||||||
addState<States.Second>()
|
|
||||||
on<Events.Cancel>(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<Events, States> {
|
|
||||||
addInitialState(States.First)
|
|
||||||
addState<States.First>()
|
|
||||||
}
|
|
||||||
}.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<Events, States> {
|
|
||||||
addInitialState(States.First) {
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
on<Events.GoToThird> { _, _ -> 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<Events, States> {
|
|
||||||
addInitialState(States.First)
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
fail("It should have thrown an error")
|
|
||||||
}.onFailure { error ->
|
|
||||||
assertThat(error.message).startsWith("Duplicate registration in state")
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -94,7 +94,6 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
implementation(project(":libraries:dateformatter:impl"))
|
implementation(project(":libraries:dateformatter:impl"))
|
||||||
implementation(project(":libraries:di"))
|
implementation(project(":libraries:di"))
|
||||||
implementation(project(":libraries:session-storage:impl"))
|
implementation(project(":libraries:session-storage:impl"))
|
||||||
implementation(project(":libraries:statemachine"))
|
|
||||||
implementation(project(":libraries:mediapickers:impl"))
|
implementation(project(":libraries:mediapickers:impl"))
|
||||||
implementation(project(":libraries:mediaupload:impl"))
|
implementation(project(":libraries:mediaupload:impl"))
|
||||||
implementation(project(":libraries:usersearch:impl"))
|
implementation(project(":libraries:usersearch:impl"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue