diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt index 0e9ddbe3649..c18fef410ee 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt @@ -30,6 +30,9 @@ data class WidgetMessage( @Serializable enum class Action { + @SerialName("io.element.join") + Join, + @SerialName("im.vector.hangup") HangUp, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index e16428de3bf..04999c82ab6 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -48,9 +48,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive import timber.log.Timber import java.util.UUID import kotlin.time.Duration.Companion.seconds @@ -91,6 +88,7 @@ class CallScreenPresenter @AssistedInject constructor( var webViewError by remember { mutableStateOf(null) } val languageTag = languageTagProvider.provideLanguageTag() val theme = if (ElementTheme.isLightTheme) "light" else "dark" + DisposableEffect(Unit) { coroutineScope.launch { // Sets the call as joined @@ -145,17 +143,25 @@ class CallScreenPresenter @AssistedInject constructor( if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) { if (parsedMessage.action == WidgetMessage.Action.Close) { close(callWidgetDriver.value, navigator) - } else if (parsedMessage.action == WidgetMessage.Action.SendEvent) { - // This event is received when a member joins the call, the first one will be the current one - val type = parsedMessage.data?.jsonObject?.get("type")?.jsonPrimitive?.contentOrNull - if (type == "org.matrix.msc3401.call.member") { - isJoinedCall = true - } + } else if (parsedMessage.action == WidgetMessage.Action.Join) { + isJoinedCall = true } } } .launchIn(this) } + + LaunchedEffect(Unit) { + // Wait for the call to be joined, if it takes too long, we display an error + delay(10.seconds) + + if (!isJoinedCall) { + Timber.w("The call took too long to be joined. Displaying an error before exiting.") + + // This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call + webViewError = "" + } + } } fun handleEvents(event: CallScreenEvents) { diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index 23253a2033b..7709066dd04 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -225,7 +225,7 @@ import kotlin.time.Duration.Companion.seconds } @Test - fun `present - a received room member message makes the call to be active`() = runTest { + fun `present - a received 'joined' action makes the call to be active`() = runTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( @@ -248,13 +248,10 @@ import kotlin.time.Duration.Companion.seconds messageInterceptor.givenInterceptedMessage( """ { - "action":"send_event", + "action":"io.element.join", "api":"fromWidget", "widgetId":"1", - "requestId":"1", - "data":{ - "type":"org.matrix.msc3401.call.member" - } + "requestId":"1" } """.trimIndent() ) @@ -264,6 +261,40 @@ import kotlin.time.Duration.Companion.seconds } } + @Test + fun `present - if in room mode and no join action is received an error is displayed`() = runTest { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeMatrixWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + screenTracker = FakeScreenTracker {}, + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Give it time to load the URL and WidgetDriver + advanceTimeBy(1.seconds) + skipItems(2) + val initialState = awaitItem() + assertThat(initialState.isCallActive).isFalse() + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + skipItems(2) + + // Wait for the timeout to trigger + advanceTimeBy(10.seconds) + + val finalState = awaitItem() + assertThat(finalState.isCallActive).isFalse() + // The error dialog that will force the user to leave the call is displayed + assertThat(finalState.webViewError).isNotNull() + assertThat(finalState.webViewError).isEmpty() + } + } + @Test fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest { val navigator = FakeCallScreenNavigator()