@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/position/DefaultPositionService.kt
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.position
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.config.ConfigStore
|
|
4
|
+
import com.brickssoft.locationtracking.core.Clock
|
|
5
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
6
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
7
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
8
|
+
import com.brickssoft.locationtracking.device.DeviceMonitor
|
|
9
|
+
import com.brickssoft.locationtracking.model.Record
|
|
10
|
+
import com.brickssoft.locationtracking.model.RecordEvent
|
|
11
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
12
|
+
import com.brickssoft.locationtracking.permission.PermissionManager
|
|
13
|
+
import com.brickssoft.locationtracking.processing.RecordFactory
|
|
14
|
+
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
15
|
+
import com.brickssoft.locationtracking.provider.LocationListener
|
|
16
|
+
import com.brickssoft.locationtracking.provider.LocationRequestSpec
|
|
17
|
+
import com.brickssoft.locationtracking.provider.ProviderFactory
|
|
18
|
+
import com.brickssoft.locationtracking.record.RecordSink
|
|
19
|
+
import kotlinx.coroutines.CoroutineScope
|
|
20
|
+
import kotlinx.coroutines.channels.Channel
|
|
21
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
22
|
+
import kotlinx.coroutines.ensureActive
|
|
23
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
24
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
25
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
26
|
+
import kotlin.math.max
|
|
27
|
+
import kotlin.math.min
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default [PositionService].
|
|
31
|
+
*
|
|
32
|
+
* - [getCurrentPosition] requires foreground location permission and enabled location services. With
|
|
33
|
+
* `maximumAgeMs > 0` it returns the freshest known fix (runtime or backend last location) if it is young
|
|
34
|
+
* enough. Otherwise it samples a temporary backend request for up to `samples` fixes, stops early on a fix of
|
|
35
|
+
* [ACCURATE_ENOUGH_M] or better, and keeps the most accurate fix. The timeout bounds the whole call; at the
|
|
36
|
+
* timeout the best fix so far is returned, or TIMEOUT is thrown if there is none.
|
|
37
|
+
* - [watchPosition] registers one backend listener per watch id. Each fix becomes a `watch_position` record,
|
|
38
|
+
* optionally persisted through [recordSink], and is delivered in order from [scope] (see [PositionWatch]).
|
|
39
|
+
*
|
|
40
|
+
* Mock fixes are ignored everywhere while `geolocation.filter.rejectMockLocations` is set.
|
|
41
|
+
*/
|
|
42
|
+
class DefaultPositionService(
|
|
43
|
+
private val providers: ProviderFactory,
|
|
44
|
+
private val configStore: ConfigStore,
|
|
45
|
+
private val permissions: PermissionManager,
|
|
46
|
+
private val device: DeviceMonitor,
|
|
47
|
+
private val recordFactory: RecordFactory,
|
|
48
|
+
private val recordSink: RecordSink,
|
|
49
|
+
private val clock: Clock,
|
|
50
|
+
private val scope: CoroutineScope,
|
|
51
|
+
) : PositionService {
|
|
52
|
+
private val watches = ConcurrentHashMap<String, PositionWatch>()
|
|
53
|
+
|
|
54
|
+
override suspend fun getCurrentPosition(o: CurrentPositionOptions): Record {
|
|
55
|
+
if (!permissions.hasForegroundLocation()) {
|
|
56
|
+
throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission is not granted")
|
|
57
|
+
}
|
|
58
|
+
if (!device.providerState().enabled) {
|
|
59
|
+
throw TrackingException(ErrorCode.LOCATION_DISABLED, "Location services are disabled")
|
|
60
|
+
}
|
|
61
|
+
val geolocation = configStore.config.value.geolocation
|
|
62
|
+
val rejectMock = geolocation.filter.rejectMockLocations
|
|
63
|
+
val timeoutMs = o.timeoutMs ?: geolocation.locationTimeout
|
|
64
|
+
val backend = providers.location()
|
|
65
|
+
val startedAt = clock.elapsedRealtime()
|
|
66
|
+
|
|
67
|
+
// The timeout covers the whole call, including the last-location lookup of the maximumAge check.
|
|
68
|
+
val location = cachedLocation(backend, o.maximumAgeMs, rejectMock, timeoutMs)
|
|
69
|
+
?: sample(backend, o, timeoutMs - (clock.elapsedRealtime() - startedAt), rejectMock)
|
|
70
|
+
?: throw TrackingException(ErrorCode.TIMEOUT, "No location fix within ${timeoutMs}ms")
|
|
71
|
+
val record = recordFactory.create(RecordEvent.CURRENT_POSITION, location, extras = o.extras)
|
|
72
|
+
return if (o.persist) recordSink.submit(record) else record
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override fun watchPosition(id: String, o: WatchPositionOptions, callback: (Record?, TrackingException?) -> Unit) {
|
|
76
|
+
watches.remove(id)?.stop()
|
|
77
|
+
if (!permissions.hasForegroundLocation()) {
|
|
78
|
+
deliverError(id, callback, TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission is not granted"))
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (!device.providerState().enabled) {
|
|
82
|
+
// Not an error: the backend starts delivering once location services are switched on.
|
|
83
|
+
Logger.w(TAG, "watch $id: location services are disabled")
|
|
84
|
+
}
|
|
85
|
+
val interval = o.intervalMs.coerceAtLeast(0L)
|
|
86
|
+
val spec = LocationRequestSpec(o.desiredAccuracy, interval, interval / 2, 0f)
|
|
87
|
+
val watch = PositionWatch(id, providers.location(), spec, callback) { fix -> watchRecord(fix, o) }
|
|
88
|
+
// A concurrent watchPosition() for the same id may have registered in the meantime: last one wins.
|
|
89
|
+
watches.put(id, watch)?.stop()
|
|
90
|
+
val error = watch.start(scope) ?: return
|
|
91
|
+
watches.remove(id, watch)
|
|
92
|
+
deliverError(id, callback, error)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override fun clearWatch(id: String): Boolean {
|
|
96
|
+
val watch = watches.remove(id) ?: return false
|
|
97
|
+
watch.stop()
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun clearAllWatches() {
|
|
102
|
+
for ((id, watch) in watches) {
|
|
103
|
+
if (watches.remove(id, watch)) watch.stop()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Freshest acceptable known fix no older than [maximumAgeMs], or null (always null when [maximumAgeMs] <= 0).
|
|
109
|
+
* The backend lookup takes at most [LAST_LOCATION_TIMEOUT_MS] and never longer than [timeoutMs].
|
|
110
|
+
*/
|
|
111
|
+
private suspend fun cachedLocation(
|
|
112
|
+
backend: LocationBackend,
|
|
113
|
+
maximumAgeMs: Long,
|
|
114
|
+
rejectMock: Boolean,
|
|
115
|
+
timeoutMs: Long,
|
|
116
|
+
): TrackedLocation? {
|
|
117
|
+
if (maximumAgeMs <= 0) return null
|
|
118
|
+
val known = listOfNotNull(
|
|
119
|
+
configStore.runtime.value.lastLocation,
|
|
120
|
+
lastBackendLocation(backend, min(LAST_LOCATION_TIMEOUT_MS, timeoutMs)),
|
|
121
|
+
)
|
|
122
|
+
val freshest = known.filter { accepts(it, rejectMock) }.minByOrNull { ageOf(it) } ?: return null
|
|
123
|
+
val age = ageOf(freshest)
|
|
124
|
+
if (age > maximumAgeMs) return null
|
|
125
|
+
Logger.d(TAG, "getCurrentPosition: using known fix (age ${age}ms, accuracy ${freshest.accuracy}m)")
|
|
126
|
+
return freshest
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private suspend fun lastBackendLocation(backend: LocationBackend, timeoutMs: Long): TrackedLocation? = try {
|
|
130
|
+
withTimeoutOrNull(timeoutMs) { backend.getLastLocation() }
|
|
131
|
+
} catch (e: CancellationException) {
|
|
132
|
+
currentCoroutineContext().ensureActive() // the caller was cancelled: propagate
|
|
133
|
+
Logger.w(TAG, "getLastLocation was cancelled by the backend", e)
|
|
134
|
+
null
|
|
135
|
+
} catch (e: Exception) {
|
|
136
|
+
Logger.w(TAG, "getLastLocation failed", e)
|
|
137
|
+
null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Age of [fix] in ms: the larger of its wall-clock age and, when the fix carries an elapsed-realtime timestamp,
|
|
142
|
+
* its elapsed-realtime age. A wall-clock change therefore cannot make an old fix look fresh, and a timestamp from
|
|
143
|
+
* before a reboot only ever yields a smaller elapsed age, which the wall-clock age then dominates.
|
|
144
|
+
*/
|
|
145
|
+
private fun ageOf(fix: TrackedLocation): Long {
|
|
146
|
+
val wallAge = clock.now() - fix.time
|
|
147
|
+
if (fix.elapsedRealtimeNanos <= 0L) return wallAge
|
|
148
|
+
return max(wallAge, clock.elapsedRealtime() - fix.elapsedRealtimeNanos / NANOS_PER_MS)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Collects fixes from a temporary backend request until [SampleCollector] is satisfied or [timeoutMs] expires,
|
|
153
|
+
* and returns the best one, or null if no acceptable fix arrived. The listener is always removed.
|
|
154
|
+
* Historical fixes computed before the request (by the elapsed-realtime clock) are ignored.
|
|
155
|
+
*/
|
|
156
|
+
private suspend fun sample(
|
|
157
|
+
backend: LocationBackend,
|
|
158
|
+
o: CurrentPositionOptions,
|
|
159
|
+
timeoutMs: Long,
|
|
160
|
+
rejectMock: Boolean,
|
|
161
|
+
): TrackedLocation? {
|
|
162
|
+
if (timeoutMs <= 0) return null
|
|
163
|
+
val collector = SampleCollector(o.samples)
|
|
164
|
+
val fixes = Channel<TrackedLocation>(Channel.UNLIMITED)
|
|
165
|
+
val listener = LocationListener { batch -> batch.forEach { fixes.trySend(it) } }
|
|
166
|
+
val spec = LocationRequestSpec(o.desiredAccuracy, SAMPLE_INTERVAL_MS, SAMPLE_FASTEST_INTERVAL_MS, 0f)
|
|
167
|
+
val requestedAt = clock.elapsedRealtime()
|
|
168
|
+
try {
|
|
169
|
+
backend.requestUpdates(spec, listener)
|
|
170
|
+
withTimeoutOrNull(timeoutMs) {
|
|
171
|
+
for (fix in fixes) {
|
|
172
|
+
if (accepts(fix, rejectMock) && !isStaleSample(fix, requestedAt) && collector.add(fix)) break
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (e: CancellationException) {
|
|
176
|
+
currentCoroutineContext().ensureActive() // the caller was cancelled: propagate
|
|
177
|
+
throw backendError(e)
|
|
178
|
+
} catch (e: Exception) {
|
|
179
|
+
throw backendError(e)
|
|
180
|
+
} finally {
|
|
181
|
+
fixes.close()
|
|
182
|
+
removeQuietly(backend, listener)
|
|
183
|
+
}
|
|
184
|
+
val best = collector.best ?: return null
|
|
185
|
+
Logger.d(TAG, "getCurrentPosition: ${collector.count} sample(s), best accuracy ${best.accuracy}m")
|
|
186
|
+
return best
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun isStaleSample(fix: TrackedLocation, requestedAt: Long): Boolean =
|
|
190
|
+
fix.elapsedRealtimeNanos > 0L && fix.elapsedRealtimeNanos / NANOS_PER_MS < requestedAt - STALE_SAMPLE_TOLERANCE_MS
|
|
191
|
+
|
|
192
|
+
/** Record for a watch fix, persisted if requested; null if the fix is rejected by the mock rule. */
|
|
193
|
+
private suspend fun watchRecord(fix: TrackedLocation, o: WatchPositionOptions): Record? {
|
|
194
|
+
if (!accepts(fix, configStore.config.value.geolocation.filter.rejectMockLocations)) return null
|
|
195
|
+
val record = recordFactory.create(RecordEvent.WATCH_POSITION, fix, extras = o.extras)
|
|
196
|
+
return if (o.persist) recordSink.submit(record) else record
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private fun deliverError(id: String, callback: (Record?, TrackingException?) -> Unit, error: TrackingException) {
|
|
200
|
+
Logger.w(TAG, "watch $id failed: ${error.code} ${error.message}")
|
|
201
|
+
try {
|
|
202
|
+
callback(null, error)
|
|
203
|
+
} catch (e: Exception) {
|
|
204
|
+
Logger.e(TAG, "watch $id callback failed", e)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
internal companion object {
|
|
209
|
+
const val TAG = "LT.Position"
|
|
210
|
+
|
|
211
|
+
/** Interval of the temporary sampling request of getCurrentPosition, ms. */
|
|
212
|
+
const val SAMPLE_INTERVAL_MS = 1_000L
|
|
213
|
+
|
|
214
|
+
/** Fastest interval of the temporary sampling request of getCurrentPosition, ms. */
|
|
215
|
+
const val SAMPLE_FASTEST_INTERVAL_MS = 500L
|
|
216
|
+
|
|
217
|
+
/** A fix at least this accurate (m) ends sampling early. */
|
|
218
|
+
const val ACCURATE_ENOUGH_M = 10f
|
|
219
|
+
|
|
220
|
+
/** Upper bound for the backend's last-location lookup of the maximumAge check, ms. */
|
|
221
|
+
const val LAST_LOCATION_TIMEOUT_MS = 5_000L
|
|
222
|
+
|
|
223
|
+
/** A sample computed more than this long before the sampling request started is historical, ms. */
|
|
224
|
+
const val STALE_SAMPLE_TOLERANCE_MS = 2_000L
|
|
225
|
+
|
|
226
|
+
private const val NANOS_PER_MS = 1_000_000L
|
|
227
|
+
|
|
228
|
+
/** False for mock fixes while [rejectMock] is set. */
|
|
229
|
+
fun accepts(fix: TrackedLocation, rejectMock: Boolean): Boolean = !(rejectMock && fix.isMock)
|
|
230
|
+
|
|
231
|
+
/** Maps a backend failure to the error reported to JS. */
|
|
232
|
+
fun backendError(e: Exception): TrackingException = when (e) {
|
|
233
|
+
is TrackingException -> e
|
|
234
|
+
is SecurityException -> TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission is not granted", e)
|
|
235
|
+
else -> TrackingException(ErrorCode.UNAVAILABLE, "Location updates are unavailable: ${e.message}", e)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fun removeQuietly(backend: LocationBackend, listener: LocationListener) {
|
|
239
|
+
try {
|
|
240
|
+
backend.removeUpdates(listener)
|
|
241
|
+
} catch (e: Exception) {
|
|
242
|
+
Logger.w(TAG, "removeUpdates failed", e)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Best-of-N selection for getCurrentPosition. [add] returns true once [wanted] fixes were collected or the best
|
|
250
|
+
* fix is at least [DefaultPositionService.ACCURATE_ENOUGH_M] accurate. A fix without a usable accuracy (0, negative
|
|
251
|
+
* or NaN) ranks last; on equal accuracy the fix with the newer (or equal) fix time wins. Not thread-safe.
|
|
252
|
+
*/
|
|
253
|
+
internal class SampleCollector(wanted: Int) {
|
|
254
|
+
private val wanted = wanted.coerceAtLeast(1)
|
|
255
|
+
|
|
256
|
+
var best: TrackedLocation? = null
|
|
257
|
+
private set
|
|
258
|
+
|
|
259
|
+
var count = 0
|
|
260
|
+
private set
|
|
261
|
+
|
|
262
|
+
fun add(fix: TrackedLocation): Boolean {
|
|
263
|
+
count++
|
|
264
|
+
val current = best
|
|
265
|
+
val winner = if (current == null || isBetter(fix, current)) fix else current
|
|
266
|
+
best = winner
|
|
267
|
+
return count >= wanted || rank(winner) <= DefaultPositionService.ACCURATE_ENOUGH_M
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun isBetter(fix: TrackedLocation, current: TrackedLocation): Boolean {
|
|
271
|
+
val a = rank(fix)
|
|
272
|
+
val b = rank(current)
|
|
273
|
+
return a < b || (a == b && fix.time >= current.time)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun rank(fix: TrackedLocation): Float = if (fix.accuracy > 0f) fix.accuracy else Float.POSITIVE_INFINITY
|
|
277
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.position
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
4
|
+
import com.brickssoft.locationtracking.model.DesiredAccuracy
|
|
5
|
+
import com.brickssoft.locationtracking.model.Record
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @property timeoutMs null = `geolocation.locationTimeout`.
|
|
9
|
+
* @property extras JSON object text.
|
|
10
|
+
*/
|
|
11
|
+
data class CurrentPositionOptions(
|
|
12
|
+
val samples: Int = 3,
|
|
13
|
+
val timeoutMs: Long? = null,
|
|
14
|
+
val maximumAgeMs: Long = 0,
|
|
15
|
+
val desiredAccuracy: DesiredAccuracy = DesiredAccuracy.HIGH,
|
|
16
|
+
val persist: Boolean = true,
|
|
17
|
+
val extras: String? = null,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
/** @property extras JSON object text. */
|
|
21
|
+
data class WatchPositionOptions(
|
|
22
|
+
val intervalMs: Long = 1000,
|
|
23
|
+
val desiredAccuracy: DesiredAccuracy = DesiredAccuracy.HIGH,
|
|
24
|
+
val persist: Boolean = false,
|
|
25
|
+
val extras: String? = null,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
interface PositionService {
|
|
29
|
+
/** @throws TrackingException PERMISSION_DENIED, LOCATION_DISABLED, TIMEOUT, ... */
|
|
30
|
+
suspend fun getCurrentPosition(o: CurrentPositionOptions): Record
|
|
31
|
+
|
|
32
|
+
/** [callback] receives either a record or an error, possibly many times, until [clearWatch]. */
|
|
33
|
+
fun watchPosition(id: String, o: WatchPositionOptions, callback: (Record?, TrackingException?) -> Unit)
|
|
34
|
+
|
|
35
|
+
/** Returns false if [id] was not watching. */
|
|
36
|
+
fun clearWatch(id: String): Boolean
|
|
37
|
+
|
|
38
|
+
fun clearAllWatches()
|
|
39
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.position
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
4
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
5
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
6
|
+
import com.brickssoft.locationtracking.model.Record
|
|
7
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
8
|
+
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
9
|
+
import com.brickssoft.locationtracking.provider.LocationListener
|
|
10
|
+
import com.brickssoft.locationtracking.provider.LocationRequestSpec
|
|
11
|
+
import kotlinx.coroutines.CoroutineScope
|
|
12
|
+
import kotlinx.coroutines.channels.BufferOverflow
|
|
13
|
+
import kotlinx.coroutines.channels.Channel
|
|
14
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
15
|
+
import kotlinx.coroutines.ensureActive
|
|
16
|
+
import kotlinx.coroutines.launch
|
|
17
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One active `watchPosition`. The backend listener only enqueues fixes; a single consumer coroutine on the
|
|
21
|
+
* given scope turns each fix into a record (through [toRecord], which may persist it) and invokes [callback],
|
|
22
|
+
* so deliveries keep the fix order and never run on the backend's thread.
|
|
23
|
+
*
|
|
24
|
+
* After [stop] returns, the listener is removed, queued fixes are dropped and [callback] is never invoked again
|
|
25
|
+
* (a callback that is running when [stop] is called finishes first); a record that is already being persisted
|
|
26
|
+
* finishes persisting. At most [BUFFER] fixes wait for the consumer; beyond that the oldest are dropped.
|
|
27
|
+
* [start] and [stop] are thread-safe and idempotent.
|
|
28
|
+
*
|
|
29
|
+
* @param toRecord returns null to skip a fix (mock rule).
|
|
30
|
+
*/
|
|
31
|
+
internal class PositionWatch(
|
|
32
|
+
val id: String,
|
|
33
|
+
private val backend: LocationBackend,
|
|
34
|
+
private val spec: LocationRequestSpec,
|
|
35
|
+
private val callback: (Record?, TrackingException?) -> Unit,
|
|
36
|
+
private val toRecord: suspend (TrackedLocation) -> Record?,
|
|
37
|
+
) {
|
|
38
|
+
/** Guards [started], [stopped], [registered] and every [callback] invocation. */
|
|
39
|
+
private val lock = Any()
|
|
40
|
+
private val fixes = Channel<TrackedLocation>(BUFFER, BufferOverflow.DROP_OLDEST)
|
|
41
|
+
private val listener = LocationListener { batch -> batch.forEach { fixes.trySend(it) } }
|
|
42
|
+
|
|
43
|
+
@Volatile
|
|
44
|
+
private var stopped = false
|
|
45
|
+
private var started = false
|
|
46
|
+
private var registered = false
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Starts the consumer on [scope] and registers the backend listener. Returns the error if the backend refused
|
|
50
|
+
* (the watch is then stopped and [callback] is not invoked); null on success or if already started/stopped.
|
|
51
|
+
*/
|
|
52
|
+
fun start(scope: CoroutineScope): TrackingException? {
|
|
53
|
+
synchronized(lock) {
|
|
54
|
+
if (stopped || started) return null
|
|
55
|
+
started = true
|
|
56
|
+
scope.launch { consume() }
|
|
57
|
+
try {
|
|
58
|
+
backend.requestUpdates(spec, listener)
|
|
59
|
+
registered = true
|
|
60
|
+
Logger.d(DefaultPositionService.TAG, "watch $id started (${spec.accuracy.wire}, ${spec.intervalMs}ms)")
|
|
61
|
+
return null
|
|
62
|
+
} catch (e: Exception) {
|
|
63
|
+
stopped = true
|
|
64
|
+
fixes.close()
|
|
65
|
+
// A backend may have registered before failing.
|
|
66
|
+
DefaultPositionService.removeQuietly(backend, listener)
|
|
67
|
+
return DefaultPositionService.backendError(e)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Removes the backend listener and ends deliveries. */
|
|
73
|
+
fun stop() {
|
|
74
|
+
val wasRegistered = synchronized(lock) {
|
|
75
|
+
if (stopped) return
|
|
76
|
+
stopped = true
|
|
77
|
+
registered.also { registered = false }
|
|
78
|
+
}
|
|
79
|
+
fixes.close()
|
|
80
|
+
if (wasRegistered) DefaultPositionService.removeQuietly(backend, listener)
|
|
81
|
+
Logger.d(DefaultPositionService.TAG, "watch $id cleared")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private suspend fun consume() {
|
|
85
|
+
for (fix in fixes) {
|
|
86
|
+
if (stopped) break
|
|
87
|
+
val record = try {
|
|
88
|
+
toRecord(fix) ?: continue
|
|
89
|
+
} catch (e: Exception) {
|
|
90
|
+
// A stray CancellationException from a collaborator is a failure; our own cancellation propagates.
|
|
91
|
+
if (e is CancellationException) currentCoroutineContext().ensureActive()
|
|
92
|
+
Logger.e(DefaultPositionService.TAG, "watch $id: failed to build record", e)
|
|
93
|
+
deliver(null, TrackingException(ErrorCode.INTERNAL, "Failed to record watched position: ${e.message}", e))
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
deliver(record, null)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private fun deliver(record: Record?, error: TrackingException?) {
|
|
101
|
+
synchronized(lock) {
|
|
102
|
+
if (stopped) return
|
|
103
|
+
try {
|
|
104
|
+
callback(record, error)
|
|
105
|
+
} catch (e: Exception) {
|
|
106
|
+
Logger.e(DefaultPositionService.TAG, "watch $id callback failed", e)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private companion object {
|
|
112
|
+
/** Fixes that may wait for the consumer (a slow sink); older ones are dropped first. */
|
|
113
|
+
const val BUFFER = 64
|
|
114
|
+
}
|
|
115
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.processing
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.config.ConfigStore
|
|
4
|
+
import com.brickssoft.locationtracking.config.LocationFilterConfig
|
|
5
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
6
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
7
|
+
import java.util.Locale
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Location filters and optional Kalman smoothing. Reads `geolocation.filter` live from [configStore] on
|
|
11
|
+
* every fix.
|
|
12
|
+
*
|
|
13
|
+
* Rules, in order (the first match rejects):
|
|
14
|
+
* 1. invalid coordinates (non-finite, out of range, or exactly 0,0 — a known provider glitch);
|
|
15
|
+
* 2. `accuracy > trackingAccuracyThreshold` (a non-finite accuracy is also rejected);
|
|
16
|
+
* 3. a mock fix when `rejectMockLocations`;
|
|
17
|
+
* 4. a stale fix, older than the previous accepted fix;
|
|
18
|
+
* 5. an identical fix (same latitude, longitude and time as the previous accepted fix) unless
|
|
19
|
+
* `allowIdenticalLocations`;
|
|
20
|
+
* 6. implied speed (haversine distance / time since the previous accepted fix, only when that time is > 0)
|
|
21
|
+
* above `maxImpliedSpeed` (0 = off).
|
|
22
|
+
*
|
|
23
|
+
* Time between fixes uses the elapsed-realtime clock when both fixes carry it (see [Geo.elapsedMs]), so
|
|
24
|
+
* wall-clock changes do not affect the checks. Accepted fixes are smoothed by [KalmanFilter] when
|
|
25
|
+
* `useKalman` is set. The stale, identical and implied-speed checks compare raw fixes. Thread-safe.
|
|
26
|
+
*/
|
|
27
|
+
class DefaultLocationProcessor(private val configStore: ConfigStore) : LocationProcessor {
|
|
28
|
+
private val lock = Any()
|
|
29
|
+
private val kalman = KalmanFilter()
|
|
30
|
+
|
|
31
|
+
/** The previous accepted fix, before smoothing. */
|
|
32
|
+
private var lastAccepted: TrackedLocation? = null
|
|
33
|
+
|
|
34
|
+
override fun process(raw: TrackedLocation, isMoving: Boolean): FilterResult {
|
|
35
|
+
val filter = configStore.config.value.geolocation.filter
|
|
36
|
+
val result = synchronized(lock) {
|
|
37
|
+
val rejection = rejectionReason(raw, filter)
|
|
38
|
+
if (rejection != null) {
|
|
39
|
+
FilterResult.Rejected(rejection)
|
|
40
|
+
} else {
|
|
41
|
+
lastAccepted = raw
|
|
42
|
+
val location = if (filter.useKalman) {
|
|
43
|
+
kalman.filter(raw)
|
|
44
|
+
} else {
|
|
45
|
+
// Start fresh if smoothing is turned on again later.
|
|
46
|
+
kalman.reset()
|
|
47
|
+
raw
|
|
48
|
+
}
|
|
49
|
+
FilterResult.Accepted(location)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (result is FilterResult.Rejected) Logger.d(TAG, "rejected fix: ${result.reason}")
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun reset() = synchronized(lock) {
|
|
57
|
+
lastAccepted = null
|
|
58
|
+
kalman.reset()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun rejectionReason(raw: TrackedLocation, filter: LocationFilterConfig): String? {
|
|
62
|
+
if (!Geo.isValid(raw.latitude, raw.longitude) || (raw.latitude == 0.0 && raw.longitude == 0.0)) {
|
|
63
|
+
return "invalid coordinates (${raw.latitude}, ${raw.longitude})"
|
|
64
|
+
}
|
|
65
|
+
val threshold = filter.trackingAccuracyThreshold
|
|
66
|
+
if (!(raw.accuracy.toDouble() <= threshold)) {
|
|
67
|
+
return "accuracy ${raw.accuracy} m > trackingAccuracyThreshold $threshold m"
|
|
68
|
+
}
|
|
69
|
+
if (raw.isMock && filter.rejectMockLocations) return "mock location"
|
|
70
|
+
val previous = lastAccepted ?: return null
|
|
71
|
+
val dtMs = Geo.elapsedMs(previous, raw)
|
|
72
|
+
if (dtMs < 0) return "stale: ${-dtMs} ms older than the previous fix"
|
|
73
|
+
if (!filter.allowIdenticalLocations &&
|
|
74
|
+
raw.latitude == previous.latitude &&
|
|
75
|
+
raw.longitude == previous.longitude &&
|
|
76
|
+
raw.time == previous.time
|
|
77
|
+
) {
|
|
78
|
+
return "identical to the previous fix"
|
|
79
|
+
}
|
|
80
|
+
val maxSpeed = filter.maxImpliedSpeed
|
|
81
|
+
if (maxSpeed > 0 && dtMs > 0) {
|
|
82
|
+
val speed = Geo.distance(previous, raw) / (dtMs / 1000.0)
|
|
83
|
+
if (speed > maxSpeed) {
|
|
84
|
+
return "implied speed ${"%.1f".format(Locale.US, speed)} m/s > maxImpliedSpeed $maxSpeed m/s"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private companion object {
|
|
91
|
+
const val TAG = "LT.Processor"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.processing
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.config.ConfigStore
|
|
4
|
+
import com.brickssoft.locationtracking.config.RuntimeState
|
|
5
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
6
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
7
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
8
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Distance accumulator persisted in `runtime.odometer` via [ConfigStore.updateRuntime].
|
|
12
|
+
*
|
|
13
|
+
* Adds the haversine distance between consecutive fixes whose accuracy is at most
|
|
14
|
+
* `geolocation.filter.odometerAccuracyThreshold`; a less accurate fix is skipped and the previous anchor is
|
|
15
|
+
* kept. [reset] also clears the anchor; [set] keeps it.
|
|
16
|
+
*
|
|
17
|
+
* The anchor belongs to one tracking session (`runtime.trackingStartedAt`), so distance travelled while tracking
|
|
18
|
+
* was off is never counted. The first fix of a session in this process is measured from `runtime.lastLocation` if
|
|
19
|
+
* that was recorded in the same session (and is accurate enough and not newer than the fix), otherwise it only
|
|
20
|
+
* becomes the anchor:
|
|
21
|
+
* - after `start()`, `lastLocation` is the session's start position (the initial `motionchange`), which the engine
|
|
22
|
+
* does not feed to the odometer while stationary, so the leg to the first moving fix is counted in every session;
|
|
23
|
+
* - after a cold process start (e.g. an OEM task killer and `restore`), it is where the previous process left off.
|
|
24
|
+
*
|
|
25
|
+
* [reset] clears the anchor for the rest of the current session. Thread-safe.
|
|
26
|
+
*/
|
|
27
|
+
class DefaultOdometer(private val configStore: ConfigStore) : Odometer {
|
|
28
|
+
private val lock = Any()
|
|
29
|
+
|
|
30
|
+
/** The last fix that passed the accuracy gate. In memory only. */
|
|
31
|
+
private var anchor: TrackedLocation? = null
|
|
32
|
+
|
|
33
|
+
/** `trackingStartedAt` of the session [anchor] belongs to; null until this process's first fix or [reset]. */
|
|
34
|
+
private var anchorSession: Long? = null
|
|
35
|
+
|
|
36
|
+
override val value: Double get() = configStore.runtime.value.odometer
|
|
37
|
+
|
|
38
|
+
override fun onLocation(location: TrackedLocation) {
|
|
39
|
+
val threshold = configStore.config.value.geolocation.filter.odometerAccuracyThreshold
|
|
40
|
+
if (!passes(location, threshold)) {
|
|
41
|
+
Logger.v(TAG, "odometer skips fix with accuracy ${location.accuracy} m (threshold $threshold m)")
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
synchronized(lock) {
|
|
45
|
+
val runtime = configStore.runtime.value
|
|
46
|
+
val session = runtime.trackingStartedAt
|
|
47
|
+
if (anchorSession != session) {
|
|
48
|
+
// Every new session is seeded, not only the first fix of the process: a later session used to start
|
|
49
|
+
// from a null anchor, which dropped the leg from its start position to its first moving fix.
|
|
50
|
+
anchor = sessionAnchor(runtime, threshold, location)
|
|
51
|
+
anchorSession = session
|
|
52
|
+
}
|
|
53
|
+
val previous = anchor
|
|
54
|
+
anchor = location
|
|
55
|
+
if (previous == null) return
|
|
56
|
+
val distance = Geo.distance(previous, location)
|
|
57
|
+
if (distance > 0) configStore.updateRuntime { it.copy(odometer = it.odometer + distance) }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @throws TrackingException INVALID_ARGUMENT if [value] is negative or not finite. */
|
|
62
|
+
override fun set(value: Double) {
|
|
63
|
+
if (!value.isFinite() || value < 0) {
|
|
64
|
+
throw TrackingException(ErrorCode.INVALID_ARGUMENT, "odometer must be a finite number >= 0, got $value")
|
|
65
|
+
}
|
|
66
|
+
synchronized(lock) {
|
|
67
|
+
configStore.updateRuntime { it.copy(odometer = value) }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun reset() {
|
|
72
|
+
synchronized(lock) {
|
|
73
|
+
anchor = null
|
|
74
|
+
// The cleared anchor belongs to the current session: its next fix is not seeded from a lastLocation that
|
|
75
|
+
// may predate the reset.
|
|
76
|
+
anchorSession = configStore.runtime.value.trackingStartedAt
|
|
77
|
+
configStore.updateRuntime { it.copy(odometer = 0.0) }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun passes(location: TrackedLocation, threshold: Double): Boolean =
|
|
82
|
+
location.accuracy.toDouble() <= threshold && Geo.isValid(location.latitude, location.longitude)
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The persisted last location, if it was recorded in the current tracking session, is accurate enough and is not
|
|
86
|
+
* newer than [next] (the fix about to be measured from it).
|
|
87
|
+
*/
|
|
88
|
+
private fun sessionAnchor(runtime: RuntimeState, threshold: Double, next: TrackedLocation): TrackedLocation? {
|
|
89
|
+
val session = runtime.trackingStartedAt ?: return null
|
|
90
|
+
val last = runtime.lastLocation ?: return null
|
|
91
|
+
if (!runtime.enabled || last.time < session || last.time > next.time || !passes(last, threshold)) return null
|
|
92
|
+
Logger.d(TAG, "odometer starts from the last recorded location of this session")
|
|
93
|
+
return last
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private companion object {
|
|
97
|
+
const val TAG = "LT.Odometer"
|
|
98
|
+
}
|
|
99
|
+
}
|