| @@ -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/" |