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 {
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
}

View File

@@ -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<MatrixWidgetDriver?>(null) }
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(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) },

View File

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

View File

@@ -21,12 +21,14 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
internal fun aCallScreenState(
urlState: AsyncData<String> = 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,

View File

@@ -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 ->

View File

@@ -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