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

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 +106 -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 +59 -4
@@ -0,0 +1,550 @@
1
+ package com.brickssoft.locationtracking.model
2
+
3
+ import com.brickssoft.locationtracking.core.ErrorCode
4
+ import com.brickssoft.locationtracking.core.Iso8601
5
+ import com.brickssoft.locationtracking.core.Logger
6
+ import com.brickssoft.locationtracking.core.TrackingEvent
7
+ import com.brickssoft.locationtracking.core.TrackingException
8
+ import com.brickssoft.locationtracking.permission.PermissionState
9
+ import com.brickssoft.locationtracking.permission.PermissionType
10
+ import org.json.JSONArray
11
+ import org.json.JSONException
12
+ import org.json.JSONObject
13
+
14
+ // Shared JSON codecs (SCAFFOLD). Every JS-facing shape and the HTTP wire format are built here.
15
+
16
+ /** Null-safe helpers around org.json (whose `opt*` methods return "" / 0 / "null" for missing or null values). */
17
+ object JsonUtil {
18
+ /** Parses JSON object text; null, blank or invalid text returns null. */
19
+ fun parseObject(text: String?): JSONObject? {
20
+ if (text.isNullOrBlank()) return null
21
+ return try {
22
+ JSONObject(text)
23
+ } catch (_: JSONException) {
24
+ null
25
+ }
26
+ }
27
+
28
+ /** String value, or null if the key is missing or JSON null. Non-string values are converted to text. */
29
+ fun optString(json: JSONObject, key: String): String? = if (json.isNull(key)) null else json.opt(key)?.toString()
30
+
31
+ /** Number value (or numeric string), or null if missing, JSON null or not numeric. */
32
+ fun optDouble(json: JSONObject, key: String): Double? {
33
+ if (json.isNull(key)) return null
34
+ return when (val v = json.opt(key)) {
35
+ is Number -> v.toDouble()
36
+ is String -> v.toDoubleOrNull()
37
+ else -> null
38
+ }?.takeIf { !it.isNaN() && !it.isInfinite() }
39
+ }
40
+
41
+ fun optFloat(json: JSONObject, key: String): Float? = optDouble(json, key)?.toFloat()
42
+
43
+ fun optLong(json: JSONObject, key: String): Long? = optDouble(json, key)?.toLong()
44
+
45
+ fun optInt(json: JSONObject, key: String): Int? = optDouble(json, key)?.toInt()
46
+
47
+ /** Boolean value (or "true"/"false" string), or null if missing, JSON null or not boolean. */
48
+ fun optBoolean(json: JSONObject, key: String): Boolean? {
49
+ if (json.isNull(key)) return null
50
+ return when (val v = json.opt(key)) {
51
+ is Boolean -> v
52
+ is String -> v.lowercase().toBooleanStrictOrNull()
53
+ else -> null
54
+ }
55
+ }
56
+
57
+ /** Converts a JSON object with scalar values to a string map (null values are dropped). */
58
+ fun toStringMap(json: JSONObject?): Map<String, String> {
59
+ if (json == null) return emptyMap()
60
+ val out = LinkedHashMap<String, String>()
61
+ val keys = json.keys()
62
+ while (keys.hasNext()) {
63
+ val key = keys.next()
64
+ optString(json, key)?.let { out[key] = it }
65
+ }
66
+ return out
67
+ }
68
+
69
+ fun fromStringMap(map: Map<String, String>): JSONObject {
70
+ val json = JSONObject()
71
+ for ((k, v) in map) json.put(k, v)
72
+ return json
73
+ }
74
+
75
+ fun toStringList(array: JSONArray?): List<String> {
76
+ if (array == null) return emptyList()
77
+ return (0 until array.length()).mapNotNull { i -> if (array.isNull(i)) null else array.opt(i)?.toString() }
78
+ }
79
+
80
+ /**
81
+ * Converts a Float to the Double with the same shortest decimal representation (5.2f -> 5.2, not
82
+ * 5.199999809265137), so JSON output is clean. Non-finite values become null.
83
+ */
84
+ fun floatToJson(value: Float): Double? = if (value.isNaN() || value.isInfinite()) null else value.toString().toDouble()
85
+
86
+ /** [floatToJson] for nullable input, returning [JSONObject.NULL] for null. */
87
+ fun floatOrNull(value: Float?): Any = value?.let { floatToJson(it) } ?: JSONObject.NULL
88
+
89
+ /** [value] or [JSONObject.NULL]. */
90
+ fun orNull(value: Any?): Any = value ?: JSONObject.NULL
91
+ }
92
+
93
+ /**
94
+ * Record <-> wire JSON (architecture §2). The same shape is sent to JS (without `sent_at`) and stored in SQLite.
95
+ */
96
+ object RecordJson {
97
+ private const val TAG = "LT.RecordJson"
98
+
99
+ /**
100
+ * Builds the wire shape. `timestamp` and `coords` are null when [Record.location] is null; `sent_at` is
101
+ * present only when [sentAt] is given; `extras`, `geofence`, `provider`, `reason` and `heartbeat` only when set.
102
+ */
103
+ fun toJson(record: Record, sentAt: Long? = null): JSONObject {
104
+ val location = record.location
105
+ val json = JSONObject()
106
+ json.put("uuid", record.uuid)
107
+ json.put("event", record.event.wire)
108
+ json.put("timestamp", location?.let { Iso8601.format(it.time) } ?: JSONObject.NULL)
109
+ json.put("recorded_at", Iso8601.format(record.recordedAt))
110
+ if (sentAt != null) json.put("sent_at", Iso8601.format(sentAt))
111
+ json.put("elapsed_realtime_ms", record.elapsedRealtimeMs)
112
+ json.put("boot_count", record.bootCount)
113
+ json.put("is_moving", record.isMoving)
114
+ json.put("odometer", record.odometer)
115
+ json.put("mock", location?.isMock ?: false)
116
+ json.put("coords", location?.let { coordsToJson(it) } ?: JSONObject.NULL)
117
+ json.put(
118
+ "activity",
119
+ JSONObject().put("type", record.activity.type.wire).put("confidence", record.activity.confidence),
120
+ )
121
+ json.put(
122
+ "battery",
123
+ JSONObject()
124
+ .put("level", JsonUtil.floatOrNull(record.battery.level))
125
+ .put("is_charging", record.battery.isCharging),
126
+ )
127
+ json.put("backend", record.backend?.wire ?: JSONObject.NULL)
128
+ if (record.extras != null) {
129
+ val extras = JsonUtil.parseObject(record.extras)
130
+ if (extras != null) json.put("extras", extras) else Logger.w(TAG, "dropping invalid extras of ${record.uuid}")
131
+ }
132
+ record.geofence?.let { json.put("geofence", geofenceHitToJson(it)) }
133
+ record.provider?.let { json.put("provider", ProviderStateJson.toJson(it)) }
134
+ record.reason?.let { json.put("reason", it) }
135
+ record.heartbeat?.let { json.put("heartbeat", heartbeatMetaToJson(it)) }
136
+ return json
137
+ }
138
+
139
+ /** The `heartbeat` object of a heartbeat record (snake_case keys, `next_at` ISO-8601 or null). */
140
+ fun heartbeatMetaToJson(meta: HeartbeatMeta): JSONObject = JSONObject()
141
+ .put("strategy", meta.strategy.wire)
142
+ .put("min_interval", meta.minInterval)
143
+ .put("max_interval", meta.maxInterval)
144
+ .put("next_at", JsonUtil.orNull(Iso8601.formatOrNull(meta.nextAt)))
145
+ .put("battery_exempt", meta.batteryExempt)
146
+ .put("device_idle", meta.deviceIdle)
147
+
148
+ /** Records as a JSON array, in the given order. */
149
+ fun toJsonArray(records: List<Record>, sentAt: Long? = null): JSONArray {
150
+ val array = JSONArray()
151
+ for (r in records) array.put(toJson(r, sentAt))
152
+ return array
153
+ }
154
+
155
+ /** The `coords` object of a location. */
156
+ fun coordsToJson(l: TrackedLocation): JSONObject = JSONObject()
157
+ .put("latitude", l.latitude)
158
+ .put("longitude", l.longitude)
159
+ .put("accuracy", JsonUtil.floatOrNull(l.accuracy))
160
+ .put("altitude", JsonUtil.orNull(l.altitude))
161
+ .put("altitude_accuracy", JsonUtil.floatOrNull(l.altitudeAccuracy))
162
+ .put("speed", JsonUtil.floatOrNull(l.speed))
163
+ .put("speed_accuracy", JsonUtil.floatOrNull(l.speedAccuracy))
164
+ .put("heading", JsonUtil.floatOrNull(l.heading))
165
+ .put("heading_accuracy", JsonUtil.floatOrNull(l.headingAccuracy))
166
+
167
+ fun geofenceHitToJson(hit: GeofenceHit): JSONObject {
168
+ val json = JSONObject().put("identifier", hit.identifier).put("action", hit.action.name)
169
+ JsonUtil.parseObject(hit.extras)?.let { json.put("extras", it) }
170
+ return json
171
+ }
172
+
173
+ /**
174
+ * Parses the wire shape (e.g. a SQLite row). `sent_at` is ignored. The non-wire location fields
175
+ * (`elapsedRealtimeNanos`, `provider`) come back as 0 / null; everything else round-trips.
176
+ *
177
+ * @throws JSONException if `uuid`, `event` or the coordinates are missing or invalid.
178
+ */
179
+ fun fromJson(json: JSONObject): Record {
180
+ val uuid = JsonUtil.optString(json, "uuid") ?: throw JSONException("record.uuid is missing")
181
+ val eventWire = JsonUtil.optString(json, "event")
182
+ val event = RecordEvent.fromWire(eventWire) ?: throw JSONException("unknown record.event '$eventWire'")
183
+ val recordedAt = Iso8601.parse(JsonUtil.optString(json, "recorded_at")) ?: 0L
184
+ val coords = json.optJSONObject("coords")
185
+ val location = coords?.let {
186
+ TrackedLocation(
187
+ latitude = JsonUtil.optDouble(it, "latitude") ?: throw JSONException("coords.latitude is missing"),
188
+ longitude = JsonUtil.optDouble(it, "longitude") ?: throw JSONException("coords.longitude is missing"),
189
+ accuracy = JsonUtil.optFloat(it, "accuracy") ?: 0f,
190
+ altitude = JsonUtil.optDouble(it, "altitude"),
191
+ altitudeAccuracy = JsonUtil.optFloat(it, "altitude_accuracy"),
192
+ speed = JsonUtil.optFloat(it, "speed"),
193
+ speedAccuracy = JsonUtil.optFloat(it, "speed_accuracy"),
194
+ heading = JsonUtil.optFloat(it, "heading"),
195
+ headingAccuracy = JsonUtil.optFloat(it, "heading_accuracy"),
196
+ time = Iso8601.parse(JsonUtil.optString(json, "timestamp")) ?: recordedAt,
197
+ isMock = JsonUtil.optBoolean(json, "mock") ?: false,
198
+ )
199
+ }
200
+ val activity = json.optJSONObject("activity")?.let {
201
+ ActivitySample(
202
+ type = ActivityType.fromWire(JsonUtil.optString(it, "type")) ?: ActivityType.UNKNOWN,
203
+ confidence = JsonUtil.optInt(it, "confidence") ?: 0,
204
+ )
205
+ } ?: ActivitySample.UNKNOWN
206
+ val battery = json.optJSONObject("battery")?.let {
207
+ BatterySnapshot(
208
+ level = JsonUtil.optFloat(it, "level") ?: -1f,
209
+ isCharging = JsonUtil.optBoolean(it, "is_charging") ?: false,
210
+ )
211
+ } ?: BatterySnapshot.UNKNOWN
212
+ return Record(
213
+ uuid = uuid,
214
+ event = event,
215
+ location = location,
216
+ recordedAt = recordedAt,
217
+ elapsedRealtimeMs = JsonUtil.optLong(json, "elapsed_realtime_ms") ?: 0L,
218
+ bootCount = JsonUtil.optInt(json, "boot_count") ?: -1,
219
+ isMoving = JsonUtil.optBoolean(json, "is_moving") ?: false,
220
+ odometer = JsonUtil.optDouble(json, "odometer") ?: 0.0,
221
+ activity = activity,
222
+ battery = battery,
223
+ backend = ProviderKind.fromWire(JsonUtil.optString(json, "backend")),
224
+ extras = json.optJSONObject("extras")?.toString(),
225
+ geofence = json.optJSONObject("geofence")?.let { geofenceHitFromJson(it) },
226
+ provider = json.optJSONObject("provider")?.let { ProviderStateJson.fromJson(it) },
227
+ reason = JsonUtil.optString(json, "reason"),
228
+ heartbeat = json.optJSONObject("heartbeat")?.let { heartbeatMetaFromJson(it) },
229
+ )
230
+ }
231
+
232
+ /** Lenient: an unknown strategy is dropped (null), missing numbers become 0 and missing flags false. */
233
+ private fun heartbeatMetaFromJson(json: JSONObject): HeartbeatMeta? {
234
+ val strategy = HeartbeatStrategy.fromWire(JsonUtil.optString(json, "strategy")) ?: return null
235
+ return HeartbeatMeta(
236
+ strategy = strategy,
237
+ minInterval = JsonUtil.optInt(json, "min_interval") ?: 0,
238
+ maxInterval = JsonUtil.optInt(json, "max_interval") ?: 0,
239
+ nextAt = Iso8601.parse(JsonUtil.optString(json, "next_at")),
240
+ batteryExempt = JsonUtil.optBoolean(json, "battery_exempt") ?: false,
241
+ deviceIdle = JsonUtil.optBoolean(json, "device_idle") ?: false,
242
+ )
243
+ }
244
+
245
+ private fun geofenceHitFromJson(json: JSONObject): GeofenceHit = GeofenceHit(
246
+ identifier = JsonUtil.optString(json, "identifier") ?: "",
247
+ action = GeofenceAction.fromWire(JsonUtil.optString(json, "action")) ?: GeofenceAction.ENTER,
248
+ extras = json.optJSONObject("extras")?.toString(),
249
+ )
250
+ }
251
+
252
+ /**
253
+ * Lossless (non-wire) JSON for a [TrackedLocation], for persisting e.g. `RuntimeState.lastLocation`.
254
+ * Keys use the Kotlin property names; `time` is epoch ms.
255
+ */
256
+ object TrackedLocationJson {
257
+ fun toJson(l: TrackedLocation): JSONObject = JSONObject()
258
+ .put("latitude", l.latitude)
259
+ .put("longitude", l.longitude)
260
+ .put("accuracy", JsonUtil.floatOrNull(l.accuracy))
261
+ .put("altitude", JsonUtil.orNull(l.altitude))
262
+ .put("altitudeAccuracy", JsonUtil.floatOrNull(l.altitudeAccuracy))
263
+ .put("speed", JsonUtil.floatOrNull(l.speed))
264
+ .put("speedAccuracy", JsonUtil.floatOrNull(l.speedAccuracy))
265
+ .put("heading", JsonUtil.floatOrNull(l.heading))
266
+ .put("headingAccuracy", JsonUtil.floatOrNull(l.headingAccuracy))
267
+ .put("time", l.time)
268
+ .put("elapsedRealtimeNanos", l.elapsedRealtimeNanos)
269
+ .put("provider", JsonUtil.orNull(l.provider))
270
+ .put("isMock", l.isMock)
271
+
272
+ /** @throws JSONException if latitude/longitude are missing. */
273
+ fun fromJson(json: JSONObject): TrackedLocation = TrackedLocation(
274
+ latitude = JsonUtil.optDouble(json, "latitude") ?: throw JSONException("latitude is missing"),
275
+ longitude = JsonUtil.optDouble(json, "longitude") ?: throw JSONException("longitude is missing"),
276
+ accuracy = JsonUtil.optFloat(json, "accuracy") ?: 0f,
277
+ altitude = JsonUtil.optDouble(json, "altitude"),
278
+ altitudeAccuracy = JsonUtil.optFloat(json, "altitudeAccuracy"),
279
+ speed = JsonUtil.optFloat(json, "speed"),
280
+ speedAccuracy = JsonUtil.optFloat(json, "speedAccuracy"),
281
+ heading = JsonUtil.optFloat(json, "heading"),
282
+ headingAccuracy = JsonUtil.optFloat(json, "headingAccuracy"),
283
+ time = JsonUtil.optLong(json, "time") ?: 0L,
284
+ elapsedRealtimeNanos = JsonUtil.optLong(json, "elapsedRealtimeNanos") ?: 0L,
285
+ provider = JsonUtil.optString(json, "provider"),
286
+ isMock = JsonUtil.optBoolean(json, "isMock") ?: false,
287
+ )
288
+ }
289
+
290
+ /** GeofenceSpec <-> JS `Geofence` (also used for SQLite rows). */
291
+ object GeofenceJson {
292
+ /**
293
+ * JS shape. latitude/longitude/radius are always present (for polygons: the enclosing circle);
294
+ * `vertices` (`[[lat,lng],...]`) and `extras` only when set.
295
+ */
296
+ fun toJson(g: GeofenceSpec): JSONObject {
297
+ val json = JSONObject()
298
+ json.put("identifier", g.identifier)
299
+ json.put("latitude", g.latitude)
300
+ json.put("longitude", g.longitude)
301
+ json.put("radius", JsonUtil.floatOrNull(g.radius))
302
+ g.vertices?.let { vertices ->
303
+ val array = JSONArray()
304
+ for (v in vertices) array.put(JSONArray().put(v.latitude).put(v.longitude))
305
+ json.put("vertices", array)
306
+ }
307
+ json.put("notifyOnEntry", g.notifyOnEntry)
308
+ json.put("notifyOnExit", g.notifyOnExit)
309
+ json.put("notifyOnDwell", g.notifyOnDwell)
310
+ json.put("loiteringDelay", g.loiteringDelay)
311
+ JsonUtil.parseObject(g.extras)?.let { json.put("extras", it) }
312
+ return json
313
+ }
314
+
315
+ fun toJsonArray(list: List<GeofenceSpec>): JSONArray {
316
+ val array = JSONArray()
317
+ for (g in list) array.put(toJson(g))
318
+ return array
319
+ }
320
+
321
+ /**
322
+ * Structural parsing only; semantic validation (vertex count, radius > 0, max 100) belongs to the
323
+ * GeofenceManager. A polygon's missing latitude/longitude/radius default to 0.
324
+ *
325
+ * @throws TrackingException INVALID_ARGUMENT if the identifier is missing, a circle lacks
326
+ * latitude/longitude/radius, or `vertices` is malformed.
327
+ */
328
+ fun fromJson(json: JSONObject): GeofenceSpec {
329
+ val identifier = JsonUtil.optString(json, "identifier")
330
+ if (identifier.isNullOrBlank()) invalid("geofence.identifier is required")
331
+ val vertices = if (json.isNull("vertices")) null else parseVertices(identifier, json.opt("vertices"))
332
+ val latitude = JsonUtil.optDouble(json, "latitude")
333
+ val longitude = JsonUtil.optDouble(json, "longitude")
334
+ val radius = JsonUtil.optDouble(json, "radius")
335
+ if (vertices == null && (latitude == null || longitude == null || radius == null)) {
336
+ invalid("geofence '$identifier' needs latitude, longitude and radius, or vertices")
337
+ }
338
+ return GeofenceSpec(
339
+ identifier = identifier,
340
+ latitude = latitude ?: 0.0,
341
+ longitude = longitude ?: 0.0,
342
+ radius = radius?.toFloat() ?: 0f,
343
+ vertices = vertices,
344
+ notifyOnEntry = JsonUtil.optBoolean(json, "notifyOnEntry") ?: true,
345
+ notifyOnExit = JsonUtil.optBoolean(json, "notifyOnExit") ?: true,
346
+ notifyOnDwell = JsonUtil.optBoolean(json, "notifyOnDwell") ?: false,
347
+ loiteringDelay = JsonUtil.optLong(json, "loiteringDelay") ?: 30_000L,
348
+ extras = json.optJSONObject("extras")?.toString(),
349
+ )
350
+ }
351
+
352
+ /** @throws TrackingException INVALID_ARGUMENT if any element is not a geofence object. */
353
+ fun fromJsonArray(array: JSONArray): List<GeofenceSpec> = (0 until array.length()).map { i ->
354
+ val item = array.optJSONObject(i) ?: invalid("geofences[$i] is not an object")
355
+ fromJson(item)
356
+ }
357
+
358
+ private fun parseVertices(identifier: String, value: Any?): List<LatLng> {
359
+ val array = value as? JSONArray ?: invalid("geofence '$identifier': vertices must be [[lat,lng],...]")
360
+ return (0 until array.length()).map { i ->
361
+ val pair = array.optJSONArray(i)
362
+ val lat = pair?.opt(0) as? Number
363
+ val lng = pair?.opt(1) as? Number
364
+ if (pair == null || pair.length() != 2 || lat == null || lng == null) {
365
+ invalid("geofence '$identifier': vertices[$i] must be [lat,lng]")
366
+ }
367
+ LatLng(lat.toDouble(), lng.toDouble())
368
+ }
369
+ }
370
+
371
+ private fun invalid(message: String): Nothing = throw TrackingException(ErrorCode.INVALID_ARGUMENT, message)
372
+ }
373
+
374
+ /** JS `ProviderState`. */
375
+ object ProviderStateJson {
376
+ fun toJson(s: ProviderState): JSONObject = JSONObject()
377
+ .put("enabled", s.enabled)
378
+ .put("gps", s.gps)
379
+ .put("network", s.network)
380
+ .put("permission", s.permission.wire)
381
+ .put("accuracy", s.accuracy.wire)
382
+ .put("backend", s.backend.wire)
383
+
384
+ /** Lenient: unknown enum values fall back to DENIED / NONE / ANDROID. */
385
+ fun fromJson(json: JSONObject): ProviderState = ProviderState(
386
+ enabled = JsonUtil.optBoolean(json, "enabled") ?: false,
387
+ gps = JsonUtil.optBoolean(json, "gps") ?: false,
388
+ network = JsonUtil.optBoolean(json, "network") ?: false,
389
+ permission = PermissionLevel.fromWire(JsonUtil.optString(json, "permission")) ?: PermissionLevel.DENIED,
390
+ accuracy = AccuracyLevel.fromWire(JsonUtil.optString(json, "accuracy")) ?: AccuracyLevel.NONE,
391
+ backend = ProviderKind.fromWire(JsonUtil.optString(json, "backend")) ?: ProviderKind.ANDROID,
392
+ )
393
+ }
394
+
395
+ /** JS `HeartbeatStatus` (times as ISO strings or null). */
396
+ object HeartbeatStatusJson {
397
+ fun toJson(s: HeartbeatStatus): JSONObject = JSONObject()
398
+ .put("enabled", s.enabled)
399
+ .put("minInterval", s.minInterval)
400
+ .put("maxInterval", s.maxInterval)
401
+ .put("lastRecordAt", JsonUtil.orNull(Iso8601.formatOrNull(s.lastRecordAt)))
402
+ .put("lastHeartbeatAt", JsonUtil.orNull(Iso8601.formatOrNull(s.lastHeartbeatAt)))
403
+ .put("nextHeartbeatAt", JsonUtil.orNull(Iso8601.formatOrNull(s.nextHeartbeatAt)))
404
+ .put("strategy", s.strategy.wire)
405
+ .put("canScheduleExactAlarms", s.canScheduleExactAlarms)
406
+ .put("isIgnoringBatteryOptimizations", s.isIgnoringBatteryOptimizations)
407
+ .put("isDeviceIdleMode", s.isDeviceIdleMode)
408
+ .put("isPowerSaveMode", s.isPowerSaveMode)
409
+ .put("pendingHeartbeats", s.pendingHeartbeats)
410
+ }
411
+
412
+ /** JS `DeviceInfo`. */
413
+ object DeviceInfoJson {
414
+ fun toJson(d: DeviceInfo): JSONObject {
415
+ val providers = JSONArray()
416
+ for (p in d.packagedProviders) providers.put(p)
417
+ return JSONObject()
418
+ .put("platform", d.platform)
419
+ .put("manufacturer", d.manufacturer)
420
+ .put("model", d.model)
421
+ .put("brand", d.brand)
422
+ .put("osVersion", d.osVersion)
423
+ .put("sdkInt", d.sdkInt)
424
+ .put("pluginVersion", d.pluginVersion)
425
+ .put("gmsAvailable", d.gmsAvailable)
426
+ .put("hmsAvailable", d.hmsAvailable)
427
+ .put("backend", d.backend.wire)
428
+ .put("packagedProviders", providers)
429
+ }
430
+ }
431
+
432
+ /** JS `Sensors`. */
433
+ object SensorsJson {
434
+ fun toJson(s: Sensors): JSONObject = JSONObject()
435
+ .put("accelerometer", s.accelerometer)
436
+ .put("gyroscope", s.gyroscope)
437
+ .put("magnetometer", s.magnetometer)
438
+ .put("significantMotion", s.significantMotion)
439
+ .put("stepCounter", s.stepCounter)
440
+ .put("stepDetector", s.stepDetector)
441
+ .put("barometer", s.barometer)
442
+ }
443
+
444
+ /** JS `PowerManagerInfo`. */
445
+ object PowerManagerInfoJson {
446
+ fun toJson(p: PowerManagerInfo): JSONObject =
447
+ JSONObject().put("manufacturer", p.manufacturer).put("available", p.available)
448
+ }
449
+
450
+ /** JS `BatteryOptimizationStatus`. */
451
+ object BatteryOptimizationStatusJson {
452
+ fun toJson(b: BatteryOptimizationStatus): JSONObject = JSONObject()
453
+ .put("isIgnoringBatteryOptimizations", b.isIgnoringBatteryOptimizations)
454
+ .put("canScheduleExactAlarms", b.canScheduleExactAlarms)
455
+ .put("isDeviceIdleMode", b.isDeviceIdleMode)
456
+ }
457
+
458
+ /** JS `PermissionStatus`: every [PermissionType] alias is present (missing entries are "prompt"). */
459
+ object PermissionStatusJson {
460
+ fun toJson(status: Map<PermissionType, PermissionState>): JSONObject {
461
+ val json = JSONObject()
462
+ for (type in PermissionType.entries) json.put(type.alias, (status[type] ?: PermissionState.PROMPT).js)
463
+ return json
464
+ }
465
+ }
466
+
467
+ /** JS `HttpEvent` shape. */
468
+ object HttpResultJson {
469
+ fun toJson(r: HttpResult): JSONObject {
470
+ val uuids = JSONArray()
471
+ for (u in r.uuids) uuids.put(u)
472
+ return JSONObject()
473
+ .put("success", r.success)
474
+ .put("status", r.status)
475
+ .put("responseText", r.responseText)
476
+ .put("uuids", uuids)
477
+ }
478
+ }
479
+
480
+ /** JS `ConnectivityChangeEvent` shape. */
481
+ object ConnectivityJson {
482
+ fun toJson(c: Connectivity): JSONObject = JSONObject().put("connected", c.connected).put("type", c.type.wire)
483
+ }
484
+
485
+ /** JS event name and payload of every [TrackingEvent] (see `LocationTrackingEventMap`). */
486
+ object EventJson {
487
+ const val LOCATION = "location"
488
+ const val MOTIONCHANGE = "motionchange"
489
+ const val ACTIVITYCHANGE = "activitychange"
490
+ const val PROVIDERCHANGE = "providerchange"
491
+ const val HEARTBEAT = "heartbeat"
492
+ const val GEOFENCE = "geofence"
493
+ const val GEOFENCESCHANGE = "geofenceschange"
494
+ const val HTTP = "http"
495
+ const val CONNECTIVITYCHANGE = "connectivitychange"
496
+ const val POWERSAVECHANGE = "powersavechange"
497
+ const val ENABLEDCHANGE = "enabledchange"
498
+ const val NOTIFICATIONACTION = "notificationaction"
499
+ const val AUTHORIZATION = "authorization"
500
+
501
+ fun name(event: TrackingEvent): String = when (event) {
502
+ is TrackingEvent.Location -> LOCATION
503
+ is TrackingEvent.MotionChange -> MOTIONCHANGE
504
+ is TrackingEvent.ActivityChange -> ACTIVITYCHANGE
505
+ is TrackingEvent.ProviderChange -> PROVIDERCHANGE
506
+ is TrackingEvent.Heartbeat -> HEARTBEAT
507
+ is TrackingEvent.Geofence -> GEOFENCE
508
+ is TrackingEvent.GeofencesChange -> GEOFENCESCHANGE
509
+ is TrackingEvent.Http -> HTTP
510
+ is TrackingEvent.ConnectivityChange -> CONNECTIVITYCHANGE
511
+ is TrackingEvent.PowerSaveChange -> POWERSAVECHANGE
512
+ is TrackingEvent.EnabledChange -> ENABLEDCHANGE
513
+ is TrackingEvent.NotificationAction -> NOTIFICATIONACTION
514
+ is TrackingEvent.Authorization -> AUTHORIZATION
515
+ }
516
+
517
+ fun payload(event: TrackingEvent): JSONObject = when (event) {
518
+ is TrackingEvent.Location -> RecordJson.toJson(event.record)
519
+ is TrackingEvent.MotionChange ->
520
+ JSONObject().put("isMoving", event.isMoving).put("location", RecordJson.toJson(event.record))
521
+ is TrackingEvent.ActivityChange ->
522
+ JSONObject().put("activity", event.activity.type.wire).put("confidence", event.activity.confidence)
523
+ is TrackingEvent.ProviderChange -> ProviderStateJson.toJson(event.state)
524
+ is TrackingEvent.Heartbeat -> JSONObject().put("location", RecordJson.toJson(event.record))
525
+ is TrackingEvent.Geofence -> {
526
+ val json = JSONObject()
527
+ .put("identifier", event.identifier)
528
+ .put("action", event.action.name)
529
+ .put("location", RecordJson.toJson(event.record))
530
+ JsonUtil.parseObject(event.extras)?.let { json.put("extras", it) }
531
+ json
532
+ }
533
+ is TrackingEvent.GeofencesChange -> {
534
+ val off = JSONArray()
535
+ for (id in event.off) off.put(id)
536
+ JSONObject().put("on", GeofenceJson.toJsonArray(event.on)).put("off", off)
537
+ }
538
+ is TrackingEvent.Http -> HttpResultJson.toJson(event.result)
539
+ is TrackingEvent.ConnectivityChange -> ConnectivityJson.toJson(event.connectivity)
540
+ is TrackingEvent.PowerSaveChange -> JSONObject().put("isPowerSaveMode", event.isPowerSaveMode)
541
+ is TrackingEvent.EnabledChange -> JSONObject().put("enabled", event.enabled)
542
+ is TrackingEvent.NotificationAction -> JSONObject().put("id", event.id)
543
+ is TrackingEvent.Authorization -> {
544
+ val json = JSONObject().put("success", event.success).put("status", event.status)
545
+ event.error?.let { json.put("error", it) }
546
+ JsonUtil.parseObject(event.responseJson)?.let { json.put("response", it) }
547
+ json
548
+ }
549
+ }
550
+ }
@@ -0,0 +1,106 @@
1
+ package com.brickssoft.locationtracking.model
2
+
3
+ /**
4
+ * Record type. [isPriority] records are uploaded immediately whenever `http.url` is set.
5
+ */
6
+ enum class RecordEvent(val wire: String, val isPriority: Boolean) {
7
+ LOCATION("location", false),
8
+ MOTIONCHANGE("motionchange", false),
9
+ CURRENT_POSITION("current_position", false),
10
+ WATCH_POSITION("watch_position", false),
11
+ HEARTBEAT("heartbeat", true),
12
+ GEOFENCE("geofence", false),
13
+ TRACKING_START("tracking_start", true),
14
+ TRACKING_STOP("tracking_stop", true),
15
+ PROVIDERCHANGE("providerchange", true),
16
+ ;
17
+
18
+ companion object {
19
+ fun fromWire(value: String?): RecordEvent? = entries.firstOrNull { it.wire == value }
20
+ }
21
+ }
22
+
23
+ enum class ActivityType(val wire: String) {
24
+ STILL("still"),
25
+ ON_FOOT("on_foot"),
26
+ WALKING("walking"),
27
+ RUNNING("running"),
28
+ ON_BICYCLE("on_bicycle"),
29
+ IN_VEHICLE("in_vehicle"),
30
+ UNKNOWN("unknown"),
31
+ ;
32
+
33
+ companion object {
34
+ fun fromWire(value: String?): ActivityType? = entries.firstOrNull { it.wire == value }
35
+ }
36
+ }
37
+
38
+ /** A detected activity with confidence 0..100. */
39
+ data class ActivitySample(val type: ActivityType, val confidence: Int) {
40
+ companion object {
41
+ val UNKNOWN = ActivitySample(ActivityType.UNKNOWN, 0)
42
+ }
43
+ }
44
+
45
+ /** Battery state; [level] is 0..1, or -1 if unknown. */
46
+ data class BatterySnapshot(val level: Float, val isCharging: Boolean) {
47
+ companion object {
48
+ val UNKNOWN = BatterySnapshot(-1f, false)
49
+ }
50
+ }
51
+
52
+ /** A native location backend; [wire] is the JS `LocationBackend` value. */
53
+ enum class ProviderKind(val wire: String) {
54
+ GMS("gms"),
55
+ HMS("hms"),
56
+ ANDROID("android"),
57
+ ;
58
+
59
+ companion object {
60
+ fun fromWire(value: String?): ProviderKind? = entries.firstOrNull { it.wire == value }
61
+ }
62
+ }
63
+
64
+ enum class GeofenceAction {
65
+ ENTER,
66
+ EXIT,
67
+ DWELL,
68
+ ;
69
+
70
+ companion object {
71
+ fun fromWire(value: String?): GeofenceAction? = entries.firstOrNull { it.name == value }
72
+ }
73
+ }
74
+
75
+ /** The `geofence` key of a geofence record; [extras] is JSON object text. */
76
+ data class GeofenceHit(val identifier: String, val action: GeofenceAction, val extras: String?)
77
+
78
+ /**
79
+ * A persisted record (location or audit). Serialized by [RecordJson] to the wire format.
80
+ *
81
+ * @property location the fix, or null if no location was ever known (audit records only).
82
+ * @property recordedAt record creation time, epoch ms.
83
+ * @property extras JSON object text, or null.
84
+ * @property geofence event `geofence` only.
85
+ * @property provider event `providerchange` only.
86
+ * @property reason `tracking_start` / `tracking_stop` only.
87
+ * @property heartbeat `heartbeat` only, and optional (round 2): the scheduling metadata of the heartbeat.
88
+ */
89
+ data class Record(
90
+ val uuid: String,
91
+ val event: RecordEvent,
92
+ val location: TrackedLocation?,
93
+ val recordedAt: Long,
94
+ val elapsedRealtimeMs: Long,
95
+ val bootCount: Int,
96
+ val isMoving: Boolean,
97
+ val odometer: Double,
98
+ val activity: ActivitySample,
99
+ val battery: BatterySnapshot,
100
+ val backend: ProviderKind?,
101
+ val extras: String? = null,
102
+ val geofence: GeofenceHit? = null,
103
+ val provider: ProviderState? = null,
104
+ val reason: String? = null,
105
+ val heartbeat: HeartbeatMeta? = null,
106
+ )