craft-native 0.0.237 → 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 +36 -5
- 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-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/index.cjs +126 -12
- package/dist/index.js +126 -12
- package/dist/ios/src/index.d.ts +5 -0
- package/dist/ios/src/index.js +9 -1
- package/dist/ios/src/testing.d.ts +2 -0
- package/dist/ios/templates/CraftApp.swift +376 -88
- package/dist/ios/templates/CraftHeartRate.swift +141 -44
- package/dist/ios/templates/CraftNativeActions.swift +46 -48
- package/dist/ios/templates/CraftNativeScreen.swift +11 -4
- 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 {
|
|
@@ -116,6 +116,9 @@ internal class CraftNativeCapabilities(
|
|
|
116
116
|
|
|
117
117
|
fun names(): JSONArray = JSONArray().apply {
|
|
118
118
|
listOf("device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "files").forEach(::put)
|
|
119
|
+
put("storageCompareAndSet")
|
|
120
|
+
put("storageSetIfAbsent")
|
|
121
|
+
put("crypto")
|
|
119
122
|
if (config.optBoolean("enableBluetooth")) put("bluetooth")
|
|
120
123
|
if (config.optBoolean("enableBiometric")) put("biometric")
|
|
121
124
|
if (config.optBoolean("enableSecureStorage")) put("secureStorage")
|
|
@@ -138,11 +141,19 @@ internal class CraftNativeCapabilities(
|
|
|
138
141
|
return
|
|
139
142
|
}
|
|
140
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
|
+
}
|
|
141
148
|
"Bluetooth" -> {
|
|
142
149
|
if (!config.optBoolean("enableBluetooth")) complete(requestToken, failure("CAPABILITY_DISABLED", "Bluetooth is disabled"), completion)
|
|
143
150
|
else bluetooth.perform(method, args) { complete(requestToken, it, completion) }
|
|
144
151
|
}
|
|
145
|
-
"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
|
+
}
|
|
146
157
|
"SecureStorage" -> {
|
|
147
158
|
if (!config.optBoolean("enableSecureStorage")) complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
|
|
148
159
|
else complete(requestToken, secureStorage(method, args), completion)
|
|
@@ -648,9 +659,9 @@ internal class CraftNativeCapabilities(
|
|
|
648
659
|
}
|
|
649
660
|
}
|
|
650
661
|
|
|
651
|
-
private fun storage(method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
662
|
+
private fun storage(method: String, args: JSONArray): CraftNativeCapabilityResult = synchronized(CraftNativeStorage.lock) {
|
|
652
663
|
if (method == "clear") {
|
|
653
|
-
|
|
664
|
+
if (!CraftNativeStorage.commit(storage) { it.clear() }) return failure("STORAGE_ERROR", "Storage could not be cleared")
|
|
654
665
|
storeObserver?.invoke("storage", null, null)
|
|
655
666
|
return success(true)
|
|
656
667
|
}
|
|
@@ -666,13 +677,33 @@ internal class CraftNativeCapabilities(
|
|
|
666
677
|
if (args.length() != 2) return failure("INVALID_ARGUMENT", "Storage.set needs a key and value")
|
|
667
678
|
val value = jsonString(args.opt(1))
|
|
668
679
|
?: return failure("INVALID_ARGUMENT", "Storage values must be JSON serializable")
|
|
669
|
-
if (
|
|
680
|
+
if (CraftNativeStorage.commit(storage) { it.putString(key, value) }) {
|
|
670
681
|
storeObserver?.invoke("storage", key, value)
|
|
671
682
|
success(true)
|
|
672
683
|
}
|
|
673
684
|
else failure("STORAGE_ERROR", "Storage could not persist the value")
|
|
674
685
|
}
|
|
675
|
-
"
|
|
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")
|
|
676
707
|
else -> failure("UNKNOWN_ACTION", "Unsupported Storage method")
|
|
677
708
|
}
|
|
678
709
|
}
|
|
@@ -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
|
}
|