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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +107 -0
  4. package/android/consumer-rules.pro +42 -0
  5. package/android/src/main/AndroidManifest.xml +52 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
  7. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
  12. package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
  15. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
  17. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
  20. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
  23. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
  30. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
  34. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
  37. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
  39. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
  40. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
  42. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
  43. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
  46. package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
  51. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
  53. package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
  54. package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
  55. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
  56. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
  57. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
  58. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
  59. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
  60. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
  61. package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
  62. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
  63. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
  64. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
  65. package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
  66. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
  67. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
  68. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
  69. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
  70. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
  71. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
  72. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
  73. package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
  74. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
  75. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
  76. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
  77. package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
  78. package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
  79. package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
  80. package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
  81. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
  82. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
  83. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
  84. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
  85. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
  86. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
  87. package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
  88. package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
  89. package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
  90. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
  91. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
  92. package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
  93. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
  94. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
  95. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
  96. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
  97. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
  98. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
  99. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
  100. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
  101. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
  102. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
  103. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
  104. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
  105. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
  106. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
  107. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
  108. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
  109. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
  110. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
  111. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
  112. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
  113. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
  114. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
  115. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
  116. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
  117. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
  118. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
  119. package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
  120. package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
  121. package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
  122. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
  123. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
  124. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
  125. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
  126. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
  127. package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
  128. package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
  129. package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
  130. package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
  131. package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
  132. package/android/src/main/res/values/lt_strings.xml +11 -0
  133. package/android/src/main/res/xml/lt_file_paths.xml +5 -0
  134. package/dist/esm/definitions.d.ts +719 -0
  135. package/dist/esm/definitions.js +2 -0
  136. package/dist/esm/definitions.js.map +1 -0
  137. package/dist/esm/events.d.ts +50 -0
  138. package/dist/esm/events.js +52 -0
  139. package/dist/esm/events.js.map +1 -0
  140. package/dist/esm/index.d.ts +3 -0
  141. package/dist/esm/index.js +5 -0
  142. package/dist/esm/index.js.map +1 -0
  143. package/dist/esm/plugin.d.ts +3 -0
  144. package/dist/esm/plugin.js +10 -0
  145. package/dist/esm/plugin.js.map +1 -0
  146. package/dist/esm/web/config.d.ts +19 -0
  147. package/dist/esm/web/config.js +243 -0
  148. package/dist/esm/web/config.js.map +1 -0
  149. package/dist/esm/web/device.d.ts +9 -0
  150. package/dist/esm/web/device.js +90 -0
  151. package/dist/esm/web/device.js.map +1 -0
  152. package/dist/esm/web/errors.d.ts +14 -0
  153. package/dist/esm/web/errors.js +19 -0
  154. package/dist/esm/web/errors.js.map +1 -0
  155. package/dist/esm/web/geolocation.d.ts +26 -0
  156. package/dist/esm/web/geolocation.js +117 -0
  157. package/dist/esm/web/geolocation.js.map +1 -0
  158. package/dist/esm/web/location.d.ts +7 -0
  159. package/dist/esm/web/location.js +70 -0
  160. package/dist/esm/web/location.js.map +1 -0
  161. package/dist/esm/web.d.ts +164 -0
  162. package/dist/esm/web.js +328 -0
  163. package/dist/esm/web.js.map +1 -0
  164. package/dist/plugin.cjs.js +945 -0
  165. package/dist/plugin.cjs.js.map +1 -0
  166. package/dist/plugin.js +948 -0
  167. package/dist/plugin.js.map +1 -0
  168. package/package.json +60 -4
@@ -0,0 +1,166 @@
1
+ package com.brickssoft.locationtracking.processing
2
+
3
+ import com.brickssoft.locationtracking.config.ConfigStore
4
+ import com.brickssoft.locationtracking.core.Clock
5
+ import com.brickssoft.locationtracking.core.ErrorCode
6
+ import com.brickssoft.locationtracking.core.Iso8601
7
+ import com.brickssoft.locationtracking.core.Logger
8
+ import com.brickssoft.locationtracking.core.TrackingException
9
+ import com.brickssoft.locationtracking.device.DeviceMonitor
10
+ import com.brickssoft.locationtracking.model.BatterySnapshot
11
+ import com.brickssoft.locationtracking.model.GeofenceHit
12
+ import com.brickssoft.locationtracking.model.JsonUtil
13
+ import com.brickssoft.locationtracking.model.ProviderKind
14
+ import com.brickssoft.locationtracking.model.ProviderState
15
+ import com.brickssoft.locationtracking.model.Record
16
+ import com.brickssoft.locationtracking.model.RecordEvent
17
+ import com.brickssoft.locationtracking.model.TrackedLocation
18
+ import com.brickssoft.locationtracking.provider.ProviderFactory
19
+ import org.json.JSONObject
20
+ import java.util.UUID
21
+
22
+ /**
23
+ * Builds records: uuid v4, clock and boot metadata, runtime state (isMoving, odometer, activity), battery and
24
+ * the current provider state (unless the caller passes one, as `providerchange` does) from [device], backend from
25
+ * [providers], and `extras` = `persistence.extras` merged with the per-call extras (per-call keys win; null when
26
+ * both are empty).
27
+ */
28
+ class DefaultRecordFactory(
29
+ private val configStore: ConfigStore,
30
+ private val device: DeviceMonitor,
31
+ private val providers: ProviderFactory,
32
+ private val clock: Clock,
33
+ ) : RecordFactory {
34
+ override fun create(
35
+ event: RecordEvent,
36
+ location: TrackedLocation?,
37
+ extras: String?,
38
+ geofence: GeofenceHit?,
39
+ provider: ProviderState?,
40
+ reason: String?,
41
+ ): Record {
42
+ val runtime = configStore.runtime.value
43
+ return Record(
44
+ uuid = UUID.randomUUID().toString(),
45
+ event = event,
46
+ location = location,
47
+ recordedAt = clock.now(),
48
+ elapsedRealtimeMs = clock.elapsedRealtime(),
49
+ bootCount = clock.bootCount(),
50
+ isMoving = runtime.isMoving,
51
+ odometer = runtime.odometer,
52
+ activity = runtime.activity,
53
+ battery = battery(),
54
+ backend = backend(),
55
+ extras = mergeExtras(configStore.config.value.persistence.extras, extras),
56
+ geofence = geofence,
57
+ provider = provider ?: providerState(),
58
+ reason = reason,
59
+ )
60
+ }
61
+
62
+ /**
63
+ * Parses a JS `InsertLocationInput`: `{coords:{latitude, longitude, accuracy?, altitude?, ...}, timestamp?,
64
+ * event?, is_moving?, extras?}`. `accuracy` defaults to 0; negative optional accuracies, speed and heading
65
+ * (the "unknown" sentinel of some platforms) become null.
66
+ *
67
+ * @throws TrackingException INVALID_ARGUMENT if coords, latitude or longitude are missing or invalid, if
68
+ * accuracy is negative, or if `timestamp`, `event`, `is_moving` or `extras` are given but invalid.
69
+ */
70
+ override fun fromExternal(input: JSONObject): Record {
71
+ val coords = input.optJSONObject("coords") ?: invalid("location.coords is required")
72
+ val latitude = JsonUtil.optDouble(coords, "latitude") ?: invalid("coords.latitude is required")
73
+ val longitude = JsonUtil.optDouble(coords, "longitude") ?: invalid("coords.longitude is required")
74
+ if (!Geo.isValid(latitude, longitude)) invalid("coords ($latitude, $longitude) are out of range")
75
+
76
+ val time = if (input.isNull("timestamp")) {
77
+ clock.now()
78
+ } else {
79
+ val text = JsonUtil.optString(input, "timestamp")
80
+ Iso8601.parse(text) ?: invalid("timestamp '$text' is not an ISO-8601 date")
81
+ }
82
+ val event = if (input.isNull("event")) {
83
+ RecordEvent.LOCATION
84
+ } else {
85
+ val wire = JsonUtil.optString(input, "event")
86
+ RecordEvent.fromWire(wire) ?: invalid("unknown event '$wire'")
87
+ }
88
+ val isMoving = if (input.isNull("is_moving")) {
89
+ null
90
+ } else {
91
+ JsonUtil.optBoolean(input, "is_moving") ?: invalid("is_moving must be a boolean")
92
+ }
93
+ val accuracy = JsonUtil.optFloat(coords, "accuracy") ?: 0f
94
+ if (accuracy < 0) invalid("coords.accuracy must be >= 0")
95
+ val extras = if (input.isNull("extras")) {
96
+ null
97
+ } else {
98
+ input.optJSONObject("extras")?.toString() ?: invalid("extras must be an object")
99
+ }
100
+ val location = TrackedLocation(
101
+ latitude = latitude,
102
+ longitude = longitude,
103
+ accuracy = accuracy,
104
+ altitude = JsonUtil.optDouble(coords, "altitude"),
105
+ altitudeAccuracy = nonNegative(coords, "altitude_accuracy"),
106
+ speed = nonNegative(coords, "speed"),
107
+ speedAccuracy = nonNegative(coords, "speed_accuracy"),
108
+ heading = nonNegative(coords, "heading"),
109
+ headingAccuracy = nonNegative(coords, "heading_accuracy"),
110
+ time = time,
111
+ )
112
+ val record = create(event, location, extras)
113
+ return if (isMoving == null) record else record.copy(isMoving = isMoving)
114
+ }
115
+
116
+ private fun nonNegative(json: JSONObject, key: String): Float? = JsonUtil.optFloat(json, key)?.takeIf { it >= 0 }
117
+
118
+ private fun battery(): BatterySnapshot = try {
119
+ device.battery()
120
+ } catch (e: Exception) {
121
+ Logger.w(TAG, "battery unavailable", e)
122
+ BatterySnapshot.UNKNOWN
123
+ }
124
+
125
+ // Read live, not the persisted runtime.providerState, which lags until the next provider check.
126
+ private fun providerState(): ProviderState? = try {
127
+ device.providerState()
128
+ } catch (e: Exception) {
129
+ Logger.w(TAG, "provider state unavailable", e)
130
+ null
131
+ }
132
+
133
+ private fun backend(): ProviderKind? = try {
134
+ providers.kind
135
+ } catch (e: Exception) {
136
+ Logger.w(TAG, "backend unavailable", e)
137
+ null
138
+ }
139
+
140
+ private fun invalid(message: String): Nothing = throw TrackingException(ErrorCode.INVALID_ARGUMENT, message)
141
+
142
+ internal companion object {
143
+ private const val TAG = "LT.RecordFactory"
144
+
145
+ /**
146
+ * Shallow merge of two JSON object texts; keys of [perCall] win over [base]. Invalid text is logged
147
+ * and ignored. Returns null when the result has no keys.
148
+ */
149
+ fun mergeExtras(base: String?, perCall: String?): String? {
150
+ val merged = parseExtras(base, "persistence.extras") ?: JSONObject()
151
+ parseExtras(perCall, "extras")?.let { overrides ->
152
+ val keys = overrides.keys()
153
+ while (keys.hasNext()) {
154
+ val key = keys.next()
155
+ merged.put(key, overrides.opt(key))
156
+ }
157
+ }
158
+ return if (merged.length() == 0) null else merged.toString()
159
+ }
160
+
161
+ private fun parseExtras(text: String?, name: String): JSONObject? {
162
+ if (text.isNullOrBlank()) return null
163
+ return JsonUtil.parseObject(text).also { if (it == null) Logger.w(TAG, "ignoring $name: not a JSON object") }
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,56 @@
1
+ package com.brickssoft.locationtracking.processing
2
+
3
+ import com.brickssoft.locationtracking.model.TrackedLocation
4
+ import kotlin.math.asin
5
+ import kotlin.math.cos
6
+ import kotlin.math.min
7
+ import kotlin.math.sin
8
+ import kotlin.math.sqrt
9
+
10
+ /** Great-circle geometry on a spherical Earth. */
11
+ internal object Geo {
12
+ /** Mean Earth radius (IUGG), meters. */
13
+ const val EARTH_RADIUS_M = 6_371_008.8
14
+
15
+ /** Haversine distance in meters between two points given in degrees. */
16
+ fun haversine(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
17
+ val phi1 = Math.toRadians(lat1)
18
+ val phi2 = Math.toRadians(lat2)
19
+ val dPhi = Math.toRadians(lat2 - lat1)
20
+ val dLambda = Math.toRadians(lng2 - lng1)
21
+ val sinPhi = sin(dPhi / 2)
22
+ val sinLambda = sin(dLambda / 2)
23
+ val h = sinPhi * sinPhi + cos(phi1) * cos(phi2) * sinLambda * sinLambda
24
+ // Clamp: rounding can push h slightly above 1 for antipodal points.
25
+ return 2 * EARTH_RADIUS_M * asin(sqrt(min(1.0, h)))
26
+ }
27
+
28
+ /** Haversine distance in meters between two fixes. */
29
+ fun distance(a: TrackedLocation, b: TrackedLocation): Double =
30
+ haversine(a.latitude, a.longitude, b.latitude, b.longitude)
31
+
32
+ /** True if both coordinates are finite and within the valid latitude / longitude ranges. */
33
+ fun isValid(latitude: Double, longitude: Double): Boolean =
34
+ latitude.isFinite() && longitude.isFinite() && latitude in -90.0..90.0 && longitude in -180.0..180.0
35
+
36
+ /** Wraps a longitude difference into [-180, 180), so steps across the antimeridian stay short. */
37
+ fun wrapLongitudeDelta(delta: Double): Double {
38
+ val wrapped = (delta + 180.0) % 360.0
39
+ return (if (wrapped < 0) wrapped + 360.0 else wrapped) - 180.0
40
+ }
41
+
42
+ /** Normalizes a longitude into [-180, 180]. */
43
+ fun normalizeLongitude(longitude: Double): Double =
44
+ if (longitude in -180.0..180.0) longitude else wrapLongitudeDelta(longitude)
45
+
46
+ /**
47
+ * Milliseconds from [from] to [to]: on the monotonic elapsed-realtime clock when both fixes carry it,
48
+ * otherwise on the wall clock (`time`).
49
+ */
50
+ fun elapsedMs(from: TrackedLocation, to: TrackedLocation): Long =
51
+ if (from.elapsedRealtimeNanos > 0 && to.elapsedRealtimeNanos > 0) {
52
+ (to.elapsedRealtimeNanos - from.elapsedRealtimeNanos) / 1_000_000
53
+ } else {
54
+ to.time - from.time
55
+ }
56
+ }
@@ -0,0 +1,72 @@
1
+ package com.brickssoft.locationtracking.processing
2
+
3
+ import com.brickssoft.locationtracking.model.TrackedLocation
4
+ import kotlin.math.max
5
+
6
+ /**
7
+ * 1-D constant-position Kalman filter applied per axis to latitude and longitude.
8
+ *
9
+ * - Measurement variance is `accuracy²` (m², accuracy floored at [MIN_ACCURACY_M]).
10
+ * - Process noise per step is `q = speed² · dt` (m², dt in seconds). `speed` is the fix's speed floored at
11
+ * [MIN_SPEED_MPS] (so the estimate never freezes), or [DEFAULT_SPEED_MPS] when the fix has none. For gaps
12
+ * over 1 s, `q` grows as `(speed · dt)²` because displacement grows linearly with time.
13
+ * - Both axes share one variance because `accuracy` is a horizontal radius. Longitude steps are wrapped, so
14
+ * tracks across the antimeridian are smoothed correctly.
15
+ * - `dt` uses the elapsed-realtime clock when both fixes carry it (see [Geo.elapsedMs]).
16
+ * - The filter restarts from the measurement when the gap exceeds [resetAfterMs], when time goes
17
+ * backwards, or after [reset].
18
+ *
19
+ * Only latitude and longitude are replaced; every other field of the fix is returned unchanged.
20
+ * Not thread-safe: [DefaultLocationProcessor] serializes access.
21
+ */
22
+ internal class KalmanFilter(
23
+ private val resetAfterMs: Long = DEFAULT_RESET_AFTER_MS,
24
+ ) {
25
+ /** The last measurement (time reference), or null before the first fix / after [reset]. */
26
+ private var last: TrackedLocation? = null
27
+ private var latitude = 0.0
28
+ private var longitude = 0.0
29
+
30
+ /** Estimate variance, m². */
31
+ private var variance = 0.0
32
+
33
+ /** Returns [location] with smoothed latitude / longitude. */
34
+ fun filter(location: TrackedLocation): TrackedLocation {
35
+ val accuracy = location.accuracy.toDouble().takeIf { it.isFinite() && it > MIN_ACCURACY_M } ?: MIN_ACCURACY_M
36
+ val measurementVariance = accuracy * accuracy
37
+ val previous = last
38
+ val dtMs = previous?.let { Geo.elapsedMs(it, location) }
39
+ if (dtMs == null || dtMs < 0 || dtMs > resetAfterMs) {
40
+ last = location
41
+ latitude = location.latitude
42
+ longitude = location.longitude
43
+ variance = measurementVariance
44
+ return location
45
+ }
46
+ last = location
47
+ if (dtMs > 0) {
48
+ val reported = location.speed?.toDouble()?.takeIf { it.isFinite() && it >= 0 }
49
+ val speed = reported?.let { max(it, MIN_SPEED_MPS) } ?: DEFAULT_SPEED_MPS
50
+ val dt = dtMs / 1000.0
51
+ variance += speed * speed * dt * max(1.0, dt)
52
+ }
53
+ val gain = variance / (variance + measurementVariance)
54
+ latitude += gain * (location.latitude - latitude)
55
+ longitude = Geo.normalizeLongitude(longitude + gain * Geo.wrapLongitudeDelta(location.longitude - longitude))
56
+ variance *= 1 - gain
57
+ return location.copy(latitude = latitude, longitude = longitude)
58
+ }
59
+
60
+ /** Forgets all history; the next fix is returned unchanged. */
61
+ fun reset() {
62
+ last = null
63
+ variance = 0.0
64
+ }
65
+
66
+ companion object {
67
+ const val DEFAULT_RESET_AFTER_MS = 60_000L
68
+ const val DEFAULT_SPEED_MPS = 3.0
69
+ const val MIN_SPEED_MPS = 1.0
70
+ const val MIN_ACCURACY_M = 1.0
71
+ }
72
+ }
@@ -0,0 +1,48 @@
1
+ package com.brickssoft.locationtracking.processing
2
+
3
+ import com.brickssoft.locationtracking.model.GeofenceHit
4
+ import com.brickssoft.locationtracking.model.ProviderState
5
+ import com.brickssoft.locationtracking.model.Record
6
+ import com.brickssoft.locationtracking.model.RecordEvent
7
+ import com.brickssoft.locationtracking.model.TrackedLocation
8
+ import org.json.JSONObject
9
+
10
+ sealed interface FilterResult {
11
+ data class Accepted(val location: TrackedLocation) : FilterResult
12
+
13
+ data class Rejected(val reason: String) : FilterResult
14
+ }
15
+
16
+ /** Accuracy / implied-speed / identical / mock filters and optional Kalman smoothing. */
17
+ interface LocationProcessor {
18
+ fun process(raw: TrackedLocation, isMoving: Boolean): FilterResult
19
+
20
+ fun reset()
21
+ }
22
+
23
+ /** Distance accumulator; persists its value via `configStore.updateRuntime`. */
24
+ interface Odometer {
25
+ val value: Double
26
+
27
+ fun onLocation(location: TrackedLocation)
28
+
29
+ fun set(value: Double)
30
+
31
+ fun reset()
32
+ }
33
+
34
+ /** Builds records with uuid, clock/boot metadata, runtime state, battery and backend. */
35
+ interface RecordFactory {
36
+ /** Merges `persistence.extras`; reads runtime (isMoving, odometer, activity), battery and clock. */
37
+ fun create(
38
+ event: RecordEvent,
39
+ location: TrackedLocation?,
40
+ extras: String? = null,
41
+ geofence: GeofenceHit? = null,
42
+ provider: ProviderState? = null,
43
+ reason: String? = null,
44
+ ): Record
45
+
46
+ /** `insertLocation`: builds a record from a JS `InsertLocationInput`. */
47
+ fun fromExternal(input: JSONObject): Record
48
+ }
@@ -0,0 +1,187 @@
1
+ package com.brickssoft.locationtracking.provider
2
+
3
+ import com.brickssoft.locationtracking.model.ActivitySample
4
+ import com.brickssoft.locationtracking.model.DesiredAccuracy
5
+ import com.brickssoft.locationtracking.model.GeofenceAction
6
+ import com.brickssoft.locationtracking.model.ProviderKind
7
+ import com.brickssoft.locationtracking.model.TrackedLocation
8
+
9
+ /** A continuous location request. */
10
+ data class LocationRequestSpec(
11
+ val accuracy: DesiredAccuracy,
12
+ val intervalMs: Long,
13
+ val fastestIntervalMs: Long,
14
+ val distanceFilterM: Float,
15
+ )
16
+
17
+ /** Receives batches of fixes, on any thread. */
18
+ fun interface LocationListener {
19
+ fun onLocations(locations: List<TrackedLocation>)
20
+ }
21
+
22
+ /** Location updates and one-shot fixes from one backend. */
23
+ interface LocationBackend {
24
+ val kind: ProviderKind
25
+
26
+ /**
27
+ * Supports multiple listeners; requesting again with the same listener replaces its request. A `distanceFilterM`
28
+ * of 0 means no distance filter (the engine applies its own elastic filter). Never throws: failures such as a
29
+ * missing permission are logged and the listener receives nothing.
30
+ */
31
+ fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener)
32
+
33
+ fun removeUpdates(listener: LocationListener)
34
+
35
+ /** The OS's cached last location. Implementations may throw `TrackingException(PERMISSION_DENIED)`; callers catch. */
36
+ suspend fun getLastLocation(): TrackedLocation?
37
+
38
+ /** Returns null on timeout. @throws com.brickssoft.locationtracking.core.TrackingException PERMISSION_DENIED */
39
+ suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation?
40
+ }
41
+
42
+ /** Activity recognition. Samples are delivered through a PendingIntent receiver to [ActivitySink]. */
43
+ interface ActivityBackend {
44
+ val kind: ProviderKind
45
+ val isSupported: Boolean
46
+
47
+ /** Returns false if updates could not be requested. */
48
+ fun start(intervalMs: Long): Boolean
49
+
50
+ fun stop()
51
+ }
52
+
53
+ /** A geofence as registered with the OS. */
54
+ data class OsGeofence(
55
+ val id: String,
56
+ val latitude: Double,
57
+ val longitude: Double,
58
+ val radius: Float,
59
+ val onEntry: Boolean,
60
+ val onExit: Boolean,
61
+ val onDwell: Boolean,
62
+ val loiteringDelayMs: Int,
63
+ val initialTriggerEntry: Boolean,
64
+ )
65
+
66
+ /** A transition reported by the OS; [location] is the triggering fix if the backend provides one. */
67
+ data class OsGeofenceTransition(val id: String, val action: GeofenceAction, val location: TrackedLocation?)
68
+
69
+ /** OS geofencing. Transitions are delivered through a PendingIntent receiver to [GeofenceTransitionSink]. */
70
+ interface GeofenceBackend {
71
+ val kind: ProviderKind
72
+ val supportsDwell: Boolean
73
+
74
+ /** @throws com.brickssoft.locationtracking.core.TrackingException */
75
+ suspend fun add(regions: List<OsGeofence>)
76
+
77
+ /** @throws com.brickssoft.locationtracking.core.TrackingException */
78
+ suspend fun remove(ids: List<String>)
79
+
80
+ /** @throws com.brickssoft.locationtracking.core.TrackingException */
81
+ suspend fun removeAll()
82
+ }
83
+
84
+ /**
85
+ * The three backends of one SDK. Implementations are created only by reflection through a public
86
+ * `(android.content.Context)` constructor; see [ProviderBundles].
87
+ */
88
+ interface ProviderBundle {
89
+ val kind: ProviderKind
90
+
91
+ /** True if the SDK is packaged and usable on this device (e.g. `GoogleApiAvailability` SUCCESS). */
92
+ fun isAvailable(): Boolean
93
+
94
+ fun location(): LocationBackend
95
+
96
+ fun activity(): ActivityBackend
97
+
98
+ fun geofence(): GeofenceBackend
99
+ }
100
+
101
+ /** Selects the active backend from config `locationProvider` (see architecture "GMS/HMS selection"). */
102
+ interface ProviderFactory {
103
+ val kind: ProviderKind
104
+
105
+ fun location(): LocationBackend
106
+
107
+ fun activity(): ActivityBackend
108
+
109
+ fun geofence(): GeofenceBackend
110
+
111
+ fun isAvailable(kind: ProviderKind): Boolean
112
+
113
+ /** Re-evaluates the selection (e.g. after setConfig). Returns true if [kind] changed. */
114
+ fun reselect(): Boolean
115
+ }
116
+
117
+ /** Implemented by the TrackingEngine. */
118
+ interface ActivitySink {
119
+ suspend fun onActivitySamples(samples: List<ActivitySample>)
120
+ }
121
+
122
+ /** Implemented by the GeofenceManager. */
123
+ interface GeofenceTransitionSink {
124
+ suspend fun onGeofenceTransitions(transitions: List<OsGeofenceTransition>)
125
+ }
126
+
127
+ /**
128
+ * Implemented by the TrackingEngine (round 2, stationary GPS-off mode). While STATIONARY the engine registers one OS
129
+ * geofence with id [com.brickssoft.locationtracking.core.Constants.STATIONARY_REGION_ID] directly with
130
+ * `providers.geofence()`. The GeofenceManager forwards every transition with that id here, and never stores, records
131
+ * or emits it.
132
+ */
133
+ interface StationaryRegionSink {
134
+ suspend fun onStationaryRegionTransition(transition: OsGeofenceTransition)
135
+
136
+ companion object {
137
+ /** Ignores every transition (tests and callers without an engine). */
138
+ val NONE: StationaryRegionSink = object : StationaryRegionSink {
139
+ override suspend fun onStationaryRegionTransition(transition: OsGeofenceTransition) = Unit
140
+ }
141
+ }
142
+ }
143
+
144
+ /** Fully qualified class names used for reflective loading. Strings only: never reference SDK types here. */
145
+ object ProviderBundles {
146
+ const val GMS_BUNDLE = "com.brickssoft.locationtracking.provider.gms.GmsProviderBundle"
147
+ const val HMS_BUNDLE = "com.brickssoft.locationtracking.provider.hms.HmsProviderBundle"
148
+ const val ANDROID_BUNDLE = "com.brickssoft.locationtracking.provider.android.AndroidProviderBundle"
149
+
150
+ /** Present only when play-services-location is packaged. */
151
+ const val GMS_SDK_CLASS = "com.google.android.gms.location.LocationServices"
152
+
153
+ /** Present only when com.huawei.hms:location is packaged. */
154
+ const val HMS_SDK_CLASS = "com.huawei.hms.location.LocationServices"
155
+
156
+ /** The receivers the GMS backend's activity and geofence results are delivered to. */
157
+ val GMS_RECEIVERS = listOf(
158
+ "com.brickssoft.locationtracking.provider.gms.GmsActivityReceiver",
159
+ "com.brickssoft.locationtracking.provider.gms.GmsGeofenceReceiver",
160
+ )
161
+
162
+ /** The receivers the HMS backend's activity and geofence results are delivered to. */
163
+ val HMS_RECEIVERS = listOf(
164
+ "com.brickssoft.locationtracking.provider.hms.HmsActivityReceiver",
165
+ "com.brickssoft.locationtracking.provider.hms.HmsGeofenceReceiver",
166
+ )
167
+
168
+ /** The SDK class whose presence means [kind]'s SDK is in the APK; null for ANDROID (part of the platform). */
169
+ fun sdkClassName(kind: ProviderKind): String? = when (kind) {
170
+ ProviderKind.GMS -> GMS_SDK_CLASS
171
+ ProviderKind.HMS -> HMS_SDK_CLASS
172
+ ProviderKind.ANDROID -> null
173
+ }
174
+
175
+ /** The receivers [kind]'s backend needs in the merged manifest; none for ANDROID. */
176
+ fun receiverClassNames(kind: ProviderKind): List<String> = when (kind) {
177
+ ProviderKind.GMS -> GMS_RECEIVERS
178
+ ProviderKind.HMS -> HMS_RECEIVERS
179
+ ProviderKind.ANDROID -> emptyList()
180
+ }
181
+
182
+ fun bundleClassName(kind: ProviderKind): String = when (kind) {
183
+ ProviderKind.GMS -> GMS_BUNDLE
184
+ ProviderKind.HMS -> HMS_BUNDLE
185
+ ProviderKind.ANDROID -> ANDROID_BUNDLE
186
+ }
187
+ }