@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +106 -0
- package/android/consumer-rules.pro +42 -0
- package/android/src/main/AndroidManifest.xml +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
- package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
- package/android/src/main/res/values/lt_strings.xml +11 -0
- package/android/src/main/res/xml/lt_file_paths.xml +5 -0
- package/dist/esm/definitions.d.ts +719 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/events.d.ts +50 -0
- package/dist/esm/events.js +52 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +10 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/web/config.d.ts +19 -0
- package/dist/esm/web/config.js +243 -0
- package/dist/esm/web/config.js.map +1 -0
- package/dist/esm/web/device.d.ts +9 -0
- package/dist/esm/web/device.js +90 -0
- package/dist/esm/web/device.js.map +1 -0
- package/dist/esm/web/errors.d.ts +14 -0
- package/dist/esm/web/errors.js +19 -0
- package/dist/esm/web/errors.js.map +1 -0
- package/dist/esm/web/geolocation.d.ts +26 -0
- package/dist/esm/web/geolocation.js +117 -0
- package/dist/esm/web/geolocation.js.map +1 -0
- package/dist/esm/web/location.d.ts +7 -0
- package/dist/esm/web/location.js +70 -0
- package/dist/esm/web/location.js.map +1 -0
- package/dist/esm/web.d.ts +164 -0
- package/dist/esm/web.js +328 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +945 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +948 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +59 -4
package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt
ADDED
|
@@ -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
|
+
}
|