diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt index 675e1e27e0..4785efe650 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt @@ -11,6 +11,6 @@ import io.element.android.features.call.impl.utils.WidgetMessageInterceptor sealed interface CallScreenEvents { data object Hangup : CallScreenEvents - data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : - CallScreenEvents + data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents + data class OnWebViewError(val description: String?) : CallScreenEvents } 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 4a6ec60e90..12a803bbcc 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,6 +17,7 @@ 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 @@ -38,6 +39,7 @@ 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 @@ -78,8 +80,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) } 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 @@ -125,6 +129,8 @@ class CallScreenPresenter @AssistedInject constructor( LaunchedEffect(Unit) { interceptor.interceptedMessages .onEach { + // We are receiving messages from the WebView, consider that the application is loaded + canRenderWebViewInCaseOfError = true // Relay message to Widget Driver callWidgetDriver.value?.send(it) @@ -163,11 +169,25 @@ class CallScreenPresenter @AssistedInject constructor( is CallScreenEvents.SetupMessageChannels -> { 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) } + } + ) + ) + } + // Else ignore the error, give a chance the Element Call to recover by itself. + } } } return CallScreenState( urlState = urlState.value, + canRenderWebViewInCaseOfError = canRenderWebViewInCaseOfError, 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 09fabfd0b0..9ceb89d45b 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,6 +11,7 @@ import io.element.android.libraries.architecture.AsyncData data class CallScreenState( val urlState: AsyncData, + val canRenderWebViewInCaseOfError: Boolean, 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 fc39c71c23..3486a1fb98 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 @@ -21,12 +21,14 @@ open class CallScreenStateProvider : PreviewParameterProvider { internal fun aCallScreenState( urlState: AsyncData = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"), + canRenderWebViewInCaseOfError: Boolean = true, userAgent: String = "", isInWidgetMode: Boolean = false, eventSink: (CallScreenEvents) -> Unit = {}, ): CallScreenState { return CallScreenState( urlState = urlState, + canRenderWebViewInCaseOfError = canRenderWebViewInCaseOfError, 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 4641e4e7bc..64f06d28f9 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,25 +85,30 @@ internal fun CallScreenView( BackHandler { handleBack() } - CallWebView( - modifier = Modifier + if (state.urlState !is AsyncData.Failure || state.canRenderWebViewInCaseOfError) { + CallWebView( + modifier = Modifier .padding(padding) .consumeWindowInsets(padding) .fillMaxSize(), - url = state.urlState, - userAgent = state.userAgent, - onPermissionsRequest = { request -> - val androidPermissions = mapWebkitPermissions(request.resources) - val callback: RequestPermissionCallback = { request.grant(it) } - requestPermissions(androidPermissions.toTypedArray(), callback) - }, - onWebViewCreate = { webView -> - val interceptor = WebViewWidgetMessageInterceptor(webView) - state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) - val pipController = WebViewPipController(webView) - pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) - } - ) + url = state.urlState, + userAgent = state.userAgent, + onPermissionsRequest = { request -> + val androidPermissions = mapWebkitPermissions(request.resources) + val callback: RequestPermissionCallback = { request.grant(it) } + requestPermissions(androidPermissions.toTypedArray(), callback) + }, + onWebViewCreate = { webView -> + val interceptor = WebViewWidgetMessageInterceptor( + webView = webView, + onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) }, + ) + state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) + val pipController = WebViewPipController(webView) + pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) + } + ) + } when (state.urlState) { AsyncData.Uninitialized, is AsyncData.Loading -> diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt index 6f200e550a..609c04a0e6 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt @@ -8,16 +8,23 @@ package io.element.android.features.call.impl.utils import android.graphics.Bitmap +import android.net.http.SslError import android.webkit.JavascriptInterface +import android.webkit.SslErrorHandler +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import io.element.android.features.call.impl.BuildConfig import kotlinx.coroutines.flow.MutableSharedFlow +import timber.log.Timber class WebViewWidgetMessageInterceptor( private val webView: WebView, + private val onError: (String?) -> Unit, ) : WidgetMessageInterceptor { companion object { // We call both the WebMessageListener and the JavascriptInterface objects in JS with this @@ -45,16 +52,35 @@ class WebViewWidgetMessageInterceptor( if (message.data.response && message.data.api == "toWidget" || !message.data.response && message.data.api == "fromWidget") { let json = JSON.stringify(event.data) - ${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } } + ${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG }} $LISTENER_NAME.postMessage(json); } else { - ${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } } + ${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG }} } }); """.trimIndent(), null ) } + + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { + // No network for instance, transmit the error + Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}") + onError(error?.description?.toString()) + super.onReceivedError(view, request, error) + } + + override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) { + Timber.e("onReceivedHttpError error: ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}") + onError(errorResponse?.statusCode.toString()) + super.onReceivedHttpError(view, request, errorResponse) + } + + override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) { + Timber.e("onReceivedSslError error: ${error?.primaryError}") + onError(error?.primaryError?.toString()) + super.onReceivedSslError(view, handler, error) + } } // Create a WebMessageListener, which will receive messages from the WebView and reply to them