@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +106 -0
- package/android/consumer-rules.pro +42 -0
- package/android/src/main/AndroidManifest.xml +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
- package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
- package/android/src/main/res/values/lt_strings.xml +11 -0
- package/android/src/main/res/xml/lt_file_paths.xml +5 -0
- package/dist/esm/definitions.d.ts +719 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/events.d.ts +50 -0
- package/dist/esm/events.js +52 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +10 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/web/config.d.ts +19 -0
- package/dist/esm/web/config.js +243 -0
- package/dist/esm/web/config.js.map +1 -0
- package/dist/esm/web/device.d.ts +9 -0
- package/dist/esm/web/device.js +90 -0
- package/dist/esm/web/device.js.map +1 -0
- package/dist/esm/web/errors.d.ts +14 -0
- package/dist/esm/web/errors.js +19 -0
- package/dist/esm/web/errors.js.map +1 -0
- package/dist/esm/web/geolocation.d.ts +26 -0
- package/dist/esm/web/geolocation.js +117 -0
- package/dist/esm/web/geolocation.js.map +1 -0
- package/dist/esm/web/location.d.ts +7 -0
- package/dist/esm/web/location.js +70 -0
- package/dist/esm/web/location.js.map +1 -0
- package/dist/esm/web.d.ts +164 -0
- package/dist/esm/web.js +328 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +945 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +948 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +59 -4
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.model
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
4
|
+
import com.brickssoft.locationtracking.core.Iso8601
|
|
5
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
6
|
+
import com.brickssoft.locationtracking.core.TrackingEvent
|
|
7
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
8
|
+
import com.brickssoft.locationtracking.permission.PermissionState
|
|
9
|
+
import com.brickssoft.locationtracking.permission.PermissionType
|
|
10
|
+
import org.json.JSONArray
|
|
11
|
+
import org.json.JSONException
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
|
|
14
|
+
// Shared JSON codecs (SCAFFOLD). Every JS-facing shape and the HTTP wire format are built here.
|
|
15
|
+
|
|
16
|
+
/** Null-safe helpers around org.json (whose `opt*` methods return "" / 0 / "null" for missing or null values). */
|
|
17
|
+
object JsonUtil {
|
|
18
|
+
/** Parses JSON object text; null, blank or invalid text returns null. */
|
|
19
|
+
fun parseObject(text: String?): JSONObject? {
|
|
20
|
+
if (text.isNullOrBlank()) return null
|
|
21
|
+
return try {
|
|
22
|
+
JSONObject(text)
|
|
23
|
+
} catch (_: JSONException) {
|
|
24
|
+
null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** String value, or null if the key is missing or JSON null. Non-string values are converted to text. */
|
|
29
|
+
fun optString(json: JSONObject, key: String): String? = if (json.isNull(key)) null else json.opt(key)?.toString()
|
|
30
|
+
|
|
31
|
+
/** Number value (or numeric string), or null if missing, JSON null or not numeric. */
|
|
32
|
+
fun optDouble(json: JSONObject, key: String): Double? {
|
|
33
|
+
if (json.isNull(key)) return null
|
|
34
|
+
return when (val v = json.opt(key)) {
|
|
35
|
+
is Number -> v.toDouble()
|
|
36
|
+
is String -> v.toDoubleOrNull()
|
|
37
|
+
else -> null
|
|
38
|
+
}?.takeIf { !it.isNaN() && !it.isInfinite() }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fun optFloat(json: JSONObject, key: String): Float? = optDouble(json, key)?.toFloat()
|
|
42
|
+
|
|
43
|
+
fun optLong(json: JSONObject, key: String): Long? = optDouble(json, key)?.toLong()
|
|
44
|
+
|
|
45
|
+
fun optInt(json: JSONObject, key: String): Int? = optDouble(json, key)?.toInt()
|
|
46
|
+
|
|
47
|
+
/** Boolean value (or "true"/"false" string), or null if missing, JSON null or not boolean. */
|
|
48
|
+
fun optBoolean(json: JSONObject, key: String): Boolean? {
|
|
49
|
+
if (json.isNull(key)) return null
|
|
50
|
+
return when (val v = json.opt(key)) {
|
|
51
|
+
is Boolean -> v
|
|
52
|
+
is String -> v.lowercase().toBooleanStrictOrNull()
|
|
53
|
+
else -> null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Converts a JSON object with scalar values to a string map (null values are dropped). */
|
|
58
|
+
fun toStringMap(json: JSONObject?): Map<String, String> {
|
|
59
|
+
if (json == null) return emptyMap()
|
|
60
|
+
val out = LinkedHashMap<String, String>()
|
|
61
|
+
val keys = json.keys()
|
|
62
|
+
while (keys.hasNext()) {
|
|
63
|
+
val key = keys.next()
|
|
64
|
+
optString(json, key)?.let { out[key] = it }
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun fromStringMap(map: Map<String, String>): JSONObject {
|
|
70
|
+
val json = JSONObject()
|
|
71
|
+
for ((k, v) in map) json.put(k, v)
|
|
72
|
+
return json
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun toStringList(array: JSONArray?): List<String> {
|
|
76
|
+
if (array == null) return emptyList()
|
|
77
|
+
return (0 until array.length()).mapNotNull { i -> if (array.isNull(i)) null else array.opt(i)?.toString() }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Converts a Float to the Double with the same shortest decimal representation (5.2f -> 5.2, not
|
|
82
|
+
* 5.199999809265137), so JSON output is clean. Non-finite values become null.
|
|
83
|
+
*/
|
|
84
|
+
fun floatToJson(value: Float): Double? = if (value.isNaN() || value.isInfinite()) null else value.toString().toDouble()
|
|
85
|
+
|
|
86
|
+
/** [floatToJson] for nullable input, returning [JSONObject.NULL] for null. */
|
|
87
|
+
fun floatOrNull(value: Float?): Any = value?.let { floatToJson(it) } ?: JSONObject.NULL
|
|
88
|
+
|
|
89
|
+
/** [value] or [JSONObject.NULL]. */
|
|
90
|
+
fun orNull(value: Any?): Any = value ?: JSONObject.NULL
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record <-> wire JSON (architecture §2). The same shape is sent to JS (without `sent_at`) and stored in SQLite.
|
|
95
|
+
*/
|
|
96
|
+
object RecordJson {
|
|
97
|
+
private const val TAG = "LT.RecordJson"
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds the wire shape. `timestamp` and `coords` are null when [Record.location] is null; `sent_at` is
|
|
101
|
+
* present only when [sentAt] is given; `extras`, `geofence`, `provider`, `reason` and `heartbeat` only when set.
|
|
102
|
+
*/
|
|
103
|
+
fun toJson(record: Record, sentAt: Long? = null): JSONObject {
|
|
104
|
+
val location = record.location
|
|
105
|
+
val json = JSONObject()
|
|
106
|
+
json.put("uuid", record.uuid)
|
|
107
|
+
json.put("event", record.event.wire)
|
|
108
|
+
json.put("timestamp", location?.let { Iso8601.format(it.time) } ?: JSONObject.NULL)
|
|
109
|
+
json.put("recorded_at", Iso8601.format(record.recordedAt))
|
|
110
|
+
if (sentAt != null) json.put("sent_at", Iso8601.format(sentAt))
|
|
111
|
+
json.put("elapsed_realtime_ms", record.elapsedRealtimeMs)
|
|
112
|
+
json.put("boot_count", record.bootCount)
|
|
113
|
+
json.put("is_moving", record.isMoving)
|
|
114
|
+
json.put("odometer", record.odometer)
|
|
115
|
+
json.put("mock", location?.isMock ?: false)
|
|
116
|
+
json.put("coords", location?.let { coordsToJson(it) } ?: JSONObject.NULL)
|
|
117
|
+
json.put(
|
|
118
|
+
"activity",
|
|
119
|
+
JSONObject().put("type", record.activity.type.wire).put("confidence", record.activity.confidence),
|
|
120
|
+
)
|
|
121
|
+
json.put(
|
|
122
|
+
"battery",
|
|
123
|
+
JSONObject()
|
|
124
|
+
.put("level", JsonUtil.floatOrNull(record.battery.level))
|
|
125
|
+
.put("is_charging", record.battery.isCharging),
|
|
126
|
+
)
|
|
127
|
+
json.put("backend", record.backend?.wire ?: JSONObject.NULL)
|
|
128
|
+
if (record.extras != null) {
|
|
129
|
+
val extras = JsonUtil.parseObject(record.extras)
|
|
130
|
+
if (extras != null) json.put("extras", extras) else Logger.w(TAG, "dropping invalid extras of ${record.uuid}")
|
|
131
|
+
}
|
|
132
|
+
record.geofence?.let { json.put("geofence", geofenceHitToJson(it)) }
|
|
133
|
+
record.provider?.let { json.put("provider", ProviderStateJson.toJson(it)) }
|
|
134
|
+
record.reason?.let { json.put("reason", it) }
|
|
135
|
+
record.heartbeat?.let { json.put("heartbeat", heartbeatMetaToJson(it)) }
|
|
136
|
+
return json
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The `heartbeat` object of a heartbeat record (snake_case keys, `next_at` ISO-8601 or null). */
|
|
140
|
+
fun heartbeatMetaToJson(meta: HeartbeatMeta): JSONObject = JSONObject()
|
|
141
|
+
.put("strategy", meta.strategy.wire)
|
|
142
|
+
.put("min_interval", meta.minInterval)
|
|
143
|
+
.put("max_interval", meta.maxInterval)
|
|
144
|
+
.put("next_at", JsonUtil.orNull(Iso8601.formatOrNull(meta.nextAt)))
|
|
145
|
+
.put("battery_exempt", meta.batteryExempt)
|
|
146
|
+
.put("device_idle", meta.deviceIdle)
|
|
147
|
+
|
|
148
|
+
/** Records as a JSON array, in the given order. */
|
|
149
|
+
fun toJsonArray(records: List<Record>, sentAt: Long? = null): JSONArray {
|
|
150
|
+
val array = JSONArray()
|
|
151
|
+
for (r in records) array.put(toJson(r, sentAt))
|
|
152
|
+
return array
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The `coords` object of a location. */
|
|
156
|
+
fun coordsToJson(l: TrackedLocation): JSONObject = JSONObject()
|
|
157
|
+
.put("latitude", l.latitude)
|
|
158
|
+
.put("longitude", l.longitude)
|
|
159
|
+
.put("accuracy", JsonUtil.floatOrNull(l.accuracy))
|
|
160
|
+
.put("altitude", JsonUtil.orNull(l.altitude))
|
|
161
|
+
.put("altitude_accuracy", JsonUtil.floatOrNull(l.altitudeAccuracy))
|
|
162
|
+
.put("speed", JsonUtil.floatOrNull(l.speed))
|
|
163
|
+
.put("speed_accuracy", JsonUtil.floatOrNull(l.speedAccuracy))
|
|
164
|
+
.put("heading", JsonUtil.floatOrNull(l.heading))
|
|
165
|
+
.put("heading_accuracy", JsonUtil.floatOrNull(l.headingAccuracy))
|
|
166
|
+
|
|
167
|
+
fun geofenceHitToJson(hit: GeofenceHit): JSONObject {
|
|
168
|
+
val json = JSONObject().put("identifier", hit.identifier).put("action", hit.action.name)
|
|
169
|
+
JsonUtil.parseObject(hit.extras)?.let { json.put("extras", it) }
|
|
170
|
+
return json
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parses the wire shape (e.g. a SQLite row). `sent_at` is ignored. The non-wire location fields
|
|
175
|
+
* (`elapsedRealtimeNanos`, `provider`) come back as 0 / null; everything else round-trips.
|
|
176
|
+
*
|
|
177
|
+
* @throws JSONException if `uuid`, `event` or the coordinates are missing or invalid.
|
|
178
|
+
*/
|
|
179
|
+
fun fromJson(json: JSONObject): Record {
|
|
180
|
+
val uuid = JsonUtil.optString(json, "uuid") ?: throw JSONException("record.uuid is missing")
|
|
181
|
+
val eventWire = JsonUtil.optString(json, "event")
|
|
182
|
+
val event = RecordEvent.fromWire(eventWire) ?: throw JSONException("unknown record.event '$eventWire'")
|
|
183
|
+
val recordedAt = Iso8601.parse(JsonUtil.optString(json, "recorded_at")) ?: 0L
|
|
184
|
+
val coords = json.optJSONObject("coords")
|
|
185
|
+
val location = coords?.let {
|
|
186
|
+
TrackedLocation(
|
|
187
|
+
latitude = JsonUtil.optDouble(it, "latitude") ?: throw JSONException("coords.latitude is missing"),
|
|
188
|
+
longitude = JsonUtil.optDouble(it, "longitude") ?: throw JSONException("coords.longitude is missing"),
|
|
189
|
+
accuracy = JsonUtil.optFloat(it, "accuracy") ?: 0f,
|
|
190
|
+
altitude = JsonUtil.optDouble(it, "altitude"),
|
|
191
|
+
altitudeAccuracy = JsonUtil.optFloat(it, "altitude_accuracy"),
|
|
192
|
+
speed = JsonUtil.optFloat(it, "speed"),
|
|
193
|
+
speedAccuracy = JsonUtil.optFloat(it, "speed_accuracy"),
|
|
194
|
+
heading = JsonUtil.optFloat(it, "heading"),
|
|
195
|
+
headingAccuracy = JsonUtil.optFloat(it, "heading_accuracy"),
|
|
196
|
+
time = Iso8601.parse(JsonUtil.optString(json, "timestamp")) ?: recordedAt,
|
|
197
|
+
isMock = JsonUtil.optBoolean(json, "mock") ?: false,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
val activity = json.optJSONObject("activity")?.let {
|
|
201
|
+
ActivitySample(
|
|
202
|
+
type = ActivityType.fromWire(JsonUtil.optString(it, "type")) ?: ActivityType.UNKNOWN,
|
|
203
|
+
confidence = JsonUtil.optInt(it, "confidence") ?: 0,
|
|
204
|
+
)
|
|
205
|
+
} ?: ActivitySample.UNKNOWN
|
|
206
|
+
val battery = json.optJSONObject("battery")?.let {
|
|
207
|
+
BatterySnapshot(
|
|
208
|
+
level = JsonUtil.optFloat(it, "level") ?: -1f,
|
|
209
|
+
isCharging = JsonUtil.optBoolean(it, "is_charging") ?: false,
|
|
210
|
+
)
|
|
211
|
+
} ?: BatterySnapshot.UNKNOWN
|
|
212
|
+
return Record(
|
|
213
|
+
uuid = uuid,
|
|
214
|
+
event = event,
|
|
215
|
+
location = location,
|
|
216
|
+
recordedAt = recordedAt,
|
|
217
|
+
elapsedRealtimeMs = JsonUtil.optLong(json, "elapsed_realtime_ms") ?: 0L,
|
|
218
|
+
bootCount = JsonUtil.optInt(json, "boot_count") ?: -1,
|
|
219
|
+
isMoving = JsonUtil.optBoolean(json, "is_moving") ?: false,
|
|
220
|
+
odometer = JsonUtil.optDouble(json, "odometer") ?: 0.0,
|
|
221
|
+
activity = activity,
|
|
222
|
+
battery = battery,
|
|
223
|
+
backend = ProviderKind.fromWire(JsonUtil.optString(json, "backend")),
|
|
224
|
+
extras = json.optJSONObject("extras")?.toString(),
|
|
225
|
+
geofence = json.optJSONObject("geofence")?.let { geofenceHitFromJson(it) },
|
|
226
|
+
provider = json.optJSONObject("provider")?.let { ProviderStateJson.fromJson(it) },
|
|
227
|
+
reason = JsonUtil.optString(json, "reason"),
|
|
228
|
+
heartbeat = json.optJSONObject("heartbeat")?.let { heartbeatMetaFromJson(it) },
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Lenient: an unknown strategy is dropped (null), missing numbers become 0 and missing flags false. */
|
|
233
|
+
private fun heartbeatMetaFromJson(json: JSONObject): HeartbeatMeta? {
|
|
234
|
+
val strategy = HeartbeatStrategy.fromWire(JsonUtil.optString(json, "strategy")) ?: return null
|
|
235
|
+
return HeartbeatMeta(
|
|
236
|
+
strategy = strategy,
|
|
237
|
+
minInterval = JsonUtil.optInt(json, "min_interval") ?: 0,
|
|
238
|
+
maxInterval = JsonUtil.optInt(json, "max_interval") ?: 0,
|
|
239
|
+
nextAt = Iso8601.parse(JsonUtil.optString(json, "next_at")),
|
|
240
|
+
batteryExempt = JsonUtil.optBoolean(json, "battery_exempt") ?: false,
|
|
241
|
+
deviceIdle = JsonUtil.optBoolean(json, "device_idle") ?: false,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private fun geofenceHitFromJson(json: JSONObject): GeofenceHit = GeofenceHit(
|
|
246
|
+
identifier = JsonUtil.optString(json, "identifier") ?: "",
|
|
247
|
+
action = GeofenceAction.fromWire(JsonUtil.optString(json, "action")) ?: GeofenceAction.ENTER,
|
|
248
|
+
extras = json.optJSONObject("extras")?.toString(),
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Lossless (non-wire) JSON for a [TrackedLocation], for persisting e.g. `RuntimeState.lastLocation`.
|
|
254
|
+
* Keys use the Kotlin property names; `time` is epoch ms.
|
|
255
|
+
*/
|
|
256
|
+
object TrackedLocationJson {
|
|
257
|
+
fun toJson(l: TrackedLocation): JSONObject = JSONObject()
|
|
258
|
+
.put("latitude", l.latitude)
|
|
259
|
+
.put("longitude", l.longitude)
|
|
260
|
+
.put("accuracy", JsonUtil.floatOrNull(l.accuracy))
|
|
261
|
+
.put("altitude", JsonUtil.orNull(l.altitude))
|
|
262
|
+
.put("altitudeAccuracy", JsonUtil.floatOrNull(l.altitudeAccuracy))
|
|
263
|
+
.put("speed", JsonUtil.floatOrNull(l.speed))
|
|
264
|
+
.put("speedAccuracy", JsonUtil.floatOrNull(l.speedAccuracy))
|
|
265
|
+
.put("heading", JsonUtil.floatOrNull(l.heading))
|
|
266
|
+
.put("headingAccuracy", JsonUtil.floatOrNull(l.headingAccuracy))
|
|
267
|
+
.put("time", l.time)
|
|
268
|
+
.put("elapsedRealtimeNanos", l.elapsedRealtimeNanos)
|
|
269
|
+
.put("provider", JsonUtil.orNull(l.provider))
|
|
270
|
+
.put("isMock", l.isMock)
|
|
271
|
+
|
|
272
|
+
/** @throws JSONException if latitude/longitude are missing. */
|
|
273
|
+
fun fromJson(json: JSONObject): TrackedLocation = TrackedLocation(
|
|
274
|
+
latitude = JsonUtil.optDouble(json, "latitude") ?: throw JSONException("latitude is missing"),
|
|
275
|
+
longitude = JsonUtil.optDouble(json, "longitude") ?: throw JSONException("longitude is missing"),
|
|
276
|
+
accuracy = JsonUtil.optFloat(json, "accuracy") ?: 0f,
|
|
277
|
+
altitude = JsonUtil.optDouble(json, "altitude"),
|
|
278
|
+
altitudeAccuracy = JsonUtil.optFloat(json, "altitudeAccuracy"),
|
|
279
|
+
speed = JsonUtil.optFloat(json, "speed"),
|
|
280
|
+
speedAccuracy = JsonUtil.optFloat(json, "speedAccuracy"),
|
|
281
|
+
heading = JsonUtil.optFloat(json, "heading"),
|
|
282
|
+
headingAccuracy = JsonUtil.optFloat(json, "headingAccuracy"),
|
|
283
|
+
time = JsonUtil.optLong(json, "time") ?: 0L,
|
|
284
|
+
elapsedRealtimeNanos = JsonUtil.optLong(json, "elapsedRealtimeNanos") ?: 0L,
|
|
285
|
+
provider = JsonUtil.optString(json, "provider"),
|
|
286
|
+
isMock = JsonUtil.optBoolean(json, "isMock") ?: false,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** GeofenceSpec <-> JS `Geofence` (also used for SQLite rows). */
|
|
291
|
+
object GeofenceJson {
|
|
292
|
+
/**
|
|
293
|
+
* JS shape. latitude/longitude/radius are always present (for polygons: the enclosing circle);
|
|
294
|
+
* `vertices` (`[[lat,lng],...]`) and `extras` only when set.
|
|
295
|
+
*/
|
|
296
|
+
fun toJson(g: GeofenceSpec): JSONObject {
|
|
297
|
+
val json = JSONObject()
|
|
298
|
+
json.put("identifier", g.identifier)
|
|
299
|
+
json.put("latitude", g.latitude)
|
|
300
|
+
json.put("longitude", g.longitude)
|
|
301
|
+
json.put("radius", JsonUtil.floatOrNull(g.radius))
|
|
302
|
+
g.vertices?.let { vertices ->
|
|
303
|
+
val array = JSONArray()
|
|
304
|
+
for (v in vertices) array.put(JSONArray().put(v.latitude).put(v.longitude))
|
|
305
|
+
json.put("vertices", array)
|
|
306
|
+
}
|
|
307
|
+
json.put("notifyOnEntry", g.notifyOnEntry)
|
|
308
|
+
json.put("notifyOnExit", g.notifyOnExit)
|
|
309
|
+
json.put("notifyOnDwell", g.notifyOnDwell)
|
|
310
|
+
json.put("loiteringDelay", g.loiteringDelay)
|
|
311
|
+
JsonUtil.parseObject(g.extras)?.let { json.put("extras", it) }
|
|
312
|
+
return json
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fun toJsonArray(list: List<GeofenceSpec>): JSONArray {
|
|
316
|
+
val array = JSONArray()
|
|
317
|
+
for (g in list) array.put(toJson(g))
|
|
318
|
+
return array
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Structural parsing only; semantic validation (vertex count, radius > 0, max 100) belongs to the
|
|
323
|
+
* GeofenceManager. A polygon's missing latitude/longitude/radius default to 0.
|
|
324
|
+
*
|
|
325
|
+
* @throws TrackingException INVALID_ARGUMENT if the identifier is missing, a circle lacks
|
|
326
|
+
* latitude/longitude/radius, or `vertices` is malformed.
|
|
327
|
+
*/
|
|
328
|
+
fun fromJson(json: JSONObject): GeofenceSpec {
|
|
329
|
+
val identifier = JsonUtil.optString(json, "identifier")
|
|
330
|
+
if (identifier.isNullOrBlank()) invalid("geofence.identifier is required")
|
|
331
|
+
val vertices = if (json.isNull("vertices")) null else parseVertices(identifier, json.opt("vertices"))
|
|
332
|
+
val latitude = JsonUtil.optDouble(json, "latitude")
|
|
333
|
+
val longitude = JsonUtil.optDouble(json, "longitude")
|
|
334
|
+
val radius = JsonUtil.optDouble(json, "radius")
|
|
335
|
+
if (vertices == null && (latitude == null || longitude == null || radius == null)) {
|
|
336
|
+
invalid("geofence '$identifier' needs latitude, longitude and radius, or vertices")
|
|
337
|
+
}
|
|
338
|
+
return GeofenceSpec(
|
|
339
|
+
identifier = identifier,
|
|
340
|
+
latitude = latitude ?: 0.0,
|
|
341
|
+
longitude = longitude ?: 0.0,
|
|
342
|
+
radius = radius?.toFloat() ?: 0f,
|
|
343
|
+
vertices = vertices,
|
|
344
|
+
notifyOnEntry = JsonUtil.optBoolean(json, "notifyOnEntry") ?: true,
|
|
345
|
+
notifyOnExit = JsonUtil.optBoolean(json, "notifyOnExit") ?: true,
|
|
346
|
+
notifyOnDwell = JsonUtil.optBoolean(json, "notifyOnDwell") ?: false,
|
|
347
|
+
loiteringDelay = JsonUtil.optLong(json, "loiteringDelay") ?: 30_000L,
|
|
348
|
+
extras = json.optJSONObject("extras")?.toString(),
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** @throws TrackingException INVALID_ARGUMENT if any element is not a geofence object. */
|
|
353
|
+
fun fromJsonArray(array: JSONArray): List<GeofenceSpec> = (0 until array.length()).map { i ->
|
|
354
|
+
val item = array.optJSONObject(i) ?: invalid("geofences[$i] is not an object")
|
|
355
|
+
fromJson(item)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private fun parseVertices(identifier: String, value: Any?): List<LatLng> {
|
|
359
|
+
val array = value as? JSONArray ?: invalid("geofence '$identifier': vertices must be [[lat,lng],...]")
|
|
360
|
+
return (0 until array.length()).map { i ->
|
|
361
|
+
val pair = array.optJSONArray(i)
|
|
362
|
+
val lat = pair?.opt(0) as? Number
|
|
363
|
+
val lng = pair?.opt(1) as? Number
|
|
364
|
+
if (pair == null || pair.length() != 2 || lat == null || lng == null) {
|
|
365
|
+
invalid("geofence '$identifier': vertices[$i] must be [lat,lng]")
|
|
366
|
+
}
|
|
367
|
+
LatLng(lat.toDouble(), lng.toDouble())
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private fun invalid(message: String): Nothing = throw TrackingException(ErrorCode.INVALID_ARGUMENT, message)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** JS `ProviderState`. */
|
|
375
|
+
object ProviderStateJson {
|
|
376
|
+
fun toJson(s: ProviderState): JSONObject = JSONObject()
|
|
377
|
+
.put("enabled", s.enabled)
|
|
378
|
+
.put("gps", s.gps)
|
|
379
|
+
.put("network", s.network)
|
|
380
|
+
.put("permission", s.permission.wire)
|
|
381
|
+
.put("accuracy", s.accuracy.wire)
|
|
382
|
+
.put("backend", s.backend.wire)
|
|
383
|
+
|
|
384
|
+
/** Lenient: unknown enum values fall back to DENIED / NONE / ANDROID. */
|
|
385
|
+
fun fromJson(json: JSONObject): ProviderState = ProviderState(
|
|
386
|
+
enabled = JsonUtil.optBoolean(json, "enabled") ?: false,
|
|
387
|
+
gps = JsonUtil.optBoolean(json, "gps") ?: false,
|
|
388
|
+
network = JsonUtil.optBoolean(json, "network") ?: false,
|
|
389
|
+
permission = PermissionLevel.fromWire(JsonUtil.optString(json, "permission")) ?: PermissionLevel.DENIED,
|
|
390
|
+
accuracy = AccuracyLevel.fromWire(JsonUtil.optString(json, "accuracy")) ?: AccuracyLevel.NONE,
|
|
391
|
+
backend = ProviderKind.fromWire(JsonUtil.optString(json, "backend")) ?: ProviderKind.ANDROID,
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** JS `HeartbeatStatus` (times as ISO strings or null). */
|
|
396
|
+
object HeartbeatStatusJson {
|
|
397
|
+
fun toJson(s: HeartbeatStatus): JSONObject = JSONObject()
|
|
398
|
+
.put("enabled", s.enabled)
|
|
399
|
+
.put("minInterval", s.minInterval)
|
|
400
|
+
.put("maxInterval", s.maxInterval)
|
|
401
|
+
.put("lastRecordAt", JsonUtil.orNull(Iso8601.formatOrNull(s.lastRecordAt)))
|
|
402
|
+
.put("lastHeartbeatAt", JsonUtil.orNull(Iso8601.formatOrNull(s.lastHeartbeatAt)))
|
|
403
|
+
.put("nextHeartbeatAt", JsonUtil.orNull(Iso8601.formatOrNull(s.nextHeartbeatAt)))
|
|
404
|
+
.put("strategy", s.strategy.wire)
|
|
405
|
+
.put("canScheduleExactAlarms", s.canScheduleExactAlarms)
|
|
406
|
+
.put("isIgnoringBatteryOptimizations", s.isIgnoringBatteryOptimizations)
|
|
407
|
+
.put("isDeviceIdleMode", s.isDeviceIdleMode)
|
|
408
|
+
.put("isPowerSaveMode", s.isPowerSaveMode)
|
|
409
|
+
.put("pendingHeartbeats", s.pendingHeartbeats)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** JS `DeviceInfo`. */
|
|
413
|
+
object DeviceInfoJson {
|
|
414
|
+
fun toJson(d: DeviceInfo): JSONObject {
|
|
415
|
+
val providers = JSONArray()
|
|
416
|
+
for (p in d.packagedProviders) providers.put(p)
|
|
417
|
+
return JSONObject()
|
|
418
|
+
.put("platform", d.platform)
|
|
419
|
+
.put("manufacturer", d.manufacturer)
|
|
420
|
+
.put("model", d.model)
|
|
421
|
+
.put("brand", d.brand)
|
|
422
|
+
.put("osVersion", d.osVersion)
|
|
423
|
+
.put("sdkInt", d.sdkInt)
|
|
424
|
+
.put("pluginVersion", d.pluginVersion)
|
|
425
|
+
.put("gmsAvailable", d.gmsAvailable)
|
|
426
|
+
.put("hmsAvailable", d.hmsAvailable)
|
|
427
|
+
.put("backend", d.backend.wire)
|
|
428
|
+
.put("packagedProviders", providers)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** JS `Sensors`. */
|
|
433
|
+
object SensorsJson {
|
|
434
|
+
fun toJson(s: Sensors): JSONObject = JSONObject()
|
|
435
|
+
.put("accelerometer", s.accelerometer)
|
|
436
|
+
.put("gyroscope", s.gyroscope)
|
|
437
|
+
.put("magnetometer", s.magnetometer)
|
|
438
|
+
.put("significantMotion", s.significantMotion)
|
|
439
|
+
.put("stepCounter", s.stepCounter)
|
|
440
|
+
.put("stepDetector", s.stepDetector)
|
|
441
|
+
.put("barometer", s.barometer)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** JS `PowerManagerInfo`. */
|
|
445
|
+
object PowerManagerInfoJson {
|
|
446
|
+
fun toJson(p: PowerManagerInfo): JSONObject =
|
|
447
|
+
JSONObject().put("manufacturer", p.manufacturer).put("available", p.available)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** JS `BatteryOptimizationStatus`. */
|
|
451
|
+
object BatteryOptimizationStatusJson {
|
|
452
|
+
fun toJson(b: BatteryOptimizationStatus): JSONObject = JSONObject()
|
|
453
|
+
.put("isIgnoringBatteryOptimizations", b.isIgnoringBatteryOptimizations)
|
|
454
|
+
.put("canScheduleExactAlarms", b.canScheduleExactAlarms)
|
|
455
|
+
.put("isDeviceIdleMode", b.isDeviceIdleMode)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** JS `PermissionStatus`: every [PermissionType] alias is present (missing entries are "prompt"). */
|
|
459
|
+
object PermissionStatusJson {
|
|
460
|
+
fun toJson(status: Map<PermissionType, PermissionState>): JSONObject {
|
|
461
|
+
val json = JSONObject()
|
|
462
|
+
for (type in PermissionType.entries) json.put(type.alias, (status[type] ?: PermissionState.PROMPT).js)
|
|
463
|
+
return json
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** JS `HttpEvent` shape. */
|
|
468
|
+
object HttpResultJson {
|
|
469
|
+
fun toJson(r: HttpResult): JSONObject {
|
|
470
|
+
val uuids = JSONArray()
|
|
471
|
+
for (u in r.uuids) uuids.put(u)
|
|
472
|
+
return JSONObject()
|
|
473
|
+
.put("success", r.success)
|
|
474
|
+
.put("status", r.status)
|
|
475
|
+
.put("responseText", r.responseText)
|
|
476
|
+
.put("uuids", uuids)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** JS `ConnectivityChangeEvent` shape. */
|
|
481
|
+
object ConnectivityJson {
|
|
482
|
+
fun toJson(c: Connectivity): JSONObject = JSONObject().put("connected", c.connected).put("type", c.type.wire)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** JS event name and payload of every [TrackingEvent] (see `LocationTrackingEventMap`). */
|
|
486
|
+
object EventJson {
|
|
487
|
+
const val LOCATION = "location"
|
|
488
|
+
const val MOTIONCHANGE = "motionchange"
|
|
489
|
+
const val ACTIVITYCHANGE = "activitychange"
|
|
490
|
+
const val PROVIDERCHANGE = "providerchange"
|
|
491
|
+
const val HEARTBEAT = "heartbeat"
|
|
492
|
+
const val GEOFENCE = "geofence"
|
|
493
|
+
const val GEOFENCESCHANGE = "geofenceschange"
|
|
494
|
+
const val HTTP = "http"
|
|
495
|
+
const val CONNECTIVITYCHANGE = "connectivitychange"
|
|
496
|
+
const val POWERSAVECHANGE = "powersavechange"
|
|
497
|
+
const val ENABLEDCHANGE = "enabledchange"
|
|
498
|
+
const val NOTIFICATIONACTION = "notificationaction"
|
|
499
|
+
const val AUTHORIZATION = "authorization"
|
|
500
|
+
|
|
501
|
+
fun name(event: TrackingEvent): String = when (event) {
|
|
502
|
+
is TrackingEvent.Location -> LOCATION
|
|
503
|
+
is TrackingEvent.MotionChange -> MOTIONCHANGE
|
|
504
|
+
is TrackingEvent.ActivityChange -> ACTIVITYCHANGE
|
|
505
|
+
is TrackingEvent.ProviderChange -> PROVIDERCHANGE
|
|
506
|
+
is TrackingEvent.Heartbeat -> HEARTBEAT
|
|
507
|
+
is TrackingEvent.Geofence -> GEOFENCE
|
|
508
|
+
is TrackingEvent.GeofencesChange -> GEOFENCESCHANGE
|
|
509
|
+
is TrackingEvent.Http -> HTTP
|
|
510
|
+
is TrackingEvent.ConnectivityChange -> CONNECTIVITYCHANGE
|
|
511
|
+
is TrackingEvent.PowerSaveChange -> POWERSAVECHANGE
|
|
512
|
+
is TrackingEvent.EnabledChange -> ENABLEDCHANGE
|
|
513
|
+
is TrackingEvent.NotificationAction -> NOTIFICATIONACTION
|
|
514
|
+
is TrackingEvent.Authorization -> AUTHORIZATION
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
fun payload(event: TrackingEvent): JSONObject = when (event) {
|
|
518
|
+
is TrackingEvent.Location -> RecordJson.toJson(event.record)
|
|
519
|
+
is TrackingEvent.MotionChange ->
|
|
520
|
+
JSONObject().put("isMoving", event.isMoving).put("location", RecordJson.toJson(event.record))
|
|
521
|
+
is TrackingEvent.ActivityChange ->
|
|
522
|
+
JSONObject().put("activity", event.activity.type.wire).put("confidence", event.activity.confidence)
|
|
523
|
+
is TrackingEvent.ProviderChange -> ProviderStateJson.toJson(event.state)
|
|
524
|
+
is TrackingEvent.Heartbeat -> JSONObject().put("location", RecordJson.toJson(event.record))
|
|
525
|
+
is TrackingEvent.Geofence -> {
|
|
526
|
+
val json = JSONObject()
|
|
527
|
+
.put("identifier", event.identifier)
|
|
528
|
+
.put("action", event.action.name)
|
|
529
|
+
.put("location", RecordJson.toJson(event.record))
|
|
530
|
+
JsonUtil.parseObject(event.extras)?.let { json.put("extras", it) }
|
|
531
|
+
json
|
|
532
|
+
}
|
|
533
|
+
is TrackingEvent.GeofencesChange -> {
|
|
534
|
+
val off = JSONArray()
|
|
535
|
+
for (id in event.off) off.put(id)
|
|
536
|
+
JSONObject().put("on", GeofenceJson.toJsonArray(event.on)).put("off", off)
|
|
537
|
+
}
|
|
538
|
+
is TrackingEvent.Http -> HttpResultJson.toJson(event.result)
|
|
539
|
+
is TrackingEvent.ConnectivityChange -> ConnectivityJson.toJson(event.connectivity)
|
|
540
|
+
is TrackingEvent.PowerSaveChange -> JSONObject().put("isPowerSaveMode", event.isPowerSaveMode)
|
|
541
|
+
is TrackingEvent.EnabledChange -> JSONObject().put("enabled", event.enabled)
|
|
542
|
+
is TrackingEvent.NotificationAction -> JSONObject().put("id", event.id)
|
|
543
|
+
is TrackingEvent.Authorization -> {
|
|
544
|
+
val json = JSONObject().put("success", event.success).put("status", event.status)
|
|
545
|
+
event.error?.let { json.put("error", it) }
|
|
546
|
+
JsonUtil.parseObject(event.responseJson)?.let { json.put("response", it) }
|
|
547
|
+
json
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.model
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Record type. [isPriority] records are uploaded immediately whenever `http.url` is set.
|
|
5
|
+
*/
|
|
6
|
+
enum class RecordEvent(val wire: String, val isPriority: Boolean) {
|
|
7
|
+
LOCATION("location", false),
|
|
8
|
+
MOTIONCHANGE("motionchange", false),
|
|
9
|
+
CURRENT_POSITION("current_position", false),
|
|
10
|
+
WATCH_POSITION("watch_position", false),
|
|
11
|
+
HEARTBEAT("heartbeat", true),
|
|
12
|
+
GEOFENCE("geofence", false),
|
|
13
|
+
TRACKING_START("tracking_start", true),
|
|
14
|
+
TRACKING_STOP("tracking_stop", true),
|
|
15
|
+
PROVIDERCHANGE("providerchange", true),
|
|
16
|
+
;
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
fun fromWire(value: String?): RecordEvent? = entries.firstOrNull { it.wire == value }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
enum class ActivityType(val wire: String) {
|
|
24
|
+
STILL("still"),
|
|
25
|
+
ON_FOOT("on_foot"),
|
|
26
|
+
WALKING("walking"),
|
|
27
|
+
RUNNING("running"),
|
|
28
|
+
ON_BICYCLE("on_bicycle"),
|
|
29
|
+
IN_VEHICLE("in_vehicle"),
|
|
30
|
+
UNKNOWN("unknown"),
|
|
31
|
+
;
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
fun fromWire(value: String?): ActivityType? = entries.firstOrNull { it.wire == value }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A detected activity with confidence 0..100. */
|
|
39
|
+
data class ActivitySample(val type: ActivityType, val confidence: Int) {
|
|
40
|
+
companion object {
|
|
41
|
+
val UNKNOWN = ActivitySample(ActivityType.UNKNOWN, 0)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Battery state; [level] is 0..1, or -1 if unknown. */
|
|
46
|
+
data class BatterySnapshot(val level: Float, val isCharging: Boolean) {
|
|
47
|
+
companion object {
|
|
48
|
+
val UNKNOWN = BatterySnapshot(-1f, false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A native location backend; [wire] is the JS `LocationBackend` value. */
|
|
53
|
+
enum class ProviderKind(val wire: String) {
|
|
54
|
+
GMS("gms"),
|
|
55
|
+
HMS("hms"),
|
|
56
|
+
ANDROID("android"),
|
|
57
|
+
;
|
|
58
|
+
|
|
59
|
+
companion object {
|
|
60
|
+
fun fromWire(value: String?): ProviderKind? = entries.firstOrNull { it.wire == value }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
enum class GeofenceAction {
|
|
65
|
+
ENTER,
|
|
66
|
+
EXIT,
|
|
67
|
+
DWELL,
|
|
68
|
+
;
|
|
69
|
+
|
|
70
|
+
companion object {
|
|
71
|
+
fun fromWire(value: String?): GeofenceAction? = entries.firstOrNull { it.name == value }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The `geofence` key of a geofence record; [extras] is JSON object text. */
|
|
76
|
+
data class GeofenceHit(val identifier: String, val action: GeofenceAction, val extras: String?)
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A persisted record (location or audit). Serialized by [RecordJson] to the wire format.
|
|
80
|
+
*
|
|
81
|
+
* @property location the fix, or null if no location was ever known (audit records only).
|
|
82
|
+
* @property recordedAt record creation time, epoch ms.
|
|
83
|
+
* @property extras JSON object text, or null.
|
|
84
|
+
* @property geofence event `geofence` only.
|
|
85
|
+
* @property provider event `providerchange` only.
|
|
86
|
+
* @property reason `tracking_start` / `tracking_stop` only.
|
|
87
|
+
* @property heartbeat `heartbeat` only, and optional (round 2): the scheduling metadata of the heartbeat.
|
|
88
|
+
*/
|
|
89
|
+
data class Record(
|
|
90
|
+
val uuid: String,
|
|
91
|
+
val event: RecordEvent,
|
|
92
|
+
val location: TrackedLocation?,
|
|
93
|
+
val recordedAt: Long,
|
|
94
|
+
val elapsedRealtimeMs: Long,
|
|
95
|
+
val bootCount: Int,
|
|
96
|
+
val isMoving: Boolean,
|
|
97
|
+
val odometer: Double,
|
|
98
|
+
val activity: ActivitySample,
|
|
99
|
+
val battery: BatterySnapshot,
|
|
100
|
+
val backend: ProviderKind?,
|
|
101
|
+
val extras: String? = null,
|
|
102
|
+
val geofence: GeofenceHit? = null,
|
|
103
|
+
val provider: ProviderState? = null,
|
|
104
|
+
val reason: String? = null,
|
|
105
|
+
val heartbeat: HeartbeatMeta? = null,
|
|
106
|
+
)
|