Browse Source

[Element Call] Keep MatrixClient alive while the call is working (#1695)

* Element Call: keep MatrixClient alive to get event updates
pull/1711/head
Jorge Martin Espinosa 11 months ago committed by GitHub
parent
commit
355ee95964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt
  2. 6
      features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt
  3. 47
      features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt
  4. 2
      features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt
  5. 6
      features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt
  6. 69
      features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
  7. 7
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt
  8. 2
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt

2
appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt

@ -49,7 +49,7 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: @@ -49,7 +49,7 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
sessionIdsToMatrixClient.remove(sessionId)
}
fun getOrNull(sessionId: SessionId): MatrixClient? {
override fun getOrNull(sessionId: SessionId): MatrixClient? {
return sessionIdsToMatrixClient[sessionId]
}

6
features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt → features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt

@ -18,7 +18,7 @@ package io.element.android.features.call.ui @@ -18,7 +18,7 @@ package io.element.android.features.call.ui
import io.element.android.features.call.utils.WidgetMessageInterceptor
sealed interface CallScreeEvents {
data object Hangup : CallScreeEvents
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreeEvents
sealed interface CallScreenEvents {
data object Hangup : CallScreenEvents
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
}

47
features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package io.element.android.features.call.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@ -37,10 +38,13 @@ import io.element.android.libraries.architecture.Async @@ -37,10 +38,13 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClientProvider
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.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -53,9 +57,11 @@ class CallScreenPresenter @AssistedInject constructor( @@ -53,9 +57,11 @@ class CallScreenPresenter @AssistedInject constructor(
@Assisted private val callType: CallType,
@Assisted private val navigator: CallScreenNavigator,
private val callWidgetProvider: CallWidgetProvider,
private val userAgentProvider: UserAgentProvider,
userAgentProvider: UserAgentProvider,
private val clock: SystemClock,
private val dispatchers: CoroutineDispatchers,
private val matrixClientsProvider: MatrixClientProvider,
private val appCoroutineScope: CoroutineScope,
) : Presenter<CallScreenState> {
@AssistedFactory
@ -78,6 +84,8 @@ class CallScreenPresenter @AssistedInject constructor( @@ -78,6 +84,8 @@ class CallScreenPresenter @AssistedInject constructor(
loadUrl(callType, urlState, callWidgetDriver)
}
HandleMatrixClientSyncState()
callWidgetDriver.value?.let { driver ->
LaunchedEffect(Unit) {
driver.incomingMessages
@ -115,21 +123,22 @@ class CallScreenPresenter @AssistedInject constructor( @@ -115,21 +123,22 @@ class CallScreenPresenter @AssistedInject constructor(
}
}
fun handleEvents(event: CallScreeEvents) {
fun handleEvents(event: CallScreenEvents) {
when (event) {
is CallScreeEvents.Hangup -> {
is CallScreenEvents.Hangup -> {
val widgetId = callWidgetDriver.value?.id
val interceptor = messageInterceptor.value
if (widgetId != null && interceptor != null && isJoinedCall) {
// If the call was joined, we need to hang up first. Then the UI will be dismissed automatically.
sendHangupMessage(widgetId, interceptor)
isJoinedCall = false
} else {
coroutineScope.launch {
close(callWidgetDriver.value, navigator)
}
}
}
is CallScreeEvents.SetupMessageChannels -> {
is CallScreenEvents.SetupMessageChannels -> {
messageInterceptor.value = event.widgetMessageInterceptor
}
}
@ -166,6 +175,36 @@ class CallScreenPresenter @AssistedInject constructor( @@ -166,6 +175,36 @@ class CallScreenPresenter @AssistedInject constructor(
}
}
@Composable
private fun HandleMatrixClientSyncState() {
val coroutineScope = rememberCoroutineScope()
DisposableEffect(Unit) {
val client = (callType as? CallType.RoomCall)?.sessionId?.let {
matrixClientsProvider.getOrNull(it)
} ?: return@DisposableEffect onDispose { }
coroutineScope.launch {
client.syncService().syncState
.onEach { state ->
if (state != SyncState.Running) {
client.syncService().startSync()
}
}
.collect()
}
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()
}
}
}
}
}
}
private fun parseMessage(message: String): WidgetMessage? {
return WidgetMessageSerializer.deserialize(message).getOrNull()
}

2
features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt

@ -22,5 +22,5 @@ data class CallScreenState( @@ -22,5 +22,5 @@ data class CallScreenState(
val urlState: Async<String>,
val userAgent: String,
val isInWidgetMode: Boolean,
val eventSink: (CallScreeEvents) -> Unit,
val eventSink: (CallScreenEvents) -> Unit,
)

6
features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt

@ -65,14 +65,14 @@ internal fun CallScreenView( @@ -65,14 +65,14 @@ internal fun CallScreenView(
navigationIcon = {
BackButton(
resourceId = CommonDrawables.ic_compound_close,
onClick = { state.eventSink(CallScreeEvents.Hangup) }
onClick = { state.eventSink(CallScreenEvents.Hangup) }
)
}
)
}
) { padding ->
BackHandler {
state.eventSink(CallScreeEvents.Hangup)
state.eventSink(CallScreenEvents.Hangup)
}
CallWebView(
modifier = Modifier
@ -88,7 +88,7 @@ internal fun CallScreenView( @@ -88,7 +88,7 @@ internal fun CallScreenView(
},
onWebViewCreated = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView)
state.eventSink(CallScreeEvents.SetupMessageChannels(interceptor))
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
}
)
}

69
features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt

@ -25,14 +25,21 @@ import io.element.android.features.call.utils.FakeCallWidgetProvider @@ -25,14 +25,21 @@ import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
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.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
@ -95,7 +102,7 @@ class CallScreenPresenterTest { @@ -95,7 +102,7 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor))
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
// And incoming message from the Widget Driver is passed to the WebView
widgetDriver.givenIncomingMessage("A message")
@ -125,9 +132,9 @@ class CallScreenPresenterTest { @@ -125,9 +132,9 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor))
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
initialState.eventSink(CallScreeEvents.Hangup)
initialState.eventSink(CallScreenEvents.Hangup)
// Let background coroutines run
runCurrent()
@ -155,7 +162,7 @@ class CallScreenPresenterTest { @@ -155,7 +162,7 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor))
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
messageInterceptor.givenInterceptedMessage("""{"action":"im.vector.hangup","api":"fromWidget","widgetId":"1","requestId":"1"}""")
@ -169,12 +176,64 @@ class CallScreenPresenterTest { @@ -169,12 +176,64 @@ class CallScreenPresenterTest {
}
}
@Test
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver()
val matrixClient = FakeMatrixClient()
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) })
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
consumeItemsUntilTimeout()
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver()
val matrixClient = FakeMatrixClient()
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) })
)
val hasRun = Mutex(true)
val job = launch {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.collect {
hasRun.unlock()
}
}
hasRun.lock()
job.cancelAndJoin()
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Terminated)
}
private fun TestScope.createCallScreenPresenter(
callType: CallType,
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider {
override fun provide(): String {
@ -189,6 +248,8 @@ class CallScreenPresenterTest { @@ -189,6 +248,8 @@ class CallScreenPresenterTest {
userAgentProvider,
clock,
dispatchers,
matrixClientsProvider,
this,
)
}
}

7
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt

@ -25,4 +25,11 @@ interface MatrixClientProvider { @@ -25,4 +25,11 @@ interface MatrixClientProvider {
* Most of the time you want to use injected constructor instead of retrieving a MatrixClient with this provider.
*/
suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient>
/**
* Can be used to retrieve an existing [MatrixClient] with the given [SessionId].
* @param sessionId the [SessionId] of the [MatrixClient] to retrieve.
* @return the [MatrixClient] if it exists.
*/
fun getOrNull(sessionId: SessionId): MatrixClient?
}

2
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt

@ -24,4 +24,6 @@ class FakeMatrixClientProvider( @@ -24,4 +24,6 @@ class FakeMatrixClientProvider(
private val getClient: (SessionId) -> Result<MatrixClient> = { Result.success(FakeMatrixClient()) }
) : MatrixClientProvider {
override suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient> = getClient(sessionId)
override fun getOrNull(sessionId: SessionId): MatrixClient? = getClient(sessionId).getOrNull()
}

Loading…
Cancel
Save