@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +107 -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 +60 -4
package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.processing
|
|
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.Iso8601
|
|
7
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
8
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
9
|
+
import com.brickssoft.locationtracking.device.DeviceMonitor
|
|
10
|
+
import com.brickssoft.locationtracking.model.BatterySnapshot
|
|
11
|
+
import com.brickssoft.locationtracking.model.GeofenceHit
|
|
12
|
+
import com.brickssoft.locationtracking.model.JsonUtil
|
|
13
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
14
|
+
import com.brickssoft.locationtracking.model.ProviderState
|
|
15
|
+
import com.brickssoft.locationtracking.model.Record
|
|
16
|
+
import com.brickssoft.locationtracking.model.RecordEvent
|
|
17
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
18
|
+
import com.brickssoft.locationtracking.provider.ProviderFactory
|
|
19
|
+
import org.json.JSONObject
|
|
20
|
+
import java.util.UUID
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds records: uuid v4, clock and boot metadata, runtime state (isMoving, odometer, activity), battery and
|
|
24
|
+
* the current provider state (unless the caller passes one, as `providerchange` does) from [device], backend from
|
|
25
|
+
* [providers], and `extras` = `persistence.extras` merged with the per-call extras (per-call keys win; null when
|
|
26
|
+
* both are empty).
|
|
27
|
+
*/
|
|
28
|
+
class DefaultRecordFactory(
|
|
29
|
+
private val configStore: ConfigStore,
|
|
30
|
+
private val device: DeviceMonitor,
|
|
31
|
+
private val providers: ProviderFactory,
|
|
32
|
+
private val clock: Clock,
|
|
33
|
+
) : RecordFactory {
|
|
34
|
+
override fun create(
|
|
35
|
+
event: RecordEvent,
|
|
36
|
+
location: TrackedLocation?,
|
|
37
|
+
extras: String?,
|
|
38
|
+
geofence: GeofenceHit?,
|
|
39
|
+
provider: ProviderState?,
|
|
40
|
+
reason: String?,
|
|
41
|
+
): Record {
|
|
42
|
+
val runtime = configStore.runtime.value
|
|
43
|
+
return Record(
|
|
44
|
+
uuid = UUID.randomUUID().toString(),
|
|
45
|
+
event = event,
|
|
46
|
+
location = location,
|
|
47
|
+
recordedAt = clock.now(),
|
|
48
|
+
elapsedRealtimeMs = clock.elapsedRealtime(),
|
|
49
|
+
bootCount = clock.bootCount(),
|
|
50
|
+
isMoving = runtime.isMoving,
|
|
51
|
+
odometer = runtime.odometer,
|
|
52
|
+
activity = runtime.activity,
|
|
53
|
+
battery = battery(),
|
|
54
|
+
backend = backend(),
|
|
55
|
+
extras = mergeExtras(configStore.config.value.persistence.extras, extras),
|
|
56
|
+
geofence = geofence,
|
|
57
|
+
provider = provider ?: providerState(),
|
|
58
|
+
reason = reason,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parses a JS `InsertLocationInput`: `{coords:{latitude, longitude, accuracy?, altitude?, ...}, timestamp?,
|
|
64
|
+
* event?, is_moving?, extras?}`. `accuracy` defaults to 0; negative optional accuracies, speed and heading
|
|
65
|
+
* (the "unknown" sentinel of some platforms) become null.
|
|
66
|
+
*
|
|
67
|
+
* @throws TrackingException INVALID_ARGUMENT if coords, latitude or longitude are missing or invalid, if
|
|
68
|
+
* accuracy is negative, or if `timestamp`, `event`, `is_moving` or `extras` are given but invalid.
|
|
69
|
+
*/
|
|
70
|
+
override fun fromExternal(input: JSONObject): Record {
|
|
71
|
+
val coords = input.optJSONObject("coords") ?: invalid("location.coords is required")
|
|
72
|
+
val latitude = JsonUtil.optDouble(coords, "latitude") ?: invalid("coords.latitude is required")
|
|
73
|
+
val longitude = JsonUtil.optDouble(coords, "longitude") ?: invalid("coords.longitude is required")
|
|
74
|
+
if (!Geo.isValid(latitude, longitude)) invalid("coords ($latitude, $longitude) are out of range")
|
|
75
|
+
|
|
76
|
+
val time = if (input.isNull("timestamp")) {
|
|
77
|
+
clock.now()
|
|
78
|
+
} else {
|
|
79
|
+
val text = JsonUtil.optString(input, "timestamp")
|
|
80
|
+
Iso8601.parse(text) ?: invalid("timestamp '$text' is not an ISO-8601 date")
|
|
81
|
+
}
|
|
82
|
+
val event = if (input.isNull("event")) {
|
|
83
|
+
RecordEvent.LOCATION
|
|
84
|
+
} else {
|
|
85
|
+
val wire = JsonUtil.optString(input, "event")
|
|
86
|
+
RecordEvent.fromWire(wire) ?: invalid("unknown event '$wire'")
|
|
87
|
+
}
|
|
88
|
+
val isMoving = if (input.isNull("is_moving")) {
|
|
89
|
+
null
|
|
90
|
+
} else {
|
|
91
|
+
JsonUtil.optBoolean(input, "is_moving") ?: invalid("is_moving must be a boolean")
|
|
92
|
+
}
|
|
93
|
+
val accuracy = JsonUtil.optFloat(coords, "accuracy") ?: 0f
|
|
94
|
+
if (accuracy < 0) invalid("coords.accuracy must be >= 0")
|
|
95
|
+
val extras = if (input.isNull("extras")) {
|
|
96
|
+
null
|
|
97
|
+
} else {
|
|
98
|
+
input.optJSONObject("extras")?.toString() ?: invalid("extras must be an object")
|
|
99
|
+
}
|
|
100
|
+
val location = TrackedLocation(
|
|
101
|
+
latitude = latitude,
|
|
102
|
+
longitude = longitude,
|
|
103
|
+
accuracy = accuracy,
|
|
104
|
+
altitude = JsonUtil.optDouble(coords, "altitude"),
|
|
105
|
+
altitudeAccuracy = nonNegative(coords, "altitude_accuracy"),
|
|
106
|
+
speed = nonNegative(coords, "speed"),
|
|
107
|
+
speedAccuracy = nonNegative(coords, "speed_accuracy"),
|
|
108
|
+
heading = nonNegative(coords, "heading"),
|
|
109
|
+
headingAccuracy = nonNegative(coords, "heading_accuracy"),
|
|
110
|
+
time = time,
|
|
111
|
+
)
|
|
112
|
+
val record = create(event, location, extras)
|
|
113
|
+
return if (isMoving == null) record else record.copy(isMoving = isMoving)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun nonNegative(json: JSONObject, key: String): Float? = JsonUtil.optFloat(json, key)?.takeIf { it >= 0 }
|
|
117
|
+
|
|
118
|
+
private fun battery(): BatterySnapshot = try {
|
|
119
|
+
device.battery()
|
|
120
|
+
} catch (e: Exception) {
|
|
121
|
+
Logger.w(TAG, "battery unavailable", e)
|
|
122
|
+
BatterySnapshot.UNKNOWN
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Read live, not the persisted runtime.providerState, which lags until the next provider check.
|
|
126
|
+
private fun providerState(): ProviderState? = try {
|
|
127
|
+
device.providerState()
|
|
128
|
+
} catch (e: Exception) {
|
|
129
|
+
Logger.w(TAG, "provider state unavailable", e)
|
|
130
|
+
null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun backend(): ProviderKind? = try {
|
|
134
|
+
providers.kind
|
|
135
|
+
} catch (e: Exception) {
|
|
136
|
+
Logger.w(TAG, "backend unavailable", e)
|
|
137
|
+
null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun invalid(message: String): Nothing = throw TrackingException(ErrorCode.INVALID_ARGUMENT, message)
|
|
141
|
+
|
|
142
|
+
internal companion object {
|
|
143
|
+
private const val TAG = "LT.RecordFactory"
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Shallow merge of two JSON object texts; keys of [perCall] win over [base]. Invalid text is logged
|
|
147
|
+
* and ignored. Returns null when the result has no keys.
|
|
148
|
+
*/
|
|
149
|
+
fun mergeExtras(base: String?, perCall: String?): String? {
|
|
150
|
+
val merged = parseExtras(base, "persistence.extras") ?: JSONObject()
|
|
151
|
+
parseExtras(perCall, "extras")?.let { overrides ->
|
|
152
|
+
val keys = overrides.keys()
|
|
153
|
+
while (keys.hasNext()) {
|
|
154
|
+
val key = keys.next()
|
|
155
|
+
merged.put(key, overrides.opt(key))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return if (merged.length() == 0) null else merged.toString()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun parseExtras(text: String?, name: String): JSONObject? {
|
|
162
|
+
if (text.isNullOrBlank()) return null
|
|
163
|
+
return JsonUtil.parseObject(text).also { if (it == null) Logger.w(TAG, "ignoring $name: not a JSON object") }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.processing
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
4
|
+
import kotlin.math.asin
|
|
5
|
+
import kotlin.math.cos
|
|
6
|
+
import kotlin.math.min
|
|
7
|
+
import kotlin.math.sin
|
|
8
|
+
import kotlin.math.sqrt
|
|
9
|
+
|
|
10
|
+
/** Great-circle geometry on a spherical Earth. */
|
|
11
|
+
internal object Geo {
|
|
12
|
+
/** Mean Earth radius (IUGG), meters. */
|
|
13
|
+
const val EARTH_RADIUS_M = 6_371_008.8
|
|
14
|
+
|
|
15
|
+
/** Haversine distance in meters between two points given in degrees. */
|
|
16
|
+
fun haversine(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
|
|
17
|
+
val phi1 = Math.toRadians(lat1)
|
|
18
|
+
val phi2 = Math.toRadians(lat2)
|
|
19
|
+
val dPhi = Math.toRadians(lat2 - lat1)
|
|
20
|
+
val dLambda = Math.toRadians(lng2 - lng1)
|
|
21
|
+
val sinPhi = sin(dPhi / 2)
|
|
22
|
+
val sinLambda = sin(dLambda / 2)
|
|
23
|
+
val h = sinPhi * sinPhi + cos(phi1) * cos(phi2) * sinLambda * sinLambda
|
|
24
|
+
// Clamp: rounding can push h slightly above 1 for antipodal points.
|
|
25
|
+
return 2 * EARTH_RADIUS_M * asin(sqrt(min(1.0, h)))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Haversine distance in meters between two fixes. */
|
|
29
|
+
fun distance(a: TrackedLocation, b: TrackedLocation): Double =
|
|
30
|
+
haversine(a.latitude, a.longitude, b.latitude, b.longitude)
|
|
31
|
+
|
|
32
|
+
/** True if both coordinates are finite and within the valid latitude / longitude ranges. */
|
|
33
|
+
fun isValid(latitude: Double, longitude: Double): Boolean =
|
|
34
|
+
latitude.isFinite() && longitude.isFinite() && latitude in -90.0..90.0 && longitude in -180.0..180.0
|
|
35
|
+
|
|
36
|
+
/** Wraps a longitude difference into [-180, 180), so steps across the antimeridian stay short. */
|
|
37
|
+
fun wrapLongitudeDelta(delta: Double): Double {
|
|
38
|
+
val wrapped = (delta + 180.0) % 360.0
|
|
39
|
+
return (if (wrapped < 0) wrapped + 360.0 else wrapped) - 180.0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Normalizes a longitude into [-180, 180]. */
|
|
43
|
+
fun normalizeLongitude(longitude: Double): Double =
|
|
44
|
+
if (longitude in -180.0..180.0) longitude else wrapLongitudeDelta(longitude)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Milliseconds from [from] to [to]: on the monotonic elapsed-realtime clock when both fixes carry it,
|
|
48
|
+
* otherwise on the wall clock (`time`).
|
|
49
|
+
*/
|
|
50
|
+
fun elapsedMs(from: TrackedLocation, to: TrackedLocation): Long =
|
|
51
|
+
if (from.elapsedRealtimeNanos > 0 && to.elapsedRealtimeNanos > 0) {
|
|
52
|
+
(to.elapsedRealtimeNanos - from.elapsedRealtimeNanos) / 1_000_000
|
|
53
|
+
} else {
|
|
54
|
+
to.time - from.time
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.processing
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
4
|
+
import kotlin.math.max
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 1-D constant-position Kalman filter applied per axis to latitude and longitude.
|
|
8
|
+
*
|
|
9
|
+
* - Measurement variance is `accuracy²` (m², accuracy floored at [MIN_ACCURACY_M]).
|
|
10
|
+
* - Process noise per step is `q = speed² · dt` (m², dt in seconds). `speed` is the fix's speed floored at
|
|
11
|
+
* [MIN_SPEED_MPS] (so the estimate never freezes), or [DEFAULT_SPEED_MPS] when the fix has none. For gaps
|
|
12
|
+
* over 1 s, `q` grows as `(speed · dt)²` because displacement grows linearly with time.
|
|
13
|
+
* - Both axes share one variance because `accuracy` is a horizontal radius. Longitude steps are wrapped, so
|
|
14
|
+
* tracks across the antimeridian are smoothed correctly.
|
|
15
|
+
* - `dt` uses the elapsed-realtime clock when both fixes carry it (see [Geo.elapsedMs]).
|
|
16
|
+
* - The filter restarts from the measurement when the gap exceeds [resetAfterMs], when time goes
|
|
17
|
+
* backwards, or after [reset].
|
|
18
|
+
*
|
|
19
|
+
* Only latitude and longitude are replaced; every other field of the fix is returned unchanged.
|
|
20
|
+
* Not thread-safe: [DefaultLocationProcessor] serializes access.
|
|
21
|
+
*/
|
|
22
|
+
internal class KalmanFilter(
|
|
23
|
+
private val resetAfterMs: Long = DEFAULT_RESET_AFTER_MS,
|
|
24
|
+
) {
|
|
25
|
+
/** The last measurement (time reference), or null before the first fix / after [reset]. */
|
|
26
|
+
private var last: TrackedLocation? = null
|
|
27
|
+
private var latitude = 0.0
|
|
28
|
+
private var longitude = 0.0
|
|
29
|
+
|
|
30
|
+
/** Estimate variance, m². */
|
|
31
|
+
private var variance = 0.0
|
|
32
|
+
|
|
33
|
+
/** Returns [location] with smoothed latitude / longitude. */
|
|
34
|
+
fun filter(location: TrackedLocation): TrackedLocation {
|
|
35
|
+
val accuracy = location.accuracy.toDouble().takeIf { it.isFinite() && it > MIN_ACCURACY_M } ?: MIN_ACCURACY_M
|
|
36
|
+
val measurementVariance = accuracy * accuracy
|
|
37
|
+
val previous = last
|
|
38
|
+
val dtMs = previous?.let { Geo.elapsedMs(it, location) }
|
|
39
|
+
if (dtMs == null || dtMs < 0 || dtMs > resetAfterMs) {
|
|
40
|
+
last = location
|
|
41
|
+
latitude = location.latitude
|
|
42
|
+
longitude = location.longitude
|
|
43
|
+
variance = measurementVariance
|
|
44
|
+
return location
|
|
45
|
+
}
|
|
46
|
+
last = location
|
|
47
|
+
if (dtMs > 0) {
|
|
48
|
+
val reported = location.speed?.toDouble()?.takeIf { it.isFinite() && it >= 0 }
|
|
49
|
+
val speed = reported?.let { max(it, MIN_SPEED_MPS) } ?: DEFAULT_SPEED_MPS
|
|
50
|
+
val dt = dtMs / 1000.0
|
|
51
|
+
variance += speed * speed * dt * max(1.0, dt)
|
|
52
|
+
}
|
|
53
|
+
val gain = variance / (variance + measurementVariance)
|
|
54
|
+
latitude += gain * (location.latitude - latitude)
|
|
55
|
+
longitude = Geo.normalizeLongitude(longitude + gain * Geo.wrapLongitudeDelta(location.longitude - longitude))
|
|
56
|
+
variance *= 1 - gain
|
|
57
|
+
return location.copy(latitude = latitude, longitude = longitude)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Forgets all history; the next fix is returned unchanged. */
|
|
61
|
+
fun reset() {
|
|
62
|
+
last = null
|
|
63
|
+
variance = 0.0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
companion object {
|
|
67
|
+
const val DEFAULT_RESET_AFTER_MS = 60_000L
|
|
68
|
+
const val DEFAULT_SPEED_MPS = 3.0
|
|
69
|
+
const val MIN_SPEED_MPS = 1.0
|
|
70
|
+
const val MIN_ACCURACY_M = 1.0
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.processing
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.model.GeofenceHit
|
|
4
|
+
import com.brickssoft.locationtracking.model.ProviderState
|
|
5
|
+
import com.brickssoft.locationtracking.model.Record
|
|
6
|
+
import com.brickssoft.locationtracking.model.RecordEvent
|
|
7
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
|
|
10
|
+
sealed interface FilterResult {
|
|
11
|
+
data class Accepted(val location: TrackedLocation) : FilterResult
|
|
12
|
+
|
|
13
|
+
data class Rejected(val reason: String) : FilterResult
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Accuracy / implied-speed / identical / mock filters and optional Kalman smoothing. */
|
|
17
|
+
interface LocationProcessor {
|
|
18
|
+
fun process(raw: TrackedLocation, isMoving: Boolean): FilterResult
|
|
19
|
+
|
|
20
|
+
fun reset()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Distance accumulator; persists its value via `configStore.updateRuntime`. */
|
|
24
|
+
interface Odometer {
|
|
25
|
+
val value: Double
|
|
26
|
+
|
|
27
|
+
fun onLocation(location: TrackedLocation)
|
|
28
|
+
|
|
29
|
+
fun set(value: Double)
|
|
30
|
+
|
|
31
|
+
fun reset()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Builds records with uuid, clock/boot metadata, runtime state, battery and backend. */
|
|
35
|
+
interface RecordFactory {
|
|
36
|
+
/** Merges `persistence.extras`; reads runtime (isMoving, odometer, activity), battery and clock. */
|
|
37
|
+
fun create(
|
|
38
|
+
event: RecordEvent,
|
|
39
|
+
location: TrackedLocation?,
|
|
40
|
+
extras: String? = null,
|
|
41
|
+
geofence: GeofenceHit? = null,
|
|
42
|
+
provider: ProviderState? = null,
|
|
43
|
+
reason: String? = null,
|
|
44
|
+
): Record
|
|
45
|
+
|
|
46
|
+
/** `insertLocation`: builds a record from a JS `InsertLocationInput`. */
|
|
47
|
+
fun fromExternal(input: JSONObject): Record
|
|
48
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.model.ActivitySample
|
|
4
|
+
import com.brickssoft.locationtracking.model.DesiredAccuracy
|
|
5
|
+
import com.brickssoft.locationtracking.model.GeofenceAction
|
|
6
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
7
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
8
|
+
|
|
9
|
+
/** A continuous location request. */
|
|
10
|
+
data class LocationRequestSpec(
|
|
11
|
+
val accuracy: DesiredAccuracy,
|
|
12
|
+
val intervalMs: Long,
|
|
13
|
+
val fastestIntervalMs: Long,
|
|
14
|
+
val distanceFilterM: Float,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
/** Receives batches of fixes, on any thread. */
|
|
18
|
+
fun interface LocationListener {
|
|
19
|
+
fun onLocations(locations: List<TrackedLocation>)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Location updates and one-shot fixes from one backend. */
|
|
23
|
+
interface LocationBackend {
|
|
24
|
+
val kind: ProviderKind
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Supports multiple listeners; requesting again with the same listener replaces its request. A `distanceFilterM`
|
|
28
|
+
* of 0 means no distance filter (the engine applies its own elastic filter). Never throws: failures such as a
|
|
29
|
+
* missing permission are logged and the listener receives nothing.
|
|
30
|
+
*/
|
|
31
|
+
fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener)
|
|
32
|
+
|
|
33
|
+
fun removeUpdates(listener: LocationListener)
|
|
34
|
+
|
|
35
|
+
/** The OS's cached last location. Implementations may throw `TrackingException(PERMISSION_DENIED)`; callers catch. */
|
|
36
|
+
suspend fun getLastLocation(): TrackedLocation?
|
|
37
|
+
|
|
38
|
+
/** Returns null on timeout. @throws com.brickssoft.locationtracking.core.TrackingException PERMISSION_DENIED */
|
|
39
|
+
suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation?
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Activity recognition. Samples are delivered through a PendingIntent receiver to [ActivitySink]. */
|
|
43
|
+
interface ActivityBackend {
|
|
44
|
+
val kind: ProviderKind
|
|
45
|
+
val isSupported: Boolean
|
|
46
|
+
|
|
47
|
+
/** Returns false if updates could not be requested. */
|
|
48
|
+
fun start(intervalMs: Long): Boolean
|
|
49
|
+
|
|
50
|
+
fun stop()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A geofence as registered with the OS. */
|
|
54
|
+
data class OsGeofence(
|
|
55
|
+
val id: String,
|
|
56
|
+
val latitude: Double,
|
|
57
|
+
val longitude: Double,
|
|
58
|
+
val radius: Float,
|
|
59
|
+
val onEntry: Boolean,
|
|
60
|
+
val onExit: Boolean,
|
|
61
|
+
val onDwell: Boolean,
|
|
62
|
+
val loiteringDelayMs: Int,
|
|
63
|
+
val initialTriggerEntry: Boolean,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
/** A transition reported by the OS; [location] is the triggering fix if the backend provides one. */
|
|
67
|
+
data class OsGeofenceTransition(val id: String, val action: GeofenceAction, val location: TrackedLocation?)
|
|
68
|
+
|
|
69
|
+
/** OS geofencing. Transitions are delivered through a PendingIntent receiver to [GeofenceTransitionSink]. */
|
|
70
|
+
interface GeofenceBackend {
|
|
71
|
+
val kind: ProviderKind
|
|
72
|
+
val supportsDwell: Boolean
|
|
73
|
+
|
|
74
|
+
/** @throws com.brickssoft.locationtracking.core.TrackingException */
|
|
75
|
+
suspend fun add(regions: List<OsGeofence>)
|
|
76
|
+
|
|
77
|
+
/** @throws com.brickssoft.locationtracking.core.TrackingException */
|
|
78
|
+
suspend fun remove(ids: List<String>)
|
|
79
|
+
|
|
80
|
+
/** @throws com.brickssoft.locationtracking.core.TrackingException */
|
|
81
|
+
suspend fun removeAll()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The three backends of one SDK. Implementations are created only by reflection through a public
|
|
86
|
+
* `(android.content.Context)` constructor; see [ProviderBundles].
|
|
87
|
+
*/
|
|
88
|
+
interface ProviderBundle {
|
|
89
|
+
val kind: ProviderKind
|
|
90
|
+
|
|
91
|
+
/** True if the SDK is packaged and usable on this device (e.g. `GoogleApiAvailability` SUCCESS). */
|
|
92
|
+
fun isAvailable(): Boolean
|
|
93
|
+
|
|
94
|
+
fun location(): LocationBackend
|
|
95
|
+
|
|
96
|
+
fun activity(): ActivityBackend
|
|
97
|
+
|
|
98
|
+
fun geofence(): GeofenceBackend
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Selects the active backend from config `locationProvider` (see architecture "GMS/HMS selection"). */
|
|
102
|
+
interface ProviderFactory {
|
|
103
|
+
val kind: ProviderKind
|
|
104
|
+
|
|
105
|
+
fun location(): LocationBackend
|
|
106
|
+
|
|
107
|
+
fun activity(): ActivityBackend
|
|
108
|
+
|
|
109
|
+
fun geofence(): GeofenceBackend
|
|
110
|
+
|
|
111
|
+
fun isAvailable(kind: ProviderKind): Boolean
|
|
112
|
+
|
|
113
|
+
/** Re-evaluates the selection (e.g. after setConfig). Returns true if [kind] changed. */
|
|
114
|
+
fun reselect(): Boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Implemented by the TrackingEngine. */
|
|
118
|
+
interface ActivitySink {
|
|
119
|
+
suspend fun onActivitySamples(samples: List<ActivitySample>)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Implemented by the GeofenceManager. */
|
|
123
|
+
interface GeofenceTransitionSink {
|
|
124
|
+
suspend fun onGeofenceTransitions(transitions: List<OsGeofenceTransition>)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Implemented by the TrackingEngine (round 2, stationary GPS-off mode). While STATIONARY the engine registers one OS
|
|
129
|
+
* geofence with id [com.brickssoft.locationtracking.core.Constants.STATIONARY_REGION_ID] directly with
|
|
130
|
+
* `providers.geofence()`. The GeofenceManager forwards every transition with that id here, and never stores, records
|
|
131
|
+
* or emits it.
|
|
132
|
+
*/
|
|
133
|
+
interface StationaryRegionSink {
|
|
134
|
+
suspend fun onStationaryRegionTransition(transition: OsGeofenceTransition)
|
|
135
|
+
|
|
136
|
+
companion object {
|
|
137
|
+
/** Ignores every transition (tests and callers without an engine). */
|
|
138
|
+
val NONE: StationaryRegionSink = object : StationaryRegionSink {
|
|
139
|
+
override suspend fun onStationaryRegionTransition(transition: OsGeofenceTransition) = Unit
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Fully qualified class names used for reflective loading. Strings only: never reference SDK types here. */
|
|
145
|
+
object ProviderBundles {
|
|
146
|
+
const val GMS_BUNDLE = "com.brickssoft.locationtracking.provider.gms.GmsProviderBundle"
|
|
147
|
+
const val HMS_BUNDLE = "com.brickssoft.locationtracking.provider.hms.HmsProviderBundle"
|
|
148
|
+
const val ANDROID_BUNDLE = "com.brickssoft.locationtracking.provider.android.AndroidProviderBundle"
|
|
149
|
+
|
|
150
|
+
/** Present only when play-services-location is packaged. */
|
|
151
|
+
const val GMS_SDK_CLASS = "com.google.android.gms.location.LocationServices"
|
|
152
|
+
|
|
153
|
+
/** Present only when com.huawei.hms:location is packaged. */
|
|
154
|
+
const val HMS_SDK_CLASS = "com.huawei.hms.location.LocationServices"
|
|
155
|
+
|
|
156
|
+
/** The receivers the GMS backend's activity and geofence results are delivered to. */
|
|
157
|
+
val GMS_RECEIVERS = listOf(
|
|
158
|
+
"com.brickssoft.locationtracking.provider.gms.GmsActivityReceiver",
|
|
159
|
+
"com.brickssoft.locationtracking.provider.gms.GmsGeofenceReceiver",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
/** The receivers the HMS backend's activity and geofence results are delivered to. */
|
|
163
|
+
val HMS_RECEIVERS = listOf(
|
|
164
|
+
"com.brickssoft.locationtracking.provider.hms.HmsActivityReceiver",
|
|
165
|
+
"com.brickssoft.locationtracking.provider.hms.HmsGeofenceReceiver",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
/** The SDK class whose presence means [kind]'s SDK is in the APK; null for ANDROID (part of the platform). */
|
|
169
|
+
fun sdkClassName(kind: ProviderKind): String? = when (kind) {
|
|
170
|
+
ProviderKind.GMS -> GMS_SDK_CLASS
|
|
171
|
+
ProviderKind.HMS -> HMS_SDK_CLASS
|
|
172
|
+
ProviderKind.ANDROID -> null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The receivers [kind]'s backend needs in the merged manifest; none for ANDROID. */
|
|
176
|
+
fun receiverClassNames(kind: ProviderKind): List<String> = when (kind) {
|
|
177
|
+
ProviderKind.GMS -> GMS_RECEIVERS
|
|
178
|
+
ProviderKind.HMS -> HMS_RECEIVERS
|
|
179
|
+
ProviderKind.ANDROID -> emptyList()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fun bundleClassName(kind: ProviderKind): String = when (kind) {
|
|
183
|
+
ProviderKind.GMS -> GMS_BUNDLE
|
|
184
|
+
ProviderKind.HMS -> HMS_BUNDLE
|
|
185
|
+
ProviderKind.ANDROID -> ANDROID_BUNDLE
|
|
186
|
+
}
|
|
187
|
+
}
|