@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,574 @@
1
+ package com.brickssoft.locationtracking.config
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.Logger
7
+ import com.brickssoft.locationtracking.core.TrackingException
8
+ import com.brickssoft.locationtracking.model.DesiredAccuracy
9
+ import com.brickssoft.locationtracking.model.JsonUtil
10
+ import com.brickssoft.locationtracking.model.LocationProviderSetting
11
+ import org.json.JSONArray
12
+ import org.json.JSONObject
13
+
14
+ /**
15
+ * Config <-> JS JSON, and `State` -> JS.
16
+ *
17
+ * [parse] deep-merges a partial TS `Config` onto a base config:
18
+ * - a missing key keeps the base value;
19
+ * - JSON `null` resets that key (or whole group) to its default;
20
+ * - arrays (`notification.actions`) and map-valued objects (`headers`, `params`, `extras`, `refreshPayload`,
21
+ * `refreshHeaders`) replace the base value as a whole;
22
+ * - unknown keys are ignored with a warning;
23
+ * - a value of the wrong type, or an enum string that is not a wire value, throws
24
+ * [TrackingException] `INVALID_ARGUMENT` (nothing is applied).
25
+ *
26
+ * [parse] does not clamp; the store runs [ConfigValidator] on the result.
27
+ */
28
+ object ConfigJson {
29
+ private const val TAG = "LT.ConfigJson"
30
+
31
+ /** Applies the partial TS `Config` [json] on top of [base]. See the class KDoc for the merge rules. */
32
+ fun parse(json: JSONObject, base: Config = Config()): Config = parse(json, base, lenient = false)
33
+
34
+ /**
35
+ * Reads a config persisted with [toJson] onto the defaults. Unlike [parse], an invalid value (e.g. an enum
36
+ * value unknown to this version) is logged and replaced by its default, so one bad field cannot discard the
37
+ * whole stored config (URL, tokens, ...).
38
+ */
39
+ internal fun parseStored(json: JSONObject): Config = parse(json, Config(), lenient = true)
40
+
41
+ private fun parse(json: JSONObject, base: Config, lenient: Boolean): Config {
42
+ val r = JsonReader(json, "", lenient)
43
+ val d = Config()
44
+ val result = Config(
45
+ geolocation = r.group("geolocation", base.geolocation, d.geolocation, ::parseGeolocation),
46
+ activity = r.group("activity", base.activity, d.activity, ::parseActivity),
47
+ heartbeat = r.group("heartbeat", base.heartbeat, d.heartbeat, ::parseHeartbeat),
48
+ http = r.group("http", base.http, d.http, ::parseHttp),
49
+ persistence = r.group("persistence", base.persistence, d.persistence, ::parsePersistence),
50
+ app = r.group("app", base.app, d.app, ::parseApp),
51
+ notification = r.group("notification", base.notification, d.notification, ::parseNotification),
52
+ geofence = r.group("geofence", base.geofence, d.geofence, ::parseGeofence),
53
+ logger = r.group("logger", base.logger, d.logger, ::parseLogger),
54
+ backgroundPermissionRationale = r.group(
55
+ "backgroundPermissionRationale",
56
+ base.backgroundPermissionRationale,
57
+ d.backgroundPermissionRationale,
58
+ ::parseRationale,
59
+ ),
60
+ locationProvider = r.enumValue(
61
+ "locationProvider",
62
+ base.locationProvider,
63
+ d.locationProvider,
64
+ LocationProviderSetting.entries.map { it.wire },
65
+ LocationProviderSetting::fromWire,
66
+ ),
67
+ )
68
+ r.warnUnknownKeys()
69
+ return result
70
+ }
71
+
72
+ /** The full TS `Config`: every key present, defaults filled, unset optional values as JSON null. */
73
+ fun toJson(c: Config): JSONObject = JSONObject()
74
+ .put("geolocation", geolocationToJson(c.geolocation))
75
+ .put("activity", activityToJson(c.activity))
76
+ .put("heartbeat", heartbeatToJson(c.heartbeat))
77
+ .put("http", httpToJson(c.http))
78
+ .put("persistence", persistenceToJson(c.persistence))
79
+ .put("app", JSONObject().put("stopOnTerminate", c.app.stopOnTerminate).put("startOnBoot", c.app.startOnBoot))
80
+ .put("notification", notificationToJson(c.notification))
81
+ .put("geofence", JSONObject().put("initialTriggerEntry", c.geofence.initialTriggerEntry))
82
+ .put("logger", JSONObject().put("logLevel", c.logger.logLevel.wire).put("logMaxDays", c.logger.logMaxDays))
83
+ .put("backgroundPermissionRationale", rationaleToJson(c.backgroundPermissionRationale))
84
+ .put("locationProvider", c.locationProvider.wire)
85
+
86
+ /** JS `State`: runtime fields, the active backend, and the fully populated config. */
87
+ fun stateToJson(s: State): JSONObject = JSONObject()
88
+ .put("enabled", s.runtime.enabled)
89
+ .put("trackingMode", s.runtime.trackingMode.wire)
90
+ .put("isMoving", s.runtime.isMoving)
91
+ .put("odometer", finiteOr(s.runtime.odometer, 0.0))
92
+ .put("backend", s.backend.wire)
93
+ .put("lastRecordAt", JsonUtil.orNull(Iso8601.formatOrNull(s.runtime.lastRecordAt)))
94
+ .put("config", toJson(s.config))
95
+
96
+ // ---- parse
97
+
98
+ private fun parseGeolocation(r: JsonReader, c: GeolocationConfig): GeolocationConfig {
99
+ val d = GeolocationConfig()
100
+ return GeolocationConfig(
101
+ desiredAccuracy = r.enumValue(
102
+ "desiredAccuracy",
103
+ c.desiredAccuracy,
104
+ d.desiredAccuracy,
105
+ DesiredAccuracy.entries.map { it.wire },
106
+ DesiredAccuracy::fromWire,
107
+ ),
108
+ distanceFilter = r.double("distanceFilter", c.distanceFilter, d.distanceFilter),
109
+ locationUpdateInterval = r.long(
110
+ "locationUpdateInterval",
111
+ c.locationUpdateInterval,
112
+ d.locationUpdateInterval,
113
+ ),
114
+ fastestLocationUpdateInterval = r.long(
115
+ "fastestLocationUpdateInterval",
116
+ c.fastestLocationUpdateInterval,
117
+ d.fastestLocationUpdateInterval,
118
+ ),
119
+ disableElasticity = r.boolean("disableElasticity", c.disableElasticity, d.disableElasticity),
120
+ elasticityMultiplier = r.double("elasticityMultiplier", c.elasticityMultiplier, d.elasticityMultiplier),
121
+ stationaryRadius = r.double("stationaryRadius", c.stationaryRadius, d.stationaryRadius),
122
+ stopTimeout = r.int("stopTimeout", c.stopTimeout, d.stopTimeout),
123
+ stopAfterElapsedMinutes = r.int(
124
+ "stopAfterElapsedMinutes",
125
+ c.stopAfterElapsedMinutes,
126
+ d.stopAfterElapsedMinutes,
127
+ ),
128
+ stopOnStationary = r.boolean("stopOnStationary", c.stopOnStationary, d.stopOnStationary),
129
+ locationTimeout = r.long("locationTimeout", c.locationTimeout, d.locationTimeout),
130
+ filter = r.group("filter", c.filter, d.filter, ::parseFilter),
131
+ )
132
+ }
133
+
134
+ private fun parseFilter(r: JsonReader, c: LocationFilterConfig): LocationFilterConfig {
135
+ val d = LocationFilterConfig()
136
+ return LocationFilterConfig(
137
+ useKalman = r.boolean("useKalman", c.useKalman, d.useKalman),
138
+ trackingAccuracyThreshold = r.double(
139
+ "trackingAccuracyThreshold",
140
+ c.trackingAccuracyThreshold,
141
+ d.trackingAccuracyThreshold,
142
+ ),
143
+ maxImpliedSpeed = r.double("maxImpliedSpeed", c.maxImpliedSpeed, d.maxImpliedSpeed),
144
+ odometerAccuracyThreshold = r.double(
145
+ "odometerAccuracyThreshold",
146
+ c.odometerAccuracyThreshold,
147
+ d.odometerAccuracyThreshold,
148
+ ),
149
+ allowIdenticalLocations = r.boolean(
150
+ "allowIdenticalLocations",
151
+ c.allowIdenticalLocations,
152
+ d.allowIdenticalLocations,
153
+ ),
154
+ rejectMockLocations = r.boolean("rejectMockLocations", c.rejectMockLocations, d.rejectMockLocations),
155
+ )
156
+ }
157
+
158
+ private fun parseActivity(r: JsonReader, c: ActivityConfig): ActivityConfig {
159
+ val d = ActivityConfig()
160
+ return ActivityConfig(
161
+ disableMotionActivityUpdates = r.boolean(
162
+ "disableMotionActivityUpdates",
163
+ c.disableMotionActivityUpdates,
164
+ d.disableMotionActivityUpdates,
165
+ ),
166
+ activityRecognitionInterval = r.long(
167
+ "activityRecognitionInterval",
168
+ c.activityRecognitionInterval,
169
+ d.activityRecognitionInterval,
170
+ ),
171
+ minimumActivityRecognitionConfidence = r.int(
172
+ "minimumActivityRecognitionConfidence",
173
+ c.minimumActivityRecognitionConfidence,
174
+ d.minimumActivityRecognitionConfidence,
175
+ ),
176
+ motionTriggerDelay = r.long("motionTriggerDelay", c.motionTriggerDelay, d.motionTriggerDelay),
177
+ disableStopDetection = r.boolean("disableStopDetection", c.disableStopDetection, d.disableStopDetection),
178
+ )
179
+ }
180
+
181
+ private fun parseHeartbeat(r: JsonReader, c: HeartbeatConfig): HeartbeatConfig {
182
+ val d = HeartbeatConfig()
183
+ return HeartbeatConfig(
184
+ enabled = r.boolean("enabled", c.enabled, d.enabled),
185
+ minInterval = r.int("minInterval", c.minInterval, d.minInterval),
186
+ maxInterval = r.int("maxInterval", c.maxInterval, d.maxInterval),
187
+ )
188
+ }
189
+
190
+ private fun parseHttp(r: JsonReader, c: HttpConfig): HttpConfig {
191
+ val d = HttpConfig()
192
+ return HttpConfig(
193
+ url = r.nullableString("url", c.url, d.url),
194
+ method = r.enumValue(
195
+ "method",
196
+ c.method,
197
+ d.method,
198
+ HttpMethod.entries.map { it.wire },
199
+ HttpMethod::fromWire,
200
+ ),
201
+ headers = r.stringMap("headers", c.headers, d.headers),
202
+ params = r.objectText("params", c.params, d.params),
203
+ autoSync = r.boolean("autoSync", c.autoSync, d.autoSync),
204
+ autoSyncThreshold = r.int("autoSyncThreshold", c.autoSyncThreshold, d.autoSyncThreshold),
205
+ syncInterval = r.int("syncInterval", c.syncInterval, d.syncInterval),
206
+ batchSync = r.boolean("batchSync", c.batchSync, d.batchSync),
207
+ maxBatchSize = r.int("maxBatchSize", c.maxBatchSize, d.maxBatchSize),
208
+ disableAutoSyncOnCellular = r.boolean(
209
+ "disableAutoSyncOnCellular",
210
+ c.disableAutoSyncOnCellular,
211
+ d.disableAutoSyncOnCellular,
212
+ ),
213
+ rootProperty = r.string("rootProperty", c.rootProperty, d.rootProperty),
214
+ locationTemplate = r.nullableString("locationTemplate", c.locationTemplate, d.locationTemplate),
215
+ geofenceTemplate = r.nullableString("geofenceTemplate", c.geofenceTemplate, d.geofenceTemplate),
216
+ timeout = r.long("timeout", c.timeout, d.timeout),
217
+ authorization = r.group<AuthorizationConfig?>("authorization", c.authorization, d.authorization) { ar, a ->
218
+ parseAuthorization(ar, a ?: AuthorizationConfig())
219
+ },
220
+ )
221
+ }
222
+
223
+ private fun parseAuthorization(r: JsonReader, c: AuthorizationConfig): AuthorizationConfig {
224
+ val d = AuthorizationConfig()
225
+ return AuthorizationConfig(
226
+ strategy = r.enumValue("strategy", c.strategy, d.strategy, listOf(d.strategy)) { value ->
227
+ d.strategy.takeIf { it.equals(value, ignoreCase = true) }
228
+ },
229
+ accessToken = r.nullableString("accessToken", c.accessToken, d.accessToken),
230
+ refreshToken = r.nullableString("refreshToken", c.refreshToken, d.refreshToken),
231
+ refreshUrl = r.nullableString("refreshUrl", c.refreshUrl, d.refreshUrl),
232
+ refreshPayload = r.objectText("refreshPayload", c.refreshPayload, d.refreshPayload),
233
+ refreshHeaders = r.stringMap("refreshHeaders", c.refreshHeaders, d.refreshHeaders),
234
+ refreshPayloadEncoding = r.enumValue(
235
+ "refreshPayloadEncoding",
236
+ c.refreshPayloadEncoding,
237
+ d.refreshPayloadEncoding,
238
+ RefreshPayloadEncoding.entries.map { it.wire },
239
+ RefreshPayloadEncoding::fromWire,
240
+ ),
241
+ expires = r.long("expires", c.expires, d.expires),
242
+ )
243
+ }
244
+
245
+ private fun parsePersistence(r: JsonReader, c: PersistenceConfig): PersistenceConfig {
246
+ val d = PersistenceConfig()
247
+ return PersistenceConfig(
248
+ maxDaysToPersist = r.int("maxDaysToPersist", c.maxDaysToPersist, d.maxDaysToPersist),
249
+ maxRecordsToPersist = r.int("maxRecordsToPersist", c.maxRecordsToPersist, d.maxRecordsToPersist),
250
+ extras = r.objectText("extras", c.extras, d.extras),
251
+ )
252
+ }
253
+
254
+ private fun parseApp(r: JsonReader, c: AppConfig): AppConfig {
255
+ val d = AppConfig()
256
+ return AppConfig(
257
+ stopOnTerminate = r.boolean("stopOnTerminate", c.stopOnTerminate, d.stopOnTerminate),
258
+ startOnBoot = r.boolean("startOnBoot", c.startOnBoot, d.startOnBoot),
259
+ )
260
+ }
261
+
262
+ private fun parseNotification(r: JsonReader, c: NotificationConfig): NotificationConfig {
263
+ val d = NotificationConfig()
264
+ return NotificationConfig(
265
+ title = r.nullableString("title", c.title, d.title),
266
+ text = r.string("text", c.text, d.text),
267
+ smallIcon = r.string("smallIcon", c.smallIcon, d.smallIcon),
268
+ largeIcon = r.nullableString("largeIcon", c.largeIcon, d.largeIcon),
269
+ color = r.nullableString("color", c.color, d.color),
270
+ priority = r.enumValue(
271
+ "priority",
272
+ c.priority,
273
+ d.priority,
274
+ NotificationPriority.entries.map { it.wire },
275
+ NotificationPriority::fromWire,
276
+ ),
277
+ channelId = r.string("channelId", c.channelId, d.channelId),
278
+ channelName = r.string("channelName", c.channelName, d.channelName),
279
+ actions = r.array("actions", c.actions, d.actions, ::parseAction),
280
+ resume = r.group("resume", c.resume, d.resume, ::parseResume),
281
+ )
282
+ }
283
+
284
+ private fun parseResume(r: JsonReader, c: ResumeNotificationConfig): ResumeNotificationConfig {
285
+ val d = ResumeNotificationConfig()
286
+ return ResumeNotificationConfig(
287
+ enabled = r.boolean("enabled", c.enabled, d.enabled),
288
+ title = r.nullableString("title", c.title, d.title),
289
+ text = r.string("text", c.text, d.text),
290
+ channelName = r.string("channelName", c.channelName, d.channelName),
291
+ )
292
+ }
293
+
294
+ private fun parseAction(item: Any, path: String): NotificationActionButton {
295
+ val obj = item as? JSONObject ?: JsonReader.invalid("$path must be an object {id, label}")
296
+ val r = JsonReader(obj, path)
297
+ val id = r.nullableString("id", null, null)
298
+ val label = r.nullableString("label", null, null)
299
+ if (id.isNullOrBlank()) JsonReader.invalid("$path.id is required")
300
+ if (label == null) JsonReader.invalid("$path.label is required")
301
+ r.warnUnknownKeys()
302
+ return NotificationActionButton(id = id, label = label)
303
+ }
304
+
305
+ private fun parseGeofence(r: JsonReader, c: GeofenceConfig): GeofenceConfig {
306
+ val d = GeofenceConfig()
307
+ return GeofenceConfig(
308
+ initialTriggerEntry = r.boolean("initialTriggerEntry", c.initialTriggerEntry, d.initialTriggerEntry),
309
+ )
310
+ }
311
+
312
+ private fun parseLogger(r: JsonReader, c: LoggerConfig): LoggerConfig {
313
+ val d = LoggerConfig()
314
+ return LoggerConfig(
315
+ logLevel = r.enumValue(
316
+ "logLevel",
317
+ c.logLevel,
318
+ d.logLevel,
319
+ LogLevel.entries.map { it.wire },
320
+ LogLevel::fromWire,
321
+ ),
322
+ logMaxDays = r.int("logMaxDays", c.logMaxDays, d.logMaxDays),
323
+ )
324
+ }
325
+
326
+ private fun parseRationale(r: JsonReader, c: BackgroundPermissionRationale): BackgroundPermissionRationale {
327
+ val d = BackgroundPermissionRationale()
328
+ return BackgroundPermissionRationale(
329
+ title = r.nullableString("title", c.title, d.title),
330
+ message = r.nullableString("message", c.message, d.message),
331
+ positiveAction = r.nullableString("positiveAction", c.positiveAction, d.positiveAction),
332
+ negativeAction = r.nullableString("negativeAction", c.negativeAction, d.negativeAction),
333
+ )
334
+ }
335
+
336
+ // ---- toJson
337
+
338
+ private fun geolocationToJson(g: GeolocationConfig): JSONObject = JSONObject()
339
+ .put("desiredAccuracy", g.desiredAccuracy.wire)
340
+ .put("distanceFilter", finiteOr(g.distanceFilter, GeolocationConfig().distanceFilter))
341
+ .put("locationUpdateInterval", g.locationUpdateInterval)
342
+ .put("fastestLocationUpdateInterval", g.fastestLocationUpdateInterval)
343
+ .put("disableElasticity", g.disableElasticity)
344
+ .put("elasticityMultiplier", finiteOr(g.elasticityMultiplier, GeolocationConfig().elasticityMultiplier))
345
+ .put("stationaryRadius", finiteOr(g.stationaryRadius, GeolocationConfig().stationaryRadius))
346
+ .put("stopTimeout", g.stopTimeout)
347
+ .put("stopAfterElapsedMinutes", g.stopAfterElapsedMinutes)
348
+ .put("stopOnStationary", g.stopOnStationary)
349
+ .put("locationTimeout", g.locationTimeout)
350
+ .put("filter", filterToJson(g.filter))
351
+
352
+ private fun filterToJson(f: LocationFilterConfig): JSONObject {
353
+ val d = LocationFilterConfig()
354
+ return JSONObject()
355
+ .put("useKalman", f.useKalman)
356
+ .put("trackingAccuracyThreshold", finiteOr(f.trackingAccuracyThreshold, d.trackingAccuracyThreshold))
357
+ .put("maxImpliedSpeed", finiteOr(f.maxImpliedSpeed, d.maxImpliedSpeed))
358
+ .put("odometerAccuracyThreshold", finiteOr(f.odometerAccuracyThreshold, d.odometerAccuracyThreshold))
359
+ .put("allowIdenticalLocations", f.allowIdenticalLocations)
360
+ .put("rejectMockLocations", f.rejectMockLocations)
361
+ }
362
+
363
+ private fun activityToJson(a: ActivityConfig): JSONObject = JSONObject()
364
+ .put("disableMotionActivityUpdates", a.disableMotionActivityUpdates)
365
+ .put("activityRecognitionInterval", a.activityRecognitionInterval)
366
+ .put("minimumActivityRecognitionConfidence", a.minimumActivityRecognitionConfidence)
367
+ .put("motionTriggerDelay", a.motionTriggerDelay)
368
+ .put("disableStopDetection", a.disableStopDetection)
369
+
370
+ private fun heartbeatToJson(h: HeartbeatConfig): JSONObject = JSONObject()
371
+ .put("enabled", h.enabled)
372
+ .put("minInterval", h.minInterval)
373
+ .put("maxInterval", h.maxInterval)
374
+
375
+ private fun httpToJson(h: HttpConfig): JSONObject = JSONObject()
376
+ .put("url", JsonUtil.orNull(h.url))
377
+ .put("method", h.method.wire)
378
+ .put("headers", JsonUtil.fromStringMap(h.headers))
379
+ .put("params", objectOrEmpty(h.params, "http.params"))
380
+ .put("autoSync", h.autoSync)
381
+ .put("autoSyncThreshold", h.autoSyncThreshold)
382
+ .put("syncInterval", h.syncInterval)
383
+ .put("batchSync", h.batchSync)
384
+ .put("maxBatchSize", h.maxBatchSize)
385
+ .put("disableAutoSyncOnCellular", h.disableAutoSyncOnCellular)
386
+ .put("rootProperty", h.rootProperty)
387
+ .put("locationTemplate", JsonUtil.orNull(h.locationTemplate))
388
+ .put("geofenceTemplate", JsonUtil.orNull(h.geofenceTemplate))
389
+ .put("timeout", h.timeout)
390
+ .put("authorization", h.authorization?.let { authorizationToJson(it) } ?: JSONObject.NULL)
391
+
392
+ private fun authorizationToJson(a: AuthorizationConfig): JSONObject = JSONObject()
393
+ .put("strategy", a.strategy)
394
+ .put("accessToken", JsonUtil.orNull(a.accessToken))
395
+ .put("refreshToken", JsonUtil.orNull(a.refreshToken))
396
+ .put("refreshUrl", JsonUtil.orNull(a.refreshUrl))
397
+ .put("refreshPayload", objectOrEmpty(a.refreshPayload, "http.authorization.refreshPayload"))
398
+ .put("refreshHeaders", JsonUtil.fromStringMap(a.refreshHeaders))
399
+ .put("refreshPayloadEncoding", a.refreshPayloadEncoding.wire)
400
+ .put("expires", a.expires)
401
+
402
+ private fun persistenceToJson(p: PersistenceConfig): JSONObject = JSONObject()
403
+ .put("maxDaysToPersist", p.maxDaysToPersist)
404
+ .put("maxRecordsToPersist", p.maxRecordsToPersist)
405
+ .put("extras", objectOrEmpty(p.extras, "persistence.extras"))
406
+
407
+ private fun notificationToJson(n: NotificationConfig): JSONObject {
408
+ val actions = JSONArray()
409
+ for (a in n.actions) actions.put(JSONObject().put("id", a.id).put("label", a.label))
410
+ return JSONObject()
411
+ .put("title", JsonUtil.orNull(n.title))
412
+ .put("text", n.text)
413
+ .put("smallIcon", n.smallIcon)
414
+ .put("largeIcon", JsonUtil.orNull(n.largeIcon))
415
+ .put("color", JsonUtil.orNull(n.color))
416
+ .put("priority", n.priority.wire)
417
+ .put("channelId", n.channelId)
418
+ .put("channelName", n.channelName)
419
+ .put("actions", actions)
420
+ .put(
421
+ "resume",
422
+ JSONObject()
423
+ .put("enabled", n.resume.enabled)
424
+ .put("title", JsonUtil.orNull(n.resume.title))
425
+ .put("text", n.resume.text)
426
+ .put("channelName", n.resume.channelName),
427
+ )
428
+ }
429
+
430
+ private fun rationaleToJson(b: BackgroundPermissionRationale): JSONObject = JSONObject()
431
+ .put("title", JsonUtil.orNull(b.title))
432
+ .put("message", JsonUtil.orNull(b.message))
433
+ .put("positiveAction", JsonUtil.orNull(b.positiveAction))
434
+ .put("negativeAction", JsonUtil.orNull(b.negativeAction))
435
+
436
+ /** Parses stored JSON object text; invalid text (only possible through `update{}`) becomes `{}`. */
437
+ private fun objectOrEmpty(text: String, path: String): JSONObject {
438
+ JsonUtil.parseObject(text)?.let { return it }
439
+ if (text.isNotBlank()) Logger.w(TAG, "$path is not a JSON object; emitting {}")
440
+ return JSONObject()
441
+ }
442
+
443
+ /** org.json rejects NaN/Infinity; such values (only possible through `update{}`) fall back to [fallback]. */
444
+ private fun finiteOr(value: Double, fallback: Double): Double = if (value.isFinite()) value else fallback
445
+ }
446
+
447
+ /**
448
+ * Typed reader over one JSON object of a partial config. Each accessor returns the current value when the key is
449
+ * missing, the default when it is JSON null, and the parsed value otherwise; wrong types throw INVALID_ARGUMENT,
450
+ * or, when [lenient], are logged and keep the current value. Every key an accessor asks for is remembered, so
451
+ * [warnUnknownKeys] can report the rest.
452
+ */
453
+ private class JsonReader(
454
+ private val json: JSONObject,
455
+ private val path: String,
456
+ private val lenient: Boolean = false,
457
+ ) {
458
+ private val known = HashSet<String>()
459
+
460
+ fun boolean(key: String, current: Boolean, default: Boolean): Boolean =
461
+ read(key, current, default, "a boolean") { v ->
462
+ when (v) {
463
+ is Boolean -> v
464
+ is String -> v.trim().lowercase().toBooleanStrictOrNull()
465
+ else -> null
466
+ }
467
+ }
468
+
469
+ fun double(key: String, current: Double, default: Double): Double =
470
+ read(key, current, default, "a number") { toDouble(it) }
471
+
472
+ fun long(key: String, current: Long, default: Long): Long =
473
+ read(key, current, default, "a number") { v ->
474
+ when (v) {
475
+ is Int, is Long, is Short, is Byte -> (v as Number).toLong()
476
+ else -> toDouble(v)?.toLong()
477
+ }
478
+ }
479
+
480
+ fun int(key: String, current: Int, default: Int): Int =
481
+ read(key, current, default, "a number") { v ->
482
+ when (v) {
483
+ is Int, is Short, is Byte -> (v as Number).toInt()
484
+ else -> toDouble(v)?.toInt()
485
+ }
486
+ }
487
+
488
+ fun string(key: String, current: String, default: String): String =
489
+ read(key, current, default, "a string") { it as? String }
490
+
491
+ fun nullableString(key: String, current: String?, default: String?): String? =
492
+ read(key, current, default, "a string") { it as? String }
493
+
494
+ fun <E> enumValue(key: String, current: E, default: E, allowed: List<String>, fromWire: (String) -> E?): E =
495
+ read(key, current, default, "one of ${allowed.joinToString("|") { "'$it'" }}") { v ->
496
+ (v as? String)?.let(fromWire)
497
+ }
498
+
499
+ /** An object of scalar values, replaced as a whole (null values are dropped). */
500
+ fun stringMap(key: String, current: Map<String, String>, default: Map<String, String>): Map<String, String> =
501
+ read(key, current, default, "an object") { v -> (v as? JSONObject)?.let { JsonUtil.toStringMap(it) } }
502
+
503
+ /** An arbitrary object, replaced as a whole and kept as JSON text. */
504
+ fun objectText(key: String, current: String, default: String): String =
505
+ read(key, current, default, "an object") { v -> (v as? JSONObject)?.toString() }
506
+
507
+ /** An array, replaced as a whole; [item] parses one non-null element (given its path) or throws. */
508
+ fun <T> array(key: String, current: List<T>, default: List<T>, item: (Any, String) -> T): List<T> =
509
+ read(key, current, default, "an array") { v ->
510
+ (v as? JSONArray)?.let { array ->
511
+ (0 until array.length()).map { i ->
512
+ val element = array.opt(i)
513
+ val itemPath = "${pathOf(key)}[$i]"
514
+ if (element == null || element == JSONObject.NULL) invalid("$itemPath must not be null")
515
+ item(element, itemPath)
516
+ }
517
+ }
518
+ }
519
+
520
+ /** A nested group: merged key by key onto [current] by [parse]; null resets it to [default]. */
521
+ fun <T> group(key: String, current: T, default: T, parse: (JsonReader, T) -> T): T =
522
+ read(key, current, default, "an object") { v ->
523
+ (v as? JSONObject)?.let { obj ->
524
+ val nested = JsonReader(obj, pathOf(key), lenient)
525
+ val result = parse(nested, current)
526
+ nested.warnUnknownKeys()
527
+ result
528
+ }
529
+ }
530
+
531
+ fun warnUnknownKeys() {
532
+ val keys = json.keys()
533
+ while (keys.hasNext()) {
534
+ val key = keys.next()
535
+ if (key !in known) Logger.w(TAG, "ignoring unknown config key '${pathOf(key)}'")
536
+ }
537
+ }
538
+
539
+ private inline fun <T> read(key: String, current: T, default: T, expected: String, convert: (Any) -> T?): T {
540
+ known += key
541
+ if (!json.has(key)) return current
542
+ val raw = json.opt(key)
543
+ if (raw == null || raw == JSONObject.NULL) return default
544
+ if (!lenient) return convert(raw) ?: invalid("${pathOf(key)} must be $expected (got ${describe(raw)})")
545
+ return try {
546
+ convert(raw) ?: invalid("${pathOf(key)} must be $expected (got ${describe(raw)})")
547
+ } catch (e: TrackingException) {
548
+ Logger.w(TAG, "ignoring invalid stored value: ${e.message}")
549
+ current
550
+ }
551
+ }
552
+
553
+ private fun pathOf(key: String): String = if (path.isEmpty()) key else "$path.$key"
554
+
555
+ private fun toDouble(v: Any): Double? = when (v) {
556
+ is Number -> v.toDouble()
557
+ is String -> v.trim().toDoubleOrNull()
558
+ else -> null
559
+ }?.takeIf { it.isFinite() }
560
+
561
+ private fun describe(raw: Any): String = when (raw) {
562
+ is String -> "'${raw.take(MAX_ECHO)}'"
563
+ is JSONObject -> "an object"
564
+ is JSONArray -> "an array"
565
+ else -> raw.toString().take(MAX_ECHO)
566
+ }
567
+
568
+ companion object {
569
+ private const val TAG = "LT.ConfigJson"
570
+ private const val MAX_ECHO = 40
571
+
572
+ fun invalid(message: String): Nothing = throw TrackingException(ErrorCode.INVALID_ARGUMENT, "config.$message")
573
+ }
574
+ }
@@ -0,0 +1,31 @@
1
+ package com.brickssoft.locationtracking.config
2
+
3
+ import kotlinx.coroutines.flow.StateFlow
4
+ import org.json.JSONObject
5
+
6
+ /**
7
+ * Persistent config and runtime state (implemented by `SharedPrefsConfigStore`, prefs file
8
+ * `location_tracking_prefs`, loaded synchronously in the constructor). All methods are thread-safe and
9
+ * persist before returning.
10
+ */
11
+ interface ConfigStore {
12
+ val config: StateFlow<Config>
13
+ val runtime: StateFlow<RuntimeState>
14
+
15
+ /**
16
+ * `ready()`: with [reset] true, config = defaults + [json]; with false, [json] is applied only on the very
17
+ * first ready (runtime.didReady false), otherwise the persisted config wins.
18
+ */
19
+ fun ready(json: JSONObject?, reset: Boolean): Config
20
+
21
+ /** `setConfig()`: deep merge; arrays replaced; null resets the key to its default. */
22
+ fun merge(json: JSONObject): Config
23
+
24
+ /** `reset()`: defaults + [json]. */
25
+ fun reset(json: JSONObject?): Config
26
+
27
+ /** Internal writers (e.g. JWT refresh). */
28
+ fun update(transform: (Config) -> Config): Config
29
+
30
+ fun updateRuntime(transform: (RuntimeState) -> RuntimeState): RuntimeState
31
+ }