@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,217 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import androidx.annotation.VisibleForTesting
6
+ import com.brickssoft.locationtracking.api.NativeListeners
7
+ import com.brickssoft.locationtracking.config.ConfigStore
8
+ import com.brickssoft.locationtracking.config.SharedPrefsConfigStore
9
+ import com.brickssoft.locationtracking.data.GeofenceStore
10
+ import com.brickssoft.locationtracking.data.LocationStore
11
+ import com.brickssoft.locationtracking.data.SqliteGeofenceStore
12
+ import com.brickssoft.locationtracking.data.SqliteLocationStore
13
+ import com.brickssoft.locationtracking.data.TrackingDatabase
14
+ import com.brickssoft.locationtracking.device.DefaultDeviceInfoProvider
15
+ import com.brickssoft.locationtracking.device.DefaultDeviceMonitor
16
+ import com.brickssoft.locationtracking.device.DeviceInfoProvider
17
+ import com.brickssoft.locationtracking.device.DeviceMonitor
18
+ import com.brickssoft.locationtracking.engine.DefaultTrackingEngine
19
+ import com.brickssoft.locationtracking.engine.TrackingEngine
20
+ import com.brickssoft.locationtracking.geofence.DefaultGeofenceManager
21
+ import com.brickssoft.locationtracking.geofence.GeofenceManager
22
+ import com.brickssoft.locationtracking.heartbeat.DefaultHeartbeatScheduler
23
+ import com.brickssoft.locationtracking.heartbeat.HeartbeatScheduler
24
+ import com.brickssoft.locationtracking.http.HttpSyncer
25
+ import com.brickssoft.locationtracking.http.OkHttpSyncer
26
+ import com.brickssoft.locationtracking.logging.FileLogger
27
+ import com.brickssoft.locationtracking.logging.LogStore
28
+ import com.brickssoft.locationtracking.permission.DefaultPermissionManager
29
+ import com.brickssoft.locationtracking.permission.PermissionManager
30
+ import com.brickssoft.locationtracking.position.DefaultPositionService
31
+ import com.brickssoft.locationtracking.position.PositionService
32
+ import com.brickssoft.locationtracking.processing.DefaultLocationProcessor
33
+ import com.brickssoft.locationtracking.processing.DefaultOdometer
34
+ import com.brickssoft.locationtracking.processing.DefaultRecordFactory
35
+ import com.brickssoft.locationtracking.processing.LocationProcessor
36
+ import com.brickssoft.locationtracking.processing.Odometer
37
+ import com.brickssoft.locationtracking.processing.RecordFactory
38
+ import com.brickssoft.locationtracking.provider.DefaultProviderFactory
39
+ import com.brickssoft.locationtracking.provider.ProviderFactory
40
+ import com.brickssoft.locationtracking.provider.StationaryRegionSink
41
+ import com.brickssoft.locationtracking.record.DefaultRecordSink
42
+ import com.brickssoft.locationtracking.record.RecordSink
43
+ import com.brickssoft.locationtracking.service.DefaultServiceController
44
+ import com.brickssoft.locationtracking.service.ServiceController
45
+ import com.brickssoft.locationtracking.settings.DefaultDeviceSettings
46
+ import com.brickssoft.locationtracking.settings.DeviceSettings
47
+ import kotlinx.coroutines.CoroutineExceptionHandler
48
+ import kotlinx.coroutines.CoroutineScope
49
+ import kotlinx.coroutines.SupervisorJob
50
+ import kotlinx.coroutines.cancel
51
+ import kotlinx.coroutines.flow.distinctUntilChanged
52
+ import kotlinx.coroutines.flow.map
53
+ import kotlinx.coroutines.launch
54
+ import okhttp3.OkHttpClient
55
+
56
+ /**
57
+ * Lazy service locator: the single place where every component is constructed. It fixes each unit's constructor
58
+ * signature. Dependency cycles are broken with [Lazy] parameters, which constructors must not touch.
59
+ *
60
+ * Obtain it with [Components.get] from any context (plugin, service, receivers).
61
+ */
62
+ class Components private constructor(context: Context) {
63
+ /** The application context. */
64
+ val context: Context = context.applicationContext ?: context
65
+
66
+ val clock: Clock = SystemClockImpl(this.context)
67
+ val dispatchers: AppDispatchers = AppDispatchers.DEFAULT
68
+ val scope: CoroutineScope = CoroutineScope(
69
+ SupervisorJob() + dispatchers.engine + CoroutineExceptionHandler { _, t -> Logger.e("LT", "uncaught", t) },
70
+ )
71
+ val events: EventBus = SimpleEventBus()
72
+
73
+ /** Round 2: every queued record (sink and insertLocation) for the companion native API (unit 5). */
74
+ val recordHooks: RecordHooks = RecordHooks()
75
+ val http: OkHttpClient by lazy { OkHttpClient() }
76
+
77
+ // U17
78
+ val logStore: LogStore by lazy { FileLogger(this.context, clock, dispatchers, lazy { http }) }
79
+
80
+ // U3
81
+ val configStore: ConfigStore by lazy { SharedPrefsConfigStore(this.context, clock) }
82
+
83
+ // U15
84
+ val permissions: PermissionManager by lazy { DefaultPermissionManager(this.context) }
85
+ val deviceSettings: DeviceSettings by lazy { DefaultDeviceSettings(this.context) }
86
+
87
+ // U6
88
+ val providers: ProviderFactory by lazy { DefaultProviderFactory(this.context, configStore) }
89
+
90
+ // U14
91
+ val device: DeviceMonitor by lazy {
92
+ DefaultDeviceMonitor(
93
+ this.context, configStore, permissions, lazy { providers }, events, clock,
94
+ lazy { recordFactory }, lazy { recordSink }, scope,
95
+ )
96
+ }
97
+ val deviceInfo: DeviceInfoProvider by lazy { DefaultDeviceInfoProvider(this.context, lazy { providers }) }
98
+
99
+ // U10
100
+ private val database by lazy { TrackingDatabase(this.context) }
101
+ val locationStore: LocationStore by lazy { SqliteLocationStore(database, configStore, clock, dispatchers) }
102
+ val geofenceStore: GeofenceStore by lazy { SqliteGeofenceStore(database, dispatchers) }
103
+
104
+ // U9
105
+ val processor: LocationProcessor by lazy { DefaultLocationProcessor(configStore) }
106
+ val odometer: Odometer by lazy { DefaultOdometer(configStore) }
107
+ val recordFactory: RecordFactory by lazy { DefaultRecordFactory(configStore, device, providers, clock) }
108
+
109
+ // U11
110
+ val syncer: HttpSyncer by lazy {
111
+ OkHttpSyncer(configStore, locationStore, device, events, clock, http, dispatchers, scope)
112
+ }
113
+
114
+ // U12
115
+ val heartbeat: HeartbeatScheduler by lazy {
116
+ DefaultHeartbeatScheduler(
117
+ this.context, configStore, device, providers, locationStore, recordFactory, lazy { recordSink }, clock, scope,
118
+ )
119
+ }
120
+
121
+ // SCAFFOLD
122
+ val recordSink: RecordSink by lazy {
123
+ DefaultRecordSink(locationStore, configStore, heartbeat, syncer, events, recordHooks)
124
+ }
125
+
126
+ // U13 (round 2: STATIONARY_REGION_ID transitions go to the engine)
127
+ val geofences: GeofenceManager by lazy {
128
+ DefaultGeofenceManager(
129
+ geofenceStore, providers, configStore, recordFactory, recordSink, events, clock, scope,
130
+ lazy { stationarySink },
131
+ )
132
+ }
133
+
134
+ // U16
135
+ val positions: PositionService by lazy {
136
+ DefaultPositionService(providers, configStore, permissions, device, recordFactory, recordSink, clock, scope)
137
+ }
138
+
139
+ // U8
140
+ val serviceController: ServiceController by lazy { DefaultServiceController(this.context, configStore, events, clock) }
141
+
142
+ // U7
143
+ val engine: TrackingEngine by lazy {
144
+ DefaultTrackingEngine(
145
+ configStore, providers, processor, odometer, recordFactory, recordSink,
146
+ heartbeat, geofences, serviceController, device, syncer, permissions, events, clock, scope,
147
+ ForceStopProbe.system(this.context),
148
+ )
149
+ }
150
+
151
+ /** Round 2: the engine receives the transitions of its stationary region (unit 2). */
152
+ val stationarySink: StationaryRegionSink get() = engine
153
+
154
+ /**
155
+ * Runs once, right after construction: installs the file logger, keeps its level in sync with config and (round 2)
156
+ * installs the native companion listeners before any component can emit.
157
+ */
158
+ private fun bootstrap() {
159
+ Logger.sink = logStore
160
+ val store = configStore
161
+ val initial = store.config.value.logger
162
+ logStore.configure(initial.logLevel, initial.logMaxDays)
163
+ scope.launch {
164
+ store.config
165
+ .map { it.logger }
166
+ .distinctUntilChanged()
167
+ .collect { logStore.configure(it.logLevel, it.logMaxDays) }
168
+ }
169
+ try {
170
+ NativeListeners.install(context, this)
171
+ } catch (e: Exception) {
172
+ Logger.e(Constants.TAG, "failed to install native listeners", e)
173
+ }
174
+ }
175
+
176
+ companion object {
177
+ @SuppressLint("StaticFieldLeak") // holds the application context only
178
+ @Volatile
179
+ private var instance: Components? = null
180
+
181
+ /** The instance whose [bootstrap] is running; only read and written while holding the lock. */
182
+ @SuppressLint("StaticFieldLeak") // holds the application context only
183
+ private var constructing: Components? = null
184
+
185
+ /**
186
+ * Thread-safe; always uses the application context.
187
+ *
188
+ * The instance is published only after [bootstrap] has run, so no other thread can use it (and emit a record)
189
+ * before the manifest-declared native listeners are installed. Other threads wait on the lock meanwhile; a
190
+ * nested call on the constructing thread (the lock is reentrant) gets the instance under construction.
191
+ */
192
+ fun get(context: Context): Components {
193
+ instance?.let { return it }
194
+ return synchronized(this) {
195
+ instance ?: constructing ?: Components(context).also {
196
+ constructing = it
197
+ try {
198
+ it.bootstrap()
199
+ } finally {
200
+ constructing = null
201
+ instance = it
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ /** Test hook: cancels the scope, detaches the log sink and forgets the instance. */
208
+ @VisibleForTesting
209
+ fun reset() {
210
+ synchronized(this) {
211
+ instance?.scope?.cancel()
212
+ instance = null
213
+ Logger.sink = null
214
+ }
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,78 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import android.app.PendingIntent
4
+ import android.os.Build
5
+
6
+ /** Shared names, ids and PendingIntent request codes. */
7
+ object Constants {
8
+ /** Tag prefix for [Logger]. */
9
+ const val TAG = "LT"
10
+
11
+ const val PREFS_NAME = "location_tracking_prefs"
12
+ const val DATABASE_NAME = "location_tracking.db"
13
+ const val LOG_DIR = "location-tracking-logs"
14
+
15
+ /** Authority suffix of `LogFileProvider`: `${applicationId}.locationtracking.logs`. */
16
+ const val LOG_FILE_PROVIDER_SUFFIX = ".locationtracking.logs"
17
+
18
+ /** Maximum number of registered geofences. */
19
+ const val MAX_GEOFENCES = 100
20
+
21
+ /**
22
+ * Id of the engine's own OS geofence while STATIONARY (GPS off): a circle around the stationary anchor whose EXIT
23
+ * wakes the engine. It is never stored, never counted against [MAX_GEOFENCES], never recorded and never emitted;
24
+ * `DefaultGeofenceManager` routes its transitions to `StationaryRegionSink` (architecture round 2, §3).
25
+ */
26
+ const val STATIONARY_REGION_ID = "__lt_stationary__"
27
+
28
+ /** Maximum number of notification action buttons. */
29
+ const val MAX_NOTIFICATION_ACTIONS = 3
30
+
31
+ const val NOTIFICATION_ID = 7301
32
+
33
+ /** The resume notification (`notification.resume`) and its channel. */
34
+ const val RESUME_NOTIFICATION_ID = 7302
35
+ const val RESUME_CHANNEL_ID = "location_tracking_resume"
36
+
37
+ // PendingIntent request codes
38
+ const val RC_HEARTBEAT = 7310
39
+ const val RC_GMS_ACTIVITY = 7320
40
+ const val RC_GMS_GEOFENCE = 7321
41
+ const val RC_HMS_ACTIVITY = 7330
42
+ const val RC_HMS_GEOFENCE = 7331
43
+ const val RC_NOTIFICATION_CONTENT = 7349
44
+ const val RC_RESUME_NOTIFICATION = 7348
45
+
46
+ /** Notification action `i` uses `RC_NOTIFICATION_ACTION_BASE + i`. */
47
+ const val RC_NOTIFICATION_ACTION_BASE = 7350
48
+
49
+ /** Android proximity alerts; told apart by the data URI [geofenceUri]. */
50
+ const val RC_ANDROID_PROXIMITY = 7400
51
+
52
+ /** Scheme of the data URI that identifies an Android proximity-alert PendingIntent. */
53
+ const val GEOFENCE_URI_SCHEME = "lt-geofence"
54
+
55
+ // Intent actions / extras
56
+ const val ACTION_HEARTBEAT = "com.brickssoft.locationtracking.HEARTBEAT"
57
+ const val ACTION_NOTIFICATION_ACTION = "com.brickssoft.locationtracking.NOTIFICATION_ACTION"
58
+ const val ACTION_GEOFENCE = "com.brickssoft.locationtracking.GEOFENCE"
59
+ const val ACTION_ACTIVITY = "com.brickssoft.locationtracking.ACTIVITY"
60
+ const val EXTRA_ACTION_ID = "com.brickssoft.locationtracking.EXTRA_ACTION_ID"
61
+
62
+ /** `lt-geofence://<id>` */
63
+ fun geofenceUri(id: String): String = "$GEOFENCE_URI_SCHEME://${android.net.Uri.encode(id)}"
64
+
65
+ /**
66
+ * Flags for PendingIntents the OS must fill in (GMS/HMS activity and geofence, proximity alerts):
67
+ * `FLAG_UPDATE_CURRENT | FLAG_MUTABLE` on API 31+.
68
+ */
69
+ fun piMutable(): Int =
70
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
71
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
72
+ } else {
73
+ PendingIntent.FLAG_UPDATE_CURRENT
74
+ }
75
+
76
+ /** Flags for alarm, notification and action PendingIntents: `FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE`. */
77
+ fun piImmutable(): Int = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
78
+ }
@@ -0,0 +1,27 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ /** Rejection codes; `name` is the JS `code` of a failed promise. */
4
+ enum class ErrorCode {
5
+ NOT_READY,
6
+ PERMISSION_DENIED,
7
+ LOCATION_DISABLED,
8
+ TIMEOUT,
9
+ UNAVAILABLE,
10
+ INVALID_ARGUMENT,
11
+ NOT_FOUND,
12
+ NO_URL,
13
+ HTTP_ERROR,
14
+ NETWORK_ERROR,
15
+ TOO_MANY_GEOFENCES,
16
+ NO_ACTIVITY,
17
+ IO_ERROR,
18
+ UNIMPLEMENTED,
19
+ INTERNAL,
20
+ }
21
+
22
+ /** The only exception type that crosses component boundaries; the bridge maps it to `call.reject(message, code.name)`. */
23
+ class TrackingException(
24
+ val code: ErrorCode,
25
+ message: String,
26
+ cause: Throwable? = null,
27
+ ) : Exception(message, cause)
@@ -0,0 +1,39 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import java.util.concurrent.CopyOnWriteArrayList
4
+
5
+ /** Handle returned by [EventBus.subscribe]. */
6
+ fun interface Subscription {
7
+ fun cancel()
8
+ }
9
+
10
+ /** In-process event bus. Delivery is synchronous, on the emitter's thread. */
11
+ interface EventBus {
12
+ fun emit(event: TrackingEvent)
13
+
14
+ fun subscribe(listener: (TrackingEvent) -> Unit): Subscription
15
+ }
16
+
17
+ /** Default [EventBus]. A throwing listener is logged and does not affect other listeners. */
18
+ class SimpleEventBus : EventBus {
19
+ private val listeners = CopyOnWriteArrayList<(TrackingEvent) -> Unit>()
20
+
21
+ override fun emit(event: TrackingEvent) {
22
+ for (listener in listeners) {
23
+ try {
24
+ listener(event)
25
+ } catch (e: Exception) {
26
+ Logger.e(TAG, "event listener failed for ${event::class.simpleName}", e)
27
+ }
28
+ }
29
+ }
30
+
31
+ override fun subscribe(listener: (TrackingEvent) -> Unit): Subscription {
32
+ listeners.add(listener)
33
+ return Subscription { listeners.remove(listener) }
34
+ }
35
+
36
+ private companion object {
37
+ const val TAG = "LT.EventBus"
38
+ }
39
+ }
@@ -0,0 +1,63 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import android.app.ActivityManager
4
+ import android.app.ApplicationExitInfo
5
+ import android.content.Context
6
+ import android.os.Build
7
+
8
+ /**
9
+ * Tells whether this process started after the user force-stopped the app (Settings → Force stop, `am force-stop`).
10
+ *
11
+ * Android cancels the app's alarms and does not restart its services after a force stop, so tracking stays off until
12
+ * the user opens the app again. A broadcast that was already on its way can still start the process (on the CI
13
+ * emulator a Play services activity update arrived 0.5 s after `am force-stop`, scenario P-L04). The engine asks this
14
+ * probe before a background trigger (activity update, stationary region EXIT) restores tracking.
15
+ */
16
+ fun interface ForceStopProbe {
17
+ /** True when the previous process of this app ended with a user force stop. */
18
+ fun startedAfterForceStop(): Boolean
19
+
20
+ companion object {
21
+ private const val TAG = "LT.ForceStop"
22
+
23
+ /** Exit records read to find the main process's newest one (other processes of the app leave records too). */
24
+ private const val EXIT_RECORDS = 16
25
+
26
+ /** Never reports a force stop (tests, and Android 10 and older). */
27
+ val NEVER: ForceStopProbe = ForceStopProbe { false }
28
+
29
+ /**
30
+ * Reads `ActivityManager.getHistoricalProcessExitReasons` (Android 11+, no permission needed for the own
31
+ * package) once per process: the newest exit record of the app's main process is the previous process.
32
+ * Records of other processes are skipped: the WebView's isolated renderer runs under the app, and a force stop
33
+ * kills it a few milliseconds after the main process ("isolated not needed"), so its record can be the newest
34
+ * one (CI emulator, API 34, scenario P-L04: reading only the newest record missed the force stop).
35
+ */
36
+ fun system(context: Context): ForceStopProbe {
37
+ val app = context.applicationContext ?: context
38
+ val result by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { lastExitWasForceStop(app) }
39
+ return ForceStopProbe { result }
40
+ }
41
+
42
+ private fun lastExitWasForceStop(context: Context): Boolean {
43
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false
44
+ return try {
45
+ val manager = context.getSystemService(ActivityManager::class.java) ?: return false
46
+ val mainProcess = context.applicationInfo?.processName ?: context.packageName
47
+ val previous = manager.getHistoricalProcessExitReasons(context.packageName, 0, EXIT_RECORDS)
48
+ .firstOrNull { it.processName == mainProcess }
49
+ ?: return false
50
+ val forceStopped = previous.reason == ApplicationExitInfo.REASON_USER_REQUESTED
51
+ Logger.i(
52
+ TAG,
53
+ "previous process ${previous.pid} exited with reason ${previous.reason} " +
54
+ "(${previous.description ?: "no description"}); force stop: $forceStopped",
55
+ )
56
+ forceStopped
57
+ } catch (e: Exception) {
58
+ Logger.w(TAG, "the previous process's exit reason is unknown", e)
59
+ false
60
+ }
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,63 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import java.text.ParsePosition
4
+ import java.text.SimpleDateFormat
5
+ import java.util.Date
6
+ import java.util.Locale
7
+ import java.util.TimeZone
8
+
9
+ /**
10
+ * ISO-8601 formatting and parsing for API 24+ (no `java.time`).
11
+ * The output format is always `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` in UTC.
12
+ */
13
+ object Iso8601 {
14
+ private const val PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
15
+
16
+ private val formatter = threadLocalFormat(PATTERN)
17
+
18
+ /** Accepted input patterns, tried in order (UTC 'Z' with/without millis, numeric offsets). */
19
+ private val parsers = listOf(
20
+ threadLocalFormat(PATTERN),
21
+ threadLocalFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
22
+ threadLocalFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"),
23
+ threadLocalFormat("yyyy-MM-dd'T'HH:mm:ssXXX"),
24
+ )
25
+
26
+ private fun threadLocalFormat(pattern: String): ThreadLocal<SimpleDateFormat> =
27
+ object : ThreadLocal<SimpleDateFormat>() {
28
+ override fun initialValue(): SimpleDateFormat = SimpleDateFormat(pattern, Locale.US).apply {
29
+ timeZone = TimeZone.getTimeZone("UTC")
30
+ isLenient = false
31
+ }
32
+ }
33
+
34
+ /** Formats epoch ms as `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`. */
35
+ fun format(epochMs: Long): String = formatter.get()!!.format(Date(epochMs))
36
+
37
+ /** [format], or null for null input. */
38
+ fun formatOrNull(epochMs: Long?): String? = epochMs?.let { format(it) }
39
+
40
+ /** Parses an ISO-8601 timestamp to epoch ms, or returns null if it is not valid. */
41
+ fun parse(text: String?): Long? {
42
+ if (text.isNullOrBlank()) return null
43
+ val trimmed = normalizeFraction(text.trim())
44
+ for (parser in parsers) {
45
+ val position = ParsePosition(0)
46
+ val date = parser.get()!!.parse(trimmed, position)
47
+ if (date != null && position.index == trimmed.length) return date.time
48
+ }
49
+ return null
50
+ }
51
+
52
+ /** Pads or truncates the fractional seconds to exactly 3 digits so SimpleDateFormat parses them as ms. */
53
+ private fun normalizeFraction(text: String): String {
54
+ val dot = text.indexOf('.', startIndex = 19)
55
+ if (dot != 19) return text
56
+ var end = dot + 1
57
+ while (end < text.length && text[end].isDigit()) end++
58
+ val digits = text.substring(dot + 1, end)
59
+ if (digits.isEmpty()) return text
60
+ val ms = digits.padEnd(3, '0').substring(0, 3)
61
+ return text.substring(0, dot + 1) + ms + text.substring(end)
62
+ }
63
+ }
@@ -0,0 +1,68 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import android.util.Log
4
+
5
+ /** Log levels, ordered from least to most verbose. [wire] is the JS `LogLevel` value. */
6
+ enum class LogLevel {
7
+ OFF,
8
+ ERROR,
9
+ WARN,
10
+ INFO,
11
+ DEBUG,
12
+ VERBOSE,
13
+ ;
14
+
15
+ val wire: String get() = name.lowercase()
16
+
17
+ /** True if a message at [level] passes a filter set to this level. */
18
+ fun allows(level: LogLevel): Boolean = level != OFF && this != OFF && level.ordinal <= ordinal
19
+
20
+ companion object {
21
+ fun fromWire(value: String?): LogLevel? = entries.firstOrNull { it.wire.equals(value, ignoreCase = true) }
22
+ }
23
+ }
24
+
25
+ /** Destination for log lines besides logcat (the file logger). */
26
+ interface LogSink {
27
+ fun write(level: LogLevel, tag: String, message: String, error: Throwable?)
28
+ }
29
+
30
+ /**
31
+ * Global logger. Writes every message to logcat and to [sink] (set by `Components.bootstrap()`).
32
+ * Never throws: failures of the sink or of logcat are swallowed.
33
+ */
34
+ object Logger {
35
+ @Volatile
36
+ var sink: LogSink? = null
37
+
38
+ fun e(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.ERROR, tag, msg, t)
39
+
40
+ fun w(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.WARN, tag, msg, t)
41
+
42
+ fun i(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.INFO, tag, msg, t)
43
+
44
+ fun d(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.DEBUG, tag, msg, t)
45
+
46
+ fun v(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.VERBOSE, tag, msg, t)
47
+
48
+ fun log(level: LogLevel, tag: String, msg: String, t: Throwable? = null) {
49
+ if (level == LogLevel.OFF) return
50
+ try {
51
+ when (level) {
52
+ LogLevel.ERROR -> Log.e(tag, msg, t)
53
+ LogLevel.WARN -> Log.w(tag, msg, t)
54
+ LogLevel.INFO -> Log.i(tag, msg, t)
55
+ LogLevel.DEBUG -> Log.d(tag, msg, t)
56
+ LogLevel.VERBOSE -> Log.v(tag, msg, t)
57
+ LogLevel.OFF -> Unit
58
+ }
59
+ } catch (_: Throwable) {
60
+ // logcat unavailable (plain JVM); ignore
61
+ }
62
+ try {
63
+ sink?.write(level, tag, msg, t)
64
+ } catch (_: Throwable) {
65
+ // a broken sink must never break the caller
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,47 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import com.brickssoft.locationtracking.model.Record
4
+ import java.util.concurrent.CopyOnWriteArrayList
5
+
6
+ /** Observes every record queued for upload. See [RecordHooks]. */
7
+ fun interface RecordObserver {
8
+ fun onRecordQueued(record: Record)
9
+ }
10
+
11
+ /**
12
+ * Fan-out of every queued record to in-process observers (SCAFFOLD, round 2). The companion native API
13
+ * (`api/NativeListeners`, unit 5) registers the observer that forwards records to `LocationTrackingListener.onRecord`.
14
+ *
15
+ * Callers (unit 5 adds the calls):
16
+ * - `DefaultRecordSink.submit`, right after the insert (also when the insert failed: a companion listener keeps its
17
+ * own audit trail, so a local database failure must not hide the record from it);
18
+ * - the bridge's `insertLocation`, right after its insert (inserted records bypass the sink).
19
+ *
20
+ * [dispatch] is synchronous on the caller's thread (the engine or an I/O coroutine), so observers must not block: they
21
+ * hand the record off to their own thread. A throwing observer is logged and does not affect the others.
22
+ */
23
+ class RecordHooks {
24
+ private val observers = CopyOnWriteArrayList<RecordObserver>()
25
+
26
+ fun add(observer: RecordObserver): Subscription {
27
+ observers.add(observer)
28
+ return Subscription { observers.remove(observer) }
29
+ }
30
+
31
+ /** True when at least one observer is registered (lets callers skip work). */
32
+ val hasObservers: Boolean get() = observers.isNotEmpty()
33
+
34
+ fun dispatch(record: Record) {
35
+ for (observer in observers) {
36
+ try {
37
+ observer.onRecordQueued(record)
38
+ } catch (e: Exception) {
39
+ Logger.e(TAG, "record observer failed for ${record.event.wire} ${record.uuid}", e)
40
+ }
41
+ }
42
+ }
43
+
44
+ private companion object {
45
+ const val TAG = "LT.RecordHooks"
46
+ }
47
+ }
@@ -0,0 +1,64 @@
1
+ package com.brickssoft.locationtracking.core
2
+
3
+ import com.brickssoft.locationtracking.model.ActivitySample
4
+ import com.brickssoft.locationtracking.model.Connectivity
5
+ import com.brickssoft.locationtracking.model.GeofenceAction
6
+ import com.brickssoft.locationtracking.model.GeofenceSpec
7
+ import com.brickssoft.locationtracking.model.HttpResult
8
+ import com.brickssoft.locationtracking.model.ProviderState
9
+ import com.brickssoft.locationtracking.model.Record
10
+
11
+ /**
12
+ * Events forwarded to JS. The JS event name and payload are built by
13
+ * [com.brickssoft.locationtracking.model.EventJson].
14
+ */
15
+ sealed interface TrackingEvent {
16
+ /** "location" */
17
+ data class Location(val record: Record) : TrackingEvent
18
+
19
+ /** "motionchange" */
20
+ data class MotionChange(val isMoving: Boolean, val record: Record) : TrackingEvent
21
+
22
+ /** "activitychange" */
23
+ data class ActivityChange(val activity: ActivitySample) : TrackingEvent
24
+
25
+ /** "providerchange" */
26
+ data class ProviderChange(val state: ProviderState) : TrackingEvent
27
+
28
+ /** "heartbeat" */
29
+ data class Heartbeat(val record: Record) : TrackingEvent
30
+
31
+ /** "geofence"; [extras] is JSON object text. */
32
+ data class Geofence(
33
+ val identifier: String,
34
+ val action: GeofenceAction,
35
+ val record: Record,
36
+ val extras: String?,
37
+ ) : TrackingEvent
38
+
39
+ /** "geofenceschange" */
40
+ data class GeofencesChange(val on: List<GeofenceSpec>, val off: List<String>) : TrackingEvent
41
+
42
+ /** "http" */
43
+ data class Http(val result: HttpResult) : TrackingEvent
44
+
45
+ /** "connectivitychange" */
46
+ data class ConnectivityChange(val connectivity: Connectivity) : TrackingEvent
47
+
48
+ /** "powersavechange" */
49
+ data class PowerSaveChange(val isPowerSaveMode: Boolean) : TrackingEvent
50
+
51
+ /** "enabledchange" */
52
+ data class EnabledChange(val enabled: Boolean) : TrackingEvent
53
+
54
+ /** "notificationaction" */
55
+ data class NotificationAction(val id: String) : TrackingEvent
56
+
57
+ /** "authorization"; [responseJson] is JSON object text. */
58
+ data class Authorization(
59
+ val success: Boolean,
60
+ val status: Int,
61
+ val error: String?,
62
+ val responseJson: String?,
63
+ ) : TrackingEvent
64
+ }