[Element Call] Keep MatrixClient alive while the call is working (#1695)
* Element Call: keep MatrixClient alive to get event updates
This commit is contained in:
committed by
GitHub
parent
06514d8be5
commit
f7f3925fc9
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
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(
|
||||
@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(
|
||||
loadUrl(callType, urlState, callWidgetDriver)
|
||||
}
|
||||
|
||||
HandleMatrixClientSyncState()
|
||||
|
||||
callWidgetDriver.value?.let { driver ->
|
||||
LaunchedEffect(Unit) {
|
||||
driver.incomingMessages
|
||||
@@ -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(
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
},
|
||||
onWebViewCreated = { webView ->
|
||||
val interceptor = WebViewWidgetMessageInterceptor(webView)
|
||||
state.eventSink(CallScreeEvents.SetupMessageChannels(interceptor))
|
||||
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
userAgentProvider,
|
||||
clock,
|
||||
dispatchers,
|
||||
matrixClientsProvider,
|
||||
this,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user