craft-native 0.0.55 → 0.0.72
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/dist/android/src/index.d.ts +73 -0
- package/dist/android/src/index.js +177 -37
- package/dist/android/templates/AndroidManifest.xml.template +10 -36
- package/dist/android/templates/CraftBridge.kt.template +192 -60
- package/dist/android/templates/CraftHealthConnect.kt.template +200 -0
- package/dist/android/templates/CraftHealthConnectStub.kt.template +28 -0
- package/dist/android/templates/LocationRecordingService.kt.template +160 -0
- package/dist/android/templates/MainActivity.kt.template +140 -19
- package/dist/android/templates/build.gradle.kts.app.template +23 -1
- package/dist/android/templates/build.gradle.kts.project.template +3 -2
- package/dist/android/templates/proguard-rules.pro.template +9 -0
- package/dist/api/android-advanced.d.ts +5 -6
- package/dist/api/app.d.ts +0 -1
- package/dist/api/clipboard.d.ts +0 -1
- package/dist/api/crypto.d.ts +1 -2
- package/dist/api/db.d.ts +1 -2
- package/dist/api/dialog.d.ts +0 -1
- package/dist/api/fs.d.ts +1 -2
- package/dist/api/gestures.d.ts +66 -0
- package/dist/api/http.d.ts +1 -2
- package/dist/api/index.d.ts +37 -34
- package/dist/api/ios-advanced.d.ts +6 -7
- package/dist/api/macos-advanced.d.ts +2 -3
- package/dist/api/media.d.ts +0 -1
- package/dist/api/mobile.d.ts +109 -1
- package/dist/api/process.d.ts +0 -1
- package/dist/api/sidebar.d.ts +3 -20
- package/dist/api/spaces.d.ts +83 -0
- package/dist/api/tray.d.ts +2 -3
- package/dist/api/window.d.ts +1 -2
- package/dist/api/windows-advanced.d.ts +6 -7
- package/dist/assets/index.d.ts +0 -1
- package/dist/binary-resolver.d.ts +23 -1
- package/dist/bridge/android.d.ts +0 -1
- package/dist/bridge/core.d.ts +0 -38
- package/dist/bridge/ids.d.ts +0 -1
- package/dist/bridge/ios.d.ts +0 -1
- package/dist/bridge/webkit-pending.d.ts +0 -1
- package/dist/build/bundle-optimizer.d.ts +0 -1
- package/dist/cli.js +666 -90
- package/dist/components/index.d.ts +6 -7
- package/dist/components/native.d.ts +0 -1
- package/dist/components/sidebar.d.ts +2 -81
- package/dist/dev/devtools.d.ts +0 -1
- package/dist/dev/error-overlay.d.ts +0 -1
- package/dist/dev/hot-reload.d.ts +0 -1
- package/dist/index.cjs +113 -3
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +112 -4
- package/dist/ios/src/index.d.ts +172 -0
- package/dist/ios/src/index.js +440 -42
- package/dist/ios/templates/CraftActivityAttributes.swift +14 -0
- package/dist/ios/templates/CraftApp.swift +1001 -73
- package/dist/ios/templates/CraftLiveActivityWidget.swift.template +62 -0
- package/dist/ios/templates/CraftWatchApp.swift.template +155 -0
- package/dist/ios/templates/Info.plist.template +10 -9
- package/dist/ios/templates/WatchApp.Info.plist.template +26 -0
- package/dist/ios/templates/WidgetExtension.Info.plist +31 -0
- package/dist/ios/templates/project.yml.template +12 -1
- package/dist/logging/index.d.ts +0 -1
- package/dist/mobile.d.ts +8 -0
- package/dist/mobile.js +755 -0
- package/dist/optimizations/index.d.ts +3 -4
- package/dist/package.d.ts +11 -1
- package/dist/performance/runtime.d.ts +0 -1
- package/dist/performance/startup.d.ts +0 -1
- package/dist/plugins/index.d.ts +0 -1
- package/dist/security/index.d.ts +0 -1
- package/dist/sidebar-types.d.ts +102 -0
- package/dist/signing/index.d.ts +0 -1
- package/dist/styles/headwind.d.ts +0 -1
- package/dist/styles/index.d.ts +9 -10
- package/dist/styles/sidebar-templates.d.ts +0 -1
- package/dist/styles/sidebars.d.ts +0 -1
- package/dist/types.d.ts +131 -1
- package/dist/updater/index.d.ts +0 -1
- package/dist/utils/audio.d.ts +0 -1
- package/dist/utils/index.d.ts +4 -5
- package/dist/utils/native.d.ts +1 -2
- package/dist/utils/react.d.ts +0 -1
- package/dist/utils/storage.d.ts +0 -1
- package/dist/utils/svelte.d.ts +0 -1
- package/dist/utils/timer.d.ts +0 -1
- package/dist/utils/vue.d.ts +0 -1
- package/package.json +32 -4
- package/dist/android/src/index.js.map +0 -10
- package/dist/api/android-advanced.d.ts.map +0 -1
- package/dist/api/app.d.ts.map +0 -1
- package/dist/api/clipboard.d.ts.map +0 -1
- package/dist/api/crypto.d.ts.map +0 -1
- package/dist/api/db.d.ts.map +0 -1
- package/dist/api/dialog.d.ts.map +0 -1
- package/dist/api/fs.d.ts.map +0 -1
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/ios-advanced.d.ts.map +0 -1
- package/dist/api/macos-advanced.d.ts.map +0 -1
- package/dist/api/media.d.ts.map +0 -1
- package/dist/api/mobile.d.ts.map +0 -1
- package/dist/api/process.d.ts.map +0 -1
- package/dist/api/sidebar.d.ts.map +0 -1
- package/dist/api/tray.d.ts.map +0 -1
- package/dist/api/window.d.ts.map +0 -1
- package/dist/api/windows-advanced.d.ts.map +0 -1
- package/dist/assets/index.d.ts.map +0 -1
- package/dist/binary-resolver.d.ts.map +0 -1
- package/dist/bridge/android.d.ts.map +0 -1
- package/dist/bridge/core.d.ts.map +0 -1
- package/dist/bridge/ids.d.ts.map +0 -1
- package/dist/bridge/ios.d.ts.map +0 -1
- package/dist/bridge/webkit-pending.d.ts.map +0 -1
- package/dist/build/bundle-optimizer.d.ts.map +0 -1
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/native.d.ts.map +0 -1
- package/dist/components/sidebar.d.ts.map +0 -1
- package/dist/dev/devtools.d.ts.map +0 -1
- package/dist/dev/error-overlay.d.ts.map +0 -1
- package/dist/dev/hot-reload.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -45
- package/dist/ios/src/index.js.map +0 -10
- package/dist/logging/index.d.ts.map +0 -1
- package/dist/optimizations/index.d.ts.map +0 -1
- package/dist/package.d.ts.map +0 -1
- package/dist/package.test.d.ts +0 -2
- package/dist/package.test.d.ts.map +0 -1
- package/dist/performance/runtime.d.ts.map +0 -1
- package/dist/performance/startup.d.ts.map +0 -1
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/security/index.d.ts.map +0 -1
- package/dist/signing/index.d.ts.map +0 -1
- package/dist/styles/headwind.d.ts.map +0 -1
- package/dist/styles/index.d.ts.map +0 -1
- package/dist/styles/sidebar-templates.d.ts.map +0 -1
- package/dist/styles/sidebars.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/updater/index.d.ts.map +0 -1
- package/dist/utils/audio.d.ts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/native.d.ts.map +0 -1
- package/dist/utils/react.d.ts.map +0 -1
- package/dist/utils/storage.d.ts.map +0 -1
- package/dist/utils/svelte.d.ts.map +0 -1
- package/dist/utils/timer.d.ts.map +0 -1
- package/dist/utils/vue.d.ts.map +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.Service
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.content.pm.PackageManager
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.os.IBinder
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import androidx.core.app.NotificationCompat
|
|
14
|
+
import androidx.core.content.ContextCompat
|
|
15
|
+
import com.google.android.gms.location.LocationCallback
|
|
16
|
+
import com.google.android.gms.location.LocationRequest
|
|
17
|
+
import com.google.android.gms.location.LocationResult
|
|
18
|
+
import com.google.android.gms.location.LocationServices
|
|
19
|
+
import com.google.android.gms.location.Priority
|
|
20
|
+
import org.json.JSONArray
|
|
21
|
+
import org.json.JSONObject
|
|
22
|
+
import java.io.File
|
|
23
|
+
|
|
24
|
+
object CraftLocationRecordingStore {
|
|
25
|
+
private const val PREFS = "craft_location_recording"
|
|
26
|
+
private const val FILE = "craft-location-recording.jsonl"
|
|
27
|
+
|
|
28
|
+
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
29
|
+
private fun file(context: Context) = File(context.filesDir, FILE)
|
|
30
|
+
|
|
31
|
+
@Synchronized
|
|
32
|
+
fun start(context: Context, id: String, startedAt: Long) {
|
|
33
|
+
file(context).writeText("")
|
|
34
|
+
prefs(context).edit()
|
|
35
|
+
.putString("id", id)
|
|
36
|
+
.putBoolean("active", true)
|
|
37
|
+
.putBoolean("paused", false)
|
|
38
|
+
.putLong("startedAt", startedAt)
|
|
39
|
+
.apply()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Synchronized
|
|
43
|
+
fun setPaused(context: Context, paused: Boolean) {
|
|
44
|
+
prefs(context).edit().putBoolean("paused", paused).apply()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Synchronized
|
|
48
|
+
fun stop(context: Context) {
|
|
49
|
+
prefs(context).edit().putBoolean("active", false).putBoolean("paused", false).apply()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun isActive(context: Context) = prefs(context).getBoolean("active", false)
|
|
53
|
+
fun isPaused(context: Context) = prefs(context).getBoolean("paused", false)
|
|
54
|
+
|
|
55
|
+
@Synchronized
|
|
56
|
+
fun append(context: Context, location: android.location.Location) {
|
|
57
|
+
if (!isActive(context) || isPaused(context)) return
|
|
58
|
+
val json = JSONObject().apply {
|
|
59
|
+
put("latitude", location.latitude)
|
|
60
|
+
put("longitude", location.longitude)
|
|
61
|
+
put("accuracy", location.accuracy)
|
|
62
|
+
put("altitude", location.altitude)
|
|
63
|
+
put("speed", location.speed)
|
|
64
|
+
put("heading", location.bearing)
|
|
65
|
+
put("timestamp", location.time)
|
|
66
|
+
}
|
|
67
|
+
file(context).appendText("$json\n")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Synchronized
|
|
71
|
+
fun locations(context: Context): JSONArray {
|
|
72
|
+
val result = JSONArray()
|
|
73
|
+
val source = file(context)
|
|
74
|
+
if (!source.exists()) return result
|
|
75
|
+
source.forEachLine { line ->
|
|
76
|
+
if (line.isNotBlank()) runCatching { result.put(JSONObject(line)) }
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun state(context: Context, includeLocations: Boolean = false): JSONObject {
|
|
82
|
+
val values = prefs(context)
|
|
83
|
+
val locations = locations(context)
|
|
84
|
+
return JSONObject().apply {
|
|
85
|
+
put("id", values.getString("id", null))
|
|
86
|
+
put("active", values.getBoolean("active", false))
|
|
87
|
+
put("paused", values.getBoolean("paused", false))
|
|
88
|
+
put("startedAt", values.getLong("startedAt", 0).takeIf { it > 0 } ?: JSONObject.NULL)
|
|
89
|
+
put("sampleCount", locations.length())
|
|
90
|
+
if (includeLocations) put("locations", locations)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class LocationRecordingService : Service() {
|
|
96
|
+
private val client by lazy { LocationServices.getFusedLocationProviderClient(this) }
|
|
97
|
+
private var callback: LocationCallback? = null
|
|
98
|
+
|
|
99
|
+
override fun onCreate() {
|
|
100
|
+
super.onCreate()
|
|
101
|
+
val manager = getSystemService(NotificationManager::class.java)
|
|
102
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
103
|
+
manager.createNotificationChannel(NotificationChannel(CHANNEL, "Activity recording", NotificationManager.IMPORTANCE_LOW))
|
|
104
|
+
}
|
|
105
|
+
val notification = NotificationCompat.Builder(this, CHANNEL)
|
|
106
|
+
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
|
107
|
+
.setContentTitle("Recording your activity")
|
|
108
|
+
.setContentText("WildLoop is securely saving your route")
|
|
109
|
+
.setOngoing(true)
|
|
110
|
+
.setSilent(true)
|
|
111
|
+
.build()
|
|
112
|
+
startForeground(NOTIFICATION_ID, notification)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
116
|
+
when (intent?.action) {
|
|
117
|
+
ACTION_STOP -> {
|
|
118
|
+
stopUpdates()
|
|
119
|
+
stopSelf()
|
|
120
|
+
return START_NOT_STICKY
|
|
121
|
+
}
|
|
122
|
+
else -> startUpdates()
|
|
123
|
+
}
|
|
124
|
+
return if (CraftLocationRecordingStore.isActive(this)) START_STICKY else START_NOT_STICKY
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private fun startUpdates() {
|
|
128
|
+
if (callback != null || !CraftLocationRecordingStore.isActive(this)) return
|
|
129
|
+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) return
|
|
130
|
+
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5_000)
|
|
131
|
+
.setMinUpdateIntervalMillis(2_000)
|
|
132
|
+
.setMaxUpdateDelayMillis(10_000)
|
|
133
|
+
.build()
|
|
134
|
+
callback = object : LocationCallback() {
|
|
135
|
+
override fun onLocationResult(result: LocationResult) {
|
|
136
|
+
for (location in result.locations) CraftLocationRecordingStore.append(this@LocationRecordingService, location)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
client.requestLocationUpdates(request, callback!!, Looper.getMainLooper())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private fun stopUpdates() {
|
|
143
|
+
callback?.let { client.removeLocationUpdates(it) }
|
|
144
|
+
callback = null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override fun onDestroy() {
|
|
148
|
+
stopUpdates()
|
|
149
|
+
super.onDestroy()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
153
|
+
|
|
154
|
+
companion object {
|
|
155
|
+
const val ACTION_START = "craft.location.START"
|
|
156
|
+
const val ACTION_STOP = "craft.location.STOP"
|
|
157
|
+
private const val CHANNEL = "craft_location_recording"
|
|
158
|
+
private const val NOTIFICATION_ID = 7001
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -2,18 +2,35 @@ package {{PACKAGE_NAME}}
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.os.Bundle
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.webkit.WebResourceRequest
|
|
8
|
+
import android.webkit.WebResourceError
|
|
9
|
+
import android.webkit.WebResourceResponse
|
|
5
10
|
import android.webkit.WebChromeClient
|
|
6
11
|
import android.webkit.WebSettings
|
|
7
12
|
import android.webkit.WebView
|
|
8
13
|
import android.webkit.WebViewClient
|
|
9
14
|
import androidx.appcompat.app.AppCompatActivity
|
|
10
15
|
import androidx.core.view.WindowCompat
|
|
11
|
-
import
|
|
12
|
-
import
|
|
16
|
+
import androidx.webkit.WebViewAssetLoader
|
|
17
|
+
import org.json.JSONObject
|
|
18
|
+
import java.io.FileNotFoundException
|
|
19
|
+
import java.net.URLConnection
|
|
13
20
|
|
|
14
21
|
class MainActivity : AppCompatActivity() {
|
|
15
22
|
private lateinit var webView: WebView
|
|
16
23
|
private lateinit var craftBridge: CraftBridge
|
|
24
|
+
private val devServerUrl: String? by lazy { loadDevServerUrl() }
|
|
25
|
+
private val trustedOrigins: Set<String> by lazy { loadTrustedOrigins() }
|
|
26
|
+
private val hasBundledFallback: Boolean by lazy { configJson()?.optBoolean("hasBundledFallback", false) == true }
|
|
27
|
+
private val bundledAssetLoader: WebViewAssetLoader by lazy {
|
|
28
|
+
WebViewAssetLoader.Builder()
|
|
29
|
+
.setDomain(BUNDLED_APP_HOST)
|
|
30
|
+
.addPathHandler("/", RouteAwareAssetPathHandler())
|
|
31
|
+
.build()
|
|
32
|
+
}
|
|
33
|
+
private var loadedBundledFallback = false
|
|
17
34
|
|
|
18
35
|
@SuppressLint("SetJavaScriptEnabled")
|
|
19
36
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
@@ -25,7 +42,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
25
42
|
setContentView(R.layout.activity_main)
|
|
26
43
|
|
|
27
44
|
webView = findViewById(R.id.webview)
|
|
28
|
-
craftBridge = CraftBridge(this, webView)
|
|
45
|
+
craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
|
|
46
|
+
handleIncomingDeepLink(intent, dispatch = false)
|
|
29
47
|
|
|
30
48
|
setupWebView()
|
|
31
49
|
loadContent()
|
|
@@ -36,10 +54,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
36
54
|
webView.settings.apply {
|
|
37
55
|
javaScriptEnabled = true
|
|
38
56
|
domStorageEnabled = true
|
|
39
|
-
allowFileAccess =
|
|
40
|
-
allowContentAccess =
|
|
57
|
+
allowFileAccess = false
|
|
58
|
+
allowContentAccess = false
|
|
59
|
+
allowFileAccessFromFileURLs = false
|
|
60
|
+
allowUniversalAccessFromFileURLs = false
|
|
41
61
|
mediaPlaybackRequiresUserGesture = false
|
|
42
|
-
mixedContentMode = WebSettings.
|
|
62
|
+
mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
|
|
43
63
|
cacheMode = WebSettings.LOAD_DEFAULT
|
|
44
64
|
setSupportZoom(false)
|
|
45
65
|
builtInZoomControls = false
|
|
@@ -49,7 +69,28 @@ class MainActivity : AppCompatActivity() {
|
|
|
49
69
|
webView.webViewClient = object : WebViewClient() {
|
|
50
70
|
override fun onPageFinished(view: WebView?, url: String?) {
|
|
51
71
|
super.onPageFinished(view, url)
|
|
52
|
-
craftBridge.injectBridge()
|
|
72
|
+
if (url != null && isTrustedUrl(url)) craftBridge.injectBridge()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
|
76
|
+
val url = request?.url ?: return null
|
|
77
|
+
return bundledAssetLoader.shouldInterceptRequest(url)
|
|
78
|
+
?: super.shouldInterceptRequest(view, request)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
|
82
|
+
val url = request?.url?.toString() ?: return true
|
|
83
|
+
if (isTrustedUrl(url)) return false
|
|
84
|
+
runCatching { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) }
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
|
89
|
+
super.onReceivedError(view, request, error)
|
|
90
|
+
if (request?.isForMainFrame == true && hasBundledFallback && !loadedBundledFallback) {
|
|
91
|
+
loadedBundledFallback = true
|
|
92
|
+
webView.loadUrl(BUNDLED_APP_URL)
|
|
93
|
+
}
|
|
53
94
|
}
|
|
54
95
|
}
|
|
55
96
|
|
|
@@ -61,29 +102,97 @@ class MainActivity : AppCompatActivity() {
|
|
|
61
102
|
|
|
62
103
|
private fun loadContent() {
|
|
63
104
|
// Check for dev server URL in config
|
|
64
|
-
val
|
|
65
|
-
if (
|
|
66
|
-
webView.loadUrl(
|
|
105
|
+
val remoteUrl = devServerUrl
|
|
106
|
+
if (remoteUrl != null) {
|
|
107
|
+
webView.loadUrl(remoteUrl)
|
|
67
108
|
} else {
|
|
68
109
|
// Load from assets
|
|
69
|
-
webView.loadUrl(
|
|
110
|
+
webView.loadUrl(BUNDLED_APP_URL)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private inner class RouteAwareAssetPathHandler : WebViewAssetLoader.PathHandler {
|
|
115
|
+
override fun handle(path: String): WebResourceResponse? {
|
|
116
|
+
val cleanPath = Uri.decode(path).substringBefore('?').trimStart('/')
|
|
117
|
+
if (cleanPath.split('/').any { it == ".." }) return null
|
|
118
|
+
val candidates = when {
|
|
119
|
+
cleanPath.isBlank() -> listOf("index.html")
|
|
120
|
+
cleanPath.endsWith('/') -> listOf("${cleanPath}index.html")
|
|
121
|
+
cleanPath.substringAfterLast('/').contains('.') -> listOf(cleanPath)
|
|
122
|
+
else -> listOf("${cleanPath}.html", "${cleanPath}/index.html")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (candidate in candidates) {
|
|
126
|
+
try {
|
|
127
|
+
val stream = assets.open(candidate)
|
|
128
|
+
val mimeType = URLConnection.guessContentTypeFromName(candidate)
|
|
129
|
+
?: when (candidate.substringAfterLast('.', "").lowercase()) {
|
|
130
|
+
"js", "mjs" -> "text/javascript"
|
|
131
|
+
"css" -> "text/css"
|
|
132
|
+
"json", "webmanifest" -> "application/json"
|
|
133
|
+
"svg" -> "image/svg+xml"
|
|
134
|
+
else -> "application/octet-stream"
|
|
135
|
+
}
|
|
136
|
+
val encoding = if (mimeType.startsWith("text/") || mimeType.contains("javascript") || mimeType.contains("json")) "UTF-8" else null
|
|
137
|
+
return WebResourceResponse(mimeType, encoding, stream)
|
|
138
|
+
} catch (_: FileNotFoundException) {
|
|
139
|
+
// Try the next clean-route candidate.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun handleIncomingDeepLink(source: Intent?, dispatch: Boolean) {
|
|
147
|
+
val url = source?.data?.toString() ?: return
|
|
148
|
+
craftBridge.setInitialURL(url)
|
|
149
|
+
if (dispatch) craftBridge.dispatchDeepLink(url)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun configJson(): JSONObject? {
|
|
153
|
+
return try {
|
|
154
|
+
assets.open("craft.config.json").bufferedReader().use { JSONObject(it.readText()) }
|
|
155
|
+
} catch (_: Exception) {
|
|
156
|
+
null
|
|
70
157
|
}
|
|
71
158
|
}
|
|
72
159
|
|
|
73
|
-
private fun
|
|
160
|
+
private fun loadDevServerUrl(): String? {
|
|
74
161
|
return try {
|
|
75
|
-
|
|
76
|
-
val reader = BufferedReader(InputStreamReader(inputStream))
|
|
77
|
-
val content = reader.readText()
|
|
78
|
-
reader.close()
|
|
79
|
-
// Simple JSON parsing for devServerURL
|
|
80
|
-
val regex = "\"devServerURL\"\\s*:\\s*\"([^\"]+)\"".toRegex()
|
|
81
|
-
regex.find(content)?.groupValues?.get(1)
|
|
162
|
+
configJson()?.optString("devServerURL")?.takeIf { it.isNotBlank() }
|
|
82
163
|
} catch (e: Exception) {
|
|
83
164
|
null
|
|
84
165
|
}
|
|
85
166
|
}
|
|
86
167
|
|
|
168
|
+
private fun loadTrustedOrigins(): Set<String> {
|
|
169
|
+
val origins = mutableSetOf<String>()
|
|
170
|
+
val config = configJson()
|
|
171
|
+
val configured = config?.optJSONArray("trustedOrigins")
|
|
172
|
+
if (configured != null) {
|
|
173
|
+
for (index in 0 until configured.length()) origins.add(configured.optString(index))
|
|
174
|
+
}
|
|
175
|
+
devServerUrl?.let { url ->
|
|
176
|
+
val uri = Uri.parse(url)
|
|
177
|
+
origins.add("${uri.scheme}://${uri.authority}")
|
|
178
|
+
}
|
|
179
|
+
return origins
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fun isTrustedUrl(value: String): Boolean {
|
|
183
|
+
val uri = Uri.parse(value)
|
|
184
|
+
if (uri.scheme == "https" && uri.host == BUNDLED_APP_HOST) return true
|
|
185
|
+
val origin = "${uri.scheme}://${uri.authority}"
|
|
186
|
+
val localDevelopment = uri.scheme == "http" && uri.host in setOf("localhost", "127.0.0.1", "10.0.2.2")
|
|
187
|
+
return (uri.scheme == "https" || localDevelopment) && origin in trustedOrigins
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override fun onNewIntent(intent: Intent) {
|
|
191
|
+
super.onNewIntent(intent)
|
|
192
|
+
setIntent(intent)
|
|
193
|
+
handleIncomingDeepLink(intent, dispatch = true)
|
|
194
|
+
}
|
|
195
|
+
|
|
87
196
|
override fun onBackPressed() {
|
|
88
197
|
if (webView.canGoBack()) {
|
|
89
198
|
webView.goBack()
|
|
@@ -103,7 +212,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
103
212
|
}
|
|
104
213
|
|
|
105
214
|
override fun onDestroy() {
|
|
215
|
+
craftBridge.close()
|
|
106
216
|
webView.destroy()
|
|
107
217
|
super.onDestroy()
|
|
108
218
|
}
|
|
219
|
+
|
|
220
|
+
@Deprecated("Delegates the Health Connect permission contract on Android 13 and earlier")
|
|
221
|
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
222
|
+
if (craftBridge.onActivityResult(requestCode, resultCode, data)) return
|
|
223
|
+
super.onActivityResult(requestCode, resultCode, data)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
companion object {
|
|
227
|
+
private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
|
|
228
|
+
private const val BUNDLED_APP_URL = "https://appassets.androidplatform.net/"
|
|
229
|
+
}
|
|
109
230
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
plugins {
|
|
2
2
|
id("com.android.application")
|
|
3
3
|
id("org.jetbrains.kotlin.android")
|
|
4
|
+
{{GOOGLE_SERVICES_PLUGIN}}
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
android {
|
|
7
8
|
namespace = "{{PACKAGE_NAME}}"
|
|
8
|
-
compileSdk = {{
|
|
9
|
+
compileSdk = {{COMPILE_SDK}}
|
|
9
10
|
|
|
10
11
|
defaultConfig {
|
|
11
12
|
applicationId = "{{PACKAGE_NAME}}"
|
|
@@ -43,12 +44,21 @@ android {
|
|
|
43
44
|
buildFeatures {
|
|
44
45
|
viewBinding = true
|
|
45
46
|
}
|
|
47
|
+
|
|
48
|
+
// Android's default AAPT ignore pattern drops underscore-prefixed asset
|
|
49
|
+
// directories. STX intentionally emits its browser runtime and generated
|
|
50
|
+
// styles under `_stx`, so keep those files in the APK.
|
|
51
|
+
androidResources {
|
|
52
|
+
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
dependencies {
|
|
49
57
|
// AndroidX Core
|
|
50
58
|
implementation("androidx.core:core-ktx:1.12.0")
|
|
51
59
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
|
60
|
+
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
|
61
|
+
implementation("androidx.webkit:webkit:1.12.1")
|
|
52
62
|
implementation("com.google.android.material:material:1.11.0")
|
|
53
63
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
|
54
64
|
|
|
@@ -69,6 +79,18 @@ dependencies {
|
|
|
69
79
|
implementation("com.google.android.play:review:2.0.1")
|
|
70
80
|
implementation("com.google.android.play:review-ktx:2.0.1")
|
|
71
81
|
|
|
82
|
+
// Native push notifications (included only when enabled)
|
|
83
|
+
{{FIREBASE_MESSAGING_DEPENDENCY}}
|
|
84
|
+
|
|
85
|
+
// Android Health Connect (included only when enabled)
|
|
86
|
+
{{HEALTH_CONNECT_DEPENDENCIES}}
|
|
87
|
+
|
|
88
|
+
// On-device computer vision APIs exposed by CraftBridge
|
|
89
|
+
implementation("com.google.mlkit:barcode-scanning:17.3.0")
|
|
90
|
+
implementation("com.google.mlkit:image-labeling:17.0.9")
|
|
91
|
+
implementation("com.google.mlkit:object-detection:17.0.2")
|
|
92
|
+
implementation("com.google.mlkit:text-recognition:16.0.1")
|
|
93
|
+
|
|
72
94
|
// Google Play Billing (In-App Purchase)
|
|
73
95
|
implementation("com.android.billingclient:billing:6.1.0")
|
|
74
96
|
implementation("com.android.billingclient:billing-ktx:6.1.0")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
2
2
|
plugins {
|
|
3
|
-
id("com.android.application") version "8.
|
|
4
|
-
id("org.jetbrains.kotlin.android") version "
|
|
3
|
+
id("com.android.application") version "8.9.1" apply false
|
|
4
|
+
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
|
|
5
|
+
{{GOOGLE_SERVICES_PLUGIN}}
|
|
5
6
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Preserve the JavaScript bridge contract and its annotated methods when R8
|
|
2
|
+
# optimizes release builds. These classes are reached from WebView by name.
|
|
3
|
+
-keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations
|
|
4
|
+
-keepclassmembers class * {
|
|
5
|
+
@android.webkit.JavascriptInterface <methods>;
|
|
6
|
+
}
|
|
7
|
+
-keep class {{PACKAGE_NAME}}.CraftBridge { *; }
|
|
8
|
+
-keep class {{PACKAGE_NAME}}.CraftHealthConnect { *; }
|
|
9
|
+
-keep class {{PACKAGE_NAME}}.LocationRecordingService { *; }
|
|
@@ -82,7 +82,7 @@ export declare const materialYou: {
|
|
|
82
82
|
/**
|
|
83
83
|
* Get colors from a specific source.
|
|
84
84
|
*/
|
|
85
|
-
getColorsFromSource(source:
|
|
85
|
+
getColorsFromSource(source: 'wallpaper' | 'content'): Promise<MaterialYouColors | null>;
|
|
86
86
|
/**
|
|
87
87
|
* Listen for color changes.
|
|
88
88
|
*/
|
|
@@ -273,7 +273,7 @@ export declare const workManager: {
|
|
|
273
273
|
/**
|
|
274
274
|
* Enqueue unique work (replaces existing work with same name).
|
|
275
275
|
*/
|
|
276
|
-
enqueueUnique(request: WorkRequest, existingWorkPolicy:
|
|
276
|
+
enqueueUnique(request: WorkRequest, existingWorkPolicy: 'replace' | 'keep' | 'append' | 'update'): Promise<string>;
|
|
277
277
|
/**
|
|
278
278
|
* Cancel work by name.
|
|
279
279
|
*/
|
|
@@ -684,7 +684,7 @@ export declare const playBilling: {
|
|
|
684
684
|
/**
|
|
685
685
|
* Query product details.
|
|
686
686
|
*/
|
|
687
|
-
getProducts(productIds: string[], productType:
|
|
687
|
+
getProducts(productIds: string[], productType: 'inapp' | 'subs'): Promise<PlayProduct[]>;
|
|
688
688
|
/**
|
|
689
689
|
* Launch purchase flow.
|
|
690
690
|
*/
|
|
@@ -692,7 +692,7 @@ export declare const playBilling: {
|
|
|
692
692
|
offerToken?: string;
|
|
693
693
|
isOfferPersonalized?: boolean;
|
|
694
694
|
oldPurchaseToken?: string;
|
|
695
|
-
replacementMode?:
|
|
695
|
+
replacementMode?: 'charge_prorated_price' | 'charge_full_price' | 'without_proration' | 'deferred';
|
|
696
696
|
}): Promise<{
|
|
697
697
|
success: boolean;
|
|
698
698
|
purchase?: PlayPurchase;
|
|
@@ -701,7 +701,7 @@ export declare const playBilling: {
|
|
|
701
701
|
/**
|
|
702
702
|
* Get purchases.
|
|
703
703
|
*/
|
|
704
|
-
getPurchases(productType:
|
|
704
|
+
getPurchases(productType: 'inapp' | 'subs'): Promise<PlayPurchase[]>;
|
|
705
705
|
/**
|
|
706
706
|
* Acknowledge a purchase.
|
|
707
707
|
*/
|
|
@@ -734,4 +734,3 @@ declare const androidAdvanced: {
|
|
|
734
734
|
playBilling: typeof playBilling;
|
|
735
735
|
};
|
|
736
736
|
export default androidAdvanced;
|
|
737
|
-
//# sourceMappingURL=android-advanced.d.ts.map
|
package/dist/api/app.d.ts
CHANGED
|
@@ -429,4 +429,3 @@ export declare const notify: (options: NotificationOptions) => Promise<void>;
|
|
|
429
429
|
export declare const registerShortcut: (accelerator: string, handler: ShortcutHandler) => Promise<boolean>;
|
|
430
430
|
export declare const unregisterShortcut: (accelerator: string) => Promise<void>;
|
|
431
431
|
export default appManager;
|
|
432
|
-
//# sourceMappingURL=app.d.ts.map
|
package/dist/api/clipboard.d.ts
CHANGED
package/dist/api/crypto.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Craft Crypto API
|
|
3
3
|
* Provides cryptographic operations through the Craft bridge
|
|
4
4
|
*/
|
|
5
|
-
import type { CraftCryptoAPI } from '../types';
|
|
5
|
+
import type { CraftCryptoAPI } from '../types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Error type for cryptographic failures (malformed input, decode errors, etc.).
|
|
8
8
|
*
|
|
@@ -76,4 +76,3 @@ export declare function hashPassword(password: string, salt?: string): Promise<{
|
|
|
76
76
|
*/
|
|
77
77
|
export declare function verifyPassword(password: string, hash: string, salt: string): Promise<boolean>;
|
|
78
78
|
export default crypto;
|
|
79
|
-
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/api/db.d.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
import { EventEmitter } from 'events';
|
|
29
|
-
import type { CraftDatabaseAPI } from '../types';
|
|
29
|
+
import type { CraftDatabaseAPI } from '../types.js';
|
|
30
30
|
/**
|
|
31
31
|
* Global emitter for `db:execute` audit events. Subscribe to log every SQL
|
|
32
32
|
* statement that goes through the SDK. The event payload is `{ name, sql,
|
|
@@ -424,4 +424,3 @@ export declare class KeyValueStore {
|
|
|
424
424
|
clear(): Promise<void>;
|
|
425
425
|
}
|
|
426
426
|
export default db;
|
|
427
|
-
//# sourceMappingURL=db.d.ts.map
|
package/dist/api/dialog.d.ts
CHANGED
package/dist/api/fs.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* })
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
|
-
import type { CraftFileSystemAPI } from '../types';
|
|
26
|
+
import type { CraftFileSystemAPI } from '../types.js';
|
|
27
27
|
/**
|
|
28
28
|
* Validate a file path to prevent directory traversal attacks.
|
|
29
29
|
*
|
|
@@ -128,4 +128,3 @@ export declare function _resetWatchWarningForTests(): void;
|
|
|
128
128
|
*/
|
|
129
129
|
export declare function watch(path: string, callback: (event: string, filename: string) => void, options?: WatchOptions): Promise<() => void>;
|
|
130
130
|
export default fs;
|
|
131
|
-
//# sourceMappingURL=fs.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Trackpad gesture phases
|
|
3
|
+
* @description Real swipe begin/change/end from the host's `NSEvent`, rather
|
|
4
|
+
* than inferred from a phase-less `wheel` stream.
|
|
5
|
+
* @module @craft-native/api/gestures
|
|
6
|
+
*/
|
|
7
|
+
export type SwipeAxis = 'horizontal' | 'vertical';
|
|
8
|
+
export type SwipePhase = 'begin' | 'change' | 'end';
|
|
9
|
+
export interface SwipeEvent {
|
|
10
|
+
/** Which way the gesture was locked at `begin`. Never changes mid-gesture. */
|
|
11
|
+
axis: SwipeAxis;
|
|
12
|
+
phase: SwipePhase;
|
|
13
|
+
/**
|
|
14
|
+
* Points since the previous event. Positive scrolls content left — i.e.
|
|
15
|
+
* advances to the next item — matching a DOM wheel event's `deltaX`.
|
|
16
|
+
*/
|
|
17
|
+
deltaX: number;
|
|
18
|
+
deltaY: number;
|
|
19
|
+
/** Points per second at release. Only meaningful on `end`. */
|
|
20
|
+
velocityX: number;
|
|
21
|
+
/**
|
|
22
|
+
* True once the fingers have lifted and the OS is coasting. The macOS host
|
|
23
|
+
* drops momentum entirely so a swipe settles at finger-up, so this is
|
|
24
|
+
* currently always `false` there; it exists for hosts that choose to forward
|
|
25
|
+
* the tail.
|
|
26
|
+
*/
|
|
27
|
+
momentum: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type SwipeListener = (swipe: SwipeEvent) => void;
|
|
30
|
+
export type Unsubscribe = () => void;
|
|
31
|
+
export interface GesturesAPI {
|
|
32
|
+
onSwipe: (listener: SwipeListener) => Unsubscribe;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether the host forwards real gesture phases.
|
|
36
|
+
*
|
|
37
|
+
* Note this is `true` as soon as the registry is installed, which happens in
|
|
38
|
+
* every Craft window — it does **not** promise that a swipe will ever arrive.
|
|
39
|
+
* A host that never emits leaves the registry dormant, which is deliberate:
|
|
40
|
+
* callers keep their own `wheel` fallback bound and let the native stream
|
|
41
|
+
* supersede it on the first `begin` it delivers.
|
|
42
|
+
*/
|
|
43
|
+
export declare function hasNativeGestures(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Subscribe to swipe phases. Returns an unsubscribe function; calling it more
|
|
46
|
+
* than once is safe.
|
|
47
|
+
*
|
|
48
|
+
* In a plain browser this is a no-op returning a no-op, so it is safe to call
|
|
49
|
+
* unconditionally.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const off = onSwipe((swipe) => {
|
|
54
|
+
* if (swipe.axis !== 'horizontal') return
|
|
55
|
+
* if (swipe.phase === 'begin') beginDrag()
|
|
56
|
+
* else if (swipe.phase === 'end') settle(swipe.velocityX)
|
|
57
|
+
* else track(swipe.deltaX)
|
|
58
|
+
* })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function onSwipe(listener: SwipeListener): Unsubscribe;
|
|
62
|
+
/** Namespace form, for `import { gestures } from 'craft-native'`. */
|
|
63
|
+
export declare const gestures: {
|
|
64
|
+
hasNativeGestures: () => boolean;
|
|
65
|
+
onSwipe: (listener: SwipeListener) => Unsubscribe;
|
|
66
|
+
};
|
package/dist/api/http.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Provides native HTTP client through the Craft bridge
|
|
4
4
|
* Bypasses CORS restrictions when running in Craft
|
|
5
5
|
*/
|
|
6
|
-
import type { CraftHttpAPI } from '../types';
|
|
6
|
+
import type { CraftHttpAPI } from '../types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Result of {@link encodeRequestBody}. `kind` lets the caller decide
|
|
9
9
|
* whether to default a `Content-Type: application/json` header.
|
|
@@ -193,4 +193,3 @@ export interface WebSocketOptions {
|
|
|
193
193
|
*/
|
|
194
194
|
export declare function createClient(options?: HttpClientOptions): HttpClient;
|
|
195
195
|
export default http;
|
|
196
|
-
//# sourceMappingURL=http.d.ts.map
|