Handle no network error when starting Element Call.

This commit is contained in:
Benoit Marty 2024-09-20 17:26:27 +02:00 committed by Benoit Marty
parent 7d3801a623
commit 0cea89edcc
6 changed files with 74 additions and 20 deletions

View file

@ -11,6 +11,6 @@ import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
sealed interface CallScreenEvents { sealed interface CallScreenEvents {
data object Hangup : CallScreenEvents data object Hangup : CallScreenEvents
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
CallScreenEvents data class OnWebViewError(val description: String?) : CallScreenEvents
} }

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject 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.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.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
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
@ -78,8 +80,10 @@ class CallScreenPresenter @AssistedInject constructor(
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) } val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) } val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
var isJoinedCall by rememberSaveable { mutableStateOf(false) } var isJoinedCall by rememberSaveable { mutableStateOf(false) }
var canRenderWebViewInCaseOfError by rememberSaveable { mutableStateOf(false) }
val languageTag = languageTagProvider.provideLanguageTag() val languageTag = languageTagProvider.provideLanguageTag()
val theme = if (ElementTheme.isLightTheme) "light" else "dark" val theme = if (ElementTheme.isLightTheme) "light" else "dark"
val errorMessage = stringResource(id = CommonStrings.error_unknown)
DisposableEffect(Unit) { DisposableEffect(Unit) {
coroutineScope.launch { coroutineScope.launch {
// Sets the call as joined // Sets the call as joined
@ -125,6 +129,8 @@ class CallScreenPresenter @AssistedInject constructor(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
interceptor.interceptedMessages interceptor.interceptedMessages
.onEach { .onEach {
// We are receiving messages from the WebView, consider that the application is loaded
canRenderWebViewInCaseOfError = true
// Relay message to Widget Driver // Relay message to Widget Driver
callWidgetDriver.value?.send(it) callWidgetDriver.value?.send(it)
@ -163,11 +169,25 @@ class CallScreenPresenter @AssistedInject constructor(
is CallScreenEvents.SetupMessageChannels -> { is CallScreenEvents.SetupMessageChannels -> {
messageInterceptor.value = event.widgetMessageInterceptor 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( return CallScreenState(
urlState = urlState.value, urlState = urlState.value,
canRenderWebViewInCaseOfError = canRenderWebViewInCaseOfError,
userAgent = userAgent, userAgent = userAgent,
isInWidgetMode = isInWidgetMode, isInWidgetMode = isInWidgetMode,
eventSink = { handleEvents(it) }, eventSink = { handleEvents(it) },

View file

@ -11,6 +11,7 @@ import io.element.android.libraries.architecture.AsyncData
data class CallScreenState( data class CallScreenState(
val urlState: AsyncData<String>, val urlState: AsyncData<String>,
val canRenderWebViewInCaseOfError: Boolean,
val userAgent: String, val userAgent: String,
val isInWidgetMode: Boolean, val isInWidgetMode: Boolean,
val eventSink: (CallScreenEvents) -> Unit, val eventSink: (CallScreenEvents) -> Unit,

View file

@ -21,12 +21,14 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
internal fun aCallScreenState( internal fun aCallScreenState(
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"), urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
canRenderWebViewInCaseOfError: Boolean = true,
userAgent: String = "", userAgent: String = "",
isInWidgetMode: Boolean = false, isInWidgetMode: Boolean = false,
eventSink: (CallScreenEvents) -> Unit = {}, eventSink: (CallScreenEvents) -> Unit = {},
): CallScreenState { ): CallScreenState {
return CallScreenState( return CallScreenState(
urlState = urlState, urlState = urlState,
canRenderWebViewInCaseOfError = canRenderWebViewInCaseOfError,
userAgent = userAgent, userAgent = userAgent,
isInWidgetMode = isInWidgetMode, isInWidgetMode = isInWidgetMode,
eventSink = eventSink, eventSink = eventSink,

View file

@ -85,25 +85,30 @@ internal fun CallScreenView(
BackHandler { BackHandler {
handleBack() handleBack()
} }
CallWebView( if (state.urlState !is AsyncData.Failure || state.canRenderWebViewInCaseOfError) {
modifier = Modifier CallWebView(
modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
.fillMaxSize(), .fillMaxSize(),
url = state.urlState, url = state.urlState,
userAgent = state.userAgent, userAgent = state.userAgent,
onPermissionsRequest = { request -> onPermissionsRequest = { request ->
val androidPermissions = mapWebkitPermissions(request.resources) val androidPermissions = mapWebkitPermissions(request.resources)
val callback: RequestPermissionCallback = { request.grant(it) } val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback) requestPermissions(androidPermissions.toTypedArray(), callback)
}, },
onWebViewCreate = { webView -> onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView) val interceptor = WebViewWidgetMessageInterceptor(
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) webView = webView,
val pipController = WebViewPipController(webView) onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) )
} state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
) val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
}
)
}
when (state.urlState) { when (state.urlState) {
AsyncData.Uninitialized, AsyncData.Uninitialized,
is AsyncData.Loading -> is AsyncData.Loading ->

View file

@ -8,16 +8,23 @@
package io.element.android.features.call.impl.utils package io.element.android.features.call.impl.utils
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.http.SslError
import android.webkit.JavascriptInterface 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.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.webkit.WebViewCompat import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature import androidx.webkit.WebViewFeature
import io.element.android.features.call.impl.BuildConfig import io.element.android.features.call.impl.BuildConfig
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
class WebViewWidgetMessageInterceptor( class WebViewWidgetMessageInterceptor(
private val webView: WebView, private val webView: WebView,
private val onError: (String?) -> Unit,
) : WidgetMessageInterceptor { ) : WidgetMessageInterceptor {
companion object { companion object {
// We call both the WebMessageListener and the JavascriptInterface objects in JS with this // 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" if (message.data.response && message.data.api == "toWidget"
|| !message.data.response && message.data.api == "fromWidget") { || !message.data.response && message.data.api == "fromWidget") {
let json = JSON.stringify(event.data) 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); $LISTENER_NAME.postMessage(json);
} else { } 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(), """.trimIndent(),
null 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 // Create a WebMessageListener, which will receive messages from the WebView and reply to them