ReviewOS

also looking at this

craft-native/craft

fix(android): recover remote loads after connectivity returns

#265
Merged glennmichael123 wants to merge codex/android-load-failure-fallback into main
4 files +130 -11
packages/android/templates/MainActivity.kt.templatemodified+71-1
Changes to packages/android/templates/MainActivity.kt.template
@@ -1,10 +1,14 @@
11package
22
33import android.annotation.SuppressLint
4import android.content.Context
45import android.os.Bundle
56import android.content.Intent
67import android.graphics.Bitmap
8import android.net.ConnectivityManager
9import android.net.Network
710import android.net.Uri
11import android.util.Log
812import android.webkit.WebResourceRequest
913import android.webkit.WebResourceError
1014import android.webkit.WebResourceResponse
@@ -19,6 +23,20 @@ import org.json.JSONObject
1923import java.io.FileNotFoundException
2024import java.net.URLConnection
2125
26/** Main-frame failures for which the bundled app is an honest fallback (#254). */
27object CraftLoadFailure {
28 fun isUnreachable(errorCode: Int): Boolean = when (errorCode) {
29 WebViewClient.ERROR_HOST_LOOKUP,
30 WebViewClient.ERROR_CONNECT,
31 WebViewClient.ERROR_TIMEOUT,
32 // Android uses ERROR_IO for connection interruptions as well as local
33 // I/O failures. Keep it here deliberately: it is the closest Android
34 // equivalent to iOS's NSURLErrorNetworkConnectionLost.
35 WebViewClient.ERROR_IO -> true
36 else -> false
37 }
38}
39
2240class MainActivity : AppCompatActivity() {
2341 private lateinit var webView: WebView
2442 private lateinit var craftBridge: CraftBridge
@@ -32,6 +50,28 @@ class MainActivity : AppCompatActivity() {
3250 .build()
3351 }
3452 private var loadedBundledFallback = false
53 private lateinit var connectivityManager: ConnectivityManager
54 @Volatile
55 private var isConnected = false
56 private var networkMonitoringRegistered = false
57 private val networkCallback = object : ConnectivityManager.NetworkCallback() {
58 override fun onAvailable(network: Network) {
59 val wasConnected = isConnected
60 isConnected = true
61 if (!wasConnected && isConnected) {
62 runOnUiThread {
63 returnFromBundledFallback("the network came back")
64 }
65 }
66 }
67
68 override fun onLost(network: Network) {
69 // A second transport can already be the default by the time the
70 // old one reports its loss, so read current state instead of
71 // blindly turning the latch off.
72 isConnected = connectivityManager.activeNetwork != null
73 }
74 }
3575
3676 @SuppressLint("SetJavaScriptEnabled")
3777 override fun onCreate(savedInstanceState: Bundle?) {
@@ -68,6 +108,7 @@ class MainActivity : AppCompatActivity() {
68108
69109 setupWebView()
70110 loadContent()
111 setupNetworkMonitoring()
71112 }
72113
73114 @SuppressLint("SetJavaScriptEnabled")
@@ -113,7 +154,13 @@ class MainActivity : AppCompatActivity() {
113154
114155 override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
115156 super.onReceivedError(view, request, error)
116 if (request?.isForMainFrame == true && hasBundledFallback && !loadedBundledFallback) {
157 if (
158 request?.isForMainFrame == true &&
159 error != null &&
160 CraftLoadFailure.isUnreachable(error.errorCode) &&
161 hasBundledFallback &&
162 !loadedBundledFallback
163 ) {
117164 loadedBundledFallback = true
118165 webView.loadUrl(BUNDLED_APP_URL)
119166 }
@@ -126,6 +173,23 @@ class MainActivity : AppCompatActivity() {
126173 webView.addJavascriptInterface(craftBridge, "CraftAndroid")
127174 }
128175
176 private fun setupNetworkMonitoring() {
177 if (!hasBundledFallback || devServerUrl == null) return
178 connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
179 isConnected = connectivityManager.activeNetwork != null
180 connectivityManager.registerDefaultNetworkCallback(networkCallback)
181 networkMonitoringRegistered = true
182 }
183
184 /** Leave the bundled copy for the configured remote origin again. */
185 private fun returnFromBundledFallback(reason: String) {
186 val remoteUrl = devServerUrl ?: return
187 if (!loadedBundledFallback) return
188 loadedBundledFallback = false
189 Log.i(TAG, "$reason; loading $remoteUrl again instead of the bundled copy")
190 webView.loadUrl(remoteUrl)
191 }
192
129193 private fun loadContent() {
130194 // Check for dev server URL in config
131195 val remoteUrl = devServerUrl
@@ -234,6 +298,7 @@ class MainActivity : AppCompatActivity() {
234298 override fun onResume() {
235299 super.onResume()
236300 webView.onResume()
301 returnFromBundledFallback("the app returned to the foreground")
237302 }
238303
239304 override fun onPause() {
@@ -242,6 +307,10 @@ class MainActivity : AppCompatActivity() {
242307 }
243308
244309 override fun onDestroy() {
310 if (networkMonitoringRegistered) {
311 connectivityManager.unregisterNetworkCallback(networkCallback)
312 networkMonitoringRegistered = false
313 }
245314 craftBridge.close()
246315 webView.destroy()
247316 super.onDestroy()
@@ -263,6 +332,7 @@ class MainActivity : AppCompatActivity() {
263332 }
264333
265334 companion object {
335 private const val TAG = "CraftMainActivity"
266336 private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
267337 private const val BUNDLED_APP_ORIGIN = "https://appassets.androidplatform.net"
268338 private const val BUNDLED_APP_URL = "$BUNDLED_APP_ORIGIN/"