@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.1

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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +107 -0
  4. package/android/consumer-rules.pro +42 -0
  5. package/android/src/main/AndroidManifest.xml +52 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
  7. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
  12. package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
  15. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
  17. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
  20. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
  23. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
  30. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
  34. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
  37. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
  39. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
  40. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
  42. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
  43. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
  46. package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
  51. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
  53. package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
  54. package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
  55. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
  56. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
  57. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
  58. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
  59. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
  60. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
  61. package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
  62. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
  63. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
  64. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
  65. package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
  66. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
  67. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
  68. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
  69. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
  70. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
  71. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
  72. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
  73. package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
  74. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
  75. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
  76. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
  77. package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
  78. package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
  79. package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
  80. package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
  81. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
  82. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
  83. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
  84. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
  85. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
  86. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
  87. package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
  88. package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
  89. package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
  90. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
  91. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
  92. package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
  93. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
  94. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
  95. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
  96. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
  97. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
  98. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
  99. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
  100. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
  101. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
  102. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
  103. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
  104. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
  105. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
  106. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
  107. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
  108. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
  109. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
  110. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
  111. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
  112. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
  113. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
  114. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
  115. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
  116. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
  117. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
  118. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
  119. package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
  120. package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
  121. package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
  122. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
  123. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
  124. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
  125. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
  126. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
  127. package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
  128. package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
  129. package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
  130. package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
  131. package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
  132. package/android/src/main/res/values/lt_strings.xml +11 -0
  133. package/android/src/main/res/xml/lt_file_paths.xml +5 -0
  134. package/dist/esm/definitions.d.ts +719 -0
  135. package/dist/esm/definitions.js +2 -0
  136. package/dist/esm/definitions.js.map +1 -0
  137. package/dist/esm/events.d.ts +50 -0
  138. package/dist/esm/events.js +52 -0
  139. package/dist/esm/events.js.map +1 -0
  140. package/dist/esm/index.d.ts +3 -0
  141. package/dist/esm/index.js +5 -0
  142. package/dist/esm/index.js.map +1 -0
  143. package/dist/esm/plugin.d.ts +3 -0
  144. package/dist/esm/plugin.js +10 -0
  145. package/dist/esm/plugin.js.map +1 -0
  146. package/dist/esm/web/config.d.ts +19 -0
  147. package/dist/esm/web/config.js +243 -0
  148. package/dist/esm/web/config.js.map +1 -0
  149. package/dist/esm/web/device.d.ts +9 -0
  150. package/dist/esm/web/device.js +90 -0
  151. package/dist/esm/web/device.js.map +1 -0
  152. package/dist/esm/web/errors.d.ts +14 -0
  153. package/dist/esm/web/errors.js +19 -0
  154. package/dist/esm/web/errors.js.map +1 -0
  155. package/dist/esm/web/geolocation.d.ts +26 -0
  156. package/dist/esm/web/geolocation.js +117 -0
  157. package/dist/esm/web/geolocation.js.map +1 -0
  158. package/dist/esm/web/location.d.ts +7 -0
  159. package/dist/esm/web/location.js +70 -0
  160. package/dist/esm/web/location.js.map +1 -0
  161. package/dist/esm/web.d.ts +164 -0
  162. package/dist/esm/web.js +328 -0
  163. package/dist/esm/web.js.map +1 -0
  164. package/dist/plugin.cjs.js +945 -0
  165. package/dist/plugin.cjs.js.map +1 -0
  166. package/dist/plugin.js +948 -0
  167. package/dist/plugin.js.map +1 -0
  168. package/package.json +60 -4
@@ -0,0 +1,41 @@
1
+ package com.brickssoft.locationtracking.bridge
2
+
3
+ import com.brickssoft.locationtracking.core.ErrorCode
4
+ import com.brickssoft.locationtracking.core.Logger
5
+ import com.brickssoft.locationtracking.core.TrackingException
6
+ import com.getcapacitor.JSObject
7
+ import com.getcapacitor.PluginCall
8
+ import org.json.JSONObject
9
+
10
+ /** A promise rejection: `call.reject(message, code.name)`. */
11
+ internal data class Rejection(val message: String, val code: ErrorCode) {
12
+ companion object {
13
+ /** [TrackingException] keeps its code; anything else is [ErrorCode.INTERNAL]. */
14
+ fun of(error: Throwable): Rejection {
15
+ val message = error.message?.takeIf { it.isNotBlank() }
16
+ return when (error) {
17
+ is TrackingException -> Rejection(message ?: error.code.name, error.code)
18
+ else -> Rejection(message ?: error.javaClass.simpleName, ErrorCode.INTERNAL)
19
+ }
20
+ }
21
+ }
22
+ }
23
+
24
+ /** Settles a [PluginCall] with a handler result (null = `void`) or error. */
25
+ internal object CallResponder {
26
+ private const val TAG = "LT.Bridge"
27
+
28
+ fun resolve(call: PluginCall, result: JSONObject?) {
29
+ if (result == null) call.resolve() else call.resolve(JSObject.fromJSONObject(result))
30
+ }
31
+
32
+ fun reject(call: PluginCall, error: Throwable) {
33
+ val rejection = Rejection.of(error)
34
+ if (rejection.code == ErrorCode.INTERNAL) {
35
+ Logger.e(TAG, "${call.methodName} failed", error)
36
+ } else {
37
+ Logger.d(TAG, "${call.methodName} rejected: ${rejection.code} ${rejection.message}")
38
+ }
39
+ call.reject(rejection.message, rejection.code.name)
40
+ }
41
+ }
@@ -0,0 +1,281 @@
1
+ package com.brickssoft.locationtracking.bridge
2
+
3
+ import com.brickssoft.locationtracking.core.ErrorCode
4
+ import com.brickssoft.locationtracking.core.Iso8601
5
+ import com.brickssoft.locationtracking.core.LogLevel
6
+ import com.brickssoft.locationtracking.core.TrackingException
7
+ import com.brickssoft.locationtracking.logging.LogQuery
8
+ import com.brickssoft.locationtracking.model.DesiredAccuracy
9
+ import com.brickssoft.locationtracking.model.GeofenceJson
10
+ import com.brickssoft.locationtracking.model.GeofenceSpec
11
+ import com.brickssoft.locationtracking.model.RecordEvent
12
+ import com.brickssoft.locationtracking.permission.PermissionType
13
+ import com.brickssoft.locationtracking.position.CurrentPositionOptions
14
+ import com.brickssoft.locationtracking.position.WatchPositionOptions
15
+ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
16
+ import org.json.JSONArray
17
+ import org.json.JSONObject
18
+ import kotlin.math.floor
19
+
20
+ /** `uploadLog` options; [paramsJson] is JSON object text. */
21
+ internal data class UploadLogOptions(val url: String, val headers: Map<String, String>, val paramsJson: String?)
22
+
23
+ /**
24
+ * Parses and validates JS call options into Kotlin option types.
25
+ *
26
+ * A missing key or JSON null means "use the default". A present value of the wrong type or out of range throws
27
+ * [TrackingException] with [ErrorCode.INVALID_ARGUMENT]. Numbers must be JSON numbers (no numeric strings) and
28
+ * booleans must be JSON booleans.
29
+ */
30
+ internal object OptionParsers {
31
+ /** `requestPermissions` default: every type, in the order of the JS contract (background last). */
32
+ val DEFAULT_PERMISSION_ORDER: List<PermissionType> = listOf(
33
+ PermissionType.LOCATION,
34
+ PermissionType.NOTIFICATIONS,
35
+ PermissionType.ACTIVITY_RECOGNITION,
36
+ PermissionType.BACKGROUND_LOCATION,
37
+ )
38
+
39
+ private val LATITUDE = -90.0..90.0
40
+ private val LONGITUDE = -180.0..180.0
41
+ private val HEADING = 0.0..360.0
42
+
43
+ // ---- typed options
44
+
45
+ /** JS `CurrentPositionOptions`. */
46
+ fun currentPositionOptions(json: JSONObject): CurrentPositionOptions {
47
+ val defaults = CurrentPositionOptions()
48
+ return CurrentPositionOptions(
49
+ samples = optInt(json, "samples", min = 1) ?: defaults.samples,
50
+ timeoutMs = optLong(json, "timeout", min = 1),
51
+ maximumAgeMs = optLong(json, "maximumAge", min = 0) ?: defaults.maximumAgeMs,
52
+ desiredAccuracy = optAccuracy(json) ?: defaults.desiredAccuracy,
53
+ persist = optBoolean(json, "persist") ?: defaults.persist,
54
+ extras = optObject(json, "extras")?.toString(),
55
+ )
56
+ }
57
+
58
+ /** JS `WatchPositionOptions`. */
59
+ fun watchPositionOptions(json: JSONObject): WatchPositionOptions {
60
+ val defaults = WatchPositionOptions()
61
+ return WatchPositionOptions(
62
+ intervalMs = optLong(json, "interval", min = 0) ?: defaults.intervalMs,
63
+ desiredAccuracy = optAccuracy(json) ?: defaults.desiredAccuracy,
64
+ persist = optBoolean(json, "persist") ?: defaults.persist,
65
+ extras = optObject(json, "extras")?.toString(),
66
+ )
67
+ }
68
+
69
+ /**
70
+ * JS `Geofence` -> [GeofenceSpec] via [GeofenceJson], plus value checks: coordinates in range, a circle's
71
+ * radius > 0, a polygon's >= 3 vertices, loiteringDelay >= 0, and typed notify flags and extras.
72
+ */
73
+ fun geofence(json: JSONObject): GeofenceSpec {
74
+ // GeofenceJson is lenient (numeric strings, any identifier coerced to text); enforce JSON types first.
75
+ val id = requireString(json, "identifier", "geofence.identifier")
76
+ for (key in listOf("latitude", "longitude", "radius")) optNumber(json, key, "geofence '$id' $key")
77
+ val spec = GeofenceJson.fromJson(json)
78
+ for (key in listOf("notifyOnEntry", "notifyOnExit", "notifyOnDwell")) {
79
+ optBoolean(json, key, "geofence '$id' $key")
80
+ }
81
+ optObject(json, "extras", "geofence '$id' extras")
82
+ optLong(json, "loiteringDelay", min = 0, path = "geofence '$id' loiteringDelay")
83
+ val vertices = spec.vertices
84
+ if (vertices == null) {
85
+ checkRange(spec.latitude, LATITUDE, "geofence '$id' latitude")
86
+ checkRange(spec.longitude, LONGITUDE, "geofence '$id' longitude")
87
+ if (!(spec.radius > 0f) || spec.radius.isInfinite()) invalid("geofence '$id' radius must be > 0")
88
+ } else {
89
+ if (vertices.size < 3) invalid("geofence '$id' needs at least 3 vertices")
90
+ vertices.forEachIndexed { i, v ->
91
+ checkRange(v.latitude, LATITUDE, "geofence '$id' vertices[$i] latitude")
92
+ checkRange(v.longitude, LONGITUDE, "geofence '$id' vertices[$i] longitude")
93
+ }
94
+ }
95
+ return spec
96
+ }
97
+
98
+ /** The required `geofences` array of `addGeofences`. */
99
+ fun geofences(json: JSONObject): List<GeofenceSpec> {
100
+ val array = requireArray(json, "geofences")
101
+ return (0 until array.length()).map { i ->
102
+ val item = array.opt(i) as? JSONObject ?: invalid("geofences[$i] must be an object")
103
+ geofence(item)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * The required `location` (JS `InsertLocationInput`) of `insertLocation`, validated and returned as is for
109
+ * `RecordFactory.fromExternal`.
110
+ */
111
+ fun insertLocationInput(json: JSONObject): JSONObject {
112
+ val location = requireObject(json, "location")
113
+ val coords = requireObject(location, "coords", "location.coords")
114
+ requireNumber(coords, "latitude", "location.coords.latitude").also {
115
+ checkRange(it, LATITUDE, "location.coords.latitude")
116
+ }
117
+ requireNumber(coords, "longitude", "location.coords.longitude").also {
118
+ checkRange(it, LONGITUDE, "location.coords.longitude")
119
+ }
120
+ optNumber(coords, "altitude", "location.coords.altitude")
121
+ for (key in listOf("accuracy", "altitude_accuracy", "speed", "speed_accuracy", "heading_accuracy")) {
122
+ val value = optNumber(coords, key, "location.coords.$key")
123
+ if (value != null && value < 0) invalid("location.coords.$key must be >= 0")
124
+ }
125
+ optNumber(coords, "heading", "location.coords.heading")?.let {
126
+ checkRange(it, HEADING, "location.coords.heading")
127
+ }
128
+ optString(location, "timestamp", "location.timestamp")?.let {
129
+ if (Iso8601.parse(it) == null) invalid("location.timestamp must be an ISO-8601 date, got '$it'")
130
+ }
131
+ optString(location, "event", "location.event")?.let {
132
+ if (RecordEvent.fromWire(it) == null) invalid("location.event '$it' is not a record event")
133
+ }
134
+ optBoolean(location, "is_moving", "location.is_moving")
135
+ optObject(location, "extras", "location.extras")
136
+ return location
137
+ }
138
+
139
+ /** JS `LogQuery`; `order` defaults to ascending. */
140
+ fun logQuery(json: JSONObject): LogQuery {
141
+ val start = optLong(json, "start", min = 0)
142
+ val end = optLong(json, "end", min = 0)
143
+ if (start != null && end != null && start > end) invalid("'start' must not be after 'end'")
144
+ val level = optString(json, "level")?.let { LogLevel.fromWire(it) ?: invalid("unknown log level '$it'") }
145
+ val ascending = when (val order = optString(json, "order")?.lowercase()) {
146
+ null, "asc" -> true
147
+ "desc" -> false
148
+ else -> invalid("'order' must be 'asc' or 'desc', got '$order'")
149
+ }
150
+ val limit = optInt(json, "limit", min = 0)
151
+ return LogQuery(start = start, end = end, level = level, limit = limit, ascending = ascending)
152
+ }
153
+
154
+ /** The required `level` of `log()`: any [LogLevel] except OFF. */
155
+ fun logLevel(json: JSONObject): LogLevel {
156
+ val wire = requireString(json, "level")
157
+ val level = LogLevel.fromWire(wire)
158
+ if (level == null || level == LogLevel.OFF) invalid("'level' must be error, warn, info, debug or verbose")
159
+ return level
160
+ }
161
+
162
+ /**
163
+ * `requestPermissions({permissions?})`: missing -> [DEFAULT_PERMISSION_ORDER]; otherwise the given types,
164
+ * de-duplicated, in that canonical order.
165
+ */
166
+ fun permissionTypes(json: JSONObject): List<PermissionType> {
167
+ val array = optArray(json, "permissions") ?: return DEFAULT_PERMISSION_ORDER
168
+ val requested = (0 until array.length()).map { i ->
169
+ val alias = array.opt(i)
170
+ PermissionType.fromAlias(alias as? String) ?: invalid("unknown permission type '$alias'")
171
+ }.toSet()
172
+ return DEFAULT_PERMISSION_ORDER.filter { it in requested }
173
+ }
174
+
175
+ /** `uploadLog({url, headers?, params?})`: url must be http(s). */
176
+ fun uploadLogOptions(json: JSONObject): UploadLogOptions {
177
+ val url = requireString(json, "url")
178
+ if (url.toHttpUrlOrNull() == null) invalid("'url' must be an http(s) URL, got '$url'")
179
+ return UploadLogOptions(url, stringMap(json, "headers"), optObject(json, "params")?.toString())
180
+ }
181
+
182
+ /**
183
+ * An optional object of header-like values; strings, numbers and booleans are converted to text,
184
+ * null values are dropped.
185
+ */
186
+ fun stringMap(json: JSONObject, key: String): Map<String, String> {
187
+ val obj = optObject(json, key) ?: return emptyMap()
188
+ val out = LinkedHashMap<String, String>()
189
+ val keys = obj.keys()
190
+ while (keys.hasNext()) {
191
+ val name = keys.next()
192
+ if (obj.isNull(name)) continue
193
+ when (val value = obj.opt(name)) {
194
+ is String, is Number, is Boolean -> out[name] = value.toString()
195
+ else -> invalid("'$key.$name' must be a string")
196
+ }
197
+ }
198
+ return out
199
+ }
200
+
201
+ // ---- primitives (path = the name used in error messages)
202
+
203
+ fun optObject(json: JSONObject, key: String, path: String = key): JSONObject? = when (val v = value(json, key)) {
204
+ null -> null
205
+ is JSONObject -> v
206
+ else -> invalid("'$path' must be an object")
207
+ }
208
+
209
+ fun requireObject(json: JSONObject, key: String, path: String = key): JSONObject =
210
+ optObject(json, key, path) ?: invalid("'$path' is required")
211
+
212
+ fun optArray(json: JSONObject, key: String, path: String = key): JSONArray? = when (val v = value(json, key)) {
213
+ null -> null
214
+ is JSONArray -> v
215
+ else -> invalid("'$path' must be an array")
216
+ }
217
+
218
+ fun requireArray(json: JSONObject, key: String, path: String = key): JSONArray =
219
+ optArray(json, key, path) ?: invalid("'$path' is required")
220
+
221
+ fun optString(json: JSONObject, key: String, path: String = key): String? = when (val v = value(json, key)) {
222
+ null -> null
223
+ is String -> v
224
+ else -> invalid("'$path' must be a string")
225
+ }
226
+
227
+ /** A required, non-blank string. */
228
+ fun requireString(json: JSONObject, key: String, path: String = key): String {
229
+ val v = optString(json, key, path) ?: invalid("'$path' is required")
230
+ if (v.isBlank()) invalid("'$path' must not be empty")
231
+ return v
232
+ }
233
+
234
+ fun optBoolean(json: JSONObject, key: String, path: String = key): Boolean? = when (val v = value(json, key)) {
235
+ null -> null
236
+ is Boolean -> v
237
+ else -> invalid("'$path' must be a boolean")
238
+ }
239
+
240
+ fun requireBoolean(json: JSONObject, key: String, path: String = key): Boolean =
241
+ optBoolean(json, key, path) ?: invalid("'$path' is required")
242
+
243
+ /** A finite number. */
244
+ fun optNumber(json: JSONObject, key: String, path: String = key): Double? = when (val v = value(json, key)) {
245
+ null -> null
246
+ is Number -> v.toDouble().takeIf { !it.isNaN() && !it.isInfinite() }
247
+ ?: invalid("'$path' must be a finite number")
248
+ else -> invalid("'$path' must be a number")
249
+ }
250
+
251
+ fun requireNumber(json: JSONObject, key: String, path: String = key): Double =
252
+ optNumber(json, key, path) ?: invalid("'$path' is required")
253
+
254
+ /** A number >= [min], truncated to whole milliseconds (or any other whole unit). */
255
+ fun optLong(json: JSONObject, key: String, min: Long, path: String = key): Long? {
256
+ val v = optNumber(json, key, path) ?: return null
257
+ if (v < min) invalid("'$path' must be >= $min")
258
+ if (v >= Long.MAX_VALUE.toDouble()) invalid("'$path' is too large")
259
+ return v.toLong()
260
+ }
261
+
262
+ /** A whole number in [min, [Int.MAX_VALUE]]. */
263
+ fun optInt(json: JSONObject, key: String, min: Int, path: String = key): Int? {
264
+ val v = optNumber(json, key, path) ?: return null
265
+ if (v != floor(v)) invalid("'$path' must be a whole number")
266
+ if (v < min || v > Int.MAX_VALUE) invalid("'$path' must be between $min and ${Int.MAX_VALUE}")
267
+ return v.toInt()
268
+ }
269
+
270
+ private fun optAccuracy(json: JSONObject): DesiredAccuracy? = optString(json, "desiredAccuracy")?.let {
271
+ DesiredAccuracy.fromWire(it) ?: invalid("'desiredAccuracy' must be high, balanced, low or passive, got '$it'")
272
+ }
273
+
274
+ private fun checkRange(value: Double, range: ClosedFloatingPointRange<Double>, path: String) {
275
+ if (value !in range) invalid("'$path' must be within ${range.start}..${range.endInclusive}, got $value")
276
+ }
277
+
278
+ private fun value(json: JSONObject, key: String): Any? = if (json.isNull(key)) null else json.opt(key)
279
+
280
+ fun invalid(message: String): Nothing = throw TrackingException(ErrorCode.INVALID_ARGUMENT, message)
281
+ }