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 12a803bbcc..3a4a31f190 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 @@ -17,7 +17,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -39,7 +38,6 @@ import io.element.android.libraries.matrix.api.core.RoomId 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.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope @@ -80,10 +78,10 @@ class CallScreenPresenter @AssistedInject constructor( val callWidgetDriver = remember { mutableStateOf(null) } val messageInterceptor = remember { mutableStateOf(null) } var isJoinedCall by rememberSaveable { mutableStateOf(false) } - var canRenderWebViewInCaseOfError by rememberSaveable { mutableStateOf(false) } + var ignoreWebViewError by rememberSaveable { mutableStateOf(false) } + var webViewError by remember { mutableStateOf(null) } val languageTag = languageTagProvider.provideLanguageTag() val theme = if (ElementTheme.isLightTheme) "light" else "dark" - val errorMessage = stringResource(id = CommonStrings.error_unknown) DisposableEffect(Unit) { coroutineScope.launch { // Sets the call as joined @@ -130,7 +128,7 @@ class CallScreenPresenter @AssistedInject constructor( interceptor.interceptedMessages .onEach { // We are receiving messages from the WebView, consider that the application is loaded - canRenderWebViewInCaseOfError = true + ignoreWebViewError = true // Relay message to Widget Driver callWidgetDriver.value?.send(it) @@ -170,15 +168,8 @@ class CallScreenPresenter @AssistedInject constructor( messageInterceptor.value = event.widgetMessageInterceptor } is CallScreenEvents.OnWebViewError -> { - if (!canRenderWebViewInCaseOfError) { - urlState.value = AsyncData.Failure( - Exception( - buildString { - append(errorMessage) - event.description?.let { append("\n\n").append(it) } - } - ) - ) + if (!ignoreWebViewError) { + webViewError = event.description.orEmpty() } // Else ignore the error, give a chance the Element Call to recover by itself. } @@ -187,7 +178,7 @@ class CallScreenPresenter @AssistedInject constructor( return CallScreenState( urlState = urlState.value, - canRenderWebViewInCaseOfError = canRenderWebViewInCaseOfError, + webViewError = webViewError, userAgent = userAgent, isInWidgetMode = isInWidgetMode, eventSink = { handleEvents(it) }, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt index 9ceb89d45b..48a4672e1a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt @@ -11,7 +11,7 @@ import io.element.android.libraries.architecture.AsyncData data class CallScreenState( val urlState: AsyncData, - val canRenderWebViewInCaseOfError: Boolean, + val webViewError: String?, val userAgent: String, val isInWidgetMode: Boolean, val eventSink: (CallScreenEvents) -> Unit, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt index 3486a1fb98..bb4794749d 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt @@ -16,19 +16,20 @@ open class CallScreenStateProvider : PreviewParameterProvider { aCallScreenState(), aCallScreenState(urlState = AsyncData.Loading()), aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))), + aCallScreenState(webViewError = "Error details from WebView"), ) } internal fun aCallScreenState( urlState: AsyncData = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"), - canRenderWebViewInCaseOfError: Boolean = true, + webViewError: String? = null, userAgent: String = "", isInWidgetMode: Boolean = false, eventSink: (CallScreenEvents) -> Unit = {}, ): CallScreenState { return CallScreenState( urlState = urlState, - canRenderWebViewInCaseOfError = canRenderWebViewInCaseOfError, + webViewError = webViewError, userAgent = userAgent, isInWidgetMode = isInWidgetMode, eventSink = eventSink, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index 64f06d28f9..5fa4856476 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -85,7 +85,15 @@ internal fun CallScreenView( BackHandler { handleBack() } - if (state.urlState !is AsyncData.Failure || state.canRenderWebViewInCaseOfError) { + if (state.webViewError != null) { + ErrorDialog( + content = buildString { + append(stringResource(CommonStrings.error_unknown)) + state.webViewError.takeIf { it.isNotEmpty() }?.let { append("\n\n").append(it) } + }, + onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, + ) + } else { CallWebView( modifier = Modifier .padding(padding) @@ -108,17 +116,17 @@ internal fun CallScreenView( pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) } ) - } - when (state.urlState) { - AsyncData.Uninitialized, - is AsyncData.Loading -> - ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait)) - is AsyncData.Failure -> - ErrorDialog( - content = state.urlState.error.message.orEmpty(), - onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, - ) - is AsyncData.Success -> Unit + when (state.urlState) { + AsyncData.Uninitialized, + is AsyncData.Loading -> + ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait)) + is AsyncData.Failure -> + ErrorDialog( + content = state.urlState.error.message.orEmpty(), + onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, + ) + is AsyncData.Success -> Unit + } } } } 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 77326b0d7d..7390b5fde9 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 @@ -71,6 +71,7 @@ class CallScreenPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) + assertThat(initialState.webViewError).isNull() assertThat(initialState.isInWidgetMode).isFalse() analyticsLambda.assertions().isNeverCalled() joinedCallLambda.assertions().isCalledOnce() @@ -270,6 +271,48 @@ class CallScreenPresenterTest { assert(stopSyncLambda).isCalledOnce() } + @Test + fun `present - error from WebView are updating the state`() = runTest { + val presenter = createCallScreenPresenter( + callType = CallType.ExternalUrl("https://call.element.io"), + activeCallManager = FakeActiveCallManager(), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Wait until the URL is loaded + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) + val finalState = awaitItem() + assertThat(finalState.webViewError).isEqualTo("A Webview error") + } + } + + @Test + fun `present - error from WebView are ignored if Element Call is loaded`() = runTest { + val presenter = createCallScreenPresenter( + callType = CallType.ExternalUrl("https://call.element.io"), + activeCallManager = FakeActiveCallManager(), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Wait until the URL is loaded + skipItems(1) + val initialState = awaitItem() + + val messageInterceptor = FakeWidgetMessageInterceptor() + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + // Emit a message + messageInterceptor.givenInterceptedMessage("A message") + // WebView emits an error, but it will be ignored + initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) + val finalState = awaitItem() + assertThat(finalState.webViewError).isNull() + } + } + private fun TestScope.createCallScreenPresenter( callType: CallType, navigator: CallScreenNavigator = FakeCallScreenNavigator(),