@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,109 @@
1
+ package com.brickssoft.locationtracking.http
2
+
3
+ import com.brickssoft.locationtracking.config.HttpConfig
4
+ import com.brickssoft.locationtracking.model.Connectivity
5
+ import com.brickssoft.locationtracking.model.ConnectivityType
6
+ import com.brickssoft.locationtracking.model.RecordEvent
7
+
8
+ /**
9
+ * Decides what an automatic upload pass sends (architecture §2, "Priority records"; round 2 §4, `http.syncInterval`).
10
+ * Manual `sync()` bypasses it.
11
+ *
12
+ * - No `http.url`, or no connectivity: nothing.
13
+ * - A queued priority record (heartbeat, tracking_start, tracking_stop, providerchange) forces an upload, ignoring
14
+ * `autoSync`, `autoSyncThreshold` and `syncInterval`: only priority records on cellular with
15
+ * `disableAutoSyncOnCellular`, otherwise the whole queue.
16
+ * - Otherwise nothing if `autoSync` is off or cellular is restricted. Then, by [IntervalCheck]:
17
+ * - [IntervalCheck.OFF] (`syncInterval = 0`, or tracking off): the whole queue once it holds at least
18
+ * `autoSyncThreshold` records (0 = every record).
19
+ * - `syncInterval > 0` while tracking: the whole queue once the oldest pending normal record is at least
20
+ * `syncInterval` seconds old (age = now − `recorded_at`; a negative age, which a wall clock set back produces,
21
+ * counts as due), or once the queue holds `autoSyncThreshold` records when that is above 0 (a size cap that
22
+ * uploads earlier); nothing while a retry after a failed upload is pending.
23
+ */
24
+ internal object SyncPolicy {
25
+ /** The records an upload pass sends. */
26
+ enum class Scope {
27
+ NONE,
28
+ ALL,
29
+ PRIORITY_ONLY,
30
+ }
31
+
32
+ /** The state of the `syncInterval` rule for one pass (decided by the syncer). */
33
+ enum class IntervalCheck {
34
+ /** The rule does not apply (`syncInterval = 0`, `autoSync` off, or tracking off): threshold rule. */
35
+ OFF,
36
+
37
+ /** The oldest pending normal record is younger than `syncInterval`: only the size cap uploads. */
38
+ NOT_DUE,
39
+
40
+ /** The oldest pending normal record is due ([isIntervalDue]). */
41
+ DUE,
42
+
43
+ /** An automatic upload failed less than `syncInterval` ago: normal records wait for the retry time. */
44
+ RETRY_PENDING,
45
+ }
46
+
47
+ val PRIORITY_EVENTS: Set<RecordEvent> = RecordEvent.entries.filter { it.isPriority }.toSet()
48
+
49
+ /** The events that `autoSync`, `autoSyncThreshold` and `syncInterval` apply to. */
50
+ val NORMAL_EVENTS: Set<RecordEvent> = RecordEvent.entries.filterNot { it.isPriority }.toSet()
51
+
52
+ private const val MS_PER_SECOND = 1_000L
53
+
54
+ fun hasUrl(http: HttpConfig): Boolean = !http.url.isNullOrBlank()
55
+
56
+ /** True if [connectivity] is cellular and `disableAutoSyncOnCellular` is set. */
57
+ fun isCellularRestricted(http: HttpConfig, connectivity: Connectivity): Boolean =
58
+ http.disableAutoSyncOnCellular && connectivity.type == ConnectivityType.CELLULAR
59
+
60
+ /** True if normal records follow `syncInterval` (it is above 0 and `autoSync` is on). */
61
+ fun usesInterval(http: HttpConfig): Boolean = http.autoSync && http.syncInterval > 0
62
+
63
+ /** `syncInterval` in milliseconds (the validator keeps it at 0 or above). */
64
+ fun intervalMs(http: HttpConfig): Long = http.syncInterval * MS_PER_SECOND
65
+
66
+ /**
67
+ * True if the oldest pending normal record, created at [oldestRecordedAt], is due for upload at [now]: its age
68
+ * is at least `syncInterval`, or negative (the wall clock was set back after it was created).
69
+ */
70
+ fun isIntervalDue(http: HttpConfig, oldestRecordedAt: Long, now: Long): Boolean {
71
+ val age = now - oldestRecordedAt
72
+ return age < 0 || age >= intervalMs(http)
73
+ }
74
+
75
+ /**
76
+ * The wall time (epoch ms) at which the oldest pending normal record, created at [oldestRecordedAt], becomes due:
77
+ * `oldestRecordedAt + syncInterval`; null if it is already due at [now] (see [isIntervalDue]).
78
+ */
79
+ fun intervalDueAt(http: HttpConfig, oldestRecordedAt: Long, now: Long): Long? =
80
+ if (isIntervalDue(http, oldestRecordedAt, now)) null else oldestRecordedAt + intervalMs(http)
81
+
82
+ /**
83
+ * @param queued number of queued records.
84
+ * @param priorityQueued number of queued priority records.
85
+ * @param interval the state of the `syncInterval` rule; [IntervalCheck.OFF] applies the threshold rule.
86
+ */
87
+ fun autoScope(
88
+ http: HttpConfig,
89
+ connectivity: Connectivity,
90
+ queued: Int,
91
+ priorityQueued: Int,
92
+ interval: IntervalCheck = IntervalCheck.OFF,
93
+ ): Scope {
94
+ if (!hasUrl(http) || !connectivity.connected) return Scope.NONE
95
+ val restricted = isCellularRestricted(http, connectivity)
96
+ if (priorityQueued > 0) return if (restricted) Scope.PRIORITY_ONLY else Scope.ALL
97
+ if (restricted || !http.autoSync || queued <= 0) return Scope.NONE
98
+ val upload = when (interval) {
99
+ IntervalCheck.OFF -> queued >= maxOf(1, http.autoSyncThreshold)
100
+ IntervalCheck.NOT_DUE -> http.autoSyncThreshold > 0 && queued >= http.autoSyncThreshold
101
+ IntervalCheck.DUE -> true
102
+ IntervalCheck.RETRY_PENDING -> false
103
+ }
104
+ return if (upload) Scope.ALL else Scope.NONE
105
+ }
106
+
107
+ /** Records per request: `maxBatchSize` (at least 1) when `batchSync`, else 1. */
108
+ fun chunkSize(http: HttpConfig): Int = if (http.batchSync) maxOf(1, http.maxBatchSize) else 1
109
+ }
@@ -0,0 +1,143 @@
1
+ package com.brickssoft.locationtracking.http
2
+
3
+ import com.brickssoft.locationtracking.config.HttpConfig
4
+ import com.brickssoft.locationtracking.core.Iso8601
5
+ import com.brickssoft.locationtracking.core.Logger
6
+ import com.brickssoft.locationtracking.model.JsonUtil
7
+ import com.brickssoft.locationtracking.model.ProviderStateJson
8
+ import com.brickssoft.locationtracking.model.Record
9
+ import com.brickssoft.locationtracking.model.RecordEvent
10
+ import com.brickssoft.locationtracking.model.RecordJson
11
+ import org.json.JSONArray
12
+ import org.json.JSONObject
13
+ import org.json.JSONTokener
14
+
15
+ /**
16
+ * Renders `http.locationTemplate` / `http.geofenceTemplate` (architecture §2).
17
+ *
18
+ * Each `<%= name %>` placeholder (surrounding whitespace allowed) is replaced by a raw JSON literal: numbers and
19
+ * booleans bare, null as `null`, strings JSON-escaped but without quotes (templates write `"<%= uuid %>"`), and
20
+ * `extras` as JSON object text (`{}` when the record has none), and `record` as the default record object (with
21
+ * `sent_at`), so `"raw": <%= record %>` keeps every field, and `provider` as the record's provider object (or null).
22
+ * A null value in a placeholder that is exactly wrapped
23
+ * in quotes (`"<%= timestamp %>"`) replaces the quotes too, so it becomes JSON `null` rather than the string "null".
24
+ * An unknown placeholder becomes "" and logs a warning.
25
+ */
26
+ internal object TemplateRenderer {
27
+ private const val TAG = "LT.Template"
28
+ private const val NULL = "null"
29
+
30
+ /** A placeholder, with the quote directly before and after it when present. */
31
+ private val PLACEHOLDER = Regex("""("?)<%=\s*(.*?)\s*%>("?)""", RegexOption.DOT_MATCHES_ALL)
32
+
33
+ /** JSON literal text per placeholder; a resolver returns null for a null value. */
34
+ private val RESOLVERS: Map<String, (Record, Long) -> String?> = linkedMapOf(
35
+ "uuid" to { r, _ -> string(r.uuid) },
36
+ "event" to { r, _ -> string(r.event.wire) },
37
+ "timestamp" to { r, _ -> r.location?.let { string(Iso8601.format(it.time)) } },
38
+ "recorded_at" to { r, _ -> string(Iso8601.format(r.recordedAt)) },
39
+ "sent_at" to { _, sentAt -> string(Iso8601.format(sentAt)) },
40
+ "latitude" to { r, _ -> number(r.location?.latitude) },
41
+ "longitude" to { r, _ -> number(r.location?.longitude) },
42
+ "accuracy" to { r, _ -> number(r.location?.accuracy) },
43
+ "altitude" to { r, _ -> number(r.location?.altitude) },
44
+ "altitude_accuracy" to { r, _ -> number(r.location?.altitudeAccuracy) },
45
+ "speed" to { r, _ -> number(r.location?.speed) },
46
+ "speed_accuracy" to { r, _ -> number(r.location?.speedAccuracy) },
47
+ "heading" to { r, _ -> number(r.location?.heading) },
48
+ "heading_accuracy" to { r, _ -> number(r.location?.headingAccuracy) },
49
+ "is_moving" to { r, _ -> r.isMoving.toString() },
50
+ "odometer" to { r, _ -> number(r.odometer) },
51
+ "mock" to { r, _ -> (r.location?.isMock ?: false).toString() },
52
+ "activity.type" to { r, _ -> string(r.activity.type.wire) },
53
+ "activity.confidence" to { r, _ -> r.activity.confidence.toString() },
54
+ "battery.level" to { r, _ -> number(r.battery.level) },
55
+ "battery.is_charging" to { r, _ -> r.battery.isCharging.toString() },
56
+ "elapsed_realtime_ms" to { r, _ -> r.elapsedRealtimeMs.toString() },
57
+ "boot_count" to { r, _ -> r.bootCount.toString() },
58
+ "backend" to { r, _ -> r.backend?.let { string(it.wire) } },
59
+ "reason" to { r, _ -> r.reason?.let { string(it) } },
60
+ "geofence.identifier" to { r, _ -> r.geofence?.let { string(it.identifier) } },
61
+ "geofence.action" to { r, _ -> r.geofence?.let { string(it.action.name) } },
62
+ "provider.enabled" to { r, _ -> r.provider?.enabled?.toString() },
63
+ "provider.gps" to { r, _ -> r.provider?.gps?.toString() },
64
+ "provider.network" to { r, _ -> r.provider?.network?.toString() },
65
+ "provider.permission" to { r, _ -> r.provider?.let { string(it.permission.wire) } },
66
+ "extras" to { r, _ -> JsonUtil.parseObject(r.extras)?.toString() ?: "{}" },
67
+ "record" to { r, sentAt -> RecordJson.toJson(r, sentAt).toString() },
68
+ "provider" to { r, _ -> r.provider?.let { ProviderStateJson.toJson(it).toString() } },
69
+ )
70
+
71
+ /** Every supported placeholder name. */
72
+ val PLACEHOLDERS: Set<String> get() = RESOLVERS.keys
73
+
74
+ /**
75
+ * The template for [record]: `geofenceTemplate` for geofence records (falling back to `locationTemplate`),
76
+ * otherwise `locationTemplate`. Blank templates count as absent; null means the default shape.
77
+ */
78
+ fun templateFor(record: Record, http: HttpConfig): String? {
79
+ val location = http.locationTemplate?.takeIf { it.isNotBlank() }
80
+ if (record.event == RecordEvent.GEOFENCE) return http.geofenceTemplate?.takeIf { it.isNotBlank() } ?: location
81
+ return location
82
+ }
83
+
84
+ /**
85
+ * Renders [template] for [record] and parses the result. Returns a [JSONObject] or [JSONArray], or null (and
86
+ * logs an error) if the rendered text is not a valid JSON object or array; callers then use the default shape.
87
+ */
88
+ fun render(template: String, record: Record, sentAt: Long): Any? {
89
+ val text = renderText(template, record, sentAt)
90
+ val value = if (StrictJson.isValid(text)) parse(text) else null
91
+ if (value == null) {
92
+ Logger.e(
93
+ TAG,
94
+ "template for ${record.event.wire} record ${record.uuid} did not render to a JSON object or array; " +
95
+ "using the default shape. Template: ${template.take(200)}",
96
+ )
97
+ }
98
+ return value
99
+ }
100
+
101
+ /** Substitutes every placeholder of [template] (no JSON validation). */
102
+ fun renderText(template: String, record: Record, sentAt: Long): String {
103
+ val unknown = LinkedHashSet<String>()
104
+ val text = PLACEHOLDER.replace(template) { match ->
105
+ val (open, name, close) = match.destructured
106
+ val resolver = RESOLVERS[name]
107
+ if (resolver == null) {
108
+ unknown += name
109
+ return@replace open + close
110
+ }
111
+ val literal = resolver(record, sentAt)
112
+ when {
113
+ literal != null -> open + literal + close
114
+ open.isNotEmpty() && close.isNotEmpty() -> NULL
115
+ else -> open + NULL + close
116
+ }
117
+ }
118
+ if (unknown.isNotEmpty()) {
119
+ Logger.w(TAG, "unknown template placeholder(s) ${unknown.joinToString { "'$it'" }} replaced by \"\"")
120
+ }
121
+ return text
122
+ }
123
+
124
+ /** The JSON literal for placeholder [name] (`null` for a null value), or null if the name is unknown. */
125
+ fun literal(name: String, record: Record, sentAt: Long): String? {
126
+ val resolver = RESOLVERS[name] ?: return null
127
+ return resolver(record, sentAt) ?: NULL
128
+ }
129
+
130
+ private fun parse(text: String): Any? = try {
131
+ JSONTokener(text).nextValue().takeIf { it is JSONObject || it is JSONArray }
132
+ } catch (e: Exception) {
133
+ // e.g. a number out of range (org.json rejects infinities)
134
+ null
135
+ }
136
+
137
+ /** JSON string-content escaping, without the surrounding quotes. */
138
+ private fun string(value: String): String = JSONObject.quote(value).let { it.substring(1, it.length - 1) }
139
+
140
+ private fun number(value: Double?): String? = value?.takeIf { !it.isNaN() && !it.isInfinite() }?.toString()
141
+
142
+ private fun number(value: Float?): String? = number(value?.let { JsonUtil.floatToJson(it) })
143
+ }