@craft-native/android 0.0.92 → 0.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,14 @@
1
1
  package {{PACKAGE_NAME}}
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?) {
@@ -46,21 +86,29 @@ class MainActivity : AppCompatActivity() {
46
86
  craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
47
87
  // The link that launched the app (#215). Queued until the page's bridge
48
88
  // is injected, then dispatched once with `initial: true`, so a page that
49
- // subscribes rather than calling getInitialURL still hears it. A
50
- // relaunch from Recents carries the old intent, and was not opened by it.
51
- // A recreated activity restores what the bridge saved: its intent may
52
- // be a later warm link that onNewIntent set.
89
+ // subscribes rather than calling getInitialURL still hears it.
90
+ //
91
+ // A relaunch from Recents carries the intent the app was first opened
92
+ // with. The person picked the task out of a list, not the link or the
93
+ // shortcut inside it, and neither is news the second time — so both
94
+ // are skipped, not just the link (#229).
95
+ //
96
+ // A recreated activity restores what the bridge saved rather than
97
+ // reading its intent, which onNewIntent may have replaced with a
98
+ // later warm link.
99
+ val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
53
100
  if (savedInstanceState == null) {
54
- val launchLink = intent.data?.toString()
55
- val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
56
- if (launchLink != null && !launchedFromHistory) craftBridge.receiveDeepLink(launchLink)
101
+ if (!launchedFromHistory) {
102
+ intent.data?.toString()?.let(craftBridge::receiveDeepLink)
103
+ handleIncomingShortcut(intent)
104
+ }
57
105
  } else {
58
106
  craftBridge.restoreDeepLinks(savedInstanceState)
59
107
  }
60
- if (savedInstanceState == null) handleIncomingShortcut(intent)
61
108
 
62
109
  setupWebView()
63
110
  loadContent()
111
+ setupNetworkMonitoring()
64
112
  }
65
113
 
66
114
  @SuppressLint("SetJavaScriptEnabled")
@@ -106,7 +154,13 @@ class MainActivity : AppCompatActivity() {
106
154
 
107
155
  override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
108
156
  super.onReceivedError(view, request, error)
109
- 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
+ ) {
110
164
  loadedBundledFallback = true
111
165
  webView.loadUrl(BUNDLED_APP_URL)
112
166
  }
@@ -119,6 +173,23 @@ class MainActivity : AppCompatActivity() {
119
173
  webView.addJavascriptInterface(craftBridge, "CraftAndroid")
120
174
  }
121
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
+
122
193
  private fun loadContent() {
123
194
  // Check for dev server URL in config
124
195
  val remoteUrl = devServerUrl
@@ -227,6 +298,7 @@ class MainActivity : AppCompatActivity() {
227
298
  override fun onResume() {
228
299
  super.onResume()
229
300
  webView.onResume()
301
+ returnFromBundledFallback("the app returned to the foreground")
230
302
  }
231
303
 
232
304
  override fun onPause() {
@@ -235,6 +307,10 @@ class MainActivity : AppCompatActivity() {
235
307
  }
236
308
 
237
309
  override fun onDestroy() {
310
+ if (networkMonitoringRegistered) {
311
+ connectivityManager.unregisterNetworkCallback(networkCallback)
312
+ networkMonitoringRegistered = false
313
+ }
238
314
  craftBridge.close()
239
315
  webView.destroy()
240
316
  super.onDestroy()
@@ -256,6 +332,7 @@ class MainActivity : AppCompatActivity() {
256
332
  }
257
333
 
258
334
  companion object {
335
+ private const val TAG = "CraftMainActivity"
259
336
  private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
260
337
  private const val BUNDLED_APP_ORIGIN = "https://appassets.androidplatform.net"
261
338
  private const val BUNDLED_APP_URL = "$BUNDLED_APP_ORIGIN/"
package/LICENSE.md DELETED
@@ -1,21 +0,0 @@
1
- # MIT License
2
-
3
- Copyright (c) 2025 Stacks.js
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.