feat: Add network error handling
- error.html: User-friendly error page with retry button - ErrorHandler.kt: Utility for handling WebView errors - ErrorHandlerTest.kt: Unit tests for error handling Shows friendly error page on network failures instead of blank/broken page. Includes error code display and one-tap retry.
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Connection Error</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.error-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.retry-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 14px 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.retry-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
.retry-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.error-code {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="icon">📡</div>
|
||||||
|
<h1>Connection Problem</h1>
|
||||||
|
<p>Unable to load the page. Please check your internet connection and try again.</p>
|
||||||
|
<button class="retry-btn" onclick="window.location.reload()">
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
<div class="error-code" id="errorCode"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Parse error code from URL if present
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const code = params.get('code');
|
||||||
|
if (code) {
|
||||||
|
document.getElementById('errorCode').textContent = 'Error code: ' + code;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package farm.hobbs.kioskbrowser
|
||||||
|
|
||||||
|
import android.webkit.WebResourceError
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles WebView errors with user-friendly error pages.
|
||||||
|
*
|
||||||
|
* Usage in WebViewClient:
|
||||||
|
* ```
|
||||||
|
* override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
||||||
|
* ErrorHandler.handleError(view, request, error)
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
object ErrorHandler {
|
||||||
|
|
||||||
|
private const val ERROR_PAGE = "file:///android_asset/error.html"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network error codes that should show the error page
|
||||||
|
*/
|
||||||
|
private val NETWORK_ERRORS = setOf(
|
||||||
|
android.webkit.WebViewClient.ERROR_HOST_LOOKUP,
|
||||||
|
android.webkit.WebViewClient.ERROR_CONNECT,
|
||||||
|
android.webkit.WebViewClient.ERROR_TIMEOUT,
|
||||||
|
android.webkit.WebViewClient.ERROR_IO,
|
||||||
|
android.webkit.WebViewClient.ERROR_UNKNOWN
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle WebView errors - show error page for main frame network errors
|
||||||
|
*/
|
||||||
|
fun handleError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError?
|
||||||
|
): Boolean {
|
||||||
|
// Only handle main frame errors
|
||||||
|
if (request?.isForMainFrame != true) return false
|
||||||
|
|
||||||
|
val errorCode = error?.errorCode ?: return false
|
||||||
|
|
||||||
|
// Show error page for network errors
|
||||||
|
if (errorCode in NETWORK_ERRORS || errorCode < 0) {
|
||||||
|
view?.loadUrl("$ERROR_PAGE?code=$errorCode")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle HTTP errors (4xx, 5xx)
|
||||||
|
*/
|
||||||
|
fun handleHttpError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
statusCode: Int
|
||||||
|
): Boolean {
|
||||||
|
// Only handle main frame errors
|
||||||
|
if (request?.isForMainFrame != true) return false
|
||||||
|
|
||||||
|
// Show error page for server errors
|
||||||
|
if (statusCode >= 500) {
|
||||||
|
view?.loadUrl("$ERROR_PAGE?code=$statusCode")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current URL is the error page
|
||||||
|
*/
|
||||||
|
fun isErrorPage(url: String?): Boolean {
|
||||||
|
return url?.startsWith(ERROR_PAGE.substringBefore("?")) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package farm.hobbs.kioskbrowser
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for ErrorHandler utility
|
||||||
|
*/
|
||||||
|
class ErrorHandlerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isErrorPage returns true for error page URL`() {
|
||||||
|
assertTrue(ErrorHandler.isErrorPage("file:///android_asset/error.html"))
|
||||||
|
assertTrue(ErrorHandler.isErrorPage("file:///android_asset/error.html?code=-2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isErrorPage returns false for regular URLs`() {
|
||||||
|
assertFalse(ErrorHandler.isErrorPage("https://hobbs.farm"))
|
||||||
|
assertFalse(ErrorHandler.isErrorPage("https://google.com"))
|
||||||
|
assertFalse(ErrorHandler.isErrorPage(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isErrorPage returns false for similar but different URLs`() {
|
||||||
|
assertFalse(ErrorHandler.isErrorPage("file:///android_asset/other.html"))
|
||||||
|
assertFalse(ErrorHandler.isErrorPage("https://error.html"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user