@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,319 @@
1
+ package com.brickssoft.locationtracking.service
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.app.Notification
5
+ import android.app.Service
6
+ import android.content.Context
7
+ import android.content.Intent
8
+ import android.content.pm.ServiceInfo
9
+ import android.os.Build
10
+ import android.os.IBinder
11
+ import androidx.annotation.VisibleForTesting
12
+ import androidx.core.app.NotificationManagerCompat
13
+ import androidx.core.app.ServiceCompat
14
+ import com.brickssoft.locationtracking.config.ConfigStore
15
+ import com.brickssoft.locationtracking.config.NotificationConfig
16
+ import com.brickssoft.locationtracking.core.AppDispatchers
17
+ import com.brickssoft.locationtracking.core.Constants
18
+ import com.brickssoft.locationtracking.core.Logger
19
+ import kotlinx.coroutines.CoroutineDispatcher
20
+ import kotlinx.coroutines.CoroutineExceptionHandler
21
+ import kotlinx.coroutines.CoroutineScope
22
+ import kotlinx.coroutines.Job
23
+ import kotlinx.coroutines.SupervisorJob
24
+ import kotlinx.coroutines.flow.distinctUntilChanged
25
+ import kotlinx.coroutines.flow.map
26
+ import kotlinx.coroutines.launch
27
+ import kotlin.coroutines.cancellation.CancellationException
28
+
29
+ /**
30
+ * The location foreground service (type `location`). It keeps the process in a foreground state while tracking is
31
+ * enabled; the engine does the actual tracking.
32
+ *
33
+ * **Foreground first.** Android requires `startForeground` within a few seconds of `startForegroundService` (the limit
34
+ * is 5 to 10 s, depending on the Android version), also when the main thread is busy; otherwise it crashes the app with
35
+ * `ForegroundServiceDidNotStartInTimeException`. So every start command (and a restart with a null intent) calls
36
+ * `startForeground` before anything touches `Components`, the config store, SharedPreferences, SQLite or JSON. That
37
+ * first notification ([NotificationFactory.buildInitial]) is built from the notification fields the start command
38
+ * carries (channel, priority, title, text, icon, color, copied by the sender from its config) or, for a null intent,
39
+ * from constants and resources; when the service is already in the foreground, the notification it shows is passed
40
+ * again instead. The location service type
41
+ * is passed on API 29+. Only after `startForeground` has returned does the service load its dependencies, on a
42
+ * background coroutine ([AppDispatchers.io], one step at a time), and replace the notification with the configured one
43
+ * (same notification id).
44
+ *
45
+ * **Commands** (the state machine is described in [ServiceCommands]):
46
+ * - A start sent by this process: enter the foreground; if a stop was requested after it (and no start after that
47
+ * stop), `stopSelf(startId)`; otherwise show the configured notification and follow notification config changes.
48
+ * - A start sent by an earlier process (it died between `startForegroundService` and `onStartCommand`, and Android
49
+ * delivered the command to the restarted service): enter the foreground, then `engine.restore("restore")` if tracking
50
+ * is enabled (the engine does nothing when a session already runs), or `stopSelf(startId)` if it is not.
51
+ * - A null intent (`START_STICKY` restart after the process died): enter the foreground, then `engine.restore("restore")`
52
+ * if tracking is enabled, or `stopSelf(startId)` if it is not.
53
+ * - A resume ([ACTION_RESUME], from a tap on the resume notification, sent with `startForegroundService`): enter the
54
+ * foreground (a start from a notification gets while-in-use location access on Android 14+), then
55
+ * `engine.resumeFromNotification()`.
56
+ * - A stop ([ACTION_STOP], sent with `startService`, so it arrives after every start sent before it): ignored if a
57
+ * start was sent after it; otherwise `stopSelf(startId)` (Android ignores that when a newer command was sent). A stop
58
+ * marked as sent with `startForegroundService` first enters the foreground if the service is not in it yet.
59
+ *
60
+ * **Failures.** If `startForeground` throws (for example a background start on Android 12+, or Android 14+ with only
61
+ * while-in-use location permission), the failure is logged, the service stops, and (for start commands) the engine
62
+ * learns about it through `onServiceStartFailed`, which ends a running or enabled session with the `tracking_stop`
63
+ * reason `service_start_failed` (`permission_denied` when foreground location permission is no longer granted).
64
+ *
65
+ * [onTaskRemoved] forwards to `engine.onTerminate()`, which applies `app.stopOnTerminate`.
66
+ */
67
+ class LocationTrackingService : Service() {
68
+ @Volatile
69
+ private var deps: ServiceDeps? = null
70
+
71
+ /** Guards [configJob] and [destroyed]. */
72
+ private val instanceLock = Any()
73
+ private var configJob: Job? = null
74
+ private var destroyed = false
75
+
76
+ override fun onBind(intent: Intent?): IBinder? = null
77
+
78
+ override fun onCreate() {
79
+ super.onCreate()
80
+ Logger.d(TAG, "created")
81
+ }
82
+
83
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
84
+ val command = ServiceCommand.parse(intent)
85
+ return if (command.kind == ServiceCommand.Kind.STOP) onStopCommand(command, startId) else onStart(command, startId)
86
+ }
87
+
88
+ private fun onStart(command: ServiceCommand, startId: Int): Int {
89
+ val failure = enterForeground(command.notification)
90
+ if (command.fromThisProcess) ServiceCommands.startHandled(command.seq)
91
+ if (failure != null) {
92
+ stopSelf(startId)
93
+ // start() already returned true to the engine; tell it, so tracking ends with an audit record instead of
94
+ // looking enabled while no location can be collected in the background.
95
+ val error = "${failure.javaClass.simpleName}: ${failure.message}"
96
+ offMain("reporting the start failure") { deps ->
97
+ deps.launchEngine(TAG, "onServiceStartFailed") { onServiceStartFailed(error) }
98
+ }
99
+ return START_NOT_STICKY
100
+ }
101
+ if (command.fromThisProcess && ServiceCommands.isStoppedAfter(command.seq)) {
102
+ // Stop racing start: the service entered the foreground, so it may stop now. If the stop command is still
103
+ // queued, Android ignores this stopSelf (the stop command has a newer start id) and the stop command stops it.
104
+ Logger.i(TAG, "a stop was requested after this start; stopping")
105
+ stopSelf(startId)
106
+ return START_NOT_STICKY
107
+ }
108
+ offMain("starting", onUnavailable = { stopSelf(startId) }) { deps -> afterForeground(deps, command, startId) }
109
+ return START_STICKY
110
+ }
111
+
112
+ private fun onStopCommand(command: ServiceCommand, startId: Int): Int {
113
+ if (command.foregroundRequired && !isRunning) {
114
+ // Sent with startForegroundService(): Android requires startForeground before the service stops. The
115
+ // plugin's controller sends stops with startService(); this guard keeps any other sender safe.
116
+ enterForeground(null)
117
+ }
118
+ if (command.fromThisProcess && ServiceCommands.isStartedAfter(command.seq)) {
119
+ Logger.d(TAG, "stop ignored: a start was sent after it")
120
+ return START_STICKY
121
+ }
122
+ Logger.i(TAG, "stop requested")
123
+ stopSelf(startId)
124
+ return START_NOT_STICKY
125
+ }
126
+
127
+ /** Runs off the main thread after `startForeground` succeeded for a start command. */
128
+ private fun afterForeground(deps: ServiceDeps, command: ServiceCommand, startId: Int) {
129
+ if (command.kind == ServiceCommand.Kind.RESUME) {
130
+ Logger.i(TAG, "resume notification tapped")
131
+ // The engine stops the service again if it does not resume.
132
+ deps.launchEngine(TAG, "resumeFromNotification") { resumeFromNotification() }
133
+ } else if (!command.fromThisProcess) {
134
+ val origin = if (command.kind == ServiceCommand.Kind.RESTART) {
135
+ "restarted by the system"
136
+ } else {
137
+ "start command from an earlier process"
138
+ }
139
+ if (!deps.configStore.runtime.value.enabled) {
140
+ Logger.i(TAG, "$origin while tracking is disabled; stopping")
141
+ stopSelf(startId)
142
+ return
143
+ }
144
+ Logger.i(TAG, "$origin; restoring tracking")
145
+ // Does nothing if a session already runs in this process.
146
+ deps.launchEngine(TAG, "restore") { restore(REASON_RESTORE) }
147
+ }
148
+ refresh(this, deps.configStore)
149
+ watchNotificationConfig(deps)
150
+ }
151
+
152
+ override fun onTaskRemoved(rootIntent: Intent?) {
153
+ super.onTaskRemoved(rootIntent)
154
+ Logger.i(TAG, "task removed")
155
+ offMain("task removal") { deps -> deps.launchEngine(TAG, "onTerminate") { onTerminate() } }
156
+ }
157
+
158
+ override fun onDestroy() {
159
+ synchronized(instanceLock) {
160
+ destroyed = true
161
+ configJob?.cancel()
162
+ configJob = null
163
+ }
164
+ synchronized(lock) {
165
+ isRunning = false
166
+ shownConfig = null
167
+ shownNotification = null
168
+ }
169
+ // The system removes the foreground notification before onDestroy; drop a copy that a concurrent refresh
170
+ // may have re-posted in between. No refresh can post after isRunning is false.
171
+ try {
172
+ NotificationManagerCompat.from(this).cancel(Constants.NOTIFICATION_ID)
173
+ } catch (e: Exception) {
174
+ Logger.w(TAG, "could not cancel the notification", e)
175
+ }
176
+ Logger.d(TAG, "destroyed")
177
+ super.onDestroy()
178
+ }
179
+
180
+ /**
181
+ * Calls `startForeground` (location type on API 29+) with the notification already shown, or with the initial
182
+ * notification ([NotificationFactory.buildInitial] of [spec], the notification fields carried by the command).
183
+ * Sets [isRunning] on success and clears it on failure. Returns the failure, never throws.
184
+ */
185
+ private fun enterForeground(spec: NotificationConfig?): Exception? = try {
186
+ val shown = synchronized(lock) { if (isRunning) shownNotification else null }
187
+ val initial = if (shown == null) NotificationFactory(this).buildInitial(spec) else null
188
+ val notification = initial?.notification ?: checkNotNull(shown)
189
+ val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
190
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
191
+ } else {
192
+ 0
193
+ }
194
+ ServiceCompat.startForeground(this, Constants.NOTIFICATION_ID, notification, type)
195
+ synchronized(lock) {
196
+ isRunning = true
197
+ if (initial != null) {
198
+ shownConfig = initial.config
199
+ shownNotification = initial.notification
200
+ }
201
+ }
202
+ null
203
+ } catch (e: Exception) {
204
+ synchronized(lock) {
205
+ isRunning = false
206
+ shownConfig = null
207
+ shownNotification = null
208
+ }
209
+ Logger.e(TAG, "startForeground failed; stopping the service", e)
210
+ e
211
+ }
212
+
213
+ /** Re-posts the notification whenever the notification config changes (once per service instance). */
214
+ private fun watchNotificationConfig(deps: ServiceDeps) {
215
+ synchronized(instanceLock) {
216
+ if (destroyed || configJob != null) return
217
+ val context: Context = this
218
+ configJob = deps.scope.launch {
219
+ deps.configStore.config
220
+ .map { it.notification }
221
+ .distinctUntilChanged()
222
+ .collect {
223
+ try {
224
+ refresh(context, deps.configStore)
225
+ } catch (e: Exception) {
226
+ Logger.w(TAG, "could not update the notification", e)
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Runs [block] with the dependencies on the background dispatcher. The dependencies are resolved there on first
235
+ * use (this loads `Components`). If they cannot be resolved, the failure is logged and [onUnavailable] runs.
236
+ */
237
+ private fun offMain(what: String, onUnavailable: () -> Unit = {}, block: suspend (ServiceDeps) -> Unit) {
238
+ backgroundScope.launch(dispatcherOverride ?: defaultDispatcher) {
239
+ val resolved = deps ?: try {
240
+ ServiceDeps.from(this@LocationTrackingService).also { deps = it }
241
+ } catch (e: Exception) {
242
+ Logger.e(TAG, "components unavailable ($what)", e)
243
+ null
244
+ }
245
+ if (resolved == null) {
246
+ onUnavailable()
247
+ return@launch
248
+ }
249
+ try {
250
+ block(resolved)
251
+ } catch (e: CancellationException) {
252
+ throw e
253
+ } catch (e: Exception) {
254
+ Logger.e(TAG, "$what failed", e)
255
+ }
256
+ }
257
+ }
258
+
259
+ companion object {
260
+ private const val TAG = "LT.Service"
261
+ private const val REASON_RESTORE = "restore"
262
+
263
+ /** Command that stops the service after any start queued before it. */
264
+ internal const val ACTION_STOP = "com.brickssoft.locationtracking.service.STOP"
265
+ internal const val ACTION_RESUME = "com.brickssoft.locationtracking.service.RESUME"
266
+
267
+ private val lock = Any()
268
+
269
+ /**
270
+ * True while the service is in the foreground: set after `startForeground` succeeded, cleared in `onDestroy`
271
+ * and when `startForeground` fails. [DefaultServiceController.isRunning] reports it.
272
+ */
273
+ @Volatile
274
+ var isRunning: Boolean = false
275
+ private set
276
+
277
+ /** The config of the notification currently shown; guarded by [lock]. */
278
+ private var shownConfig: NotificationConfig? = null
279
+
280
+ /** The notification currently shown; guarded by [lock]. */
281
+ private var shownNotification: Notification? = null
282
+
283
+ /** Test hook: the dispatcher of the service's background steps instead of [defaultDispatcher]. */
284
+ @VisibleForTesting
285
+ @Volatile
286
+ internal var dispatcherOverride: CoroutineDispatcher? = null
287
+
288
+ /** One background step at a time, in the order the commands arrived. */
289
+ private val defaultDispatcher: CoroutineDispatcher by lazy { AppDispatchers.DEFAULT.io.limitedParallelism(1) }
290
+
291
+ /** Outlives service instances, so a failure report is not cancelled by the service's own `onDestroy`. */
292
+ private val backgroundScope = CoroutineScope(
293
+ SupervisorJob() + CoroutineExceptionHandler { _, t -> Logger.e(TAG, "uncaught", t) },
294
+ )
295
+
296
+ /**
297
+ * Re-posts the notification from the current notification config of [configStore], if the service is
298
+ * running and that config differs from the one shown. Returns true if it posted.
299
+ *
300
+ * Nothing is posted once the service is destroyed, so no notification can outlive it. If the config changes
301
+ * while the notification is being built, the stale one is dropped; the service's config watcher posts the
302
+ * newer one.
303
+ */
304
+ @SuppressLint("MissingPermission") // without POST_NOTIFICATIONS the system hides it; nothing to handle
305
+ internal fun refresh(context: Context, configStore: ConfigStore): Boolean {
306
+ val config = configStore.config.value.notification
307
+ synchronized(lock) { if (!isRunning || config == shownConfig) return false }
308
+ val notification = NotificationFactory(context).buildSafely(config)
309
+ synchronized(lock) {
310
+ if (!isRunning || config == shownConfig || config != configStore.config.value.notification) return false
311
+ NotificationManagerCompat.from(context).notify(Constants.NOTIFICATION_ID, notification)
312
+ shownConfig = config
313
+ shownNotification = notification
314
+ }
315
+ Logger.d(TAG, "notification updated")
316
+ return true
317
+ }
318
+ }
319
+ }
@@ -0,0 +1,42 @@
1
+ package com.brickssoft.locationtracking.service
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import com.brickssoft.locationtracking.core.Constants
7
+ import com.brickssoft.locationtracking.core.EventBus
8
+ import com.brickssoft.locationtracking.core.Logger
9
+ import com.brickssoft.locationtracking.core.TrackingEvent
10
+
11
+ /** Turns a tap on a notification action button into a `notificationaction` event (`{ id }`). */
12
+ class NotificationActionReceiver : BroadcastReceiver() {
13
+ override fun onReceive(context: Context, intent: Intent) {
14
+ val events = try {
15
+ ServiceDeps.from(context).events
16
+ } catch (e: Exception) {
17
+ Logger.e(TAG, "components unavailable", e)
18
+ return
19
+ }
20
+ dispatch(intent, events)
21
+ }
22
+
23
+ internal companion object {
24
+ private const val TAG = "LT.NotificationAction"
25
+
26
+ /** Emits [TrackingEvent.NotificationAction] for a valid action intent; returns false otherwise. */
27
+ fun dispatch(intent: Intent, events: EventBus): Boolean {
28
+ if (intent.action != Constants.ACTION_NOTIFICATION_ACTION) {
29
+ Logger.w(TAG, "unexpected action ${intent.action}")
30
+ return false
31
+ }
32
+ val id = intent.getStringExtra(Constants.EXTRA_ACTION_ID)
33
+ if (id.isNullOrEmpty()) {
34
+ Logger.w(TAG, "notification action without an id")
35
+ return false
36
+ }
37
+ Logger.d(TAG, "notification action '$id'")
38
+ events.emit(TrackingEvent.NotificationAction(id))
39
+ return true
40
+ }
41
+ }
42
+ }