also looking at this
fix(android): recover remote loads after connectivity returns
#265
4 files
+130
-11
| @@ -1,10 +1,14 @@ | ||
| 1 | 1 | package |
| 2 | 2 | |
| 3 | 3 | import android.annotation.SuppressLint |
| 4 | import android.content.Context | |
| 4 | 5 | import android.os.Bundle |
| 5 | 6 | import android.content.Intent |
| 6 | 7 | import android.graphics.Bitmap |
| 8 | import android.net.ConnectivityManager | |
| 9 | import android.net.Network | |
| 7 | 10 | import android.net.Uri |
| 11 | import android.util.Log | |
| 8 | 12 | import android.webkit.WebResourceRequest |
| 9 | 13 | import android.webkit.WebResourceError |
| 10 | 14 | import android.webkit.WebResourceResponse |
| @@ -19,6 +23,20 @@ import org.json.JSONObject | ||
| 19 | 23 | import java.io.FileNotFoundException |
| 20 | 24 | import java.net.URLConnection |
| 21 | 25 | |
| 26 | /** Main-frame failures for which the bundled app is an honest fallback (#254). */ | |
| 27 | object 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 | ||
| 22 | 40 | class MainActivity : AppCompatActivity() { |
| 23 | 41 | private lateinit var webView: WebView |
| 24 | 42 | private lateinit var craftBridge: CraftBridge |
| @@ -32,6 +50,28 @@ class MainActivity : AppCompatActivity() { | ||
| 32 | 50 | .build() |
| 33 | 51 | } |
| 34 | 52 | 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 | } | |
| 35 | 75 | |
| 36 | 76 | @SuppressLint("SetJavaScriptEnabled") |
| 37 | 77 | override fun onCreate(savedInstanceState: Bundle?) { |
| @@ -68,6 +108,7 @@ class MainActivity : AppCompatActivity() { | ||
| 68 | 108 | |
| 69 | 109 | setupWebView() |
| 70 | 110 | loadContent() |
| 111 | setupNetworkMonitoring() | |
| 71 | 112 | } |
| 72 | 113 | |
| 73 | 114 | @SuppressLint("SetJavaScriptEnabled") |
| @@ -113,7 +154,13 @@ class MainActivity : AppCompatActivity() { | ||
| 113 | 154 | |
| 114 | 155 | override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { |
| 115 | 156 | 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 | ) { | |
| 117 | 164 | loadedBundledFallback = true |
| 118 | 165 | webView.loadUrl(BUNDLED_APP_URL) |
| 119 | 166 | } |
| @@ -126,6 +173,23 @@ class MainActivity : AppCompatActivity() { | ||
| 126 | 173 | webView.addJavascriptInterface(craftBridge, "CraftAndroid") |
| 127 | 174 | } |
| 128 | 175 | |
| 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 | ||
| 129 | 193 | private fun loadContent() { |
| 130 | 194 | // Check for dev server URL in config |
| 131 | 195 | val remoteUrl = devServerUrl |
| @@ -234,6 +298,7 @@ class MainActivity : AppCompatActivity() { | ||
| 234 | 298 | override fun onResume() { |
| 235 | 299 | super.onResume() |
| 236 | 300 | webView.onResume() |
| 301 | returnFromBundledFallback("the app returned to the foreground") | |
| 237 | 302 | } |
| 238 | 303 | |
| 239 | 304 | override fun onPause() { |
| @@ -242,6 +307,10 @@ class MainActivity : AppCompatActivity() { | ||
| 242 | 307 | } |
| 243 | 308 | |
| 244 | 309 | override fun onDestroy() { |
| 310 | if (networkMonitoringRegistered) { | |
| 311 | connectivityManager.unregisterNetworkCallback(networkCallback) | |
| 312 | networkMonitoringRegistered = false | |
| 313 | } | |
| 245 | 314 | craftBridge.close() |
| 246 | 315 | webView.destroy() |
| 247 | 316 | super.onDestroy() |
| @@ -263,6 +332,7 @@ class MainActivity : AppCompatActivity() { | ||
| 263 | 332 | } |
| 264 | 333 | |
| 265 | 334 | companion object { |
| 335 | private const val TAG = "CraftMainActivity" | |
| 266 | 336 | private const val BUNDLED_APP_HOST = "appassets.androidplatform.net" |
| 267 | 337 | private const val BUNDLED_APP_ORIGIN = "https://appassets.androidplatform.net" |
| 268 | 338 | private const val BUNDLED_APP_URL = "$BUNDLED_APP_ORIGIN/" |