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.
Files changed (31) hide show
  1. package/README.md +71 -0
  2. package/dist/android/src/emulator.d.ts +23 -0
  3. package/dist/android/src/index.d.ts +7 -1
  4. package/dist/android/src/index.js +280 -98
  5. package/dist/android/templates/CraftBridge.kt.template +65 -54
  6. package/dist/android/templates/CraftNative.kt.template +27 -0
  7. package/dist/android/templates/CraftNativeCapabilities.kt.template +36 -5
  8. package/dist/android/templates/CraftNativeServices.kt.template +45 -35
  9. package/dist/android/templates/CraftNativeShell.kt.template +103 -20
  10. package/dist/android/templates/LocationRecordingService.kt.template +252 -46
  11. package/dist/android-adb.d.ts +2 -0
  12. package/dist/api/index.d.ts +1 -1
  13. package/dist/api/mobile.d.ts +49 -14
  14. package/dist/browser/storage.d.ts +3 -0
  15. package/dist/browser.js +163 -51
  16. package/dist/cli.js +1 -1
  17. package/dist/index.cjs +126 -12
  18. package/dist/index.js +126 -12
  19. package/dist/ios/src/index.d.ts +5 -0
  20. package/dist/ios/src/index.js +9 -1
  21. package/dist/ios/src/testing.d.ts +2 -0
  22. package/dist/ios/templates/CraftApp.swift +376 -88
  23. package/dist/ios/templates/CraftHeartRate.swift +141 -44
  24. package/dist/ios/templates/CraftNativeActions.swift +46 -48
  25. package/dist/ios/templates/CraftNativeScreen.swift +11 -4
  26. package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
  27. package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
  28. package/dist/ios/templates/testing/DriverTests.swift.template +8 -1
  29. package/dist/mobile-testing.js +30 -4
  30. package/dist/mobile.js +169 -17
  31. 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 Promise.resolve().then(function() { return JSON.parse(CraftAndroid.startLocationRecording(JSON.stringify(options || {}))); }); },
1777
- pauseRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.pauseLocationRecording()); }); },
1778
- resumeRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.resumeLocationRecording()); }); },
1779
- stopRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.stopLocationRecording()); }); },
1780
- getRecordingState: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.getLocationRecordingState()); }); },
1781
- readRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.readLocationRecording()); }); }
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 startLocationRecording(optionsJson: String): String {
2742
- // Null is "Zig declined". A permission-denied answer is not null — it
2743
- // is a state object saying so, and returning it here is the answer.
2744
- CraftNative.startLocationRecording(activity)?.let { return it }
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
- if (!hasForegroundLocationPermission()) {
2747
- return JSONObject().apply {
2748
- put("id", JSONObject.NULL)
2749
- put("active", false)
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 pauseLocationRecording(): String {
2766
- CraftNative.pauseLocationRecording(activity)?.let { return it }
2794
+ fun startLocationRecording(optionsJson: String): String {
2795
+ return legacyRecordingAction("startRecording", optionsJson)
2796
+ }
2767
2797
 
2768
- CraftLocationRecordingStore.setPaused(activity, true)
2769
- return CraftLocationRecordingStore.state(activity).toString()
2798
+ @JavascriptInterface
2799
+ fun pauseLocationRecording(): String {
2800
+ return legacyRecordingAction("pauseRecording")
2770
2801
  }
2771
2802
 
2772
2803
  @JavascriptInterface
2773
2804
  fun resumeLocationRecording(): String {
2774
- CraftNative.resumeLocationRecording(activity)?.let { return it }
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
- CraftNative.stopLocationRecording(activity)?.let { return it }
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
- // Null is "Zig declined", not "no recording" — an absent recording is
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
- CraftNative.readLocationRecording(activity)?.let { return it }
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" -> complete(requestToken, storage(method, args), completion)
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
- storage.edit().clear().commit()
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 (storage.edit().putString(key, value).commit()) {
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
- "remove" -> success(storage.edit().remove(key).commit().also { storeObserver?.invoke("storage", key, null) })
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")) add("geolocation")
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
- if (hasLocation()) return then()
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) then() else done(failure("PERMISSION_DENIED", "Location is not allowed")) }
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(fix(location)) else failure("POSITION_UNAVAILABLE", "No location fix")) }
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", fix(location)))
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" -> withLocation(done) {
422
- val options = args.optJSONObject(0) ?: JSONObject()
423
- val id = options.optString("id").ifBlank { UUID.randomUUID().toString() }
424
- store.start(context, id, System.currentTimeMillis())
425
- store.listener = { location -> CraftNativeHub.broadcast("CRAFT_EVENT", JSONObject().put("event", "craftLocationUpdate").put("detail", fix(location))) }
426
- val intent = Intent(context, com.craft.runtime.LocationRecordingService::class.java).setAction(com.craft.runtime.LocationRecordingService.ACTION_START)
427
- runCatching { ContextCompat.startForegroundService(context, intent) }
428
- .onFailure { store.stop(context) }
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
  }