Skip to content

Commit

Permalink
feat: App Level WebView/No Internet Error Handling (openedx#392)
Browse files Browse the repository at this point in the history
Co-authored-by: Farhan Arshad <[email protected]>
  • Loading branch information
dixidroid and farhan-arshad-dev authored Oct 31, 2024
1 parent 190dd87 commit 2839535
Show file tree
Hide file tree
Showing 20 changed files with 402 additions and 182 deletions.
9 changes: 9 additions & 0 deletions core/src/main/java/org/openedx/core/extension/StringExt.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.openedx.core.extension

import android.util.Patterns
import java.net.URL
import java.util.Locale
import java.util.regex.Pattern

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

fun String?.equalsHost(host: String?): Boolean {
return try {
host?.startsWith(URL(this).host, ignoreCase = true) == true
} catch (e: Exception) {
false
}
}

fun String.toImageLink(apiHostURL: String): String =
if (this.isLinkValid()) {
this
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.openedx.core.presentation.global

import org.openedx.core.R

enum class ErrorType(
val iconResId: Int = 0,
val titleResId: Int = 0,
val descriptionResId: Int = 0,
val actionResId: Int = 0,
) {
CONNECTION_ERROR(
iconResId = R.drawable.core_no_internet_connection,
titleResId = R.string.core_no_internet_connection,
descriptionResId = R.string.core_no_internet_connection_description,
actionResId = R.string.core_reload,
),
UNKNOWN_ERROR(
iconResId = R.drawable.core_ic_unknown_error,
titleResId = R.string.core_try_again,
descriptionResId = R.string.core_something_went_wrong_description,
actionResId = R.string.core_reload,
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.openedx.core.presentation.global.webview

import org.openedx.core.presentation.global.ErrorType

sealed class WebViewUIState {
data object Loading : WebViewUIState()
data object Loaded : WebViewUIState()
data class Error(val errorType: ErrorType) : WebViewUIState()
}

enum class WebViewUIAction {
WEB_PAGE_LOADED,
WEB_PAGE_ERROR,
RELOAD_WEB_PAGE
}
31 changes: 18 additions & 13 deletions core/src/main/java/org/openedx/core/ui/ComposeCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import org.openedx.core.domain.model.RegistrationField
import org.openedx.core.extension.LinkedImageText
import org.openedx.core.extension.tagId
import org.openedx.core.extension.toastMessage
import org.openedx.core.presentation.global.ErrorType
import org.openedx.core.ui.theme.OpenEdXTheme
import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appShapes
Expand Down Expand Up @@ -1133,33 +1134,41 @@ fun BackBtn(
}

@Composable
fun ConnectionErrorView(
modifier: Modifier,
onReloadClick: () -> Unit,
fun ConnectionErrorView(onReloadClick: () -> Unit) {
FullScreenErrorView(errorType = ErrorType.CONNECTION_ERROR, onReloadClick = onReloadClick)
}

@Composable
fun FullScreenErrorView(
modifier: Modifier = Modifier,
errorType: ErrorType,
onReloadClick: () -> Unit
) {
Column(
modifier = modifier,
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.appColors.background),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
modifier = Modifier.size(100.dp),
painter = painterResource(id = R.drawable.core_no_internet_connection),
painter = painterResource(id = errorType.iconResId),
contentDescription = null,
tint = MaterialTheme.appColors.onSurface
)
Spacer(Modifier.height(28.dp))
Text(
modifier = Modifier.fillMaxWidth(0.8f),
text = stringResource(id = R.string.core_no_internet_connection),
text = stringResource(id = errorType.titleResId),
color = MaterialTheme.appColors.textPrimary,
style = MaterialTheme.appTypography.titleLarge,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(16.dp))
Text(
modifier = Modifier.fillMaxWidth(0.8f),
text = stringResource(id = R.string.core_no_internet_connection_description),
text = stringResource(id = errorType.descriptionResId),
color = MaterialTheme.appColors.textPrimary,
style = MaterialTheme.appTypography.bodyLarge,
textAlign = TextAlign.Center
Expand All @@ -1168,7 +1177,7 @@ fun ConnectionErrorView(
OpenEdXButton(
modifier = Modifier
.widthIn(Dp.Unspecified, 162.dp),
text = stringResource(id = R.string.core_reload),
text = stringResource(id = errorType.actionResId),
textColor = MaterialTheme.appColors.primaryButtonText,
backgroundColor = MaterialTheme.appColors.secondaryButtonBackground,
onClick = onReloadClick,
Expand Down Expand Up @@ -1369,11 +1378,7 @@ private fun IconTextPreview() {
@Composable
private fun ConnectionErrorViewPreview() {
OpenEdXTheme(darkTheme = true) {
ConnectionErrorView(
modifier = Modifier
.fillMaxSize(),
onReloadClick = {}
)
ConnectionErrorView(onReloadClick = {})
}
}

Expand Down
23 changes: 23 additions & 0 deletions core/src/main/res/drawable/core_ic_unknown_error.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="86dp"
android:height="86dp"
android:viewportWidth="100"
android:viewportHeight="100">

<path
android:fillColor="#FFFFFF"
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" />

<path
android:fillColor="#FFFFFF"
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" />

<path
android:fillColor="#FFFFFF"
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" />

<path
android:fillColor="#FFFFFF"
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" />

</vector>
2 changes: 2 additions & 0 deletions core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
<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>
<string name="core_no_internet_connection">No internet connection</string>
<string name="core_no_internet_connection_description">Please connect to the internet to view this content.</string>
<string name="core_try_again">Try Again</string>
<string name="core_something_went_wrong_description">Something went wrong</string>
<string name="core_ok" tools:ignore="MissingTranslation">OK</string>
<string name="core_continue" tools:ignore="MissingTranslation">Continue</string>
<string name="core_leaving_the_app" tools:ignore="MissingTranslation">Leaving the app</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
Expand All @@ -18,7 +19,6 @@ import android.webkit.WebViewClient
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
Expand Down Expand Up @@ -53,10 +53,11 @@ import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.openedx.core.extension.applyDarkModeIfEnabled
import org.openedx.core.extension.equalsHost
import org.openedx.core.extension.isEmailValid
import org.openedx.core.extension.loadUrl
import org.openedx.core.system.AppCookieManager
import org.openedx.core.ui.ConnectionErrorView
import org.openedx.core.ui.FullScreenErrorView
import org.openedx.core.ui.WindowSize
import org.openedx.core.ui.rememberWindowSize
import org.openedx.core.ui.roundBorderWithoutBottom
Expand Down Expand Up @@ -96,10 +97,6 @@ class HtmlUnitFragment : Fragment() {
OpenEdXTheme {
val windowSize = rememberWindowSize()

var isLoading by remember {
mutableStateOf(true)
}

var hasInternetConnection by remember {
mutableStateOf(viewModel.isOnline)
}
Expand Down Expand Up @@ -148,49 +145,54 @@ class HtmlUnitFragment : Fragment() {
.then(border),
contentAlignment = Alignment.TopCenter
) {
if (uiState.isLoadingEnabled) {
if (uiState is HtmlUnitUIState.Initialization) return@Box
if ((uiState is HtmlUnitUIState.Error).not()) {
if (hasInternetConnection || fromDownloadedContent) {
HTMLContentView(
uiState = uiState,
windowSize = windowSize,
url = url,
cookieManager = viewModel.cookieManager,
apiHostURL = viewModel.apiHostURL,
isLoading = isLoading,
isLoading = uiState is HtmlUnitUIState.Loading,
injectJSList = injectJSList,
onCompletionSet = {
viewModel.notifyCompletionSet()
},
onWebPageLoading = {
isLoading = true
viewModel.onWebPageLoading()
},
onWebPageLoaded = {
isLoading = false
if ((uiState is HtmlUnitUIState.Error).not()) {
viewModel.onWebPageLoaded()
}
if (isAdded) viewModel.setWebPageLoaded(requireContext().assets)
},
onWebPageLoadError = {
viewModel.onWebPageLoadError()
},
saveXBlockProgress = { jsonProgress ->
viewModel.saveXBlockProgress(jsonProgress)
},
)
} else {
ConnectionErrorView(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(MaterialTheme.appColors.background)
) {
hasInternetConnection = viewModel.isOnline
}
viewModel.onWebPageLoadError()
}
if (isLoading && hasInternetConnection) {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.appColors.primary)
}
} else {
val errorType = (uiState as HtmlUnitUIState.Error).errorType
FullScreenErrorView(errorType = errorType) {
hasInternetConnection = viewModel.isOnline
viewModel.onWebPageLoading()
}
}
if (uiState is HtmlUnitUIState.Loading && hasInternetConnection) {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.appColors.primary)
}
}
}
Expand Down Expand Up @@ -239,7 +241,8 @@ private fun HTMLContentView(
onCompletionSet: () -> Unit,
onWebPageLoading: () -> Unit,
onWebPageLoaded: () -> Unit,
saveXBlockProgress: (String) -> Unit
onWebPageLoadError: () -> Unit,
saveXBlockProgress: (String) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
Expand Down Expand Up @@ -333,6 +336,17 @@ private fun HTMLContentView(
}
super.onReceivedHttpError(view, request, errorResponse)
}

override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
if (view.url.equalsHost(request.url.host)) {
onWebPageLoadError()
}
super.onReceivedError(view, request, error)
}
}
with(settings) {
javaScriptEnabled = true
Expand All @@ -356,7 +370,7 @@ private fun HTMLContentView(
update = { webView ->
if (!isLoading && injectJSList.isNotEmpty()) {
injectJSList.forEach { webView.evaluateJavascript(it, null) }
val jsonProgress = uiState.jsonProgress
val jsonProgress = (uiState as? HtmlUnitUIState.Loaded)?.jsonProgress
if (!jsonProgress.isNullOrEmpty()) {
webView.setupOfflineProgress(jsonProgress)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.openedx.course.presentation.unit.html

data class HtmlUnitUIState(
val jsonProgress: String?,
val isLoadingEnabled: Boolean
)
import org.openedx.core.presentation.global.ErrorType

sealed class HtmlUnitUIState {
data object Initialization : HtmlUnitUIState()
data object Loading : HtmlUnitUIState()
data class Loaded(val jsonProgress: String? = null) : HtmlUnitUIState()
data class Error(val errorType: ErrorType) : HtmlUnitUIState()
}
Loading

0 comments on commit 2839535

Please sign in to comment.