@craft-native/android 0.0.91 → 0.0.94
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.
- package/README.md +27 -1
- package/dist/index.d.ts +111 -0
- package/dist/index.js +702 -0
- package/dist/index.js.map +11 -0
- package/dist/promise-runtime.d.ts +3 -0
- package/package.json +1 -1
- package/templates/CraftBridge.kt.template +534 -103
- package/templates/CraftNative.kt.template +38 -13
- package/templates/MainActivity.kt.template +98 -10
- package/LICENSE.md +0 -21
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.craft.runtime
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.content.IntentSender
|
|
4
5
|
import com.google.android.gms.location.Priority
|
|
5
6
|
import com.google.android.gms.location.LocationServices
|
|
6
7
|
import com.google.android.gms.location.LocationResult
|
|
@@ -110,10 +111,24 @@ object CraftNative {
|
|
|
110
111
|
*/
|
|
111
112
|
val isAvailable: Boolean = try {
|
|
112
113
|
System.loadLibrary("craft")
|
|
114
|
+
// Said out loud, because the two outcomes below are otherwise
|
|
115
|
+
// indistinguishable from outside: a library that never shipped and a
|
|
116
|
+
// library that loaded and then failed to bind both end with the shim
|
|
117
|
+
// answering every call. Zig logs its own registration under the same
|
|
118
|
+
// tag, so the pair reads as a sequence.
|
|
119
|
+
android.util.Log.i("CraftNative", "craft: libcraft.so loaded")
|
|
113
120
|
true
|
|
114
121
|
} catch (e: UnsatisfiedLinkError) {
|
|
122
|
+
// The reason, not just the fact. This used to be a bare `false`, and
|
|
123
|
+
// the first time the runtime was genuinely shipped and genuinely
|
|
124
|
+
// failed to load, the only evidence anywhere was that nothing
|
|
125
|
+
// happened — no linker line, no exception, nothing in logcat at all.
|
|
126
|
+
// The message carries the dynamic linker's own complaint, which is the
|
|
127
|
+
// one thing that says *why*.
|
|
128
|
+
android.util.Log.w("CraftNative", "craft: libcraft.so did not load: ${e.message}")
|
|
115
129
|
false
|
|
116
130
|
} catch (e: SecurityException) {
|
|
131
|
+
android.util.Log.w("CraftNative", "craft: libcraft.so was refused: ${e.message}")
|
|
117
132
|
false
|
|
118
133
|
}
|
|
119
134
|
|
|
@@ -197,14 +212,20 @@ object CraftNative {
|
|
|
197
212
|
/**
|
|
198
213
|
* Run `script` in the page. Called from Zig, on any thread.
|
|
199
214
|
*
|
|
200
|
-
*
|
|
201
|
-
* WebView exists
|
|
202
|
-
*
|
|
203
|
-
*
|
|
215
|
+
* Returns whether a deliverer took it. False is every moment before the
|
|
216
|
+
* WebView exists and after it is torn down, and it used to be
|
|
217
|
+
* indistinguishable from a delivery: the script was dropped, Zig's caller
|
|
218
|
+
* reported success, and the page waited on a promise nothing would settle
|
|
219
|
+
* (#231). Zig counts a false, and says so once.
|
|
220
|
+
*
|
|
221
|
+
* Still no throw. A throw here would cross back into Zig as a pending
|
|
222
|
+
* exception on a thread with nowhere to report it; false says the same
|
|
223
|
+
* thing and the caller can act on it.
|
|
204
224
|
*/
|
|
205
225
|
@JvmStatic
|
|
206
|
-
fun deliver(script: String) {
|
|
207
|
-
|
|
226
|
+
fun deliver(script: String): Boolean {
|
|
227
|
+
val target = deliverer ?: return false
|
|
228
|
+
return runCatching { target(script); true }.getOrDefault(false)
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
private external fun nativeGetDeviceInfo(activity: Activity): String?
|
|
@@ -213,7 +234,7 @@ object CraftNative {
|
|
|
213
234
|
private external fun nativeClipboardRead(activity: Activity): String?
|
|
214
235
|
private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
|
|
215
236
|
private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
|
|
216
|
-
private external fun nativeShare(activity: Activity, text: String, title: String): Boolean
|
|
237
|
+
private external fun nativeShare(activity: Activity, text: String, title: String, chosen: IntentSender, requestCode: Int): Boolean
|
|
217
238
|
private external fun nativeOpenCamera(activity: Activity): Boolean
|
|
218
239
|
private external fun nativePickImage(activity: Activity): Boolean
|
|
219
240
|
private external fun nativePickFile(activity: Activity): Boolean
|
|
@@ -235,7 +256,7 @@ object CraftNative {
|
|
|
235
256
|
private external fun nativeResetDeepLinks(): Boolean
|
|
236
257
|
private external fun nativeSetInitialURL(url: String?): Boolean
|
|
237
258
|
private external fun nativeGetInitialURL(): Boolean
|
|
238
|
-
private external fun nativeDispatchDeepLink(url: String): Boolean
|
|
259
|
+
private external fun nativeDispatchDeepLink(url: String, initial: Boolean): Boolean
|
|
239
260
|
private external fun nativeScreenshotReady(bytes: ByteArray)
|
|
240
261
|
private external fun nativeScreenshotError(message: String)
|
|
241
262
|
private external fun nativeTakeScreenshot(activity: Activity, webView: WebView): Boolean
|
|
@@ -411,11 +432,15 @@ object CraftNative {
|
|
|
411
432
|
}
|
|
412
433
|
}
|
|
413
434
|
|
|
414
|
-
/**
|
|
415
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Returns whether Zig launched the share menu for a result; false falls
|
|
437
|
+
* through. `chosen` fires on a pick and `requestCode` routes the result
|
|
438
|
+
* back to CraftBridge, which settles the page's promise from the two.
|
|
439
|
+
*/
|
|
440
|
+
fun share(activity: Activity, text: String, title: String, chosen: IntentSender, requestCode: Int): Boolean {
|
|
416
441
|
if (!isAvailable) return false
|
|
417
442
|
return try {
|
|
418
|
-
nativeShare(activity, text, title)
|
|
443
|
+
nativeShare(activity, text, title, chosen, requestCode)
|
|
419
444
|
} catch (e: UnsatisfiedLinkError) {
|
|
420
445
|
false
|
|
421
446
|
}
|
|
@@ -697,9 +722,9 @@ object CraftNative {
|
|
|
697
722
|
return try { nativeGetInitialURL() } catch (e: UnsatisfiedLinkError) { false }
|
|
698
723
|
}
|
|
699
724
|
|
|
700
|
-
fun dispatchDeepLink(url: String): Boolean {
|
|
725
|
+
fun dispatchDeepLink(url: String, initial: Boolean): Boolean {
|
|
701
726
|
if (!isAvailable) return false
|
|
702
|
-
return try { nativeDispatchDeepLink(url) } catch (e: UnsatisfiedLinkError) { false }
|
|
727
|
+
return try { nativeDispatchDeepLink(url, initial) } catch (e: UnsatisfiedLinkError) { false }
|
|
703
728
|
}
|
|
704
729
|
|
|
705
730
|
@JvmStatic
|
|
@@ -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?) {
|
|
@@ -44,11 +84,31 @@ class MainActivity : AppCompatActivity() {
|
|
|
44
84
|
|
|
45
85
|
webView = findViewById(R.id.webview)
|
|
46
86
|
craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
|
|
47
|
-
|
|
48
|
-
|
|
87
|
+
// The link that launched the app (#215). Queued until the page's bridge
|
|
88
|
+
// is injected, then dispatched once with `initial: true`, so a page that
|
|
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
|
|
100
|
+
if (savedInstanceState == null) {
|
|
101
|
+
if (!launchedFromHistory) {
|
|
102
|
+
intent.data?.toString()?.let(craftBridge::receiveDeepLink)
|
|
103
|
+
handleIncomingShortcut(intent)
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
craftBridge.restoreDeepLinks(savedInstanceState)
|
|
107
|
+
}
|
|
49
108
|
|
|
50
109
|
setupWebView()
|
|
51
110
|
loadContent()
|
|
111
|
+
setupNetworkMonitoring()
|
|
52
112
|
}
|
|
53
113
|
|
|
54
114
|
@SuppressLint("SetJavaScriptEnabled")
|
|
@@ -94,7 +154,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
94
154
|
|
|
95
155
|
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
|
96
156
|
super.onReceivedError(view, request, error)
|
|
97
|
-
if (
|
|
157
|
+
if (
|
|
158
|
+
request?.isForMainFrame == true &&
|
|
159
|
+
error != null &&
|
|
160
|
+
CraftLoadFailure.isUnreachable(error.errorCode) &&
|
|
161
|
+
hasBundledFallback &&
|
|
162
|
+
!loadedBundledFallback
|
|
163
|
+
) {
|
|
98
164
|
loadedBundledFallback = true
|
|
99
165
|
webView.loadUrl(BUNDLED_APP_URL)
|
|
100
166
|
}
|
|
@@ -107,6 +173,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
107
173
|
webView.addJavascriptInterface(craftBridge, "CraftAndroid")
|
|
108
174
|
}
|
|
109
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
|
+
|
|
110
193
|
private fun loadContent() {
|
|
111
194
|
// Check for dev server URL in config
|
|
112
195
|
val remoteUrl = devServerUrl
|
|
@@ -150,12 +233,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
150
233
|
}
|
|
151
234
|
}
|
|
152
235
|
|
|
153
|
-
private fun handleIncomingDeepLink(source: Intent?, dispatch: Boolean) {
|
|
154
|
-
val url = source?.data?.toString() ?: return
|
|
155
|
-
craftBridge.setInitialURL(url)
|
|
156
|
-
if (dispatch) craftBridge.dispatchDeepLink(url)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
236
|
private fun handleIncomingShortcut(source: Intent?) {
|
|
160
237
|
source?.getStringExtra(SHORTCUT_TYPE_EXTRA)?.let(craftBridge::dispatchShortcut)
|
|
161
238
|
}
|
|
@@ -198,10 +275,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
198
275
|
return (uri.scheme == "https" || localDevelopment) && origin in trustedOrigins
|
|
199
276
|
}
|
|
200
277
|
|
|
278
|
+
override fun onSaveInstanceState(outState: Bundle) {
|
|
279
|
+
super.onSaveInstanceState(outState)
|
|
280
|
+
craftBridge.saveDeepLinks(outState)
|
|
281
|
+
}
|
|
282
|
+
|
|
201
283
|
override fun onNewIntent(intent: Intent) {
|
|
202
284
|
super.onNewIntent(intent)
|
|
203
285
|
setIntent(intent)
|
|
204
|
-
|
|
286
|
+
intent.data?.toString()?.let(craftBridge::receiveDeepLink)
|
|
205
287
|
handleIncomingShortcut(intent)
|
|
206
288
|
}
|
|
207
289
|
|
|
@@ -216,6 +298,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
216
298
|
override fun onResume() {
|
|
217
299
|
super.onResume()
|
|
218
300
|
webView.onResume()
|
|
301
|
+
returnFromBundledFallback("the app returned to the foreground")
|
|
219
302
|
}
|
|
220
303
|
|
|
221
304
|
override fun onPause() {
|
|
@@ -224,6 +307,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
224
307
|
}
|
|
225
308
|
|
|
226
309
|
override fun onDestroy() {
|
|
310
|
+
if (networkMonitoringRegistered) {
|
|
311
|
+
connectivityManager.unregisterNetworkCallback(networkCallback)
|
|
312
|
+
networkMonitoringRegistered = false
|
|
313
|
+
}
|
|
227
314
|
craftBridge.close()
|
|
228
315
|
webView.destroy()
|
|
229
316
|
super.onDestroy()
|
|
@@ -245,6 +332,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
245
332
|
}
|
|
246
333
|
|
|
247
334
|
companion object {
|
|
335
|
+
private const val TAG = "CraftMainActivity"
|
|
248
336
|
private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
|
|
249
337
|
private const val BUNDLED_APP_ORIGIN = "https://appassets.androidplatform.net"
|
|
250
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.
|