Skip to content

Commit 2839535

Browse files
feat: App Level WebView/No Internet Error Handling (#392)
Co-authored-by: Farhan Arshad <[email protected]>
1 parent 190dd87 commit 2839535

File tree

20 files changed

+402
-182
lines changed

20 files changed

+402
-182
lines changed

core/src/main/java/org/openedx/core/extension/StringExt.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.openedx.core.extension
22

33
import android.util.Patterns
4+
import java.net.URL
45
import java.util.Locale
56
import java.util.regex.Pattern
67

@@ -38,6 +39,14 @@ fun String.takeIfNotEmpty(): String? {
3839
return if (this.isEmpty().not()) this else null
3940
}
4041

42+
fun String?.equalsHost(host: String?): Boolean {
43+
return try {
44+
host?.startsWith(URL(this).host, ignoreCase = true) == true
45+
} catch (e: Exception) {
46+
false
47+
}
48+
}
49+
4150
fun String.toImageLink(apiHostURL: String): String =
4251
if (this.isLinkValid()) {
4352
this
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.openedx.core.presentation.global
2+
3+
import org.openedx.core.R
4+
5+
enum class ErrorType(
6+
val iconResId: Int = 0,
7+
val titleResId: Int = 0,
8+
val descriptionResId: Int = 0,
9+
val actionResId: Int = 0,
10+
) {
11+
CONNECTION_ERROR(
12+
iconResId = R.drawable.core_no_internet_connection,
13+
titleResId = R.string.core_no_internet_connection,
14+
descriptionResId = R.string.core_no_internet_connection_description,
15+
actionResId = R.string.core_reload,
16+
),
17+
UNKNOWN_ERROR(
18+
iconResId = R.drawable.core_ic_unknown_error,
19+
titleResId = R.string.core_try_again,
20+
descriptionResId = R.string.core_something_went_wrong_description,
21+
actionResId = R.string.core_reload,
22+
),
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.openedx.core.presentation.global.webview
2+
3+
import org.openedx.core.presentation.global.ErrorType
4+
5+
sealed class WebViewUIState {
6+
data object Loading : WebViewUIState()
7+
data object Loaded : WebViewUIState()
8+
data class Error(val errorType: ErrorType) : WebViewUIState()
9+
}
10+
11+
enum class WebViewUIAction {
12+
WEB_PAGE_LOADED,
13+
WEB_PAGE_ERROR,
14+
RELOAD_WEB_PAGE
15+
}

core/src/main/java/org/openedx/core/ui/ComposeCommon.kt

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import org.openedx.core.domain.model.RegistrationField
108108
import org.openedx.core.extension.LinkedImageText
109109
import org.openedx.core.extension.tagId
110110
import org.openedx.core.extension.toastMessage
111+
import org.openedx.core.presentation.global.ErrorType
111112
import org.openedx.core.ui.theme.OpenEdXTheme
112113
import org.openedx.core.ui.theme.appColors
113114
import org.openedx.core.ui.theme.appShapes
@@ -1133,33 +1134,41 @@ fun BackBtn(
11331134
}
11341135

11351136
@Composable
1136-
fun ConnectionErrorView(
1137-
modifier: Modifier,
1138-
onReloadClick: () -> Unit,
1137+
fun ConnectionErrorView(onReloadClick: () -> Unit) {
1138+
FullScreenErrorView(errorType = ErrorType.CONNECTION_ERROR, onReloadClick = onReloadClick)
1139+
}
1140+
1141+
@Composable
1142+
fun FullScreenErrorView(
1143+
modifier: Modifier = Modifier,
1144+
errorType: ErrorType,
1145+
onReloadClick: () -> Unit
11391146
) {
11401147
Column(
1141-
modifier = modifier,
1148+
modifier = modifier
1149+
.fillMaxSize()
1150+
.background(MaterialTheme.appColors.background),
11421151
verticalArrangement = Arrangement.Center,
11431152
horizontalAlignment = Alignment.CenterHorizontally
11441153
) {
11451154
Icon(
11461155
modifier = Modifier.size(100.dp),
1147-
painter = painterResource(id = R.drawable.core_no_internet_connection),
1156+
painter = painterResource(id = errorType.iconResId),
11481157
contentDescription = null,
11491158
tint = MaterialTheme.appColors.onSurface
11501159
)
11511160
Spacer(Modifier.height(28.dp))
11521161
Text(
11531162
modifier = Modifier.fillMaxWidth(0.8f),
1154-
text = stringResource(id = R.string.core_no_internet_connection),
1163+
text = stringResource(id = errorType.titleResId),
11551164
color = MaterialTheme.appColors.textPrimary,
11561165
style = MaterialTheme.appTypography.titleLarge,
11571166
textAlign = TextAlign.Center
11581167
)
11591168
Spacer(Modifier.height(16.dp))
11601169
Text(
11611170
modifier = Modifier.fillMaxWidth(0.8f),
1162-
text = stringResource(id = R.string.core_no_internet_connection_description),
1171+
text = stringResource(id = errorType.descriptionResId),
11631172
color = MaterialTheme.appColors.textPrimary,
11641173
style = MaterialTheme.appTypography.bodyLarge,
11651174
textAlign = TextAlign.Center
@@ -1168,7 +1177,7 @@ fun ConnectionErrorView(
11681177
OpenEdXButton(
11691178
modifier = Modifier
11701179
.widthIn(Dp.Unspecified, 162.dp),
1171-
text = stringResource(id = R.string.core_reload),
1180+
text = stringResource(id = errorType.actionResId),
11721181
textColor = MaterialTheme.appColors.primaryButtonText,
11731182
backgroundColor = MaterialTheme.appColors.secondaryButtonBackground,
11741183
onClick = onReloadClick,
@@ -1369,11 +1378,7 @@ private fun IconTextPreview() {
13691378
@Composable
13701379
private fun ConnectionErrorViewPreview() {
13711380
OpenEdXTheme(darkTheme = true) {
1372-
ConnectionErrorView(
1373-
modifier = Modifier
1374-
.fillMaxSize(),
1375-
onReloadClick = {}
1376-
)
1381+
ConnectionErrorView(onReloadClick = {})
13771382
}
13781383
}
13791384

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="86dp"
3+
android:height="86dp"
4+
android:viewportWidth="100"
5+
android:viewportHeight="100">
6+
7+
<path
8+
android:fillColor="#FFFFFF"
9+
android:pathData="M85,61.35V20.67C85,18.09 82.91,16 80.33,16H19.67C17.09,16 15,18.09 15,20.67V61.35C15,62 15.52,62.52 16.17,62.52H83.83C84.48,62.52 85,62 85,61.35ZM55.74,34.47C55.28,34.01 55.29,33.25 55.77,32.8C56.23,32.36 56.97,32.4 57.43,32.86L58.44,33.87C58.67,34.1 59.04,34.1 59.26,33.87L60.28,32.86C60.73,32.4 61.47,32.36 61.93,32.8C62.42,33.25 62.42,34.01 61.96,34.47L60.91,35.52C60.69,35.75 60.69,36.11 60.91,36.34L61.93,37.36C62.68,38.11 62.19,39.38 61.14,39.38C60.84,39.38 60.54,39.27 60.31,39.04L59.26,37.99C59.04,37.76 58.67,37.76 58.44,37.99L57.39,39.04C56.92,39.52 56.13,39.49 55.68,38.98C55.28,38.51 55.33,37.8 55.77,37.36L56.79,36.34C57.02,36.11 57.02,35.75 56.79,35.52L55.74,34.47ZM59.67,50.9C60.41,51.63 59.89,52.9 58.85,52.9C58.56,52.9 58.26,52.78 58.03,52.56C54.93,49.52 51.1,49.28 50,49.28C45.65,49.28 42.75,51.79 41.97,52.56C41.51,53.01 40.77,53.01 40.32,52.55C39.86,52.09 39.87,51.35 40.33,50.9C45.81,45.52 54.43,45.75 59.67,50.9ZM38.04,34.47C37.58,34.01 37.58,33.25 38.07,32.8C38.53,32.36 39.27,32.4 39.72,32.86L40.74,33.87C40.96,34.1 41.33,34.1 41.56,33.87L42.57,32.86C43.02,32.4 43.77,32.36 44.23,32.8C44.71,33.25 44.72,34.01 44.26,34.47L43.21,35.52C42.98,35.75 42.98,36.11 43.21,36.34L44.23,37.36C44.98,38.11 44.49,39.38 43.43,39.38C43.13,39.38 42.84,39.27 42.61,39.04L41.56,37.99C41.33,37.76 40.96,37.76 40.74,37.99L39.69,39.04C39.21,39.52 38.43,39.49 37.98,38.98C37.58,38.51 37.63,37.8 38.07,37.36L39.09,36.34C39.31,36.11 39.31,35.75 39.09,35.52L38.04,34.47Z" />
10+
11+
<path
12+
android:fillColor="#FFFFFF"
13+
android:pathData="M15,66.02V67.48C15,70.06 17.09,72.15 19.67,72.15H80.33C82.88,72.15 84.96,70.09 85,67.55V66.02C85,65.38 84.48,64.85 83.83,64.85H16.17C15.52,64.85 15,65.38 15,66.02Z" />
14+
15+
<path
16+
android:fillColor="#FFFFFF"
17+
android:pathData="M32.55,83.28C32.36,82.67 32.71,82.01 33.32,81.82C43.55,78.64 56.47,78.65 66.68,81.82C67.3,82.01 67.64,82.67 67.45,83.28C67.26,83.9 66.6,84.24 65.99,84.05C56.28,81.03 43.72,81.03 34.01,84.05C33.4,84.24 32.74,83.9 32.55,83.28Z" />
18+
19+
<path
20+
android:fillColor="#FFFFFF"
21+
android:pathData="M56.92,77.47C52.43,76.98 47.67,76.97 43.08,77.47C42.26,77.56 41.61,76.79 41.83,75.99L42.02,75.33C42.16,74.83 42.62,74.48 43.14,74.48H56.86C57.38,74.48 57.84,74.83 57.98,75.33L58.17,75.99C58.39,76.79 57.74,77.56 56.92,77.47Z" />
22+
23+
</vector>

core/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
<string name="core_thank_you_dialog_negative_description">We received your feedback and will use it to help improve your learning experience going forward. Thank you for sharing!</string>
7676
<string name="core_no_internet_connection">No internet connection</string>
7777
<string name="core_no_internet_connection_description">Please connect to the internet to view this content.</string>
78+
<string name="core_try_again">Try Again</string>
79+
<string name="core_something_went_wrong_description">Something went wrong</string>
7880
<string name="core_ok" tools:ignore="MissingTranslation">OK</string>
7981
<string name="core_continue" tools:ignore="MissingTranslation">Continue</string>
8082
<string name="core_leaving_the_app" tools:ignore="MissingTranslation">Leaving the app</string>

course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.util.Log
1010
import android.view.LayoutInflater
1111
import android.view.ViewGroup
1212
import android.webkit.JavascriptInterface
13+
import android.webkit.WebResourceError
1314
import android.webkit.WebResourceRequest
1415
import android.webkit.WebResourceResponse
1516
import android.webkit.WebSettings
@@ -18,7 +19,6 @@ import android.webkit.WebViewClient
1819
import androidx.compose.foundation.background
1920
import androidx.compose.foundation.isSystemInDarkTheme
2021
import androidx.compose.foundation.layout.Box
21-
import androidx.compose.foundation.layout.fillMaxHeight
2222
import androidx.compose.foundation.layout.fillMaxSize
2323
import androidx.compose.foundation.layout.fillMaxWidth
2424
import androidx.compose.foundation.layout.padding
@@ -53,10 +53,11 @@ import kotlinx.coroutines.launch
5353
import org.koin.androidx.viewmodel.ext.android.viewModel
5454
import org.koin.core.parameter.parametersOf
5555
import org.openedx.core.extension.applyDarkModeIfEnabled
56+
import org.openedx.core.extension.equalsHost
5657
import org.openedx.core.extension.isEmailValid
5758
import org.openedx.core.extension.loadUrl
5859
import org.openedx.core.system.AppCookieManager
59-
import org.openedx.core.ui.ConnectionErrorView
60+
import org.openedx.core.ui.FullScreenErrorView
6061
import org.openedx.core.ui.WindowSize
6162
import org.openedx.core.ui.rememberWindowSize
6263
import org.openedx.core.ui.roundBorderWithoutBottom
@@ -96,10 +97,6 @@ class HtmlUnitFragment : Fragment() {
9697
OpenEdXTheme {
9798
val windowSize = rememberWindowSize()
9899

99-
var isLoading by remember {
100-
mutableStateOf(true)
101-
}
102-
103100
var hasInternetConnection by remember {
104101
mutableStateOf(viewModel.isOnline)
105102
}
@@ -148,49 +145,54 @@ class HtmlUnitFragment : Fragment() {
148145
.then(border),
149146
contentAlignment = Alignment.TopCenter
150147
) {
151-
if (uiState.isLoadingEnabled) {
148+
if (uiState is HtmlUnitUIState.Initialization) return@Box
149+
if ((uiState is HtmlUnitUIState.Error).not()) {
152150
if (hasInternetConnection || fromDownloadedContent) {
153151
HTMLContentView(
154152
uiState = uiState,
155153
windowSize = windowSize,
156154
url = url,
157155
cookieManager = viewModel.cookieManager,
158156
apiHostURL = viewModel.apiHostURL,
159-
isLoading = isLoading,
157+
isLoading = uiState is HtmlUnitUIState.Loading,
160158
injectJSList = injectJSList,
161159
onCompletionSet = {
162160
viewModel.notifyCompletionSet()
163161
},
164162
onWebPageLoading = {
165-
isLoading = true
163+
viewModel.onWebPageLoading()
166164
},
167165
onWebPageLoaded = {
168-
isLoading = false
166+
if ((uiState is HtmlUnitUIState.Error).not()) {
167+
viewModel.onWebPageLoaded()
168+
}
169169
if (isAdded) viewModel.setWebPageLoaded(requireContext().assets)
170170
},
171+
onWebPageLoadError = {
172+
viewModel.onWebPageLoadError()
173+
},
171174
saveXBlockProgress = { jsonProgress ->
172175
viewModel.saveXBlockProgress(jsonProgress)
173176
},
174177
)
175178
} else {
176-
ConnectionErrorView(
177-
modifier = Modifier
178-
.fillMaxWidth()
179-
.fillMaxHeight()
180-
.background(MaterialTheme.appColors.background)
181-
) {
182-
hasInternetConnection = viewModel.isOnline
183-
}
179+
viewModel.onWebPageLoadError()
184180
}
185-
if (isLoading && hasInternetConnection) {
186-
Box(
187-
modifier = Modifier
188-
.fillMaxSize()
189-
.zIndex(1f),
190-
contentAlignment = Alignment.Center
191-
) {
192-
CircularProgressIndicator(color = MaterialTheme.appColors.primary)
193-
}
181+
} else {
182+
val errorType = (uiState as HtmlUnitUIState.Error).errorType
183+
FullScreenErrorView(errorType = errorType) {
184+
hasInternetConnection = viewModel.isOnline
185+
viewModel.onWebPageLoading()
186+
}
187+
}
188+
if (uiState is HtmlUnitUIState.Loading && hasInternetConnection) {
189+
Box(
190+
modifier = Modifier
191+
.fillMaxSize()
192+
.zIndex(1f),
193+
contentAlignment = Alignment.Center
194+
) {
195+
CircularProgressIndicator(color = MaterialTheme.appColors.primary)
194196
}
195197
}
196198
}
@@ -239,7 +241,8 @@ private fun HTMLContentView(
239241
onCompletionSet: () -> Unit,
240242
onWebPageLoading: () -> Unit,
241243
onWebPageLoaded: () -> Unit,
242-
saveXBlockProgress: (String) -> Unit
244+
onWebPageLoadError: () -> Unit,
245+
saveXBlockProgress: (String) -> Unit,
243246
) {
244247
val coroutineScope = rememberCoroutineScope()
245248
val context = LocalContext.current
@@ -333,6 +336,17 @@ private fun HTMLContentView(
333336
}
334337
super.onReceivedHttpError(view, request, errorResponse)
335338
}
339+
340+
override fun onReceivedError(
341+
view: WebView,
342+
request: WebResourceRequest,
343+
error: WebResourceError
344+
) {
345+
if (view.url.equalsHost(request.url.host)) {
346+
onWebPageLoadError()
347+
}
348+
super.onReceivedError(view, request, error)
349+
}
336350
}
337351
with(settings) {
338352
javaScriptEnabled = true
@@ -356,7 +370,7 @@ private fun HTMLContentView(
356370
update = { webView ->
357371
if (!isLoading && injectJSList.isNotEmpty()) {
358372
injectJSList.forEach { webView.evaluateJavascript(it, null) }
359-
val jsonProgress = uiState.jsonProgress
373+
val jsonProgress = (uiState as? HtmlUnitUIState.Loaded)?.jsonProgress
360374
if (!jsonProgress.isNullOrEmpty()) {
361375
webView.setupOfflineProgress(jsonProgress)
362376
}
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package org.openedx.course.presentation.unit.html
22

3-
data class HtmlUnitUIState(
4-
val jsonProgress: String?,
5-
val isLoadingEnabled: Boolean
6-
)
3+
import org.openedx.core.presentation.global.ErrorType
4+
5+
sealed class HtmlUnitUIState {
6+
data object Initialization : HtmlUnitUIState()
7+
data object Loading : HtmlUnitUIState()
8+
data class Loaded(val jsonProgress: String? = null) : HtmlUnitUIState()
9+
data class Error(val errorType: ErrorType) : HtmlUnitUIState()
10+
}

0 commit comments

Comments
 (0)