@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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.config
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.Constants
|
|
4
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Clamps config values to their allowed ranges and logs a warning for each value it changes. Never throws.
|
|
8
|
+
*
|
|
9
|
+
* - `heartbeat.minInterval` >= 60 s and `heartbeat.maxInterval` >= `minInterval`;
|
|
10
|
+
* - `activity.minimumActivityRecognitionConfidence` in 0..100;
|
|
11
|
+
* - `geolocation.stationaryRadius` >= 1; distances, intervals, delays and filter thresholds >= 0;
|
|
12
|
+
* - `geolocation.locationTimeout` and `http.timeout` > 0 (otherwise their defaults);
|
|
13
|
+
* - `http.maxBatchSize` >= 1, `http.autoSyncThreshold` >= 0, `http.syncInterval` >= 0;
|
|
14
|
+
* - `persistence.maxDaysToPersist` >= 1, `persistence.maxRecordsToPersist` >= -1 (-1 = unlimited);
|
|
15
|
+
* - `logger.logMaxDays` >= 1;
|
|
16
|
+
* - at most [Constants.MAX_NOTIFICATION_ACTIONS] notification actions (the first ones are kept);
|
|
17
|
+
* - non-finite numbers become their defaults.
|
|
18
|
+
*/
|
|
19
|
+
object ConfigValidator {
|
|
20
|
+
private const val TAG = "LT.ConfigValidator"
|
|
21
|
+
|
|
22
|
+
/** Smallest allowed `heartbeat.minInterval`, seconds. */
|
|
23
|
+
const val MIN_HEARTBEAT_INTERVAL_S = 60
|
|
24
|
+
|
|
25
|
+
const val MIN_STATIONARY_RADIUS_M = 1.0
|
|
26
|
+
|
|
27
|
+
/** Returns [config] with every out-of-range value clamped (equal to [config] when all values are valid). */
|
|
28
|
+
fun validate(config: Config): Config {
|
|
29
|
+
val validated = config.copy(
|
|
30
|
+
geolocation = geolocation(config.geolocation),
|
|
31
|
+
activity = activity(config.activity),
|
|
32
|
+
heartbeat = heartbeat(config.heartbeat),
|
|
33
|
+
http = http(config.http),
|
|
34
|
+
persistence = persistence(config.persistence),
|
|
35
|
+
notification = notification(config.notification),
|
|
36
|
+
logger = config.logger.copy(logMaxDays = atLeast("logger.logMaxDays", config.logger.logMaxDays, 1)),
|
|
37
|
+
)
|
|
38
|
+
return if (validated == config) config else validated
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun geolocation(g: GeolocationConfig): GeolocationConfig {
|
|
42
|
+
val d = GeolocationConfig()
|
|
43
|
+
return g.copy(
|
|
44
|
+
distanceFilter = atLeast("geolocation.distanceFilter", g.distanceFilter, 0.0, d.distanceFilter),
|
|
45
|
+
locationUpdateInterval = atLeast("geolocation.locationUpdateInterval", g.locationUpdateInterval, 0L),
|
|
46
|
+
fastestLocationUpdateInterval = atLeast(
|
|
47
|
+
"geolocation.fastestLocationUpdateInterval",
|
|
48
|
+
g.fastestLocationUpdateInterval,
|
|
49
|
+
0L,
|
|
50
|
+
),
|
|
51
|
+
elasticityMultiplier = atLeast(
|
|
52
|
+
"geolocation.elasticityMultiplier",
|
|
53
|
+
g.elasticityMultiplier,
|
|
54
|
+
0.0,
|
|
55
|
+
d.elasticityMultiplier,
|
|
56
|
+
),
|
|
57
|
+
stationaryRadius = atLeast(
|
|
58
|
+
"geolocation.stationaryRadius",
|
|
59
|
+
g.stationaryRadius,
|
|
60
|
+
MIN_STATIONARY_RADIUS_M,
|
|
61
|
+
d.stationaryRadius,
|
|
62
|
+
),
|
|
63
|
+
stopTimeout = atLeast("geolocation.stopTimeout", g.stopTimeout, 0),
|
|
64
|
+
stopAfterElapsedMinutes = atLeast("geolocation.stopAfterElapsedMinutes", g.stopAfterElapsedMinutes, 0),
|
|
65
|
+
locationTimeout = positive("geolocation.locationTimeout", g.locationTimeout, d.locationTimeout),
|
|
66
|
+
filter = filter(g.filter),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun filter(f: LocationFilterConfig): LocationFilterConfig {
|
|
71
|
+
val d = LocationFilterConfig()
|
|
72
|
+
return f.copy(
|
|
73
|
+
trackingAccuracyThreshold = atLeast(
|
|
74
|
+
"geolocation.filter.trackingAccuracyThreshold",
|
|
75
|
+
f.trackingAccuracyThreshold,
|
|
76
|
+
0.0,
|
|
77
|
+
d.trackingAccuracyThreshold,
|
|
78
|
+
),
|
|
79
|
+
maxImpliedSpeed = atLeast("geolocation.filter.maxImpliedSpeed", f.maxImpliedSpeed, 0.0, d.maxImpliedSpeed),
|
|
80
|
+
odometerAccuracyThreshold = atLeast(
|
|
81
|
+
"geolocation.filter.odometerAccuracyThreshold",
|
|
82
|
+
f.odometerAccuracyThreshold,
|
|
83
|
+
0.0,
|
|
84
|
+
d.odometerAccuracyThreshold,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun activity(a: ActivityConfig): ActivityConfig = a.copy(
|
|
90
|
+
activityRecognitionInterval = atLeast(
|
|
91
|
+
"activity.activityRecognitionInterval",
|
|
92
|
+
a.activityRecognitionInterval,
|
|
93
|
+
0L,
|
|
94
|
+
),
|
|
95
|
+
minimumActivityRecognitionConfidence = inRange(
|
|
96
|
+
"activity.minimumActivityRecognitionConfidence",
|
|
97
|
+
a.minimumActivityRecognitionConfidence,
|
|
98
|
+
0,
|
|
99
|
+
100,
|
|
100
|
+
),
|
|
101
|
+
motionTriggerDelay = atLeast("activity.motionTriggerDelay", a.motionTriggerDelay, 0L),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
private fun heartbeat(h: HeartbeatConfig): HeartbeatConfig {
|
|
105
|
+
val min = atLeast("heartbeat.minInterval", h.minInterval, MIN_HEARTBEAT_INTERVAL_S)
|
|
106
|
+
val max = atLeast("heartbeat.maxInterval", h.maxInterval, min)
|
|
107
|
+
return h.copy(minInterval = min, maxInterval = max)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun http(h: HttpConfig): HttpConfig = h.copy(
|
|
111
|
+
autoSyncThreshold = atLeast("http.autoSyncThreshold", h.autoSyncThreshold, 0),
|
|
112
|
+
syncInterval = atLeast("http.syncInterval", h.syncInterval, 0),
|
|
113
|
+
maxBatchSize = atLeast("http.maxBatchSize", h.maxBatchSize, 1),
|
|
114
|
+
timeout = positive("http.timeout", h.timeout, HttpConfig().timeout),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
private fun persistence(p: PersistenceConfig): PersistenceConfig = p.copy(
|
|
118
|
+
maxDaysToPersist = atLeast("persistence.maxDaysToPersist", p.maxDaysToPersist, 1),
|
|
119
|
+
maxRecordsToPersist = atLeast("persistence.maxRecordsToPersist", p.maxRecordsToPersist, -1),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
private fun notification(n: NotificationConfig): NotificationConfig {
|
|
123
|
+
val max = Constants.MAX_NOTIFICATION_ACTIONS
|
|
124
|
+
if (n.actions.size <= max) return n
|
|
125
|
+
Logger.w(TAG, "notification.actions has ${n.actions.size} entries; keeping the first $max")
|
|
126
|
+
return n.copy(actions = n.actions.take(max))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- clamps (each logs when it changes a value)
|
|
130
|
+
|
|
131
|
+
private fun atLeast(path: String, value: Int, min: Int): Int {
|
|
132
|
+
if (value >= min) return value
|
|
133
|
+
clamped(path, value, min)
|
|
134
|
+
return min
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun atLeast(path: String, value: Long, min: Long): Long {
|
|
138
|
+
if (value >= min) return value
|
|
139
|
+
clamped(path, value, min)
|
|
140
|
+
return min
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Non-finite [value]s become [fallback]. */
|
|
144
|
+
private fun atLeast(path: String, value: Double, min: Double, fallback: Double): Double {
|
|
145
|
+
if (!value.isFinite()) {
|
|
146
|
+
clamped(path, value, fallback)
|
|
147
|
+
return fallback
|
|
148
|
+
}
|
|
149
|
+
if (value >= min) return value
|
|
150
|
+
clamped(path, value, min)
|
|
151
|
+
return min
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private fun inRange(path: String, value: Int, min: Int, max: Int): Int {
|
|
155
|
+
val result = value.coerceIn(min, max)
|
|
156
|
+
if (result != value) clamped(path, value, result)
|
|
157
|
+
return result
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Values <= 0 become [fallback]. */
|
|
161
|
+
private fun positive(path: String, value: Long, fallback: Long): Long {
|
|
162
|
+
if (value > 0) return value
|
|
163
|
+
clamped(path, value, fallback)
|
|
164
|
+
return fallback
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun clamped(path: String, from: Any, to: Any) {
|
|
168
|
+
Logger.w(TAG, "config $path=$from is out of range; using $to")
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.config
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
4
|
+
import com.brickssoft.locationtracking.model.ActivitySample
|
|
5
|
+
import com.brickssoft.locationtracking.model.ActivityType
|
|
6
|
+
import com.brickssoft.locationtracking.model.JsonUtil
|
|
7
|
+
import com.brickssoft.locationtracking.model.ProviderStateJson
|
|
8
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
9
|
+
import com.brickssoft.locationtracking.model.TrackedLocationJson
|
|
10
|
+
import org.json.JSONObject
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lossless persistence JSON for [RuntimeState] (not a JS shape). Keys use the Kotlin property names; times are
|
|
14
|
+
* epoch ms. [fromJson] is lenient: a missing or invalid field falls back to its default.
|
|
15
|
+
*/
|
|
16
|
+
internal object RuntimeStateJson {
|
|
17
|
+
private const val TAG = "LT.RuntimeStateJson"
|
|
18
|
+
|
|
19
|
+
fun toJson(r: RuntimeState): JSONObject = JSONObject()
|
|
20
|
+
.put("enabled", r.enabled)
|
|
21
|
+
.put("trackingMode", r.trackingMode.wire)
|
|
22
|
+
.put("isMoving", r.isMoving)
|
|
23
|
+
.put("odometer", if (r.odometer.isFinite()) r.odometer else 0.0)
|
|
24
|
+
.put("activity", JSONObject().put("type", r.activity.type.wire).put("confidence", r.activity.confidence))
|
|
25
|
+
.put("lastLocation", r.lastLocation?.let { lastLocationToJson(it) } ?: JSONObject.NULL)
|
|
26
|
+
.put("lastRecordAt", JsonUtil.orNull(r.lastRecordAt))
|
|
27
|
+
.put("lastRecordElapsed", JsonUtil.orNull(r.lastRecordElapsed))
|
|
28
|
+
.put("lastRecordBootCount", JsonUtil.orNull(r.lastRecordBootCount))
|
|
29
|
+
.put("lastHeartbeatAt", JsonUtil.orNull(r.lastHeartbeatAt))
|
|
30
|
+
.put("trackingStartedAt", JsonUtil.orNull(r.trackingStartedAt))
|
|
31
|
+
.put("providerState", r.providerState?.let { ProviderStateJson.toJson(it) } ?: JSONObject.NULL)
|
|
32
|
+
.put("didReady", r.didReady)
|
|
33
|
+
|
|
34
|
+
fun fromJson(json: JSONObject): RuntimeState {
|
|
35
|
+
val d = RuntimeState()
|
|
36
|
+
return RuntimeState(
|
|
37
|
+
enabled = JsonUtil.optBoolean(json, "enabled") ?: d.enabled,
|
|
38
|
+
trackingMode = TrackingMode.fromWire(JsonUtil.optString(json, "trackingMode")) ?: d.trackingMode,
|
|
39
|
+
isMoving = JsonUtil.optBoolean(json, "isMoving") ?: d.isMoving,
|
|
40
|
+
odometer = JsonUtil.optDouble(json, "odometer") ?: d.odometer,
|
|
41
|
+
activity = json.optJSONObject("activity")?.let { a ->
|
|
42
|
+
ActivitySample(
|
|
43
|
+
type = ActivityType.fromWire(JsonUtil.optString(a, "type")) ?: ActivityType.UNKNOWN,
|
|
44
|
+
confidence = JsonUtil.optInt(a, "confidence") ?: 0,
|
|
45
|
+
)
|
|
46
|
+
} ?: d.activity,
|
|
47
|
+
lastLocation = json.optJSONObject("lastLocation")?.let { l ->
|
|
48
|
+
try {
|
|
49
|
+
TrackedLocationJson.fromJson(l)
|
|
50
|
+
} catch (e: Exception) {
|
|
51
|
+
Logger.w(TAG, "dropping invalid persisted lastLocation", e)
|
|
52
|
+
null
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
lastRecordAt = JsonUtil.optLong(json, "lastRecordAt"),
|
|
56
|
+
lastRecordElapsed = JsonUtil.optLong(json, "lastRecordElapsed"),
|
|
57
|
+
lastRecordBootCount = JsonUtil.optInt(json, "lastRecordBootCount"),
|
|
58
|
+
lastHeartbeatAt = JsonUtil.optLong(json, "lastHeartbeatAt"),
|
|
59
|
+
trackingStartedAt = JsonUtil.optLong(json, "trackingStartedAt"),
|
|
60
|
+
providerState = json.optJSONObject("providerState")?.let { ProviderStateJson.fromJson(it) },
|
|
61
|
+
didReady = JsonUtil.optBoolean(json, "didReady") ?: d.didReady,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** org.json rejects NaN/Infinity, so a location with non-finite coordinates is not persisted. */
|
|
66
|
+
private fun lastLocationToJson(l: TrackedLocation): Any =
|
|
67
|
+
if (l.latitude.isFinite() && l.longitude.isFinite() && (l.altitude?.isFinite() != false)) {
|
|
68
|
+
TrackedLocationJson.toJson(l)
|
|
69
|
+
} else {
|
|
70
|
+
Logger.w(TAG, "not persisting lastLocation with non-finite coordinates")
|
|
71
|
+
JSONObject.NULL
|
|
72
|
+
}
|
|
73
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.config
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import com.brickssoft.locationtracking.core.Clock
|
|
6
|
+
import com.brickssoft.locationtracking.core.Constants
|
|
7
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
8
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
9
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
10
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
11
|
+
import org.json.JSONObject
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* [ConfigStore] backed by the SharedPreferences file [Constants.PREFS_NAME].
|
|
15
|
+
*
|
|
16
|
+
* Config and runtime state are loaded synchronously in the constructor. An unreadable stored value falls back to
|
|
17
|
+
* its default; unreadable JSON text falls back to all defaults.
|
|
18
|
+
*
|
|
19
|
+
* Every write runs under one lock: it computes the new value, persists it with `apply()` and publishes it on the
|
|
20
|
+
* matching [StateFlow] before returning. Config writes go through [ConfigJson.parse] and [ConfigValidator].
|
|
21
|
+
* Only the `cfg_` keys of the shared prefs file are touched.
|
|
22
|
+
*/
|
|
23
|
+
class SharedPrefsConfigStore(
|
|
24
|
+
context: Context,
|
|
25
|
+
@Suppress("unused") private val clock: Clock,
|
|
26
|
+
) : ConfigStore {
|
|
27
|
+
private val prefs: SharedPreferences =
|
|
28
|
+
(context.applicationContext ?: context).getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
|
29
|
+
private val lock = Any()
|
|
30
|
+
|
|
31
|
+
private val configFlow = MutableStateFlow(loadConfig())
|
|
32
|
+
private val runtimeFlow = MutableStateFlow(loadRuntime())
|
|
33
|
+
|
|
34
|
+
override val config: StateFlow<Config> = configFlow.asStateFlow()
|
|
35
|
+
override val runtime: StateFlow<RuntimeState> = runtimeFlow.asStateFlow()
|
|
36
|
+
|
|
37
|
+
override fun ready(json: JSONObject?, reset: Boolean): Config = synchronized(lock) {
|
|
38
|
+
val current = configFlow.value
|
|
39
|
+
val next = when {
|
|
40
|
+
reset -> build(json, Config())
|
|
41
|
+
!runtimeFlow.value.didReady -> build(json, current)
|
|
42
|
+
else -> {
|
|
43
|
+
if (json != null && json.length() > 0) {
|
|
44
|
+
Logger.i(TAG, "ready(reset=false): keeping the persisted config; the given config is ignored")
|
|
45
|
+
}
|
|
46
|
+
current
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
val runtime = runtimeFlow.value.copy(didReady = true)
|
|
50
|
+
persist(next.takeIf { it != current }, runtime.takeIf { it != runtimeFlow.value })
|
|
51
|
+
configFlow.value = next
|
|
52
|
+
runtimeFlow.value = runtime
|
|
53
|
+
next
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun merge(json: JSONObject): Config = setConfig { build(json, it) }
|
|
57
|
+
|
|
58
|
+
override fun reset(json: JSONObject?): Config = setConfig { build(json, Config()) }
|
|
59
|
+
|
|
60
|
+
override fun update(transform: (Config) -> Config): Config = setConfig { ConfigValidator.validate(transform(it)) }
|
|
61
|
+
|
|
62
|
+
override fun updateRuntime(transform: (RuntimeState) -> RuntimeState): RuntimeState = synchronized(lock) {
|
|
63
|
+
val current = runtimeFlow.value
|
|
64
|
+
val next = transform(current)
|
|
65
|
+
if (next != current) {
|
|
66
|
+
persist(null, next)
|
|
67
|
+
runtimeFlow.value = next
|
|
68
|
+
}
|
|
69
|
+
next
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private inline fun setConfig(compute: (Config) -> Config): Config = synchronized(lock) {
|
|
73
|
+
val current = configFlow.value
|
|
74
|
+
val next = compute(current)
|
|
75
|
+
if (next != current) {
|
|
76
|
+
persist(next, null)
|
|
77
|
+
configFlow.value = next
|
|
78
|
+
}
|
|
79
|
+
next
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Defaults-or-[base] plus [json], clamped. Throws INVALID_ARGUMENT (before anything changes) on bad input. */
|
|
83
|
+
private fun build(json: JSONObject?, base: Config): Config =
|
|
84
|
+
ConfigValidator.validate(if (json == null) base else ConfigJson.parse(json, base))
|
|
85
|
+
|
|
86
|
+
/** Writes the non-null values in one `apply()`. A value that cannot be serialized is logged and skipped. */
|
|
87
|
+
private fun persist(config: Config?, runtime: RuntimeState?) {
|
|
88
|
+
if (config == null && runtime == null) return
|
|
89
|
+
val editor = prefs.edit()
|
|
90
|
+
if (config != null) {
|
|
91
|
+
serialize("config") { ConfigJson.toJson(config) }?.let { editor.putString(KEY_CONFIG, it) }
|
|
92
|
+
}
|
|
93
|
+
if (runtime != null) {
|
|
94
|
+
serialize("runtime state") { RuntimeStateJson.toJson(runtime) }?.let { editor.putString(KEY_RUNTIME, it) }
|
|
95
|
+
}
|
|
96
|
+
editor.apply()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private inline fun serialize(what: String, toJson: () -> JSONObject): String? = try {
|
|
100
|
+
toJson().toString()
|
|
101
|
+
} catch (e: Exception) {
|
|
102
|
+
Logger.e(TAG, "cannot persist $what", e)
|
|
103
|
+
null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun loadConfig(): Config {
|
|
107
|
+
val text = prefs.getString(KEY_CONFIG, null) ?: return Config()
|
|
108
|
+
return try {
|
|
109
|
+
ConfigValidator.validate(ConfigJson.parseStored(JSONObject(text)))
|
|
110
|
+
} catch (e: Exception) {
|
|
111
|
+
Logger.e(TAG, "persisted config is unreadable; using defaults", e)
|
|
112
|
+
Config()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun loadRuntime(): RuntimeState {
|
|
117
|
+
val text = prefs.getString(KEY_RUNTIME, null) ?: return RuntimeState()
|
|
118
|
+
return try {
|
|
119
|
+
RuntimeStateJson.fromJson(JSONObject(text))
|
|
120
|
+
} catch (e: Exception) {
|
|
121
|
+
Logger.e(TAG, "persisted runtime state is unreadable; using defaults", e)
|
|
122
|
+
RuntimeState()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
internal companion object {
|
|
127
|
+
private const val TAG = "LT.ConfigStore"
|
|
128
|
+
|
|
129
|
+
/** Full TS-shaped config JSON (`ConfigJson.toJson`). */
|
|
130
|
+
const val KEY_CONFIG = "cfg_config"
|
|
131
|
+
|
|
132
|
+
/** Runtime state JSON (`RuntimeStateJson`). */
|
|
133
|
+
const val KEY_RUNTIME = "cfg_runtime"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.config
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.model.ActivitySample
|
|
4
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
5
|
+
import com.brickssoft.locationtracking.model.ProviderState
|
|
6
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
7
|
+
|
|
8
|
+
/** JS `TrackingMode`. */
|
|
9
|
+
enum class TrackingMode(val wire: String) {
|
|
10
|
+
LOCATION("location"),
|
|
11
|
+
GEOFENCES("geofences"),
|
|
12
|
+
;
|
|
13
|
+
|
|
14
|
+
companion object {
|
|
15
|
+
fun fromWire(value: String?): TrackingMode? = entries.firstOrNull { it.wire == value }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Persisted runtime state (survives process death).
|
|
21
|
+
*
|
|
22
|
+
* @property lastRecordAt wall time of the last persisted record, epoch ms.
|
|
23
|
+
* @property lastRecordElapsed elapsed-realtime ms of the last persisted record.
|
|
24
|
+
* @property lastRecordBootCount boot count when the last record was persisted.
|
|
25
|
+
* @property providerState last provider state seen, used to detect `providerchange` across process restarts.
|
|
26
|
+
* @property didReady true once `ready()` has ever completed (drives `ReadyOptions.reset=false`).
|
|
27
|
+
*/
|
|
28
|
+
data class RuntimeState(
|
|
29
|
+
val enabled: Boolean = false,
|
|
30
|
+
val trackingMode: TrackingMode = TrackingMode.LOCATION,
|
|
31
|
+
val isMoving: Boolean = false,
|
|
32
|
+
val odometer: Double = 0.0,
|
|
33
|
+
val activity: ActivitySample = ActivitySample.UNKNOWN,
|
|
34
|
+
val lastLocation: TrackedLocation? = null,
|
|
35
|
+
val lastRecordAt: Long? = null,
|
|
36
|
+
val lastRecordElapsed: Long? = null,
|
|
37
|
+
val lastRecordBootCount: Int? = null,
|
|
38
|
+
val lastHeartbeatAt: Long? = null,
|
|
39
|
+
val trackingStartedAt: Long? = null,
|
|
40
|
+
val providerState: ProviderState? = null,
|
|
41
|
+
val didReady: Boolean = false,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
/** Snapshot returned to JS as `State` (see `ConfigJson.stateToJson`). */
|
|
45
|
+
data class State(val config: Config, val runtime: RuntimeState, val backend: ProviderKind)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.CoroutineDispatcher
|
|
4
|
+
import kotlinx.coroutines.Dispatchers
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dispatchers used by all components. Never hard-code `Dispatchers.IO`; inject this instead.
|
|
8
|
+
*
|
|
9
|
+
* @property main Android main thread.
|
|
10
|
+
* @property io SQLite, files and HTTP.
|
|
11
|
+
* @property engine single-threaded dispatcher on which all engine state changes happen.
|
|
12
|
+
*/
|
|
13
|
+
class AppDispatchers(
|
|
14
|
+
val main: CoroutineDispatcher,
|
|
15
|
+
val io: CoroutineDispatcher,
|
|
16
|
+
val engine: CoroutineDispatcher,
|
|
17
|
+
) {
|
|
18
|
+
companion object {
|
|
19
|
+
val DEFAULT = AppDispatchers(Dispatchers.Main, Dispatchers.IO, Dispatchers.Default.limitedParallelism(1))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.SystemClock
|
|
5
|
+
import android.provider.Settings
|
|
6
|
+
|
|
7
|
+
/** Time source. Units must use this instead of `System.currentTimeMillis()` so tests can control time. */
|
|
8
|
+
interface Clock {
|
|
9
|
+
/** Wall-clock time, epoch ms. */
|
|
10
|
+
fun now(): Long
|
|
11
|
+
|
|
12
|
+
/** Milliseconds since boot, including deep sleep ([SystemClock.elapsedRealtime]). */
|
|
13
|
+
fun elapsedRealtime(): Long
|
|
14
|
+
|
|
15
|
+
/** `Settings.Global.BOOT_COUNT`, or -1 if unavailable. */
|
|
16
|
+
fun bootCount(): Int
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Production [Clock]. */
|
|
20
|
+
class SystemClockImpl(context: Context) : Clock {
|
|
21
|
+
private val appContext = context.applicationContext ?: context
|
|
22
|
+
|
|
23
|
+
@Volatile
|
|
24
|
+
private var cachedBootCount: Int? = null
|
|
25
|
+
|
|
26
|
+
override fun now(): Long = System.currentTimeMillis()
|
|
27
|
+
|
|
28
|
+
override fun elapsedRealtime(): Long = SystemClock.elapsedRealtime()
|
|
29
|
+
|
|
30
|
+
override fun bootCount(): Int {
|
|
31
|
+
cachedBootCount?.let { return it }
|
|
32
|
+
val value = try {
|
|
33
|
+
Settings.Global.getInt(appContext.contentResolver, Settings.Global.BOOT_COUNT, -1)
|
|
34
|
+
} catch (e: Exception) {
|
|
35
|
+
-1
|
|
36
|
+
}
|
|
37
|
+
// The boot count cannot change while this process is alive.
|
|
38
|
+
if (value >= 0) cachedBootCount = value
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
}
|