@bricks-soft/capacitor-location-tracking 8.0.1 → 8.2.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/BricksSoftCapacitorLocationTracking.podspec +18 -0
- package/Package.swift +13 -0
- package/README.md +476 -147
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +4 -1
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +14 -5
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +10 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +70 -15
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +6 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +8 -3
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +10 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SecretStore.kt +188 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +188 -17
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +11 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +21 -5
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +20 -9
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +24 -2
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +43 -1
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +17 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceDetails.kt +167 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +191 -34
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +6 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +3 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +52 -8
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +78 -27
- package/android/src/main/java/com/brickssoft/locationtracking/http/ConfigChecks.kt +140 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +50 -12
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +17 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +507 -92
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +6 -6
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +23 -1
- package/android/src/main/java/com/brickssoft/locationtracking/http/UploadJobs.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/UploadWait.kt +184 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +66 -7
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogRedaction.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +44 -1
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +79 -8
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +12 -2
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +10 -2
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +24 -1
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +31 -10
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +48 -3
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/LocationSubscriptions.kt +180 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +200 -126
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +20 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +39 -51
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +8 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +47 -53
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +34 -2
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +19 -1
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +7 -0
- package/android/src/main/res/xml/lt_file_paths.xml +1 -1
- package/dist/esm/definitions.d.ts +117 -8
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/events.d.ts +8 -2
- package/dist/esm/events.js +7 -1
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web/config.js +10 -0
- package/dist/esm/web/config.js.map +1 -1
- package/dist/esm/web/device.d.ts +1 -1
- package/dist/esm/web/device.js +8 -1
- package/dist/esm/web/device.js.map +1 -1
- package/dist/esm/web.d.ts +0 -8
- package/dist/esm/web.js +12 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +39 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +39 -5
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
- package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
- package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
- package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
- package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
- package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
- package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
- package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
- package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
- package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
- package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
- package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
- package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
- package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
- package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
- package/package.json +10 -4
|
@@ -139,7 +139,17 @@ data class HeartbeatStatus(
|
|
|
139
139
|
|
|
140
140
|
// ---- device
|
|
141
141
|
|
|
142
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* JS `DeviceInfo`.
|
|
144
|
+
*
|
|
145
|
+
* @property backendFallbackReason why a better location backend is not used (recommendation 15), or null.
|
|
146
|
+
* @property gmsStatus `GoogleApiAvailability` result code (0 = SUCCESS), null if GMS is not packaged or not checked.
|
|
147
|
+
* @property hmsStatus `HuaweiApiAvailability` result code (0 = SUCCESS), null if HMS is not packaged or not checked.
|
|
148
|
+
* @property hmsCoreOutdated true when HMS Core is older than 6.3.0.301; null if HMS is not packaged.
|
|
149
|
+
* @property locationAvailableHint the last `LocationAvailability` of GMS or HMS; a hint, null if none was reported.
|
|
150
|
+
* @property cpuCores `Runtime.availableProcessors()`.
|
|
151
|
+
* @property supportedAbis `Build.SUPPORTED_ABIS`.
|
|
152
|
+
*/
|
|
143
153
|
data class DeviceInfo(
|
|
144
154
|
val platform: String = "android",
|
|
145
155
|
val manufacturer: String,
|
|
@@ -152,8 +162,41 @@ data class DeviceInfo(
|
|
|
152
162
|
val hmsAvailable: Boolean,
|
|
153
163
|
val backend: ProviderKind,
|
|
154
164
|
val packagedProviders: List<String>,
|
|
165
|
+
val backendFallbackReason: String? = null,
|
|
166
|
+
val gmsStatus: Int? = null,
|
|
167
|
+
val hmsStatus: Int? = null,
|
|
168
|
+
val hmsCoreOutdated: Boolean? = null,
|
|
169
|
+
val locationAvailableHint: Boolean? = null,
|
|
170
|
+
val cpuCores: Int = 0,
|
|
171
|
+
val supportedAbis: List<String> = emptyList(),
|
|
155
172
|
)
|
|
156
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The `device` object of records (see `DeviceDetails`); a null field is not written. Every record: [deviceId].
|
|
176
|
+
* Heartbeat: [backgroundRestricted], [standbyBucket], [diskFreeBytes], [thermalStatus]. `tracking_start`:
|
|
177
|
+
* [manufacturer], [model], [osVersion], [sdkInt], [lowRamDevice], [ramTotalBytes].
|
|
178
|
+
*
|
|
179
|
+
* @property deviceId the device id: `Settings.Secure.ANDROID_ID` on Android; null in records queued by an older version.
|
|
180
|
+
*/
|
|
181
|
+
data class DeviceSnapshot(
|
|
182
|
+
val backgroundRestricted: Boolean? = null,
|
|
183
|
+
val standbyBucket: String? = null,
|
|
184
|
+
val diskFreeBytes: Long? = null,
|
|
185
|
+
val thermalStatus: String? = null,
|
|
186
|
+
val manufacturer: String? = null,
|
|
187
|
+
val model: String? = null,
|
|
188
|
+
val osVersion: String? = null,
|
|
189
|
+
val sdkInt: Int? = null,
|
|
190
|
+
val deviceId: String? = null,
|
|
191
|
+
val lowRamDevice: Boolean? = null,
|
|
192
|
+
val ramTotalBytes: Long? = null,
|
|
193
|
+
) {
|
|
194
|
+
val isEmpty: Boolean get() = this == DeviceSnapshot()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Battery readings added to the `battery` object of heartbeat records: °C, health and power source. */
|
|
198
|
+
data class BatteryDetails(val temperature: Float?, val health: String?, val plugged: String?)
|
|
199
|
+
|
|
157
200
|
/** JS `Sensors`. */
|
|
158
201
|
data class Sensors(
|
|
159
202
|
val accelerometer: Boolean,
|
|
@@ -118,13 +118,18 @@ object RecordJson {
|
|
|
118
118
|
"activity",
|
|
119
119
|
JSONObject().put("type", record.activity.type.wire).put("confidence", record.activity.confidence),
|
|
120
120
|
)
|
|
121
|
-
|
|
122
|
-
"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
val battery = JSONObject()
|
|
122
|
+
.put("level", JsonUtil.floatOrNull(record.battery.level))
|
|
123
|
+
.put("is_charging", record.battery.isCharging)
|
|
124
|
+
record.batteryDetails?.let { details ->
|
|
125
|
+
details.temperature?.let { battery.put("temperature", JsonUtil.floatOrNull(it)) }
|
|
126
|
+
details.health?.let { battery.put("health", it) }
|
|
127
|
+
details.plugged?.let { battery.put("plugged", it) }
|
|
128
|
+
}
|
|
129
|
+
json.put("battery", battery)
|
|
127
130
|
json.put("backend", record.backend?.wire ?: JSONObject.NULL)
|
|
131
|
+
record.network?.let { json.put("network", it.wire) }
|
|
132
|
+
record.device?.takeUnless { it.isEmpty }?.let { json.put("device", deviceToJson(it)) }
|
|
128
133
|
if (record.extras != null) {
|
|
129
134
|
val extras = JsonUtil.parseObject(record.extras)
|
|
130
135
|
if (extras != null) json.put("extras", extras) else Logger.w(TAG, "dropping invalid extras of ${record.uuid}")
|
|
@@ -133,9 +138,42 @@ object RecordJson {
|
|
|
133
138
|
record.provider?.let { json.put("provider", ProviderStateJson.toJson(it)) }
|
|
134
139
|
record.reason?.let { json.put("reason", it) }
|
|
135
140
|
record.heartbeat?.let { json.put("heartbeat", heartbeatMetaToJson(it)) }
|
|
141
|
+
record.stoppedAt?.let { json.put("stopped_at", Iso8601.format(it)) }
|
|
142
|
+
return json
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The `device` object of a record: only the readings that are set. */
|
|
146
|
+
fun deviceToJson(d: DeviceSnapshot): JSONObject {
|
|
147
|
+
val json = JSONObject()
|
|
148
|
+
d.deviceId?.let { json.put("device_id", it) }
|
|
149
|
+
d.backgroundRestricted?.let { json.put("background_restricted", it) }
|
|
150
|
+
d.standbyBucket?.let { json.put("standby_bucket", it) }
|
|
151
|
+
d.diskFreeBytes?.let { json.put("disk_free_bytes", it) }
|
|
152
|
+
d.thermalStatus?.let { json.put("thermal_status", it) }
|
|
153
|
+
d.manufacturer?.let { json.put("manufacturer", it) }
|
|
154
|
+
d.model?.let { json.put("model", it) }
|
|
155
|
+
d.osVersion?.let { json.put("os_version", it) }
|
|
156
|
+
d.sdkInt?.let { json.put("sdk_int", it) }
|
|
157
|
+
d.lowRamDevice?.let { json.put("low_ram_device", it) }
|
|
158
|
+
d.ramTotalBytes?.let { json.put("ram_total_bytes", it) }
|
|
136
159
|
return json
|
|
137
160
|
}
|
|
138
161
|
|
|
162
|
+
private fun deviceFromJson(json: JSONObject): DeviceSnapshot = DeviceSnapshot(
|
|
163
|
+
backgroundRestricted = JsonUtil.optBoolean(json, "background_restricted"),
|
|
164
|
+
standbyBucket = JsonUtil.optString(json, "standby_bucket"),
|
|
165
|
+
diskFreeBytes = JsonUtil.optLong(json, "disk_free_bytes"),
|
|
166
|
+
thermalStatus = JsonUtil.optString(json, "thermal_status"),
|
|
167
|
+
manufacturer = JsonUtil.optString(json, "manufacturer"),
|
|
168
|
+
model = JsonUtil.optString(json, "model"),
|
|
169
|
+
osVersion = JsonUtil.optString(json, "os_version"),
|
|
170
|
+
sdkInt = JsonUtil.optInt(json, "sdk_int"),
|
|
171
|
+
// `android_id`: the name before the rename, in tracking_start records queued by earlier round-3 builds
|
|
172
|
+
deviceId = JsonUtil.optString(json, "device_id") ?: JsonUtil.optString(json, "android_id"),
|
|
173
|
+
lowRamDevice = JsonUtil.optBoolean(json, "low_ram_device"),
|
|
174
|
+
ramTotalBytes = JsonUtil.optLong(json, "ram_total_bytes"),
|
|
175
|
+
)
|
|
176
|
+
|
|
139
177
|
/** The `heartbeat` object of a heartbeat record (snake_case keys, `next_at` ISO-8601 or null). */
|
|
140
178
|
fun heartbeatMetaToJson(meta: HeartbeatMeta): JSONObject = JSONObject()
|
|
141
179
|
.put("strategy", meta.strategy.wire)
|
|
@@ -203,12 +241,20 @@ object RecordJson {
|
|
|
203
241
|
confidence = JsonUtil.optInt(it, "confidence") ?: 0,
|
|
204
242
|
)
|
|
205
243
|
} ?: ActivitySample.UNKNOWN
|
|
206
|
-
val
|
|
244
|
+
val batteryJson = json.optJSONObject("battery")
|
|
245
|
+
val battery = batteryJson?.let {
|
|
207
246
|
BatterySnapshot(
|
|
208
247
|
level = JsonUtil.optFloat(it, "level") ?: -1f,
|
|
209
248
|
isCharging = JsonUtil.optBoolean(it, "is_charging") ?: false,
|
|
210
249
|
)
|
|
211
250
|
} ?: BatterySnapshot.UNKNOWN
|
|
251
|
+
val batteryDetails = batteryJson?.let {
|
|
252
|
+
BatteryDetails(
|
|
253
|
+
temperature = JsonUtil.optFloat(it, "temperature"),
|
|
254
|
+
health = JsonUtil.optString(it, "health"),
|
|
255
|
+
plugged = JsonUtil.optString(it, "plugged"),
|
|
256
|
+
)
|
|
257
|
+
}?.takeUnless { it.temperature == null && it.health == null && it.plugged == null }
|
|
212
258
|
return Record(
|
|
213
259
|
uuid = uuid,
|
|
214
260
|
event = event,
|
|
@@ -226,6 +272,10 @@ object RecordJson {
|
|
|
226
272
|
provider = json.optJSONObject("provider")?.let { ProviderStateJson.fromJson(it) },
|
|
227
273
|
reason = JsonUtil.optString(json, "reason"),
|
|
228
274
|
heartbeat = json.optJSONObject("heartbeat")?.let { heartbeatMetaFromJson(it) },
|
|
275
|
+
stoppedAt = Iso8601.parse(JsonUtil.optString(json, "stopped_at")),
|
|
276
|
+
network = ConnectivityType.fromWire(JsonUtil.optString(json, "network")),
|
|
277
|
+
device = json.optJSONObject("device")?.let { deviceFromJson(it) },
|
|
278
|
+
batteryDetails = batteryDetails,
|
|
229
279
|
)
|
|
230
280
|
}
|
|
231
281
|
|
|
@@ -426,6 +476,13 @@ object DeviceInfoJson {
|
|
|
426
476
|
.put("hmsAvailable", d.hmsAvailable)
|
|
427
477
|
.put("backend", d.backend.wire)
|
|
428
478
|
.put("packagedProviders", providers)
|
|
479
|
+
.put("backendFallbackReason", JsonUtil.orNull(d.backendFallbackReason))
|
|
480
|
+
.put("gmsStatus", JsonUtil.orNull(d.gmsStatus))
|
|
481
|
+
.put("hmsStatus", JsonUtil.orNull(d.hmsStatus))
|
|
482
|
+
.put("hmsCoreOutdated", JsonUtil.orNull(d.hmsCoreOutdated))
|
|
483
|
+
.put("locationAvailableHint", JsonUtil.orNull(d.locationAvailableHint))
|
|
484
|
+
.put("cpuCores", d.cpuCores)
|
|
485
|
+
.put("supportedAbis", JSONArray().also { abis -> d.supportedAbis.forEach { abis.put(it) } })
|
|
429
486
|
}
|
|
430
487
|
}
|
|
431
488
|
|
|
@@ -497,6 +554,8 @@ object EventJson {
|
|
|
497
554
|
const val ENABLEDCHANGE = "enabledchange"
|
|
498
555
|
const val NOTIFICATIONACTION = "notificationaction"
|
|
499
556
|
const val AUTHORIZATION = "authorization"
|
|
557
|
+
const val LOCATIONERROR = "locationerror"
|
|
558
|
+
const val STORAGEFULL = "storagefull"
|
|
500
559
|
|
|
501
560
|
fun name(event: TrackingEvent): String = when (event) {
|
|
502
561
|
is TrackingEvent.Location -> LOCATION
|
|
@@ -512,6 +571,8 @@ object EventJson {
|
|
|
512
571
|
is TrackingEvent.EnabledChange -> ENABLEDCHANGE
|
|
513
572
|
is TrackingEvent.NotificationAction -> NOTIFICATIONACTION
|
|
514
573
|
is TrackingEvent.Authorization -> AUTHORIZATION
|
|
574
|
+
is TrackingEvent.LocationError -> LOCATIONERROR
|
|
575
|
+
is TrackingEvent.StorageFull -> STORAGEFULL
|
|
515
576
|
}
|
|
516
577
|
|
|
517
578
|
fun payload(event: TrackingEvent): JSONObject = when (event) {
|
|
@@ -538,7 +599,8 @@ object EventJson {
|
|
|
538
599
|
is TrackingEvent.Http -> HttpResultJson.toJson(event.result)
|
|
539
600
|
is TrackingEvent.ConnectivityChange -> ConnectivityJson.toJson(event.connectivity)
|
|
540
601
|
is TrackingEvent.PowerSaveChange -> JSONObject().put("isPowerSaveMode", event.isPowerSaveMode)
|
|
541
|
-
is TrackingEvent.EnabledChange ->
|
|
602
|
+
is TrackingEvent.EnabledChange ->
|
|
603
|
+
JSONObject().put("enabled", event.enabled).put("reason", JsonUtil.orNull(event.reason))
|
|
542
604
|
is TrackingEvent.NotificationAction -> JSONObject().put("id", event.id)
|
|
543
605
|
is TrackingEvent.Authorization -> {
|
|
544
606
|
val json = JSONObject().put("success", event.success).put("status", event.status)
|
|
@@ -546,5 +608,14 @@ object EventJson {
|
|
|
546
608
|
JsonUtil.parseObject(event.responseJson)?.let { json.put("response", it) }
|
|
547
609
|
json
|
|
548
610
|
}
|
|
611
|
+
is TrackingEvent.LocationError -> JSONObject()
|
|
612
|
+
.put("code", event.code.name)
|
|
613
|
+
.put("message", event.message)
|
|
614
|
+
.put("backend", event.backend.wire)
|
|
615
|
+
.put("retryInMs", JsonUtil.orNull(event.retryInMs))
|
|
616
|
+
is TrackingEvent.StorageFull -> JSONObject()
|
|
617
|
+
.put("uuid", event.uuid)
|
|
618
|
+
.put("event", event.event.wire)
|
|
619
|
+
.put("diskFreeBytes", JsonUtil.orNull(event.diskFreeBytes))
|
|
549
620
|
}
|
|
550
621
|
}
|
|
@@ -85,6 +85,11 @@ data class GeofenceHit(val identifier: String, val action: GeofenceAction, val e
|
|
|
85
85
|
* @property provider event `providerchange` only.
|
|
86
86
|
* @property reason `tracking_start` / `tracking_stop` only.
|
|
87
87
|
* @property heartbeat `heartbeat` only, and optional (round 2): the scheduling metadata of the heartbeat.
|
|
88
|
+
* @property stoppedAt `tracking_stop` only, when the stop happened before the record could be written (reason
|
|
89
|
+
* `user_stopped`: the time the user stopped the app), epoch ms.
|
|
90
|
+
* @property network the connection type when the record was created (every record; null if unknown).
|
|
91
|
+
* @property device device readings (heartbeat and `tracking_start` records only).
|
|
92
|
+
* @property batteryDetails battery temperature, health and power source (heartbeat records only).
|
|
88
93
|
*/
|
|
89
94
|
data class Record(
|
|
90
95
|
val uuid: String,
|
|
@@ -103,4 +108,8 @@ data class Record(
|
|
|
103
108
|
val provider: ProviderState? = null,
|
|
104
109
|
val reason: String? = null,
|
|
105
110
|
val heartbeat: HeartbeatMeta? = null,
|
|
111
|
+
val stoppedAt: Long? = null,
|
|
112
|
+
val network: ConnectivityType? = null,
|
|
113
|
+
val device: DeviceSnapshot? = null,
|
|
114
|
+
val batteryDetails: BatteryDetails? = null,
|
|
106
115
|
)
|
|
@@ -26,13 +26,23 @@ data class TrackedLocation(
|
|
|
26
26
|
val isMock: Boolean = false,
|
|
27
27
|
) {
|
|
28
28
|
companion object {
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The accuracy of a fix that has none (`Location.hasAccuracy()` false; some platform providers): 1,000 km,
|
|
31
|
+
* so the accuracy filters (`trackingAccuracyThreshold`, default 100 m) reject it. Before, it was stored as
|
|
32
|
+
* 0 m, which passes every accuracy filter (recommendation 12).
|
|
33
|
+
*/
|
|
34
|
+
const val UNKNOWN_ACCURACY_M = 1_000_000f
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts an Android [android.location.Location], keeping only the fields it actually has. A missing
|
|
38
|
+
* horizontal accuracy becomes [UNKNOWN_ACCURACY_M].
|
|
39
|
+
*/
|
|
30
40
|
fun from(l: android.location.Location): TrackedLocation {
|
|
31
41
|
val api26 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
|
32
42
|
return TrackedLocation(
|
|
33
43
|
latitude = l.latitude,
|
|
34
44
|
longitude = l.longitude,
|
|
35
|
-
accuracy = l.accuracy,
|
|
45
|
+
accuracy = if (l.hasAccuracy()) l.accuracy else UNKNOWN_ACCURACY_M,
|
|
36
46
|
altitude = if (l.hasAltitude()) l.altitude else null,
|
|
37
47
|
altitudeAccuracy = if (api26 && l.hasVerticalAccuracy()) l.verticalAccuracyMeters else null,
|
|
38
48
|
speed = if (l.hasSpeed()) l.speed else null,
|
package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt
CHANGED
|
@@ -33,7 +33,8 @@ import kotlin.math.min
|
|
|
33
33
|
* `maximumAgeMs > 0` it returns the freshest known fix (runtime or backend last location) if it is young
|
|
34
34
|
* enough. Otherwise it samples a temporary backend request for up to `samples` fixes, stops early on a fix of
|
|
35
35
|
* [ACCURATE_ENOUGH_M] or better, and keeps the most accurate fix. The timeout bounds the whole call; at the
|
|
36
|
-
* timeout the best fix so far is returned, or TIMEOUT is thrown if there is none.
|
|
36
|
+
* timeout the best fix so far is returned, or TIMEOUT is thrown if there is none. If the backend refuses the request
|
|
37
|
+
* (also later, through `LocationListener.onError`), the call rejects at once with that error.
|
|
37
38
|
* - [watchPosition] registers one backend listener per watch id. Each fix becomes a `watch_position` record,
|
|
38
39
|
* optionally persisted through [recordSink], and is delivered in order from [scope] (see [PositionWatch]).
|
|
39
40
|
*
|
|
@@ -162,7 +163,14 @@ class DefaultPositionService(
|
|
|
162
163
|
if (timeoutMs <= 0) return null
|
|
163
164
|
val collector = SampleCollector(o.samples)
|
|
164
165
|
val fixes = Channel<TrackedLocation>(Channel.UNLIMITED)
|
|
165
|
-
val listener =
|
|
166
|
+
val listener = object : LocationListener {
|
|
167
|
+
override fun onLocations(locations: List<TrackedLocation>) = locations.forEach { fixes.trySend(it) }
|
|
168
|
+
|
|
169
|
+
// The request failed (recommendation 3c): reject at once with that error instead of TIMEOUT.
|
|
170
|
+
override fun onError(error: TrackingException) {
|
|
171
|
+
fixes.close(error)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
166
174
|
val spec = LocationRequestSpec(o.desiredAccuracy, SAMPLE_INTERVAL_MS, SAMPLE_FASTEST_INTERVAL_MS, 0f)
|
|
167
175
|
val requestedAt = clock.elapsedRealtime()
|
|
168
176
|
try {
|
|
@@ -38,7 +38,11 @@ internal class PositionWatch(
|
|
|
38
38
|
/** Guards [started], [stopped], [registered] and every [callback] invocation. */
|
|
39
39
|
private val lock = Any()
|
|
40
40
|
private val fixes = Channel<TrackedLocation>(BUFFER, BufferOverflow.DROP_OLDEST)
|
|
41
|
-
private val listener =
|
|
41
|
+
private val listener = object : LocationListener {
|
|
42
|
+
override fun onLocations(locations: List<TrackedLocation>) = locations.forEach { fixes.trySend(it) }
|
|
43
|
+
|
|
44
|
+
override fun onError(error: TrackingException) = onRequestFailed(error)
|
|
45
|
+
}
|
|
42
46
|
|
|
43
47
|
@Volatile
|
|
44
48
|
private var stopped = false
|
|
@@ -69,6 +73,25 @@ internal class PositionWatch(
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The backend refused the request after [start] returned (recommendation 3): [callback] gets the error once and the
|
|
78
|
+
* watch ends (the backend no longer has the listener).
|
|
79
|
+
*/
|
|
80
|
+
private fun onRequestFailed(error: TrackingException) {
|
|
81
|
+
synchronized(lock) {
|
|
82
|
+
if (stopped) return
|
|
83
|
+
try {
|
|
84
|
+
callback(null, error)
|
|
85
|
+
} catch (e: Exception) {
|
|
86
|
+
Logger.e(DefaultPositionService.TAG, "watch $id callback failed", e)
|
|
87
|
+
}
|
|
88
|
+
stopped = true
|
|
89
|
+
registered = false
|
|
90
|
+
}
|
|
91
|
+
fixes.close()
|
|
92
|
+
Logger.w(DefaultPositionService.TAG, "watch $id ended: ${error.code} ${error.message}")
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
/** Removes the backend listener and ends deliveries. */
|
|
73
96
|
fun stop() {
|
|
74
97
|
val wasRegistered = synchronized(lock) {
|
package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt
CHANGED
|
@@ -6,8 +6,11 @@ import com.brickssoft.locationtracking.core.ErrorCode
|
|
|
6
6
|
import com.brickssoft.locationtracking.core.Iso8601
|
|
7
7
|
import com.brickssoft.locationtracking.core.Logger
|
|
8
8
|
import com.brickssoft.locationtracking.core.TrackingException
|
|
9
|
+
import com.brickssoft.locationtracking.device.DeviceDetails
|
|
9
10
|
import com.brickssoft.locationtracking.device.DeviceMonitor
|
|
10
11
|
import com.brickssoft.locationtracking.model.BatterySnapshot
|
|
12
|
+
import com.brickssoft.locationtracking.model.ConnectivityType
|
|
13
|
+
import com.brickssoft.locationtracking.model.DeviceSnapshot
|
|
11
14
|
import com.brickssoft.locationtracking.model.GeofenceHit
|
|
12
15
|
import com.brickssoft.locationtracking.model.JsonUtil
|
|
13
16
|
import com.brickssoft.locationtracking.model.ProviderKind
|
|
@@ -20,16 +23,19 @@ import org.json.JSONObject
|
|
|
20
23
|
import java.util.UUID
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
|
-
* Builds records: uuid v4, clock and boot metadata, runtime state (isMoving, odometer, activity), battery
|
|
24
|
-
* the current provider state (unless the caller passes one, as `providerchange` does) from
|
|
25
|
-
* [providers], and `extras` = `persistence.extras` merged with the per-call extras (per-call keys
|
|
26
|
-
* both are empty).
|
|
26
|
+
* Builds records: uuid v4, clock and boot metadata, runtime state (isMoving, odometer, activity), battery, the
|
|
27
|
+
* connection type and the current provider state (unless the caller passes one, as `providerchange` does) from
|
|
28
|
+
* [device], backend from [providers], and `extras` = `persistence.extras` merged with the per-call extras (per-call keys
|
|
29
|
+
* win; null when both are empty). Every record gets `device.device_id` from [details]; heartbeat records also get its
|
|
30
|
+
* device and battery readings, and `tracking_start` records the phone's model, Android version and memory.
|
|
27
31
|
*/
|
|
28
32
|
class DefaultRecordFactory(
|
|
29
33
|
private val configStore: ConfigStore,
|
|
30
34
|
private val device: DeviceMonitor,
|
|
31
35
|
private val providers: ProviderFactory,
|
|
32
36
|
private val clock: Clock,
|
|
37
|
+
/** The device id of every record, and readings for heartbeat and `tracking_start` records (parts d1 to d7). */
|
|
38
|
+
private val details: DeviceDetails = DeviceDetails.NONE,
|
|
33
39
|
) : RecordFactory {
|
|
34
40
|
override fun create(
|
|
35
41
|
event: RecordEvent,
|
|
@@ -56,16 +62,30 @@ class DefaultRecordFactory(
|
|
|
56
62
|
geofence = geofence,
|
|
57
63
|
provider = provider ?: providerState(),
|
|
58
64
|
reason = reason,
|
|
65
|
+
network = network(),
|
|
66
|
+
device = when (event) {
|
|
67
|
+
RecordEvent.HEARTBEAT -> details.forHeartbeat()
|
|
68
|
+
RecordEvent.TRACKING_START -> details.forTrackingStart()
|
|
69
|
+
else -> DeviceSnapshot()
|
|
70
|
+
}.copy(deviceId = details.deviceId()).takeUnless { it.isEmpty },
|
|
71
|
+
batteryDetails = if (event == RecordEvent.HEARTBEAT) details.batteryDetails() else null,
|
|
59
72
|
)
|
|
60
73
|
}
|
|
61
74
|
|
|
75
|
+
private fun network(): ConnectivityType? = try {
|
|
76
|
+
device.connectivity().type
|
|
77
|
+
} catch (e: Exception) {
|
|
78
|
+
Logger.w(TAG, "connectivity unavailable", e)
|
|
79
|
+
null
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
/**
|
|
63
|
-
* Parses a JS `InsertLocationInput`: `{coords:{latitude, longitude, accuracy
|
|
64
|
-
* event?, is_moving?, extras?}`.
|
|
65
|
-
*
|
|
83
|
+
* Parses a JS `InsertLocationInput`: `{coords:{latitude, longitude, accuracy, altitude?, ...}, timestamp?,
|
|
84
|
+
* event?, is_moving?, extras?}`. Negative optional accuracies, speed and heading (the "unknown" sentinel of some
|
|
85
|
+
* platforms) become null.
|
|
66
86
|
*
|
|
67
87
|
* @throws TrackingException INVALID_ARGUMENT if coords, latitude or longitude are missing or invalid, if
|
|
68
|
-
* accuracy is
|
|
88
|
+
* accuracy is missing or not greater than 0, or if `timestamp`, `event`, `is_moving` or `extras` are given but invalid.
|
|
69
89
|
*/
|
|
70
90
|
override fun fromExternal(input: JSONObject): Record {
|
|
71
91
|
val coords = input.optJSONObject("coords") ?: invalid("location.coords is required")
|
|
@@ -90,8 +110,9 @@ class DefaultRecordFactory(
|
|
|
90
110
|
} else {
|
|
91
111
|
JsonUtil.optBoolean(input, "is_moving") ?: invalid("is_moving must be a boolean")
|
|
92
112
|
}
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
// Round 3: no default; an accuracy of 0 would read as a perfect fix.
|
|
114
|
+
val accuracy = JsonUtil.optFloat(coords, "accuracy") ?: invalid("coords.accuracy is required")
|
|
115
|
+
if (!(accuracy > 0)) invalid("coords.accuracy must be > 0")
|
|
95
116
|
val extras = if (input.isNull("extras")) {
|
|
96
117
|
null
|
|
97
118
|
} else {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.brickssoft.locationtracking.provider
|
|
2
2
|
|
|
3
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
3
4
|
import com.brickssoft.locationtracking.model.ActivitySample
|
|
4
5
|
import com.brickssoft.locationtracking.model.DesiredAccuracy
|
|
5
6
|
import com.brickssoft.locationtracking.model.GeofenceAction
|
|
@@ -17,6 +18,13 @@ data class LocationRequestSpec(
|
|
|
17
18
|
/** Receives batches of fixes, on any thread. */
|
|
18
19
|
fun interface LocationListener {
|
|
19
20
|
fun onLocations(locations: List<TrackedLocation>)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The request made with this listener failed, synchronously or later (recommendation 3): for example a missing
|
|
24
|
+
* permission, Google Play services or HMS Core refusing the request, or no usable platform provider. The listener is
|
|
25
|
+
* no longer registered. Called at most once per request, on any thread.
|
|
26
|
+
*/
|
|
27
|
+
fun onError(error: TrackingException) = Unit
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/** Location updates and one-shot fixes from one backend. */
|
|
@@ -25,8 +33,8 @@ interface LocationBackend {
|
|
|
25
33
|
|
|
26
34
|
/**
|
|
27
35
|
* Supports multiple listeners; requesting again with the same listener replaces its request. A `distanceFilterM`
|
|
28
|
-
* of 0 means no distance filter (the engine applies its own elastic filter). Never throws:
|
|
29
|
-
*
|
|
36
|
+
* of 0 means no distance filter (the engine applies its own elastic filter). Never throws: a failure, also one the
|
|
37
|
+
* OS reports later, is logged and reported to [LocationListener.onError] (see [LocationSubscriptions]).
|
|
30
38
|
*/
|
|
31
39
|
fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener)
|
|
32
40
|
|
|
@@ -35,8 +43,18 @@ interface LocationBackend {
|
|
|
35
43
|
/** The OS's cached last location. Implementations may throw `TrackingException(PERMISSION_DENIED)`; callers catch. */
|
|
36
44
|
suspend fun getLastLocation(): TrackedLocation?
|
|
37
45
|
|
|
38
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Returns null on timeout. A request the OS refuses throws at once (recommendation 3c), not after the timeout.
|
|
48
|
+
* @throws com.brickssoft.locationtracking.core.TrackingException PERMISSION_DENIED, or the backend's mapping of
|
|
49
|
+
* the OS error (for example UNAVAILABLE).
|
|
50
|
+
*/
|
|
39
51
|
suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation?
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The last `LocationAvailability` that GMS or HMS reported for this backend's requests, or null (none yet, or the
|
|
55
|
+
* platform backend). A hint only: Google documents that it can be wrong in both directions.
|
|
56
|
+
*/
|
|
57
|
+
val availabilityHint: Boolean? get() = null
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
/** Activity recognition. Samples are delivered through a PendingIntent receiver to [ActivitySink]. */
|
|
@@ -91,6 +109,12 @@ interface ProviderBundle {
|
|
|
91
109
|
/** True if the SDK is packaged and usable on this device (e.g. `GoogleApiAvailability` SUCCESS). */
|
|
92
110
|
fun isAvailable(): Boolean
|
|
93
111
|
|
|
112
|
+
/** The raw availability result code of the last [isAvailable] (0 = SUCCESS), or null if unknown. */
|
|
113
|
+
fun availabilityStatus(): Int? = null
|
|
114
|
+
|
|
115
|
+
/** A diagnostic about the installed SDK service that does not change the choice (HMS: an old HMS Core), or null. */
|
|
116
|
+
fun versionDiagnostic(): String? = null
|
|
117
|
+
|
|
94
118
|
fun location(): LocationBackend
|
|
95
119
|
|
|
96
120
|
fun activity(): ActivityBackend
|
|
@@ -112,8 +136,29 @@ interface ProviderFactory {
|
|
|
112
136
|
|
|
113
137
|
/** Re-evaluates the selection (e.g. after setConfig). Returns true if [kind] changed. */
|
|
114
138
|
fun reselect(): Boolean
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What [reselect] would select now, without changing the selection. With `locationProvider: 'auto'` the engine
|
|
142
|
+
* compares it with [kind] on provider changes and heartbeats, and switches when it is better (recommendation 15).
|
|
143
|
+
*/
|
|
144
|
+
fun bestAvailable(): ProviderKind = kind
|
|
145
|
+
|
|
146
|
+
/** Why the current backend was selected and the raw availability codes (recommendation 15); diagnostic only. */
|
|
147
|
+
fun selectionInfo(): ProviderSelectionInfo = ProviderSelectionInfo(fallbackReason = null, gmsStatus = null, hmsStatus = null)
|
|
115
148
|
}
|
|
116
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Diagnostics of the provider choice: [fallbackReason] says why a better backend was not used (null when the preferred
|
|
152
|
+
* one was), [gmsStatus] / [hmsStatus] are the raw `GoogleApiAvailability` / `HuaweiApiAvailability` result codes (null
|
|
153
|
+
* when that SDK is not packaged), and [hmsCoreOutdated] is true when HMS Core is older than 6.3.0.301.
|
|
154
|
+
*/
|
|
155
|
+
data class ProviderSelectionInfo(
|
|
156
|
+
val fallbackReason: String?,
|
|
157
|
+
val gmsStatus: Int?,
|
|
158
|
+
val hmsStatus: Int?,
|
|
159
|
+
val hmsCoreOutdated: Boolean? = null,
|
|
160
|
+
)
|
|
161
|
+
|
|
117
162
|
/** Implemented by the TrackingEngine. */
|
|
118
163
|
interface ActivitySink {
|
|
119
164
|
suspend fun onActivitySamples(samples: List<ActivitySample>)
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt
CHANGED
|
@@ -43,6 +43,9 @@ class DefaultProviderFactory(
|
|
|
43
43
|
/** Created bundles; a null value means the bundle could not be loaded. Guarded by [lock]. */
|
|
44
44
|
private val bundles = HashMap<ProviderKind, ProviderBundle?>()
|
|
45
45
|
|
|
46
|
+
/** Bundles whose [ProviderBundle.versionDiagnostic] is logged once, when they are created. Guarded by [lock]. */
|
|
47
|
+
private val bundleVersionWarnings = HashSet<ProviderKind>()
|
|
48
|
+
|
|
46
49
|
override val kind: ProviderKind get() = current().kind
|
|
47
50
|
|
|
48
51
|
override fun location(): LocationBackend = current().bundle.location()
|
|
@@ -56,6 +59,56 @@ class DefaultProviderFactory(
|
|
|
56
59
|
return isUsable(bundle)
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
override fun bestAvailable(): ProviderKind = synchronized(lock) {
|
|
63
|
+
if (configStore.config.value.locationProvider != LocationProviderSetting.AUTO) return@synchronized kind
|
|
64
|
+
when {
|
|
65
|
+
availableLocked(ProviderKind.GMS) -> ProviderKind.GMS
|
|
66
|
+
availableLocked(ProviderKind.HMS) -> ProviderKind.HMS
|
|
67
|
+
else -> ProviderKind.ANDROID
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Why the current backend was selected (recommendation 15): for each better backend that was not used, its reason
|
|
73
|
+
* (not listed, SDK not packaged, receivers removed, or the availability status code); null when the preferred
|
|
74
|
+
* backend is used. The status codes are those of the last availability checks.
|
|
75
|
+
*/
|
|
76
|
+
override fun selectionInfo(): ProviderSelectionInfo = synchronized(lock) {
|
|
77
|
+
val setting = configStore.config.value.locationProvider
|
|
78
|
+
val current = current().kind
|
|
79
|
+
val preferred = when (setting) {
|
|
80
|
+
LocationProviderSetting.AUTO -> listOf(ProviderKind.GMS, ProviderKind.HMS)
|
|
81
|
+
LocationProviderSetting.GMS -> listOf(ProviderKind.GMS)
|
|
82
|
+
LocationProviderSetting.HMS -> listOf(ProviderKind.HMS)
|
|
83
|
+
LocationProviderSetting.ANDROID -> emptyList()
|
|
84
|
+
}
|
|
85
|
+
val skipped = preferred.takeWhile { it != current }.map { kind -> "${kind.wire}: ${skipReasonLocked(kind)}" }
|
|
86
|
+
val hms = bundles[ProviderKind.HMS]
|
|
87
|
+
ProviderSelectionInfo(
|
|
88
|
+
fallbackReason = skipped.takeIf { it.isNotEmpty() }?.joinToString("; "),
|
|
89
|
+
gmsStatus = bundles[ProviderKind.GMS]?.availabilityStatus(),
|
|
90
|
+
hmsStatus = hms?.availabilityStatus(),
|
|
91
|
+
hmsCoreOutdated = hms?.let { guard("HMS Core version check") { it.versionDiagnostic() } != null },
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private fun skipReasonLocked(kind: ProviderKind): String {
|
|
96
|
+
when (packaging.status(kind)) {
|
|
97
|
+
ProviderPackaging.Status.NOT_LISTED -> return "not listed in ${ProviderPackaging.PROVIDERS_META_DATA}"
|
|
98
|
+
ProviderPackaging.Status.SDK_MISSING -> return "SDK not packaged"
|
|
99
|
+
ProviderPackaging.Status.RECEIVERS_REMOVED -> return "receivers not in the manifest"
|
|
100
|
+
ProviderPackaging.Status.PACKAGED -> Unit
|
|
101
|
+
}
|
|
102
|
+
val bundle = bundleLocked(kind) ?: return "backend could not be loaded"
|
|
103
|
+
val available = isUsable(bundle)
|
|
104
|
+
val status = bundle.availabilityStatus()
|
|
105
|
+
return when {
|
|
106
|
+
available -> "available, not selected"
|
|
107
|
+
status != null -> "unavailable (status $status)"
|
|
108
|
+
else -> "unavailable"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
59
112
|
override fun reselect(): Boolean = synchronized(lock) {
|
|
60
113
|
val previous = selection
|
|
61
114
|
val next = selectLocked()
|
|
@@ -106,6 +159,10 @@ class DefaultProviderFactory(
|
|
|
106
159
|
ProviderPackaging.Status.SDK_MISSING -> "SDK is not packaged"
|
|
107
160
|
ProviderPackaging.Status.RECEIVERS_REMOVED -> "SDK is present but its receivers are not in the manifest"
|
|
108
161
|
}
|
|
162
|
+
if (reason == null && kind == ProviderKind.HMS) {
|
|
163
|
+
// Diagnostic only (recommendation 15): HMS is used whatever the HMS Core version.
|
|
164
|
+
bundleVersionWarnings += kind
|
|
165
|
+
}
|
|
109
166
|
if (reason != null) {
|
|
110
167
|
// Listed in the app's PROVIDERS meta-data but unusable is a setup error; otherwise it is expected.
|
|
111
168
|
if (packaging.listedExplicitly(kind)) {
|
|
@@ -117,6 +174,9 @@ class DefaultProviderFactory(
|
|
|
117
174
|
}
|
|
118
175
|
val name = ProviderBundles.bundleClassName(kind)
|
|
119
176
|
val bundle = guard("create $name") { createBundle(name) }
|
|
177
|
+
if (bundle != null && kind in bundleVersionWarnings) {
|
|
178
|
+
guard("${kind.wire} version check") { bundle.versionDiagnostic() }?.let { Logger.w(TAG, it) }
|
|
179
|
+
}
|
|
120
180
|
if (bundle == null && kind == ProviderKind.ANDROID) {
|
|
121
181
|
// The last resort must always exist; it lives in this module, so it can be constructed directly.
|
|
122
182
|
Logger.e(TAG, "reflective creation of the android bundle failed; constructing it directly")
|