Create SyncOrchestrator (#4176)

* Create `SyncOrchestrator` to centralise the sync start/stop flow through the whole app: the decision is based on several inputs: sync state, network available, app in foreground, app in call, app needing to sync an event for a notification.

* Make network monitor return network connectivity status, not internet connectivity

* Don't stop the `SyncService` when network connection is lost, let it fail instead. This prevents an issue when using the offline mode of the SDK, which made the wrong UI states to be shown when the `SyncState` is `Idle` (that is, after the service being manually stopped).

* Rename `NetworkStatus.Online/Offline` to `Connected/Disconnected` so they're not easily mistaken with internet connectivity instead
This commit is contained in:
Jorge Martin Espinosa 2025-02-06 16:36:57 +01:00 committed by GitHub
parent ce1c01e626
commit 3c87fb05b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 851 additions and 344 deletions

View file

@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@ -58,9 +59,9 @@ class CallScreenPresenter @AssistedInject constructor(
private val dispatchers: CoroutineDispatchers,
private val matrixClientsProvider: MatrixClientProvider,
private val screenTracker: ScreenTracker,
private val appCoroutineScope: CoroutineScope,
private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
) : Presenter<CallScreenState> {
@AssistedFactory
interface Factory {
@ -226,19 +227,13 @@ class CallScreenPresenter @AssistedInject constructor(
if (state == SyncState.Running) {
client.notifyCallStartIfNeeded(callType.roomId)
} else {
client.syncService().startSync()
appForegroundStateService.updateIsInCallState(true)
}
}
}
onDispose {
// We can't use the local coroutine scope here because it will be disposed before this effect
appCoroutineScope.launch {
client.syncService().run {
if (syncState.value == SyncState.Running) {
stopSync()
}
}
}
// Make sure we mark the call as ended in the app state
appForegroundStateService.updateIsInCallState(false)
}
}
}

View file

@ -32,10 +32,9 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.analytics.test.FakeScreenTracker
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers
@ -243,7 +242,7 @@ class CallScreenPresenterTest {
}
@Test
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
@ -251,6 +250,7 @@ class CallScreenPresenterTest {
this.startSyncLambda = startSyncLambda
}
val matrixClient = FakeMatrixClient(syncService = syncService)
val appForegroundStateService = FakeAppForegroundStateService()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
@ -258,34 +258,7 @@ class CallScreenPresenterTest {
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }),
screenTracker = FakeScreenTracker {},
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
consumeItemsUntilTimeout()
assert(startSyncLambda).isCalledOnce()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(SyncState.Running).apply {
this.stopSyncLambda = stopSyncLambda
}
val matrixClient = FakeMatrixClient(syncService = syncService)
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }),
screenTracker = FakeScreenTracker {},
appForegroundStateService = appForegroundStateService,
)
val hasRun = Mutex(true)
val job = launch {
@ -296,11 +269,25 @@ class CallScreenPresenterTest {
}
}
hasRun.lock()
appForegroundStateService.isInCall.test {
// The initial isInCall state will always be false
assertThat(awaitItem()).isFalse()
job.cancelAndJoin()
// Wait until the call starts
hasRun.lock()
assert(stopSyncLambda).isCalledOnce()
// Then it'll be true once the call is active
assertThat(awaitItem()).isTrue()
// If we dispose the screen
job.cancelAndJoin()
// The isInCall state is now false
assertThat(awaitItem()).isFalse()
// And there are no more events
ensureAllEventsConsumed()
}
}
@Test
@ -354,6 +341,7 @@ class CallScreenPresenterTest {
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider {
override fun provide(): String {
@ -369,10 +357,10 @@ class CallScreenPresenterTest {
clock = clock,
dispatchers = dispatchers,
matrixClientsProvider = matrixClientsProvider,
appCoroutineScope = this,
activeCallManager = activeCallManager,
screenTracker = screenTracker,
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
)
}
}