@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
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.config
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
4
|
+
import com.brickssoft.locationtracking.core.Iso8601
|
|
5
|
+
import com.brickssoft.locationtracking.core.LogLevel
|
|
6
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
7
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
8
|
+
import com.brickssoft.locationtracking.model.DesiredAccuracy
|
|
9
|
+
import com.brickssoft.locationtracking.model.JsonUtil
|
|
10
|
+
import com.brickssoft.locationtracking.model.LocationProviderSetting
|
|
11
|
+
import org.json.JSONArray
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Config <-> JS JSON, and `State` -> JS.
|
|
16
|
+
*
|
|
17
|
+
* [parse] deep-merges a partial TS `Config` onto a base config:
|
|
18
|
+
* - a missing key keeps the base value;
|
|
19
|
+
* - JSON `null` resets that key (or whole group) to its default;
|
|
20
|
+
* - arrays (`notification.actions`) and map-valued objects (`headers`, `params`, `extras`, `refreshPayload`,
|
|
21
|
+
* `refreshHeaders`) replace the base value as a whole;
|
|
22
|
+
* - unknown keys are ignored with a warning;
|
|
23
|
+
* - a value of the wrong type, or an enum string that is not a wire value, throws
|
|
24
|
+
* [TrackingException] `INVALID_ARGUMENT` (nothing is applied).
|
|
25
|
+
*
|
|
26
|
+
* [parse] does not clamp; the store runs [ConfigValidator] on the result.
|
|
27
|
+
*/
|
|
28
|
+
object ConfigJson {
|
|
29
|
+
private const val TAG = "LT.ConfigJson"
|
|
30
|
+
|
|
31
|
+
/** Applies the partial TS `Config` [json] on top of [base]. See the class KDoc for the merge rules. */
|
|
32
|
+
fun parse(json: JSONObject, base: Config = Config()): Config = parse(json, base, lenient = false)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reads a config persisted with [toJson] onto the defaults. Unlike [parse], an invalid value (e.g. an enum
|
|
36
|
+
* value unknown to this version) is logged and replaced by its default, so one bad field cannot discard the
|
|
37
|
+
* whole stored config (URL, tokens, ...).
|
|
38
|
+
*/
|
|
39
|
+
internal fun parseStored(json: JSONObject): Config = parse(json, Config(), lenient = true)
|
|
40
|
+
|
|
41
|
+
private fun parse(json: JSONObject, base: Config, lenient: Boolean): Config {
|
|
42
|
+
val r = JsonReader(json, "", lenient)
|
|
43
|
+
val d = Config()
|
|
44
|
+
val result = Config(
|
|
45
|
+
geolocation = r.group("geolocation", base.geolocation, d.geolocation, ::parseGeolocation),
|
|
46
|
+
activity = r.group("activity", base.activity, d.activity, ::parseActivity),
|
|
47
|
+
heartbeat = r.group("heartbeat", base.heartbeat, d.heartbeat, ::parseHeartbeat),
|
|
48
|
+
http = r.group("http", base.http, d.http, ::parseHttp),
|
|
49
|
+
persistence = r.group("persistence", base.persistence, d.persistence, ::parsePersistence),
|
|
50
|
+
app = r.group("app", base.app, d.app, ::parseApp),
|
|
51
|
+
notification = r.group("notification", base.notification, d.notification, ::parseNotification),
|
|
52
|
+
geofence = r.group("geofence", base.geofence, d.geofence, ::parseGeofence),
|
|
53
|
+
logger = r.group("logger", base.logger, d.logger, ::parseLogger),
|
|
54
|
+
backgroundPermissionRationale = r.group(
|
|
55
|
+
"backgroundPermissionRationale",
|
|
56
|
+
base.backgroundPermissionRationale,
|
|
57
|
+
d.backgroundPermissionRationale,
|
|
58
|
+
::parseRationale,
|
|
59
|
+
),
|
|
60
|
+
locationProvider = r.enumValue(
|
|
61
|
+
"locationProvider",
|
|
62
|
+
base.locationProvider,
|
|
63
|
+
d.locationProvider,
|
|
64
|
+
LocationProviderSetting.entries.map { it.wire },
|
|
65
|
+
LocationProviderSetting::fromWire,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
r.warnUnknownKeys()
|
|
69
|
+
return result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The full TS `Config`: every key present, defaults filled, unset optional values as JSON null. */
|
|
73
|
+
fun toJson(c: Config): JSONObject = JSONObject()
|
|
74
|
+
.put("geolocation", geolocationToJson(c.geolocation))
|
|
75
|
+
.put("activity", activityToJson(c.activity))
|
|
76
|
+
.put("heartbeat", heartbeatToJson(c.heartbeat))
|
|
77
|
+
.put("http", httpToJson(c.http))
|
|
78
|
+
.put("persistence", persistenceToJson(c.persistence))
|
|
79
|
+
.put("app", JSONObject().put("stopOnTerminate", c.app.stopOnTerminate).put("startOnBoot", c.app.startOnBoot))
|
|
80
|
+
.put("notification", notificationToJson(c.notification))
|
|
81
|
+
.put("geofence", JSONObject().put("initialTriggerEntry", c.geofence.initialTriggerEntry))
|
|
82
|
+
.put("logger", JSONObject().put("logLevel", c.logger.logLevel.wire).put("logMaxDays", c.logger.logMaxDays))
|
|
83
|
+
.put("backgroundPermissionRationale", rationaleToJson(c.backgroundPermissionRationale))
|
|
84
|
+
.put("locationProvider", c.locationProvider.wire)
|
|
85
|
+
|
|
86
|
+
/** JS `State`: runtime fields, the active backend, and the fully populated config. */
|
|
87
|
+
fun stateToJson(s: State): JSONObject = JSONObject()
|
|
88
|
+
.put("enabled", s.runtime.enabled)
|
|
89
|
+
.put("trackingMode", s.runtime.trackingMode.wire)
|
|
90
|
+
.put("isMoving", s.runtime.isMoving)
|
|
91
|
+
.put("odometer", finiteOr(s.runtime.odometer, 0.0))
|
|
92
|
+
.put("backend", s.backend.wire)
|
|
93
|
+
.put("lastRecordAt", JsonUtil.orNull(Iso8601.formatOrNull(s.runtime.lastRecordAt)))
|
|
94
|
+
.put("config", toJson(s.config))
|
|
95
|
+
|
|
96
|
+
// ---- parse
|
|
97
|
+
|
|
98
|
+
private fun parseGeolocation(r: JsonReader, c: GeolocationConfig): GeolocationConfig {
|
|
99
|
+
val d = GeolocationConfig()
|
|
100
|
+
return GeolocationConfig(
|
|
101
|
+
desiredAccuracy = r.enumValue(
|
|
102
|
+
"desiredAccuracy",
|
|
103
|
+
c.desiredAccuracy,
|
|
104
|
+
d.desiredAccuracy,
|
|
105
|
+
DesiredAccuracy.entries.map { it.wire },
|
|
106
|
+
DesiredAccuracy::fromWire,
|
|
107
|
+
),
|
|
108
|
+
distanceFilter = r.double("distanceFilter", c.distanceFilter, d.distanceFilter),
|
|
109
|
+
locationUpdateInterval = r.long(
|
|
110
|
+
"locationUpdateInterval",
|
|
111
|
+
c.locationUpdateInterval,
|
|
112
|
+
d.locationUpdateInterval,
|
|
113
|
+
),
|
|
114
|
+
fastestLocationUpdateInterval = r.long(
|
|
115
|
+
"fastestLocationUpdateInterval",
|
|
116
|
+
c.fastestLocationUpdateInterval,
|
|
117
|
+
d.fastestLocationUpdateInterval,
|
|
118
|
+
),
|
|
119
|
+
disableElasticity = r.boolean("disableElasticity", c.disableElasticity, d.disableElasticity),
|
|
120
|
+
elasticityMultiplier = r.double("elasticityMultiplier", c.elasticityMultiplier, d.elasticityMultiplier),
|
|
121
|
+
stationaryRadius = r.double("stationaryRadius", c.stationaryRadius, d.stationaryRadius),
|
|
122
|
+
stopTimeout = r.int("stopTimeout", c.stopTimeout, d.stopTimeout),
|
|
123
|
+
stopAfterElapsedMinutes = r.int(
|
|
124
|
+
"stopAfterElapsedMinutes",
|
|
125
|
+
c.stopAfterElapsedMinutes,
|
|
126
|
+
d.stopAfterElapsedMinutes,
|
|
127
|
+
),
|
|
128
|
+
stopOnStationary = r.boolean("stopOnStationary", c.stopOnStationary, d.stopOnStationary),
|
|
129
|
+
locationTimeout = r.long("locationTimeout", c.locationTimeout, d.locationTimeout),
|
|
130
|
+
filter = r.group("filter", c.filter, d.filter, ::parseFilter),
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun parseFilter(r: JsonReader, c: LocationFilterConfig): LocationFilterConfig {
|
|
135
|
+
val d = LocationFilterConfig()
|
|
136
|
+
return LocationFilterConfig(
|
|
137
|
+
useKalman = r.boolean("useKalman", c.useKalman, d.useKalman),
|
|
138
|
+
trackingAccuracyThreshold = r.double(
|
|
139
|
+
"trackingAccuracyThreshold",
|
|
140
|
+
c.trackingAccuracyThreshold,
|
|
141
|
+
d.trackingAccuracyThreshold,
|
|
142
|
+
),
|
|
143
|
+
maxImpliedSpeed = r.double("maxImpliedSpeed", c.maxImpliedSpeed, d.maxImpliedSpeed),
|
|
144
|
+
odometerAccuracyThreshold = r.double(
|
|
145
|
+
"odometerAccuracyThreshold",
|
|
146
|
+
c.odometerAccuracyThreshold,
|
|
147
|
+
d.odometerAccuracyThreshold,
|
|
148
|
+
),
|
|
149
|
+
allowIdenticalLocations = r.boolean(
|
|
150
|
+
"allowIdenticalLocations",
|
|
151
|
+
c.allowIdenticalLocations,
|
|
152
|
+
d.allowIdenticalLocations,
|
|
153
|
+
),
|
|
154
|
+
rejectMockLocations = r.boolean("rejectMockLocations", c.rejectMockLocations, d.rejectMockLocations),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun parseActivity(r: JsonReader, c: ActivityConfig): ActivityConfig {
|
|
159
|
+
val d = ActivityConfig()
|
|
160
|
+
return ActivityConfig(
|
|
161
|
+
disableMotionActivityUpdates = r.boolean(
|
|
162
|
+
"disableMotionActivityUpdates",
|
|
163
|
+
c.disableMotionActivityUpdates,
|
|
164
|
+
d.disableMotionActivityUpdates,
|
|
165
|
+
),
|
|
166
|
+
activityRecognitionInterval = r.long(
|
|
167
|
+
"activityRecognitionInterval",
|
|
168
|
+
c.activityRecognitionInterval,
|
|
169
|
+
d.activityRecognitionInterval,
|
|
170
|
+
),
|
|
171
|
+
minimumActivityRecognitionConfidence = r.int(
|
|
172
|
+
"minimumActivityRecognitionConfidence",
|
|
173
|
+
c.minimumActivityRecognitionConfidence,
|
|
174
|
+
d.minimumActivityRecognitionConfidence,
|
|
175
|
+
),
|
|
176
|
+
motionTriggerDelay = r.long("motionTriggerDelay", c.motionTriggerDelay, d.motionTriggerDelay),
|
|
177
|
+
disableStopDetection = r.boolean("disableStopDetection", c.disableStopDetection, d.disableStopDetection),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private fun parseHeartbeat(r: JsonReader, c: HeartbeatConfig): HeartbeatConfig {
|
|
182
|
+
val d = HeartbeatConfig()
|
|
183
|
+
return HeartbeatConfig(
|
|
184
|
+
enabled = r.boolean("enabled", c.enabled, d.enabled),
|
|
185
|
+
minInterval = r.int("minInterval", c.minInterval, d.minInterval),
|
|
186
|
+
maxInterval = r.int("maxInterval", c.maxInterval, d.maxInterval),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private fun parseHttp(r: JsonReader, c: HttpConfig): HttpConfig {
|
|
191
|
+
val d = HttpConfig()
|
|
192
|
+
return HttpConfig(
|
|
193
|
+
url = r.nullableString("url", c.url, d.url),
|
|
194
|
+
method = r.enumValue(
|
|
195
|
+
"method",
|
|
196
|
+
c.method,
|
|
197
|
+
d.method,
|
|
198
|
+
HttpMethod.entries.map { it.wire },
|
|
199
|
+
HttpMethod::fromWire,
|
|
200
|
+
),
|
|
201
|
+
headers = r.stringMap("headers", c.headers, d.headers),
|
|
202
|
+
params = r.objectText("params", c.params, d.params),
|
|
203
|
+
autoSync = r.boolean("autoSync", c.autoSync, d.autoSync),
|
|
204
|
+
autoSyncThreshold = r.int("autoSyncThreshold", c.autoSyncThreshold, d.autoSyncThreshold),
|
|
205
|
+
syncInterval = r.int("syncInterval", c.syncInterval, d.syncInterval),
|
|
206
|
+
batchSync = r.boolean("batchSync", c.batchSync, d.batchSync),
|
|
207
|
+
maxBatchSize = r.int("maxBatchSize", c.maxBatchSize, d.maxBatchSize),
|
|
208
|
+
disableAutoSyncOnCellular = r.boolean(
|
|
209
|
+
"disableAutoSyncOnCellular",
|
|
210
|
+
c.disableAutoSyncOnCellular,
|
|
211
|
+
d.disableAutoSyncOnCellular,
|
|
212
|
+
),
|
|
213
|
+
rootProperty = r.string("rootProperty", c.rootProperty, d.rootProperty),
|
|
214
|
+
locationTemplate = r.nullableString("locationTemplate", c.locationTemplate, d.locationTemplate),
|
|
215
|
+
geofenceTemplate = r.nullableString("geofenceTemplate", c.geofenceTemplate, d.geofenceTemplate),
|
|
216
|
+
timeout = r.long("timeout", c.timeout, d.timeout),
|
|
217
|
+
authorization = r.group<AuthorizationConfig?>("authorization", c.authorization, d.authorization) { ar, a ->
|
|
218
|
+
parseAuthorization(ar, a ?: AuthorizationConfig())
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private fun parseAuthorization(r: JsonReader, c: AuthorizationConfig): AuthorizationConfig {
|
|
224
|
+
val d = AuthorizationConfig()
|
|
225
|
+
return AuthorizationConfig(
|
|
226
|
+
strategy = r.enumValue("strategy", c.strategy, d.strategy, listOf(d.strategy)) { value ->
|
|
227
|
+
d.strategy.takeIf { it.equals(value, ignoreCase = true) }
|
|
228
|
+
},
|
|
229
|
+
accessToken = r.nullableString("accessToken", c.accessToken, d.accessToken),
|
|
230
|
+
refreshToken = r.nullableString("refreshToken", c.refreshToken, d.refreshToken),
|
|
231
|
+
refreshUrl = r.nullableString("refreshUrl", c.refreshUrl, d.refreshUrl),
|
|
232
|
+
refreshPayload = r.objectText("refreshPayload", c.refreshPayload, d.refreshPayload),
|
|
233
|
+
refreshHeaders = r.stringMap("refreshHeaders", c.refreshHeaders, d.refreshHeaders),
|
|
234
|
+
refreshPayloadEncoding = r.enumValue(
|
|
235
|
+
"refreshPayloadEncoding",
|
|
236
|
+
c.refreshPayloadEncoding,
|
|
237
|
+
d.refreshPayloadEncoding,
|
|
238
|
+
RefreshPayloadEncoding.entries.map { it.wire },
|
|
239
|
+
RefreshPayloadEncoding::fromWire,
|
|
240
|
+
),
|
|
241
|
+
expires = r.long("expires", c.expires, d.expires),
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private fun parsePersistence(r: JsonReader, c: PersistenceConfig): PersistenceConfig {
|
|
246
|
+
val d = PersistenceConfig()
|
|
247
|
+
return PersistenceConfig(
|
|
248
|
+
maxDaysToPersist = r.int("maxDaysToPersist", c.maxDaysToPersist, d.maxDaysToPersist),
|
|
249
|
+
maxRecordsToPersist = r.int("maxRecordsToPersist", c.maxRecordsToPersist, d.maxRecordsToPersist),
|
|
250
|
+
extras = r.objectText("extras", c.extras, d.extras),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private fun parseApp(r: JsonReader, c: AppConfig): AppConfig {
|
|
255
|
+
val d = AppConfig()
|
|
256
|
+
return AppConfig(
|
|
257
|
+
stopOnTerminate = r.boolean("stopOnTerminate", c.stopOnTerminate, d.stopOnTerminate),
|
|
258
|
+
startOnBoot = r.boolean("startOnBoot", c.startOnBoot, d.startOnBoot),
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private fun parseNotification(r: JsonReader, c: NotificationConfig): NotificationConfig {
|
|
263
|
+
val d = NotificationConfig()
|
|
264
|
+
return NotificationConfig(
|
|
265
|
+
title = r.nullableString("title", c.title, d.title),
|
|
266
|
+
text = r.string("text", c.text, d.text),
|
|
267
|
+
smallIcon = r.string("smallIcon", c.smallIcon, d.smallIcon),
|
|
268
|
+
largeIcon = r.nullableString("largeIcon", c.largeIcon, d.largeIcon),
|
|
269
|
+
color = r.nullableString("color", c.color, d.color),
|
|
270
|
+
priority = r.enumValue(
|
|
271
|
+
"priority",
|
|
272
|
+
c.priority,
|
|
273
|
+
d.priority,
|
|
274
|
+
NotificationPriority.entries.map { it.wire },
|
|
275
|
+
NotificationPriority::fromWire,
|
|
276
|
+
),
|
|
277
|
+
channelId = r.string("channelId", c.channelId, d.channelId),
|
|
278
|
+
channelName = r.string("channelName", c.channelName, d.channelName),
|
|
279
|
+
actions = r.array("actions", c.actions, d.actions, ::parseAction),
|
|
280
|
+
resume = r.group("resume", c.resume, d.resume, ::parseResume),
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private fun parseResume(r: JsonReader, c: ResumeNotificationConfig): ResumeNotificationConfig {
|
|
285
|
+
val d = ResumeNotificationConfig()
|
|
286
|
+
return ResumeNotificationConfig(
|
|
287
|
+
enabled = r.boolean("enabled", c.enabled, d.enabled),
|
|
288
|
+
title = r.nullableString("title", c.title, d.title),
|
|
289
|
+
text = r.string("text", c.text, d.text),
|
|
290
|
+
channelName = r.string("channelName", c.channelName, d.channelName),
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private fun parseAction(item: Any, path: String): NotificationActionButton {
|
|
295
|
+
val obj = item as? JSONObject ?: JsonReader.invalid("$path must be an object {id, label}")
|
|
296
|
+
val r = JsonReader(obj, path)
|
|
297
|
+
val id = r.nullableString("id", null, null)
|
|
298
|
+
val label = r.nullableString("label", null, null)
|
|
299
|
+
if (id.isNullOrBlank()) JsonReader.invalid("$path.id is required")
|
|
300
|
+
if (label == null) JsonReader.invalid("$path.label is required")
|
|
301
|
+
r.warnUnknownKeys()
|
|
302
|
+
return NotificationActionButton(id = id, label = label)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private fun parseGeofence(r: JsonReader, c: GeofenceConfig): GeofenceConfig {
|
|
306
|
+
val d = GeofenceConfig()
|
|
307
|
+
return GeofenceConfig(
|
|
308
|
+
initialTriggerEntry = r.boolean("initialTriggerEntry", c.initialTriggerEntry, d.initialTriggerEntry),
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private fun parseLogger(r: JsonReader, c: LoggerConfig): LoggerConfig {
|
|
313
|
+
val d = LoggerConfig()
|
|
314
|
+
return LoggerConfig(
|
|
315
|
+
logLevel = r.enumValue(
|
|
316
|
+
"logLevel",
|
|
317
|
+
c.logLevel,
|
|
318
|
+
d.logLevel,
|
|
319
|
+
LogLevel.entries.map { it.wire },
|
|
320
|
+
LogLevel::fromWire,
|
|
321
|
+
),
|
|
322
|
+
logMaxDays = r.int("logMaxDays", c.logMaxDays, d.logMaxDays),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private fun parseRationale(r: JsonReader, c: BackgroundPermissionRationale): BackgroundPermissionRationale {
|
|
327
|
+
val d = BackgroundPermissionRationale()
|
|
328
|
+
return BackgroundPermissionRationale(
|
|
329
|
+
title = r.nullableString("title", c.title, d.title),
|
|
330
|
+
message = r.nullableString("message", c.message, d.message),
|
|
331
|
+
positiveAction = r.nullableString("positiveAction", c.positiveAction, d.positiveAction),
|
|
332
|
+
negativeAction = r.nullableString("negativeAction", c.negativeAction, d.negativeAction),
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---- toJson
|
|
337
|
+
|
|
338
|
+
private fun geolocationToJson(g: GeolocationConfig): JSONObject = JSONObject()
|
|
339
|
+
.put("desiredAccuracy", g.desiredAccuracy.wire)
|
|
340
|
+
.put("distanceFilter", finiteOr(g.distanceFilter, GeolocationConfig().distanceFilter))
|
|
341
|
+
.put("locationUpdateInterval", g.locationUpdateInterval)
|
|
342
|
+
.put("fastestLocationUpdateInterval", g.fastestLocationUpdateInterval)
|
|
343
|
+
.put("disableElasticity", g.disableElasticity)
|
|
344
|
+
.put("elasticityMultiplier", finiteOr(g.elasticityMultiplier, GeolocationConfig().elasticityMultiplier))
|
|
345
|
+
.put("stationaryRadius", finiteOr(g.stationaryRadius, GeolocationConfig().stationaryRadius))
|
|
346
|
+
.put("stopTimeout", g.stopTimeout)
|
|
347
|
+
.put("stopAfterElapsedMinutes", g.stopAfterElapsedMinutes)
|
|
348
|
+
.put("stopOnStationary", g.stopOnStationary)
|
|
349
|
+
.put("locationTimeout", g.locationTimeout)
|
|
350
|
+
.put("filter", filterToJson(g.filter))
|
|
351
|
+
|
|
352
|
+
private fun filterToJson(f: LocationFilterConfig): JSONObject {
|
|
353
|
+
val d = LocationFilterConfig()
|
|
354
|
+
return JSONObject()
|
|
355
|
+
.put("useKalman", f.useKalman)
|
|
356
|
+
.put("trackingAccuracyThreshold", finiteOr(f.trackingAccuracyThreshold, d.trackingAccuracyThreshold))
|
|
357
|
+
.put("maxImpliedSpeed", finiteOr(f.maxImpliedSpeed, d.maxImpliedSpeed))
|
|
358
|
+
.put("odometerAccuracyThreshold", finiteOr(f.odometerAccuracyThreshold, d.odometerAccuracyThreshold))
|
|
359
|
+
.put("allowIdenticalLocations", f.allowIdenticalLocations)
|
|
360
|
+
.put("rejectMockLocations", f.rejectMockLocations)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private fun activityToJson(a: ActivityConfig): JSONObject = JSONObject()
|
|
364
|
+
.put("disableMotionActivityUpdates", a.disableMotionActivityUpdates)
|
|
365
|
+
.put("activityRecognitionInterval", a.activityRecognitionInterval)
|
|
366
|
+
.put("minimumActivityRecognitionConfidence", a.minimumActivityRecognitionConfidence)
|
|
367
|
+
.put("motionTriggerDelay", a.motionTriggerDelay)
|
|
368
|
+
.put("disableStopDetection", a.disableStopDetection)
|
|
369
|
+
|
|
370
|
+
private fun heartbeatToJson(h: HeartbeatConfig): JSONObject = JSONObject()
|
|
371
|
+
.put("enabled", h.enabled)
|
|
372
|
+
.put("minInterval", h.minInterval)
|
|
373
|
+
.put("maxInterval", h.maxInterval)
|
|
374
|
+
|
|
375
|
+
private fun httpToJson(h: HttpConfig): JSONObject = JSONObject()
|
|
376
|
+
.put("url", JsonUtil.orNull(h.url))
|
|
377
|
+
.put("method", h.method.wire)
|
|
378
|
+
.put("headers", JsonUtil.fromStringMap(h.headers))
|
|
379
|
+
.put("params", objectOrEmpty(h.params, "http.params"))
|
|
380
|
+
.put("autoSync", h.autoSync)
|
|
381
|
+
.put("autoSyncThreshold", h.autoSyncThreshold)
|
|
382
|
+
.put("syncInterval", h.syncInterval)
|
|
383
|
+
.put("batchSync", h.batchSync)
|
|
384
|
+
.put("maxBatchSize", h.maxBatchSize)
|
|
385
|
+
.put("disableAutoSyncOnCellular", h.disableAutoSyncOnCellular)
|
|
386
|
+
.put("rootProperty", h.rootProperty)
|
|
387
|
+
.put("locationTemplate", JsonUtil.orNull(h.locationTemplate))
|
|
388
|
+
.put("geofenceTemplate", JsonUtil.orNull(h.geofenceTemplate))
|
|
389
|
+
.put("timeout", h.timeout)
|
|
390
|
+
.put("authorization", h.authorization?.let { authorizationToJson(it) } ?: JSONObject.NULL)
|
|
391
|
+
|
|
392
|
+
private fun authorizationToJson(a: AuthorizationConfig): JSONObject = JSONObject()
|
|
393
|
+
.put("strategy", a.strategy)
|
|
394
|
+
.put("accessToken", JsonUtil.orNull(a.accessToken))
|
|
395
|
+
.put("refreshToken", JsonUtil.orNull(a.refreshToken))
|
|
396
|
+
.put("refreshUrl", JsonUtil.orNull(a.refreshUrl))
|
|
397
|
+
.put("refreshPayload", objectOrEmpty(a.refreshPayload, "http.authorization.refreshPayload"))
|
|
398
|
+
.put("refreshHeaders", JsonUtil.fromStringMap(a.refreshHeaders))
|
|
399
|
+
.put("refreshPayloadEncoding", a.refreshPayloadEncoding.wire)
|
|
400
|
+
.put("expires", a.expires)
|
|
401
|
+
|
|
402
|
+
private fun persistenceToJson(p: PersistenceConfig): JSONObject = JSONObject()
|
|
403
|
+
.put("maxDaysToPersist", p.maxDaysToPersist)
|
|
404
|
+
.put("maxRecordsToPersist", p.maxRecordsToPersist)
|
|
405
|
+
.put("extras", objectOrEmpty(p.extras, "persistence.extras"))
|
|
406
|
+
|
|
407
|
+
private fun notificationToJson(n: NotificationConfig): JSONObject {
|
|
408
|
+
val actions = JSONArray()
|
|
409
|
+
for (a in n.actions) actions.put(JSONObject().put("id", a.id).put("label", a.label))
|
|
410
|
+
return JSONObject()
|
|
411
|
+
.put("title", JsonUtil.orNull(n.title))
|
|
412
|
+
.put("text", n.text)
|
|
413
|
+
.put("smallIcon", n.smallIcon)
|
|
414
|
+
.put("largeIcon", JsonUtil.orNull(n.largeIcon))
|
|
415
|
+
.put("color", JsonUtil.orNull(n.color))
|
|
416
|
+
.put("priority", n.priority.wire)
|
|
417
|
+
.put("channelId", n.channelId)
|
|
418
|
+
.put("channelName", n.channelName)
|
|
419
|
+
.put("actions", actions)
|
|
420
|
+
.put(
|
|
421
|
+
"resume",
|
|
422
|
+
JSONObject()
|
|
423
|
+
.put("enabled", n.resume.enabled)
|
|
424
|
+
.put("title", JsonUtil.orNull(n.resume.title))
|
|
425
|
+
.put("text", n.resume.text)
|
|
426
|
+
.put("channelName", n.resume.channelName),
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private fun rationaleToJson(b: BackgroundPermissionRationale): JSONObject = JSONObject()
|
|
431
|
+
.put("title", JsonUtil.orNull(b.title))
|
|
432
|
+
.put("message", JsonUtil.orNull(b.message))
|
|
433
|
+
.put("positiveAction", JsonUtil.orNull(b.positiveAction))
|
|
434
|
+
.put("negativeAction", JsonUtil.orNull(b.negativeAction))
|
|
435
|
+
|
|
436
|
+
/** Parses stored JSON object text; invalid text (only possible through `update{}`) becomes `{}`. */
|
|
437
|
+
private fun objectOrEmpty(text: String, path: String): JSONObject {
|
|
438
|
+
JsonUtil.parseObject(text)?.let { return it }
|
|
439
|
+
if (text.isNotBlank()) Logger.w(TAG, "$path is not a JSON object; emitting {}")
|
|
440
|
+
return JSONObject()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** org.json rejects NaN/Infinity; such values (only possible through `update{}`) fall back to [fallback]. */
|
|
444
|
+
private fun finiteOr(value: Double, fallback: Double): Double = if (value.isFinite()) value else fallback
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Typed reader over one JSON object of a partial config. Each accessor returns the current value when the key is
|
|
449
|
+
* missing, the default when it is JSON null, and the parsed value otherwise; wrong types throw INVALID_ARGUMENT,
|
|
450
|
+
* or, when [lenient], are logged and keep the current value. Every key an accessor asks for is remembered, so
|
|
451
|
+
* [warnUnknownKeys] can report the rest.
|
|
452
|
+
*/
|
|
453
|
+
private class JsonReader(
|
|
454
|
+
private val json: JSONObject,
|
|
455
|
+
private val path: String,
|
|
456
|
+
private val lenient: Boolean = false,
|
|
457
|
+
) {
|
|
458
|
+
private val known = HashSet<String>()
|
|
459
|
+
|
|
460
|
+
fun boolean(key: String, current: Boolean, default: Boolean): Boolean =
|
|
461
|
+
read(key, current, default, "a boolean") { v ->
|
|
462
|
+
when (v) {
|
|
463
|
+
is Boolean -> v
|
|
464
|
+
is String -> v.trim().lowercase().toBooleanStrictOrNull()
|
|
465
|
+
else -> null
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
fun double(key: String, current: Double, default: Double): Double =
|
|
470
|
+
read(key, current, default, "a number") { toDouble(it) }
|
|
471
|
+
|
|
472
|
+
fun long(key: String, current: Long, default: Long): Long =
|
|
473
|
+
read(key, current, default, "a number") { v ->
|
|
474
|
+
when (v) {
|
|
475
|
+
is Int, is Long, is Short, is Byte -> (v as Number).toLong()
|
|
476
|
+
else -> toDouble(v)?.toLong()
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
fun int(key: String, current: Int, default: Int): Int =
|
|
481
|
+
read(key, current, default, "a number") { v ->
|
|
482
|
+
when (v) {
|
|
483
|
+
is Int, is Short, is Byte -> (v as Number).toInt()
|
|
484
|
+
else -> toDouble(v)?.toInt()
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
fun string(key: String, current: String, default: String): String =
|
|
489
|
+
read(key, current, default, "a string") { it as? String }
|
|
490
|
+
|
|
491
|
+
fun nullableString(key: String, current: String?, default: String?): String? =
|
|
492
|
+
read(key, current, default, "a string") { it as? String }
|
|
493
|
+
|
|
494
|
+
fun <E> enumValue(key: String, current: E, default: E, allowed: List<String>, fromWire: (String) -> E?): E =
|
|
495
|
+
read(key, current, default, "one of ${allowed.joinToString("|") { "'$it'" }}") { v ->
|
|
496
|
+
(v as? String)?.let(fromWire)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** An object of scalar values, replaced as a whole (null values are dropped). */
|
|
500
|
+
fun stringMap(key: String, current: Map<String, String>, default: Map<String, String>): Map<String, String> =
|
|
501
|
+
read(key, current, default, "an object") { v -> (v as? JSONObject)?.let { JsonUtil.toStringMap(it) } }
|
|
502
|
+
|
|
503
|
+
/** An arbitrary object, replaced as a whole and kept as JSON text. */
|
|
504
|
+
fun objectText(key: String, current: String, default: String): String =
|
|
505
|
+
read(key, current, default, "an object") { v -> (v as? JSONObject)?.toString() }
|
|
506
|
+
|
|
507
|
+
/** An array, replaced as a whole; [item] parses one non-null element (given its path) or throws. */
|
|
508
|
+
fun <T> array(key: String, current: List<T>, default: List<T>, item: (Any, String) -> T): List<T> =
|
|
509
|
+
read(key, current, default, "an array") { v ->
|
|
510
|
+
(v as? JSONArray)?.let { array ->
|
|
511
|
+
(0 until array.length()).map { i ->
|
|
512
|
+
val element = array.opt(i)
|
|
513
|
+
val itemPath = "${pathOf(key)}[$i]"
|
|
514
|
+
if (element == null || element == JSONObject.NULL) invalid("$itemPath must not be null")
|
|
515
|
+
item(element, itemPath)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** A nested group: merged key by key onto [current] by [parse]; null resets it to [default]. */
|
|
521
|
+
fun <T> group(key: String, current: T, default: T, parse: (JsonReader, T) -> T): T =
|
|
522
|
+
read(key, current, default, "an object") { v ->
|
|
523
|
+
(v as? JSONObject)?.let { obj ->
|
|
524
|
+
val nested = JsonReader(obj, pathOf(key), lenient)
|
|
525
|
+
val result = parse(nested, current)
|
|
526
|
+
nested.warnUnknownKeys()
|
|
527
|
+
result
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
fun warnUnknownKeys() {
|
|
532
|
+
val keys = json.keys()
|
|
533
|
+
while (keys.hasNext()) {
|
|
534
|
+
val key = keys.next()
|
|
535
|
+
if (key !in known) Logger.w(TAG, "ignoring unknown config key '${pathOf(key)}'")
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private inline fun <T> read(key: String, current: T, default: T, expected: String, convert: (Any) -> T?): T {
|
|
540
|
+
known += key
|
|
541
|
+
if (!json.has(key)) return current
|
|
542
|
+
val raw = json.opt(key)
|
|
543
|
+
if (raw == null || raw == JSONObject.NULL) return default
|
|
544
|
+
if (!lenient) return convert(raw) ?: invalid("${pathOf(key)} must be $expected (got ${describe(raw)})")
|
|
545
|
+
return try {
|
|
546
|
+
convert(raw) ?: invalid("${pathOf(key)} must be $expected (got ${describe(raw)})")
|
|
547
|
+
} catch (e: TrackingException) {
|
|
548
|
+
Logger.w(TAG, "ignoring invalid stored value: ${e.message}")
|
|
549
|
+
current
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private fun pathOf(key: String): String = if (path.isEmpty()) key else "$path.$key"
|
|
554
|
+
|
|
555
|
+
private fun toDouble(v: Any): Double? = when (v) {
|
|
556
|
+
is Number -> v.toDouble()
|
|
557
|
+
is String -> v.trim().toDoubleOrNull()
|
|
558
|
+
else -> null
|
|
559
|
+
}?.takeIf { it.isFinite() }
|
|
560
|
+
|
|
561
|
+
private fun describe(raw: Any): String = when (raw) {
|
|
562
|
+
is String -> "'${raw.take(MAX_ECHO)}'"
|
|
563
|
+
is JSONObject -> "an object"
|
|
564
|
+
is JSONArray -> "an array"
|
|
565
|
+
else -> raw.toString().take(MAX_ECHO)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
companion object {
|
|
569
|
+
private const val TAG = "LT.ConfigJson"
|
|
570
|
+
private const val MAX_ECHO = 40
|
|
571
|
+
|
|
572
|
+
fun invalid(message: String): Nothing = throw TrackingException(ErrorCode.INVALID_ARGUMENT, "config.$message")
|
|
573
|
+
}
|
|
574
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.config
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persistent config and runtime state (implemented by `SharedPrefsConfigStore`, prefs file
|
|
8
|
+
* `location_tracking_prefs`, loaded synchronously in the constructor). All methods are thread-safe and
|
|
9
|
+
* persist before returning.
|
|
10
|
+
*/
|
|
11
|
+
interface ConfigStore {
|
|
12
|
+
val config: StateFlow<Config>
|
|
13
|
+
val runtime: StateFlow<RuntimeState>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `ready()`: with [reset] true, config = defaults + [json]; with false, [json] is applied only on the very
|
|
17
|
+
* first ready (runtime.didReady false), otherwise the persisted config wins.
|
|
18
|
+
*/
|
|
19
|
+
fun ready(json: JSONObject?, reset: Boolean): Config
|
|
20
|
+
|
|
21
|
+
/** `setConfig()`: deep merge; arrays replaced; null resets the key to its default. */
|
|
22
|
+
fun merge(json: JSONObject): Config
|
|
23
|
+
|
|
24
|
+
/** `reset()`: defaults + [json]. */
|
|
25
|
+
fun reset(json: JSONObject?): Config
|
|
26
|
+
|
|
27
|
+
/** Internal writers (e.g. JWT refresh). */
|
|
28
|
+
fun update(transform: (Config) -> Config): Config
|
|
29
|
+
|
|
30
|
+
fun updateRuntime(transform: (RuntimeState) -> RuntimeState): RuntimeState
|
|
31
|
+
}
|