craft-native 0.0.236 → 0.0.238
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 +71 -0
- package/dist/android/src/emulator.d.ts +23 -0
- package/dist/android/src/index.d.ts +7 -1
- package/dist/android/src/index.js +280 -98
- package/dist/android/templates/CraftBridge.kt.template +65 -54
- package/dist/android/templates/CraftNative.kt.template +27 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +55 -6
- package/dist/android/templates/CraftNativeServices.kt.template +45 -35
- package/dist/android/templates/CraftNativeShell.kt.template +103 -20
- package/dist/android/templates/LocationRecordingService.kt.template +252 -46
- package/dist/android/templates/MainActivityNative.kt.template +163 -5
- package/dist/android-adb.d.ts +2 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/mobile.d.ts +49 -14
- package/dist/browser/storage.d.ts +3 -0
- package/dist/browser.js +163 -51
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +33 -0
- package/dist/index.cjs +126 -12
- package/dist/index.js +126 -12
- package/dist/ios/src/app-shortcuts.d.ts +11 -0
- package/dist/ios/src/index.d.ts +7 -0
- package/dist/ios/src/index.js +65 -1
- package/dist/ios/src/testing.d.ts +2 -0
- package/dist/ios/templates/CraftApp.swift +392 -88
- package/dist/ios/templates/CraftHeartRate.swift +141 -44
- package/dist/ios/templates/CraftNativeActions.swift +46 -48
- package/dist/ios/templates/CraftNativeScreen.swift +177 -10
- package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
- package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +8 -1
- package/dist/mobile-testing.js +30 -4
- package/dist/mobile.js +169 -17
- package/package.json +1 -1
|
@@ -410,6 +410,9 @@ class CraftBridge(
|
|
|
410
410
|
pushNotifications: {{ENABLE_PUSH}},
|
|
411
411
|
secureStorage: {{ENABLE_SECURE_STORAGE}},
|
|
412
412
|
geolocation: {{ENABLE_GEOLOCATION}},
|
|
413
|
+
crypto: true,
|
|
414
|
+
locationRecordingOwnership: {{ENABLE_GEOLOCATION}} && ${CraftNative.supportsRecordingOwnership()},
|
|
415
|
+
locationRecordingIntentIdentity: {{ENABLE_GEOLOCATION}} && ${CraftNative.supportsRecordingIntentIdentity()},
|
|
413
416
|
backgroundLocation: {{ENABLE_BACKGROUND_LOCATION}},
|
|
414
417
|
clipboard: false,
|
|
415
418
|
deviceInfo: true,
|
|
@@ -1769,16 +1772,41 @@ class CraftBridge(
|
|
|
1769
1772
|
return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.crashReporterContext()); });
|
|
1770
1773
|
}
|
|
1771
1774
|
};
|
|
1775
|
+
function recordingCall(method, options) {
|
|
1776
|
+
return Promise.resolve().then(function() {
|
|
1777
|
+
if (options && Object.prototype.hasOwnProperty.call(options, 'expectedId') &&
|
|
1778
|
+
(typeof options.expectedId !== 'string' || !options.expectedId.trim())) {
|
|
1779
|
+
var invalid = new Error('expectedId must be a nonempty recording ID');
|
|
1780
|
+
invalid.code = 'INVALID_ARGUMENT';
|
|
1781
|
+
throw invalid;
|
|
1782
|
+
}
|
|
1783
|
+
if (options && Object.prototype.hasOwnProperty.call(options, 'recordingId') &&
|
|
1784
|
+
(typeof options.recordingId !== 'string' || !options.recordingId.trim())) {
|
|
1785
|
+
var invalidStart = new Error('recordingId must be a nonempty recording ID');
|
|
1786
|
+
invalidStart.code = 'INVALID_ARGUMENT';
|
|
1787
|
+
throw invalidStart;
|
|
1788
|
+
}
|
|
1789
|
+
var result = JSON.parse(CraftAndroid.recordingAction(method, JSON.stringify(options || {})));
|
|
1790
|
+
if (!result.ok) {
|
|
1791
|
+
var error = new Error(result.message || 'Recording failed');
|
|
1792
|
+
error.code = result.code;
|
|
1793
|
+
throw error;
|
|
1794
|
+
}
|
|
1795
|
+
return result.data;
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
craft.crypto = { randomUUID: function() { return Promise.resolve().then(function() { return CraftAndroid.randomUUID(); }); } };
|
|
1772
1799
|
craft.location = {
|
|
1773
1800
|
getCurrentPosition: craft.getCurrentPosition,
|
|
1774
1801
|
watchPosition: craft.watchPosition,
|
|
1775
1802
|
clearWatch: craft.clearWatch,
|
|
1776
|
-
startRecording: function(options) { return
|
|
1777
|
-
pauseRecording: function() { return
|
|
1778
|
-
resumeRecording: function() { return
|
|
1779
|
-
stopRecording: function() { return
|
|
1780
|
-
getRecordingState: function() { return
|
|
1781
|
-
readRecording: function() { return
|
|
1803
|
+
startRecording: function(options) { return recordingCall('startRecording', options); },
|
|
1804
|
+
pauseRecording: function(options) { return recordingCall('pauseRecording', options); },
|
|
1805
|
+
resumeRecording: function(options) { return recordingCall('resumeRecording', options); },
|
|
1806
|
+
stopRecording: function(options) { return recordingCall('stopRecording', options); },
|
|
1807
|
+
getRecordingState: function(options) { return recordingCall('getRecordingState', options); },
|
|
1808
|
+
readRecording: function(options) { return recordingCall('readRecording', options); },
|
|
1809
|
+
acknowledgeRecording: function(options) { return recordingCall('acknowledgeRecording', options); }
|
|
1782
1810
|
};
|
|
1783
1811
|
var shareApi = function(text, title) { return legacyShare(text, title); };
|
|
1784
1812
|
shareApi.share = function(options) {
|
|
@@ -1840,6 +1868,9 @@ class CraftBridge(
|
|
|
1840
1868
|
navigationGeneration += 1
|
|
1841
1869
|
}
|
|
1842
1870
|
|
|
1871
|
+
@JavascriptInterface
|
|
1872
|
+
fun randomUUID(): String = CraftNative.randomUUID() ?: java.util.UUID.randomUUID().toString()
|
|
1873
|
+
|
|
1843
1874
|
// ==================== Haptics ====================
|
|
1844
1875
|
|
|
1845
1876
|
/**
|
|
@@ -2738,75 +2769,55 @@ class CraftBridge(
|
|
|
2738
2769
|
}
|
|
2739
2770
|
|
|
2740
2771
|
@JavascriptInterface
|
|
2741
|
-
fun
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2772
|
+
fun recordingAction(method: String, optionsJson: String): String {
|
|
2773
|
+
if (!{{ENABLE_GEOLOCATION}}) return JSONObject().put("ok", false).put("code", "CAPABILITY_DISABLED").put("message", "Geolocation is disabled").toString()
|
|
2774
|
+
val result = CraftNative.recordingAction(activity, method, optionsJson)
|
|
2775
|
+
?: CraftLocationRecordingStore.perform(activity, method, optionsJson)
|
|
2776
|
+
if (method == "startRecording" && JSONObject(result).optBoolean("ok")) {
|
|
2777
|
+
CraftLocationRecordingStore.listen(activity, JSONObject(result).getJSONObject("data").getString("id")) { location, id ->
|
|
2778
|
+
val detail = mapOf<String, Any>("latitude" to location.latitude, "longitude" to location.longitude,
|
|
2779
|
+
"accuracy" to location.accuracy, "altitude" to location.altitude, "speed" to location.speed,
|
|
2780
|
+
"heading" to location.bearing, "timestamp" to location.time, "recordingId" to id)
|
|
2781
|
+
sendEvent("craftLocationUpdate", detail)
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return result
|
|
2785
|
+
}
|
|
2745
2786
|
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
put("paused", false)
|
|
2751
|
-
put("startedAt", JSONObject.NULL)
|
|
2752
|
-
put("sampleCount", 0)
|
|
2753
|
-
}.toString()
|
|
2754
|
-
}
|
|
2755
|
-
val id = UUID.randomUUID().toString()
|
|
2756
|
-
CraftLocationRecordingStore.start(activity, id, System.currentTimeMillis())
|
|
2757
|
-
ContextCompat.startForegroundService(
|
|
2758
|
-
activity,
|
|
2759
|
-
Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
|
|
2760
|
-
)
|
|
2761
|
-
return CraftLocationRecordingStore.state(activity).toString()
|
|
2787
|
+
private fun legacyRecordingAction(method: String, optionsJson: String = "{}"): String {
|
|
2788
|
+
val answer = JSONObject(recordingAction(method, optionsJson))
|
|
2789
|
+
if (!answer.optBoolean("ok")) throw com.craft.runtime.CraftRecordingException(answer.optString("code"), answer.optString("message"))
|
|
2790
|
+
return answer.get("data").toString()
|
|
2762
2791
|
}
|
|
2763
2792
|
|
|
2764
2793
|
@JavascriptInterface
|
|
2765
|
-
fun
|
|
2766
|
-
|
|
2794
|
+
fun startLocationRecording(optionsJson: String): String {
|
|
2795
|
+
return legacyRecordingAction("startRecording", optionsJson)
|
|
2796
|
+
}
|
|
2767
2797
|
|
|
2768
|
-
|
|
2769
|
-
|
|
2798
|
+
@JavascriptInterface
|
|
2799
|
+
fun pauseLocationRecording(): String {
|
|
2800
|
+
return legacyRecordingAction("pauseRecording")
|
|
2770
2801
|
}
|
|
2771
2802
|
|
|
2772
2803
|
@JavascriptInterface
|
|
2773
2804
|
fun resumeLocationRecording(): String {
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
CraftLocationRecordingStore.setPaused(activity, false)
|
|
2777
|
-
if (CraftLocationRecordingStore.isActive(activity)) {
|
|
2778
|
-
ContextCompat.startForegroundService(
|
|
2779
|
-
activity,
|
|
2780
|
-
Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
|
|
2781
|
-
)
|
|
2782
|
-
}
|
|
2783
|
-
return CraftLocationRecordingStore.state(activity).toString()
|
|
2805
|
+
return legacyRecordingAction("resumeRecording")
|
|
2784
2806
|
}
|
|
2785
2807
|
|
|
2786
2808
|
@JavascriptInterface
|
|
2787
2809
|
fun stopLocationRecording(): String {
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
CraftLocationRecordingStore.stop(activity)
|
|
2791
|
-
val result = CraftLocationRecordingStore.state(activity, includeLocations = true).toString()
|
|
2792
|
-
activity.stopService(Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_STOP))
|
|
2793
|
-
return result
|
|
2810
|
+
return legacyRecordingAction("stopRecording")
|
|
2794
2811
|
}
|
|
2795
2812
|
|
|
2796
2813
|
@JavascriptInterface
|
|
2797
2814
|
fun getLocationRecordingState(): String {
|
|
2798
|
-
|
|
2799
|
-
// a state object saying so, and returning it here is the answer.
|
|
2800
|
-
CraftNative.getLocationRecordingState(activity)?.let { return it }
|
|
2801
|
-
|
|
2802
|
-
return CraftLocationRecordingStore.state(activity).toString()
|
|
2815
|
+
return legacyRecordingAction("getRecordingState")
|
|
2803
2816
|
}
|
|
2804
2817
|
|
|
2805
2818
|
@JavascriptInterface
|
|
2806
2819
|
fun readLocationRecording(): String {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
return CraftLocationRecordingStore.locations(activity).toString()
|
|
2820
|
+
return legacyRecordingAction("readRecording")
|
|
2810
2821
|
}
|
|
2811
2822
|
|
|
2812
2823
|
// ==================== Clipboard ====================
|
|
@@ -230,6 +230,7 @@ object CraftNative {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
private external fun nativeGetDeviceInfo(activity: Activity): String?
|
|
233
|
+
private external fun nativeRandomUUID(): String?
|
|
233
234
|
private external fun nativeGetMemoryUsage(): String?
|
|
234
235
|
private external fun nativeLog(message: String): Boolean
|
|
235
236
|
private external fun nativeFlexRow(
|
|
@@ -437,6 +438,8 @@ object CraftNative {
|
|
|
437
438
|
private external fun nativeSetKeepAwake(activity: Activity, enabled: Boolean): Boolean
|
|
438
439
|
private external fun nativeDownloadFile(activity: Activity, url: String, filename: String): Boolean
|
|
439
440
|
private external fun nativeSaveFile(activity: Activity, data: String, filename: String): Boolean
|
|
441
|
+
private external fun nativeRecordingAction(activity: Activity, method: String, options: String): String?
|
|
442
|
+
private external fun nativeRecordingOwnershipVersion(): Int
|
|
440
443
|
private external fun nativeGetLocationRecordingState(activity: Activity): String?
|
|
441
444
|
private external fun nativeReadLocationRecording(activity: Activity): String?
|
|
442
445
|
private external fun nativeStartLocationRecording(activity: Activity): String?
|
|
@@ -487,6 +490,11 @@ object CraftNative {
|
|
|
487
490
|
}
|
|
488
491
|
}
|
|
489
492
|
|
|
493
|
+
fun randomUUID(): String? {
|
|
494
|
+
if (!isAvailable) return null
|
|
495
|
+
return try { nativeRandomUUID() } catch (_: UnsatisfiedLinkError) { null }
|
|
496
|
+
}
|
|
497
|
+
|
|
490
498
|
/** The JVM heap as JSON, or null to use the Kotlin implementation. */
|
|
491
499
|
fun getMemoryUsage(): String? {
|
|
492
500
|
if (!isAvailable) return null
|
|
@@ -1921,6 +1929,25 @@ object CraftNative {
|
|
|
1921
1929
|
* The read side of the recording store. Null is "Zig declined", not "no
|
|
1922
1930
|
* recording" — an absent recording is a state object saying so.
|
|
1923
1931
|
*/
|
|
1932
|
+
fun supportsRecordingOwnership(): Boolean {
|
|
1933
|
+
if (!isAvailable) return true
|
|
1934
|
+
return try { nativeRecordingOwnershipVersion() >= 1 } catch (_: UnsatisfiedLinkError) { false }
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
fun supportsRecordingIntentIdentity(): Boolean {
|
|
1938
|
+
if (!isAvailable) return true
|
|
1939
|
+
return try { nativeRecordingOwnershipVersion() >= 2 } catch (_: UnsatisfiedLinkError) { false }
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
fun recordingAction(activity: Activity, method: String, options: String): String? {
|
|
1943
|
+
if (!isAvailable) return null
|
|
1944
|
+
return try { nativeRecordingAction(activity, method, options)
|
|
1945
|
+
?: org.json.JSONObject().put("ok", false).put("code", "RECORDING_OWNERSHIP_UNSUPPORTED").put("message", "The linked runtime did not serve the recorder request").toString() }
|
|
1946
|
+
catch (error: UnsatisfiedLinkError) {
|
|
1947
|
+
org.json.JSONObject().put("ok", false).put("code", "RECORDING_OWNERSHIP_UNSUPPORTED").put("message", "The linked runtime lacks the shared recorder API").toString()
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1924
1951
|
fun getLocationRecordingState(activity: Activity): String? {
|
|
1925
1952
|
if (!isAvailable) return null
|
|
1926
1953
|
return try {
|
|
@@ -43,6 +43,7 @@ import kotlin.math.roundToInt
|
|
|
43
43
|
private const val CRAFT_NATIVE_FETCH_MAX_RESPONSE_BYTES = 8 * 1024 * 1024
|
|
44
44
|
private const val CRAFT_NATIVE_FETCH_TIMEOUT_MS = 30_000
|
|
45
45
|
private const val CRAFT_NATIVE_FILE_PICK_REQUEST = 4101
|
|
46
|
+
private const val CRAFT_NATIVE_NOTIFICATION_PERMISSION_REQUEST = 4102
|
|
46
47
|
private const val CRAFT_NATIVE_FILE_DEFAULT_MAX_PIXELS = 2048
|
|
47
48
|
private const val CRAFT_NATIVE_FILE_LARGEST_MAX_PIXELS = 8192
|
|
48
49
|
private const val CRAFT_NATIVE_FILE_DEFAULT_QUALITY = 0.85
|
|
@@ -88,6 +89,7 @@ internal class CraftNativeCapabilities(
|
|
|
88
89
|
private val fetchConnections = ConcurrentHashMap<String, HttpURLConnection>()
|
|
89
90
|
private val fileRoot by lazy { File(context.cacheDir, "craft-files").apply { mkdirs() } }
|
|
90
91
|
private var pendingFilePick: PendingFilePick? = null
|
|
92
|
+
private var pendingNotificationPermission: ((Boolean) -> Unit)? = null
|
|
91
93
|
private var database: SQLiteDatabase? = null
|
|
92
94
|
private val biometricPrompts = ConcurrentHashMap<String, BiometricPrompt>()
|
|
93
95
|
|
|
@@ -114,6 +116,9 @@ internal class CraftNativeCapabilities(
|
|
|
114
116
|
|
|
115
117
|
fun names(): JSONArray = JSONArray().apply {
|
|
116
118
|
listOf("device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "files").forEach(::put)
|
|
119
|
+
put("storageCompareAndSet")
|
|
120
|
+
put("storageSetIfAbsent")
|
|
121
|
+
put("crypto")
|
|
117
122
|
if (config.optBoolean("enableBluetooth")) put("bluetooth")
|
|
118
123
|
if (config.optBoolean("enableBiometric")) put("biometric")
|
|
119
124
|
if (config.optBoolean("enableSecureStorage")) put("secureStorage")
|
|
@@ -136,11 +141,19 @@ internal class CraftNativeCapabilities(
|
|
|
136
141
|
return
|
|
137
142
|
}
|
|
138
143
|
when (module) {
|
|
144
|
+
"Crypto" -> {
|
|
145
|
+
if (method == "randomUUID") complete(requestToken, success(UUID.randomUUID().toString()), completion)
|
|
146
|
+
else complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Crypto method"), completion)
|
|
147
|
+
}
|
|
139
148
|
"Bluetooth" -> {
|
|
140
149
|
if (!config.optBoolean("enableBluetooth")) complete(requestToken, failure("CAPABILITY_DISABLED", "Bluetooth is disabled"), completion)
|
|
141
150
|
else bluetooth.perform(method, args) { complete(requestToken, it, completion) }
|
|
142
151
|
}
|
|
143
|
-
"Storage" ->
|
|
152
|
+
"Storage" -> mainHandler.post {
|
|
153
|
+
val answer = runCatching { storage(method, args) }
|
|
154
|
+
.getOrElse { failure("STORAGE_ERROR", it.message ?: "Storage operation failed") }
|
|
155
|
+
complete(requestToken, answer, completion)
|
|
156
|
+
}
|
|
144
157
|
"SecureStorage" -> {
|
|
145
158
|
if (!config.optBoolean("enableSecureStorage")) complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
|
|
146
159
|
else complete(requestToken, secureStorage(method, args), completion)
|
|
@@ -646,9 +659,9 @@ internal class CraftNativeCapabilities(
|
|
|
646
659
|
}
|
|
647
660
|
}
|
|
648
661
|
|
|
649
|
-
private fun storage(method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
662
|
+
private fun storage(method: String, args: JSONArray): CraftNativeCapabilityResult = synchronized(CraftNativeStorage.lock) {
|
|
650
663
|
if (method == "clear") {
|
|
651
|
-
|
|
664
|
+
if (!CraftNativeStorage.commit(storage) { it.clear() }) return failure("STORAGE_ERROR", "Storage could not be cleared")
|
|
652
665
|
storeObserver?.invoke("storage", null, null)
|
|
653
666
|
return success(true)
|
|
654
667
|
}
|
|
@@ -664,13 +677,33 @@ internal class CraftNativeCapabilities(
|
|
|
664
677
|
if (args.length() != 2) return failure("INVALID_ARGUMENT", "Storage.set needs a key and value")
|
|
665
678
|
val value = jsonString(args.opt(1))
|
|
666
679
|
?: return failure("INVALID_ARGUMENT", "Storage values must be JSON serializable")
|
|
667
|
-
if (
|
|
680
|
+
if (CraftNativeStorage.commit(storage) { it.putString(key, value) }) {
|
|
668
681
|
storeObserver?.invoke("storage", key, value)
|
|
669
682
|
success(true)
|
|
670
683
|
}
|
|
671
684
|
else failure("STORAGE_ERROR", "Storage could not persist the value")
|
|
672
685
|
}
|
|
673
|
-
"
|
|
686
|
+
"compareAndSet" -> {
|
|
687
|
+
if (args.length() != 3) return failure("INVALID_ARGUMENT", "Storage.compareAndSet needs a key, expected value and next value")
|
|
688
|
+
val current = storage.getString(key, null) ?: return success(false)
|
|
689
|
+
if (!CraftNativeStorage.jsonEqual(JSONTokener(current).nextValue(), args.opt(1))) return success(false)
|
|
690
|
+
val next = jsonString(args.opt(2)) ?: return failure("INVALID_ARGUMENT", "Storage values must be JSON serializable")
|
|
691
|
+
if (!CraftNativeStorage.commit(storage) { it.putString(key, next) }) return failure("STORAGE_ERROR", "Storage could not persist the checkpoint")
|
|
692
|
+
storeObserver?.invoke("storage", key, next)
|
|
693
|
+
success(true)
|
|
694
|
+
}
|
|
695
|
+
"setIfAbsent" -> {
|
|
696
|
+
if (args.length() != 2) return failure("INVALID_ARGUMENT", "Storage.setIfAbsent needs a key and value")
|
|
697
|
+
if (storage.contains(key)) return success(false)
|
|
698
|
+
val value = jsonString(args.opt(1)) ?: return failure("INVALID_ARGUMENT", "Storage values must be JSON serializable")
|
|
699
|
+
if (!CraftNativeStorage.commit(storage) { it.putString(key, value) }) return failure("STORAGE_ERROR", "Storage could not persist the new intent")
|
|
700
|
+
storeObserver?.invoke("storage", key, value)
|
|
701
|
+
success(true)
|
|
702
|
+
}
|
|
703
|
+
"remove" -> if (CraftNativeStorage.commit(storage) { it.remove(key) }) {
|
|
704
|
+
storeObserver?.invoke("storage", key, null)
|
|
705
|
+
success(true)
|
|
706
|
+
} else failure("STORAGE_ERROR", "Storage could not remove the value")
|
|
674
707
|
else -> failure("UNKNOWN_ACTION", "Unsupported Storage method")
|
|
675
708
|
}
|
|
676
709
|
}
|
|
@@ -891,7 +924,16 @@ internal class CraftNativeCapabilities(
|
|
|
891
924
|
return
|
|
892
925
|
}
|
|
893
926
|
when (method) {
|
|
894
|
-
"schedule" ->
|
|
927
|
+
"schedule" -> {
|
|
928
|
+
val notification = args.optJSONObject(0)
|
|
929
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
930
|
+
val host = activity
|
|
931
|
+
if (host == null) { complete(requestToken, failure("PERMISSION_DENIED", "Notification permission must be requested while the app is open"), completion); return }
|
|
932
|
+
if (pendingNotificationPermission != null) { complete(requestToken, failure("BUSY", "A notification permission request is already open"), completion); return }
|
|
933
|
+
pendingNotificationPermission = { granted -> complete(requestToken, if (granted) schedule(notification) else failure("PERMISSION_DENIED", "Notification permission denied"), completion) }
|
|
934
|
+
mainHandler.post { androidx.core.app.ActivityCompat.requestPermissions(host, arrayOf(Manifest.permission.POST_NOTIFICATIONS), CRAFT_NATIVE_NOTIFICATION_PERMISSION_REQUEST) }
|
|
935
|
+
} else complete(requestToken, schedule(notification), completion)
|
|
936
|
+
}
|
|
895
937
|
"cancel" -> {
|
|
896
938
|
val id = validKey(args.opt(0))
|
|
897
939
|
complete(requestToken, if (id == null) failure("INVALID_ARGUMENT", "Notifications.cancel needs an id")
|
|
@@ -909,6 +951,13 @@ internal class CraftNativeCapabilities(
|
|
|
909
951
|
}
|
|
910
952
|
}
|
|
911
953
|
|
|
954
|
+
fun onNotificationPermissions(requestCode: Int) {
|
|
955
|
+
if (requestCode != CRAFT_NATIVE_NOTIFICATION_PERMISSION_REQUEST) return
|
|
956
|
+
val complete = pendingNotificationPermission ?: return
|
|
957
|
+
pendingNotificationPermission = null
|
|
958
|
+
complete(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)
|
|
959
|
+
}
|
|
960
|
+
|
|
912
961
|
private fun schedule(value: JSONObject?): CraftNativeCapabilityResult {
|
|
913
962
|
val title = value?.optString("title").orEmpty()
|
|
914
963
|
if (title.isEmpty()) return failure("INVALID_ARGUMENT", "Notifications.schedule needs a title")
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
|
+
import com.craft.runtime.CraftNative
|
|
4
|
+
|
|
3
5
|
import android.Manifest
|
|
4
6
|
import android.app.Activity
|
|
5
7
|
import android.content.ActivityNotFoundException
|
|
@@ -29,6 +31,15 @@ import java.util.Locale
|
|
|
29
31
|
import java.util.UUID
|
|
30
32
|
{{NATIVE_PUSH_IMPORTS}}
|
|
31
33
|
|
|
34
|
+
private fun craftLocationFix(location: android.location.Location): JSONObject = JSONObject()
|
|
35
|
+
.put("latitude", location.latitude)
|
|
36
|
+
.put("longitude", location.longitude)
|
|
37
|
+
.put("altitude", location.altitude)
|
|
38
|
+
.put("accuracy", location.accuracy.toDouble())
|
|
39
|
+
.put("speed", location.speed.toDouble())
|
|
40
|
+
.put("heading", location.bearing.toDouble())
|
|
41
|
+
.put("timestamp", location.time.toDouble())
|
|
42
|
+
|
|
32
43
|
/**
|
|
33
44
|
* The capabilities a native screen asks for beyond Craft's core set
|
|
34
45
|
* (CraftNativeCapabilities): the system's dialogs, the in-app browser, the
|
|
@@ -59,6 +70,16 @@ internal class CraftNativeServices(
|
|
|
59
70
|
private val speechQueue = mutableListOf<() -> Unit>()
|
|
60
71
|
private val speeches = mutableMapOf<String, (CraftNativeCapabilityResult) -> Unit>()
|
|
61
72
|
|
|
73
|
+
init {
|
|
74
|
+
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation")) {
|
|
75
|
+
// Native screens must also hear a recording started by the page.
|
|
76
|
+
// Keep this process sink independent from the page's listener.
|
|
77
|
+
com.craft.runtime.CraftLocationRecordingStore.nativeListener = { location, id ->
|
|
78
|
+
CraftNativeHub.broadcast("CRAFT_EVENT", JSONObject().put("event", "craftLocationUpdate").put("detail", craftLocationFix(location).put("recordingId", id)))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
/** `background.complete(ok, id)`: who is waiting for a background refresh's answer. */
|
|
63
84
|
var onBackgroundComplete: ((Boolean, String?) -> Unit)? = null
|
|
64
85
|
|
|
@@ -84,7 +105,11 @@ internal class CraftNativeServices(
|
|
|
84
105
|
add("healthAvailability")
|
|
85
106
|
}
|
|
86
107
|
if (config.optBoolean("enableKeepAwake")) add("keepAwake")
|
|
87
|
-
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation"))
|
|
108
|
+
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation")) {
|
|
109
|
+
add("geolocation")
|
|
110
|
+
if (CraftNative.supportsRecordingOwnership()) add("locationRecordingOwnership")
|
|
111
|
+
if (CraftNative.supportsRecordingIntentIdentity()) add("locationRecordingIntentIdentity")
|
|
112
|
+
}
|
|
88
113
|
if (config.optJSONObject("backgroundRefresh")?.optBoolean("enabled") == true || config.optBoolean("enablePushNotifications")) add("background")
|
|
89
114
|
// craft.crashReporter: the app's crash and error reporter (bughq).
|
|
90
115
|
if (CraftCrashReporter.enabled) add("crashReporter")
|
|
@@ -130,6 +155,8 @@ internal class CraftNativeServices(
|
|
|
130
155
|
"CrashReporter" -> crashReporter(method, args, done)
|
|
131
156
|
else -> done(failure("UNKNOWN_ACTION", "Unsupported native API"))
|
|
132
157
|
}
|
|
158
|
+
} catch (error: com.craft.runtime.CraftRecordingException) {
|
|
159
|
+
done(failure(error.code, error.message ?: "Recording failed"))
|
|
133
160
|
} catch (error: Exception) {
|
|
134
161
|
Log.e("CraftNativeAndroid", "$module.$method failed", error)
|
|
135
162
|
done(failure("CRAFT_ERROR", error.message ?: "$module.$method failed"))
|
|
@@ -359,14 +386,6 @@ internal class CraftNativeServices(
|
|
|
359
386
|
private var watchCallback: com.google.android.gms.location.LocationCallback? = null
|
|
360
387
|
private var pendingLocationPermission: ((Boolean) -> Unit)? = null
|
|
361
388
|
|
|
362
|
-
private fun fix(location: android.location.Location): JSONObject = JSONObject()
|
|
363
|
-
.put("latitude", location.latitude)
|
|
364
|
-
.put("longitude", location.longitude)
|
|
365
|
-
.put("altitude", location.altitude)
|
|
366
|
-
.put("accuracy", location.accuracy.toDouble())
|
|
367
|
-
.put("speed", location.speed.toDouble())
|
|
368
|
-
.put("heading", location.bearing.toDouble())
|
|
369
|
-
.put("timestamp", location.time.toDouble())
|
|
370
389
|
|
|
371
390
|
private fun hasLocation(): Boolean =
|
|
372
391
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
@@ -374,10 +393,15 @@ internal class CraftNativeServices(
|
|
|
374
393
|
|
|
375
394
|
/** Location, asked for once a person starts something that needs it. */
|
|
376
395
|
private fun withLocation(done: (CraftNativeCapabilityResult) -> Unit, then: () -> Unit) {
|
|
377
|
-
|
|
396
|
+
val authorized = {
|
|
397
|
+
try { then() }
|
|
398
|
+
catch (error: com.craft.runtime.CraftRecordingException) { done(failure(error.code, error.message ?: "Recording failed")) }
|
|
399
|
+
catch (error: Exception) { done(failure("CRAFT_ERROR", error.message ?: "Location failed")) }
|
|
400
|
+
}
|
|
401
|
+
if (hasLocation()) return authorized()
|
|
378
402
|
val host = activity() ?: return done(failure("PERMISSION_DENIED", "Location is not allowed"))
|
|
379
403
|
pendingLocationPermission?.invoke(false)
|
|
380
|
-
pendingLocationPermission = { granted -> if (granted)
|
|
404
|
+
pendingLocationPermission = { granted -> if (granted) authorized() else done(failure("PERMISSION_DENIED", "Location is not allowed")) }
|
|
381
405
|
ActivityCompat.requestPermissions(host, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), LOCATION_PERMISSION_REQUEST)
|
|
382
406
|
}
|
|
383
407
|
|
|
@@ -396,7 +420,7 @@ internal class CraftNativeServices(
|
|
|
396
420
|
val priority = if (options.optBoolean("enableHighAccuracy", true)) com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY
|
|
397
421
|
else com.google.android.gms.location.Priority.PRIORITY_BALANCED_POWER_ACCURACY
|
|
398
422
|
locationClient.getCurrentLocation(priority, null)
|
|
399
|
-
.addOnSuccessListener { location -> done(if (location != null) success(
|
|
423
|
+
.addOnSuccessListener { location -> done(if (location != null) success(craftLocationFix(location)) else failure("POSITION_UNAVAILABLE", "No location fix")) }
|
|
400
424
|
.addOnFailureListener { error -> done(failure("POSITION_UNAVAILABLE", error.message ?: "No location fix")) }
|
|
401
425
|
}
|
|
402
426
|
"watchPosition" -> withLocation(done) {
|
|
@@ -404,7 +428,7 @@ internal class CraftNativeServices(
|
|
|
404
428
|
val callback = object : com.google.android.gms.location.LocationCallback() {
|
|
405
429
|
override fun onLocationResult(result: com.google.android.gms.location.LocationResult) {
|
|
406
430
|
for (location in result.locations) {
|
|
407
|
-
CraftNativeHub.broadcast("CRAFT_EVENT", JSONObject().put("event", "craftLocationUpdate").put("detail",
|
|
431
|
+
CraftNativeHub.broadcast("CRAFT_EVENT", JSONObject().put("event", "craftLocationUpdate").put("detail", craftLocationFix(location)))
|
|
408
432
|
}
|
|
409
433
|
}
|
|
410
434
|
}
|
|
@@ -418,29 +442,15 @@ internal class CraftNativeServices(
|
|
|
418
442
|
watchCallback = null
|
|
419
443
|
done(success(true))
|
|
420
444
|
}
|
|
421
|
-
"startRecording" ->
|
|
422
|
-
val
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
done(success(store.state(context)))
|
|
430
|
-
}
|
|
431
|
-
"pauseRecording", "resumeRecording" -> {
|
|
432
|
-
store.setPaused(context, method == "pauseRecording")
|
|
433
|
-
done(success(store.state(context)))
|
|
434
|
-
}
|
|
435
|
-
"stopRecording" -> {
|
|
436
|
-
val state = store.state(context, includeLocations = true)
|
|
437
|
-
store.stop(context)
|
|
438
|
-
store.listener = null
|
|
439
|
-
runCatching { context.startService(Intent(context, com.craft.runtime.LocationRecordingService::class.java).setAction(com.craft.runtime.LocationRecordingService.ACTION_STOP)) }
|
|
440
|
-
done(success(state.put("active", false).put("paused", false)))
|
|
445
|
+
"startRecording", "pauseRecording", "resumeRecording", "stopRecording", "getRecordingState", "readRecording", "acknowledgeRecording" -> {
|
|
446
|
+
val invoke = {
|
|
447
|
+
val answer = JSONObject(store.perform(context, method, (args.optJSONObject(0) ?: JSONObject()).toString()))
|
|
448
|
+
if (answer.optBoolean("ok")) {
|
|
449
|
+
done(success(answer.opt("data")))
|
|
450
|
+
} else done(failure(answer.optString("code"), answer.optString("message")))
|
|
451
|
+
}
|
|
452
|
+
if (method == "startRecording") withLocation(done, invoke) else invoke()
|
|
441
453
|
}
|
|
442
|
-
"getRecordingState" -> done(success(store.state(context)))
|
|
443
|
-
"readRecording" -> done(success(store.locations(context)))
|
|
444
454
|
else -> done(failure("UNKNOWN_ACTION", "Unsupported Location method"))
|
|
445
455
|
}
|
|
446
456
|
}
|