[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:
parent
06514d8be5
commit
f7f3925fc9
8 changed files with 125 additions and 16 deletions
|
|
@ -49,7 +49,7 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
|
||||||
sessionIdsToMatrixClient.remove(sessionId)
|
sessionIdsToMatrixClient.remove(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOrNull(sessionId: SessionId): MatrixClient? {
|
override fun getOrNull(sessionId: SessionId): MatrixClient? {
|
||||||
return sessionIdsToMatrixClient[sessionId]
|
return sessionIdsToMatrixClient[sessionId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.call.ui
|
||||||
|
|
||||||
import io.element.android.features.call.utils.WidgetMessageInterceptor
|
import io.element.android.features.call.utils.WidgetMessageInterceptor
|
||||||
|
|
||||||
sealed interface CallScreeEvents {
|
sealed interface CallScreenEvents {
|
||||||
data object Hangup : CallScreeEvents
|
data object Hangup : CallScreenEvents
|
||||||
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreeEvents
|
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.call.ui
|
package io.element.android.features.call.ui
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
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.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -53,9 +57,11 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
@Assisted private val callType: CallType,
|
@Assisted private val callType: CallType,
|
||||||
@Assisted private val navigator: CallScreenNavigator,
|
@Assisted private val navigator: CallScreenNavigator,
|
||||||
private val callWidgetProvider: CallWidgetProvider,
|
private val callWidgetProvider: CallWidgetProvider,
|
||||||
private val userAgentProvider: UserAgentProvider,
|
userAgentProvider: UserAgentProvider,
|
||||||
private val clock: SystemClock,
|
private val clock: SystemClock,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<CallScreenState> {
|
) : Presenter<CallScreenState> {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -78,6 +84,8 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
loadUrl(callType, urlState, callWidgetDriver)
|
loadUrl(callType, urlState, callWidgetDriver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HandleMatrixClientSyncState()
|
||||||
|
|
||||||
callWidgetDriver.value?.let { driver ->
|
callWidgetDriver.value?.let { driver ->
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
driver.incomingMessages
|
driver.incomingMessages
|
||||||
|
|
@ -115,21 +123,22 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(event: CallScreeEvents) {
|
fun handleEvents(event: CallScreenEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is CallScreeEvents.Hangup -> {
|
is CallScreenEvents.Hangup -> {
|
||||||
val widgetId = callWidgetDriver.value?.id
|
val widgetId = callWidgetDriver.value?.id
|
||||||
val interceptor = messageInterceptor.value
|
val interceptor = messageInterceptor.value
|
||||||
if (widgetId != null && interceptor != null && isJoinedCall) {
|
if (widgetId != null && interceptor != null && isJoinedCall) {
|
||||||
// If the call was joined, we need to hang up first. Then the UI will be dismissed automatically.
|
// If the call was joined, we need to hang up first. Then the UI will be dismissed automatically.
|
||||||
sendHangupMessage(widgetId, interceptor)
|
sendHangupMessage(widgetId, interceptor)
|
||||||
|
isJoinedCall = false
|
||||||
} else {
|
} else {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
close(callWidgetDriver.value, navigator)
|
close(callWidgetDriver.value, navigator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CallScreeEvents.SetupMessageChannels -> {
|
is CallScreenEvents.SetupMessageChannels -> {
|
||||||
messageInterceptor.value = event.widgetMessageInterceptor
|
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? {
|
private fun parseMessage(message: String): WidgetMessage? {
|
||||||
return WidgetMessageSerializer.deserialize(message).getOrNull()
|
return WidgetMessageSerializer.deserialize(message).getOrNull()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,5 @@ data class CallScreenState(
|
||||||
val urlState: Async<String>,
|
val urlState: Async<String>,
|
||||||
val userAgent: String,
|
val userAgent: String,
|
||||||
val isInWidgetMode: Boolean,
|
val isInWidgetMode: Boolean,
|
||||||
val eventSink: (CallScreeEvents) -> Unit,
|
val eventSink: (CallScreenEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -65,14 +65,14 @@ internal fun CallScreenView(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
BackButton(
|
BackButton(
|
||||||
resourceId = CommonDrawables.ic_compound_close,
|
resourceId = CommonDrawables.ic_compound_close,
|
||||||
onClick = { state.eventSink(CallScreeEvents.Hangup) }
|
onClick = { state.eventSink(CallScreenEvents.Hangup) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
BackHandler {
|
BackHandler {
|
||||||
state.eventSink(CallScreeEvents.Hangup)
|
state.eventSink(CallScreenEvents.Hangup)
|
||||||
}
|
}
|
||||||
CallWebView(
|
CallWebView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -88,7 +88,7 @@ internal fun CallScreenView(
|
||||||
},
|
},
|
||||||
onWebViewCreated = { webView ->
|
onWebViewCreated = { webView ->
|
||||||
val interceptor = WebViewWidgetMessageInterceptor(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.features.call.utils.FakeWidgetMessageInterceptor
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_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.matrix.test.widget.FakeWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
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.TestScope
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
|
@ -95,7 +102,7 @@ class CallScreenPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
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
|
// And incoming message from the Widget Driver is passed to the WebView
|
||||||
widgetDriver.givenIncomingMessage("A message")
|
widgetDriver.givenIncomingMessage("A message")
|
||||||
|
|
@ -125,9 +132,9 @@ class CallScreenPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
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
|
// Let background coroutines run
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
@ -155,7 +162,7 @@ class CallScreenPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
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"}""")
|
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(
|
private fun TestScope.createCallScreenPresenter(
|
||||||
callType: CallType,
|
callType: CallType,
|
||||||
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
|
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
|
||||||
widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
|
widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
|
||||||
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
|
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
|
||||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
): CallScreenPresenter {
|
): CallScreenPresenter {
|
||||||
val userAgentProvider = object : UserAgentProvider {
|
val userAgentProvider = object : UserAgentProvider {
|
||||||
override fun provide(): String {
|
override fun provide(): String {
|
||||||
|
|
@ -189,6 +248,8 @@ class CallScreenPresenterTest {
|
||||||
userAgentProvider,
|
userAgentProvider,
|
||||||
clock,
|
clock,
|
||||||
dispatchers,
|
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.
|
* 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>
|
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()) }
|
private val getClient: (SessionId) -> Result<MatrixClient> = { Result.success(FakeMatrixClient()) }
|
||||||
) : MatrixClientProvider {
|
) : MatrixClientProvider {
|
||||||
override suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient> = getClient(sessionId)
|
override suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient> = getClient(sessionId)
|
||||||
|
|
||||||
|
override fun getOrNull(sessionId: SessionId): MatrixClient? = getClient(sessionId).getOrNull()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue