Skip to content

Commit c5f5ff3

Browse files
authored
Merge pull request #3527 from element-hq/feature/bma/elementCallNoNetwork
Handle no network error when starting Element Call.
2 parents 2ce8bb7 + 6ce19f8 commit c5f5ff3

File tree

9 files changed

+132
-29
lines changed

9 files changed

+132
-29
lines changed

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
1111

1212
sealed interface CallScreenEvents {
1313
data object Hangup : CallScreenEvents
14-
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) :
15-
CallScreenEvents
14+
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
15+
data class OnWebViewError(val description: String?) : CallScreenEvents
1616
}

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ class CallScreenPresenter @AssistedInject constructor(
7878
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
7979
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
8080
var isJoinedCall by rememberSaveable { mutableStateOf(false) }
81+
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
82+
var webViewError by remember { mutableStateOf<String?>(null) }
8183
val languageTag = languageTagProvider.provideLanguageTag()
8284
val theme = if (ElementTheme.isLightTheme) "light" else "dark"
8385
DisposableEffect(Unit) {
@@ -125,6 +127,8 @@ class CallScreenPresenter @AssistedInject constructor(
125127
LaunchedEffect(Unit) {
126128
interceptor.interceptedMessages
127129
.onEach {
130+
// We are receiving messages from the WebView, consider that the application is loaded
131+
ignoreWebViewError = true
128132
// Relay message to Widget Driver
129133
callWidgetDriver.value?.send(it)
130134

@@ -163,11 +167,18 @@ class CallScreenPresenter @AssistedInject constructor(
163167
is CallScreenEvents.SetupMessageChannels -> {
164168
messageInterceptor.value = event.widgetMessageInterceptor
165169
}
170+
is CallScreenEvents.OnWebViewError -> {
171+
if (!ignoreWebViewError) {
172+
webViewError = event.description.orEmpty()
173+
}
174+
// Else ignore the error, give a chance the Element Call to recover by itself.
175+
}
166176
}
167177
}
168178

169179
return CallScreenState(
170180
urlState = urlState.value,
181+
webViewError = webViewError,
171182
userAgent = userAgent,
172183
isInWidgetMode = isInWidgetMode,
173184
eventSink = { handleEvents(it) },

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.element.android.libraries.architecture.AsyncData
1111

1212
data class CallScreenState(
1313
val urlState: AsyncData<String>,
14+
val webViewError: String?,
1415
val userAgent: String,
1516
val isInWidgetMode: Boolean,
1617
val eventSink: (CallScreenEvents) -> Unit,

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
1616
aCallScreenState(),
1717
aCallScreenState(urlState = AsyncData.Loading()),
1818
aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))),
19+
aCallScreenState(webViewError = "Error details from WebView"),
1920
)
2021
}
2122

2223
internal fun aCallScreenState(
2324
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
25+
webViewError: String? = null,
2426
userAgent: String = "",
2527
isInWidgetMode: Boolean = false,
2628
eventSink: (CallScreenEvents) -> Unit = {},
2729
): CallScreenState {
2830
return CallScreenState(
2931
urlState = urlState,
32+
webViewError = webViewError,
3033
userAgent = userAgent,
3134
isInWidgetMode = isInWidgetMode,
3235
eventSink = eventSink,

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -85,35 +85,48 @@ internal fun CallScreenView(
8585
BackHandler {
8686
handleBack()
8787
}
88-
CallWebView(
89-
modifier = Modifier
88+
if (state.webViewError != null) {
89+
ErrorDialog(
90+
content = buildString {
91+
append(stringResource(CommonStrings.error_unknown))
92+
state.webViewError.takeIf { it.isNotEmpty() }?.let { append("\n\n").append(it) }
93+
},
94+
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
95+
)
96+
} else {
97+
CallWebView(
98+
modifier = Modifier
9099
.padding(padding)
91100
.consumeWindowInsets(padding)
92101
.fillMaxSize(),
93-
url = state.urlState,
94-
userAgent = state.userAgent,
95-
onPermissionsRequest = { request ->
96-
val androidPermissions = mapWebkitPermissions(request.resources)
97-
val callback: RequestPermissionCallback = { request.grant(it) }
98-
requestPermissions(androidPermissions.toTypedArray(), callback)
99-
},
100-
onWebViewCreate = { webView ->
101-
val interceptor = WebViewWidgetMessageInterceptor(webView)
102-
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
103-
val pipController = WebViewPipController(webView)
104-
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
102+
url = state.urlState,
103+
userAgent = state.userAgent,
104+
onPermissionsRequest = { request ->
105+
val androidPermissions = mapWebkitPermissions(request.resources)
106+
val callback: RequestPermissionCallback = { request.grant(it) }
107+
requestPermissions(androidPermissions.toTypedArray(), callback)
108+
},
109+
onWebViewCreate = { webView ->
110+
val interceptor = WebViewWidgetMessageInterceptor(
111+
webView = webView,
112+
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
113+
)
114+
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
115+
val pipController = WebViewPipController(webView)
116+
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
117+
}
118+
)
119+
when (state.urlState) {
120+
AsyncData.Uninitialized,
121+
is AsyncData.Loading ->
122+
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
123+
is AsyncData.Failure ->
124+
ErrorDialog(
125+
content = state.urlState.error.message.orEmpty(),
126+
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
127+
)
128+
is AsyncData.Success -> Unit
105129
}
106-
)
107-
when (state.urlState) {
108-
AsyncData.Uninitialized,
109-
is AsyncData.Loading ->
110-
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
111-
is AsyncData.Failure ->
112-
ErrorDialog(
113-
content = state.urlState.error.message.orEmpty(),
114-
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
115-
)
116-
is AsyncData.Success -> Unit
117130
}
118131
}
119132
}

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,23 @@
88
package io.element.android.features.call.impl.utils
99

1010
import android.graphics.Bitmap
11+
import android.net.http.SslError
1112
import android.webkit.JavascriptInterface
13+
import android.webkit.SslErrorHandler
14+
import android.webkit.WebResourceError
15+
import android.webkit.WebResourceRequest
16+
import android.webkit.WebResourceResponse
1217
import android.webkit.WebView
1318
import android.webkit.WebViewClient
1419
import androidx.webkit.WebViewCompat
1520
import androidx.webkit.WebViewFeature
1621
import io.element.android.features.call.impl.BuildConfig
1722
import kotlinx.coroutines.flow.MutableSharedFlow
23+
import timber.log.Timber
1824

1925
class WebViewWidgetMessageInterceptor(
2026
private val webView: WebView,
27+
private val onError: (String?) -> Unit,
2128
) : WidgetMessageInterceptor {
2229
companion object {
2330
// We call both the WebMessageListener and the JavascriptInterface objects in JS with this
@@ -45,16 +52,35 @@ class WebViewWidgetMessageInterceptor(
4552
if (message.data.response && message.data.api == "toWidget"
4653
|| !message.data.response && message.data.api == "fromWidget") {
4754
let json = JSON.stringify(event.data)
48-
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } }
55+
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG }}
4956
$LISTENER_NAME.postMessage(json);
5057
} else {
51-
${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } }
58+
${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG }}
5259
}
5360
});
5461
""".trimIndent(),
5562
null
5663
)
5764
}
65+
66+
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
67+
// No network for instance, transmit the error
68+
Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}")
69+
onError(error?.description?.toString())
70+
super.onReceivedError(view, request, error)
71+
}
72+
73+
override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) {
74+
Timber.e("onReceivedHttpError error: ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}")
75+
onError(errorResponse?.statusCode.toString())
76+
super.onReceivedHttpError(view, request, errorResponse)
77+
}
78+
79+
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
80+
Timber.e("onReceivedSslError error: ${error?.primaryError}")
81+
onError(error?.primaryError?.toString())
82+
super.onReceivedSslError(view, handler, error)
83+
}
5884
}
5985

6086
// Create a WebMessageListener, which will receive messages from the WebView and reply to them

features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class CallScreenPresenterTest {
7171
skipItems(1)
7272
val initialState = awaitItem()
7373
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
74+
assertThat(initialState.webViewError).isNull()
7475
assertThat(initialState.isInWidgetMode).isFalse()
7576
analyticsLambda.assertions().isNeverCalled()
7677
joinedCallLambda.assertions().isCalledOnce()
@@ -270,6 +271,48 @@ class CallScreenPresenterTest {
270271
assert(stopSyncLambda).isCalledOnce()
271272
}
272273

274+
@Test
275+
fun `present - error from WebView are updating the state`() = runTest {
276+
val presenter = createCallScreenPresenter(
277+
callType = CallType.ExternalUrl("https://call.element.io"),
278+
activeCallManager = FakeActiveCallManager(),
279+
)
280+
moleculeFlow(RecompositionMode.Immediate) {
281+
presenter.present()
282+
}.test {
283+
// Wait until the URL is loaded
284+
skipItems(1)
285+
val initialState = awaitItem()
286+
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
287+
val finalState = awaitItem()
288+
assertThat(finalState.webViewError).isEqualTo("A Webview error")
289+
}
290+
}
291+
292+
@Test
293+
fun `present - error from WebView are ignored if Element Call is loaded`() = runTest {
294+
val presenter = createCallScreenPresenter(
295+
callType = CallType.ExternalUrl("https://call.element.io"),
296+
activeCallManager = FakeActiveCallManager(),
297+
)
298+
moleculeFlow(RecompositionMode.Immediate) {
299+
presenter.present()
300+
}.test {
301+
// Wait until the URL is loaded
302+
skipItems(1)
303+
val initialState = awaitItem()
304+
305+
val messageInterceptor = FakeWidgetMessageInterceptor()
306+
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
307+
// Emit a message
308+
messageInterceptor.givenInterceptedMessage("A message")
309+
// WebView emits an error, but it will be ignored
310+
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
311+
val finalState = awaitItem()
312+
assertThat(finalState.webViewError).isNull()
313+
}
314+
}
315+
273316
private fun TestScope.createCallScreenPresenter(
274317
callType: CallType,
275318
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)