@@ -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 ⋯ 8 lines Show the 8 lines above this hunk @@ -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 ⋯ 7 lines Show the 7 lines above this hunk @@ -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?) { ⋯ 30 lines Show the 30 lines above this hunk @@ -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") ⋯ 39 lines Show the 39 lines above this hunk @@ -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 } ⋯ 6 lines Show the 6 lines above this hunk @@ -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 ⋯ 102 lines Show the 102 lines above this hunk @@ -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() { ⋯ 2 lines Show the 2 lines above this hunk @@ -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() ⋯ 15 lines Show the 15 lines above this hunk @@ -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/"