Handle no network error when starting Element Call.
This commit is contained in:
parent
7d3801a623
commit
0cea89edcc
6 changed files with 74 additions and 20 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue