@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
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt
CHANGED
|
@@ -9,6 +9,9 @@ import com.brickssoft.locationtracking.model.TrackedLocation
|
|
|
9
9
|
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
10
10
|
import com.brickssoft.locationtracking.provider.LocationListener
|
|
11
11
|
import com.brickssoft.locationtracking.provider.LocationRequestSpec
|
|
12
|
+
import com.brickssoft.locationtracking.provider.LocationSubscriptions
|
|
13
|
+
import com.brickssoft.locationtracking.provider.OsRegistration
|
|
14
|
+
import com.brickssoft.locationtracking.provider.RegistrationEvents
|
|
12
15
|
import com.google.android.gms.location.CurrentLocationRequest
|
|
13
16
|
import com.google.android.gms.location.FusedLocationProviderClient
|
|
14
17
|
import com.google.android.gms.location.LocationAvailability
|
|
@@ -25,11 +28,12 @@ import kotlin.coroutines.cancellation.CancellationException
|
|
|
25
28
|
/**
|
|
26
29
|
* Fused location provider backend.
|
|
27
30
|
*
|
|
28
|
-
*
|
|
29
|
-
* listener
|
|
31
|
+
* Listener bookkeeping is [LocationSubscriptions]: every request gets its own [LocationCallback] on the main looper,
|
|
32
|
+
* a request for the same listener removes the previous callback first, and a failure of `requestLocationUpdates`
|
|
33
|
+
* (thrown, or reported later by its Task) reaches the listener's `onError` (recommendation 3).
|
|
34
|
+
* `onLocationAvailability` is kept as [availabilityHint].
|
|
30
35
|
*
|
|
31
|
-
* Only [getCurrentLocation] throws: the [LocationBackend] contract declares no exception for the other methods
|
|
32
|
-
* their failures are logged (a missing permission also shows in the provider state).
|
|
36
|
+
* Only [getCurrentLocation] throws: the [LocationBackend] contract declares no exception for the other methods.
|
|
33
37
|
*/
|
|
34
38
|
internal class GmsLocationBackend(
|
|
35
39
|
private val client: FusedLocationProviderClient,
|
|
@@ -37,40 +41,48 @@ internal class GmsLocationBackend(
|
|
|
37
41
|
) : LocationBackend {
|
|
38
42
|
override val kind: ProviderKind = ProviderKind.GMS
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
private val subscriptions = LocationSubscriptions(TAG) { GmsErrors.map(it, "requestLocationUpdates") }
|
|
45
|
+
|
|
46
|
+
override val availabilityHint: Boolean? get() = subscriptions.availabilityHint
|
|
42
47
|
|
|
43
|
-
/**
|
|
44
|
-
* Never throws. A synchronous rejection (e.g. `SecurityException` without location permission) is logged and
|
|
45
|
-
* leaves a new listener unregistered, or a replaced listener on its previous request. Asynchronous failures of
|
|
46
|
-
* the GMS Task are logged too.
|
|
47
|
-
*/
|
|
48
48
|
override fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener) {
|
|
49
49
|
val request = try {
|
|
50
50
|
buildLocationRequest(spec)
|
|
51
51
|
} catch (e: Exception) {
|
|
52
52
|
Logger.e(TAG, "invalid location request $spec", e)
|
|
53
|
+
listener.onError(GmsErrors.map(e, "requestLocationUpdates"))
|
|
53
54
|
return
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
val callback = existing ?: ListenerCallback(listener)
|
|
58
|
-
try {
|
|
59
|
-
client.requestLocationUpdates(request, callback, looper()).logFailure(TAG, "requestLocationUpdates")
|
|
60
|
-
} catch (e: Exception) {
|
|
61
|
-
val mapped = GmsErrors.map(e, "requestLocationUpdates")
|
|
62
|
-
Logger.e(TAG, "${mapped.code}: ${mapped.message}", e)
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
if (existing == null) callbacks[listener] = callback
|
|
66
|
-
Logger.d(TAG, "location updates ${if (existing == null) "requested" else "replaced"}: $spec")
|
|
67
|
-
}
|
|
56
|
+
subscriptions.request(listener, spec) { events -> Registration(request, events) }
|
|
57
|
+
Logger.d(TAG, "location updates requested: $spec")
|
|
68
58
|
}
|
|
69
59
|
|
|
70
60
|
override fun removeUpdates(listener: LocationListener) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
subscriptions.remove(listener)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One `requestLocationUpdates` with its own callback. */
|
|
65
|
+
private inner class Registration(
|
|
66
|
+
private val request: LocationRequest,
|
|
67
|
+
private val events: RegistrationEvents,
|
|
68
|
+
) : OsRegistration {
|
|
69
|
+
private val callback = object : LocationCallback() {
|
|
70
|
+
override fun onLocationResult(result: LocationResult) {
|
|
71
|
+
events.locations(result.locations.map(TrackedLocation::from))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override fun onLocationAvailability(availability: LocationAvailability) {
|
|
75
|
+
events.availability(availability.isLocationAvailable)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override fun start(done: (Exception?) -> Unit) {
|
|
80
|
+
client.requestLocationUpdates(request, callback, looper()).addOnCompleteListener(DirectExecutor) { task ->
|
|
81
|
+
done(task.exception ?: if (task.isCanceled) IllegalStateException("request cancelled by GMS") else null)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override fun remove() {
|
|
74
86
|
try {
|
|
75
87
|
client.removeLocationUpdates(callback).logFailure(TAG, "removeLocationUpdates")
|
|
76
88
|
} catch (e: Exception) {
|
|
@@ -126,30 +138,6 @@ internal class GmsLocationBackend(
|
|
|
126
138
|
}
|
|
127
139
|
}
|
|
128
140
|
|
|
129
|
-
/**
|
|
130
|
-
* Forwards fixes to one listener until [removeUpdates] deactivates it. Best effort: a batch whose delivery already
|
|
131
|
-
* started on the looper thread may still reach the listener while [removeUpdates] runs on another thread.
|
|
132
|
-
*/
|
|
133
|
-
private class ListenerCallback(private val listener: LocationListener) : LocationCallback() {
|
|
134
|
-
@Volatile
|
|
135
|
-
var active = true
|
|
136
|
-
|
|
137
|
-
override fun onLocationResult(result: LocationResult) {
|
|
138
|
-
if (!active) return
|
|
139
|
-
val locations = result.locations.map(TrackedLocation::from)
|
|
140
|
-
if (locations.isEmpty()) return
|
|
141
|
-
try {
|
|
142
|
-
listener.onLocations(locations)
|
|
143
|
-
} catch (e: Exception) {
|
|
144
|
-
Logger.e(TAG, "location listener failed", e)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
override fun onLocationAvailability(availability: LocationAvailability) {
|
|
149
|
-
Logger.v(TAG, "location available: ${availability.isLocationAvailable}")
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
141
|
companion object {
|
|
154
142
|
private const val TAG = "LT.GmsLocation"
|
|
155
143
|
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt
CHANGED
|
@@ -40,16 +40,24 @@ class GmsProviderBundle @JvmOverloads constructor(
|
|
|
40
40
|
private val activityBackend by lazy { GmsActivityBackend(appContext, activityClient(appContext)) }
|
|
41
41
|
private val geofenceBackend by lazy { GmsGeofenceBackend(appContext, geofencingClient(appContext)) }
|
|
42
42
|
|
|
43
|
+
@Volatile
|
|
44
|
+
private var lastStatus: Int? = null
|
|
45
|
+
|
|
43
46
|
/** True if `GoogleApiAvailability` reports SUCCESS; false on any other result or failure. */
|
|
44
47
|
override fun isAvailable(): Boolean = try {
|
|
45
48
|
val status = availabilityCheck(appContext)
|
|
49
|
+
lastStatus = status
|
|
46
50
|
if (status != ConnectionResult.SUCCESS) Logger.d(TAG, "Google Play services unavailable: status $status")
|
|
47
51
|
status == ConnectionResult.SUCCESS
|
|
48
52
|
} catch (t: Throwable) {
|
|
53
|
+
lastStatus = null
|
|
49
54
|
Logger.w(TAG, "Google Play services availability check failed", t)
|
|
50
55
|
false
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
/** The `GoogleApiAvailability` result of the last [isAvailable] (`ConnectionResult` code), or null. */
|
|
59
|
+
override fun availabilityStatus(): Int? = lastStatus
|
|
60
|
+
|
|
53
61
|
override fun location(): LocationBackend = locationBackend
|
|
54
62
|
|
|
55
63
|
override fun activity(): ActivityBackend = activityBackend
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt
CHANGED
|
@@ -10,8 +10,12 @@ import com.brickssoft.locationtracking.model.TrackedLocation
|
|
|
10
10
|
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
11
11
|
import com.brickssoft.locationtracking.provider.LocationListener
|
|
12
12
|
import com.brickssoft.locationtracking.provider.LocationRequestSpec
|
|
13
|
+
import com.brickssoft.locationtracking.provider.LocationSubscriptions
|
|
14
|
+
import com.brickssoft.locationtracking.provider.OsRegistration
|
|
15
|
+
import com.brickssoft.locationtracking.provider.RegistrationEvents
|
|
13
16
|
import com.huawei.hmf.tasks.TaskExecutors
|
|
14
17
|
import com.huawei.hms.location.FusedLocationProviderClient
|
|
18
|
+
import com.huawei.hms.location.LocationAvailability
|
|
15
19
|
import com.huawei.hms.location.LocationCallback
|
|
16
20
|
import com.huawei.hms.location.LocationRequest
|
|
17
21
|
import com.huawei.hms.location.LocationResult
|
|
@@ -22,9 +26,14 @@ import kotlinx.coroutines.withTimeoutOrNull
|
|
|
22
26
|
/**
|
|
23
27
|
* [LocationBackend] over the HMS fused location client.
|
|
24
28
|
*
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* - Listener bookkeeping is [LocationSubscriptions]: each request gets its own [LocationCallback] on the main looper; a
|
|
30
|
+
* request for the same listener removes the previous callback first, and late fixes of a replaced callback are
|
|
31
|
+
* dropped. A failure of `requestLocationUpdates` (thrown, or reported later by its task) reaches the listener's
|
|
32
|
+
* `onError` (recommendation 3). The first delivered fix counts as a successful registration: Huawei does not
|
|
33
|
+
* guarantee that the task succeeds before the first result.
|
|
34
|
+
* - `getCurrentLocation` is a one-shot request (`numUpdates = 1`) whose callback is always removed. It returns the
|
|
35
|
+
* first fix even while the request task has not completed, and a failed request ends it at once with the error
|
|
36
|
+
* instead of waiting for the timeout.
|
|
28
37
|
* - Permission failures surface as `TrackingException(PERMISSION_DENIED)`; other HMS failures are mapped by
|
|
29
38
|
* [toTrackingException].
|
|
30
39
|
*
|
|
@@ -40,35 +49,17 @@ internal class HmsLocationBackend(
|
|
|
40
49
|
|
|
41
50
|
private val client: FusedLocationProviderClient by lazy(clientProvider)
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* submit work and return a task; callbacks never take the lock (they check [ListenerCallback.active]).
|
|
47
|
-
*/
|
|
48
|
-
private val callbacks = HashMap<LocationListener, ListenerCallback>()
|
|
49
|
-
private val lock = Any()
|
|
52
|
+
private val subscriptions = LocationSubscriptions(TAG) { it.toTrackingException("requestLocationUpdates") }
|
|
53
|
+
|
|
54
|
+
override val availabilityHint: Boolean? get() = subscriptions.availabilityHint
|
|
50
55
|
|
|
51
56
|
override fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener) {
|
|
52
57
|
val request = buildLocationRequest(spec)
|
|
53
|
-
|
|
54
|
-
callbacks.remove(listener)?.let(::deactivateAndRemove)
|
|
55
|
-
val callback = ListenerCallback(listener)
|
|
56
|
-
callbacks[listener] = callback
|
|
57
|
-
try {
|
|
58
|
-
client.requestLocationUpdates(request, callback, looperProvider())
|
|
59
|
-
.addOnFailureListener(TaskExecutors.immediate()) { e ->
|
|
60
|
-
Logger.e(TAG, "requestLocationUpdates failed: ${describeHmsError(e)}", e)
|
|
61
|
-
forget(callback)
|
|
62
|
-
}
|
|
63
|
-
} catch (e: Exception) {
|
|
64
|
-
Logger.e(TAG, "requestLocationUpdates failed: ${describeHmsError(e)}", e)
|
|
65
|
-
forget(callback)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
58
|
+
subscriptions.request(listener, spec) { events -> Registration(request, events) }
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
override fun removeUpdates(listener: LocationListener) {
|
|
71
|
-
|
|
62
|
+
subscriptions.remove(listener)
|
|
72
63
|
}
|
|
73
64
|
|
|
74
65
|
override suspend fun getLastLocation(): TrackedLocation? = hmsCall("getLastLocation") {
|
|
@@ -87,9 +78,15 @@ internal class HmsLocationBackend(
|
|
|
87
78
|
var requested = false
|
|
88
79
|
try {
|
|
89
80
|
return withTimeoutOrNull(timeoutMs.coerceAtLeast(0)) {
|
|
90
|
-
hmsCall("getCurrentLocation") {
|
|
81
|
+
val task = hmsCall("getCurrentLocation") {
|
|
91
82
|
requested = true
|
|
92
|
-
client.requestLocationUpdates(buildOneShotRequest(accuracy), callback, looperProvider())
|
|
83
|
+
client.requestLocationUpdates(buildOneShotRequest(accuracy), callback, looperProvider())
|
|
84
|
+
}
|
|
85
|
+
task.addOnFailureListener(TaskExecutors.immediate()) { e ->
|
|
86
|
+
fix.completeExceptionally(e.toTrackingException("getCurrentLocation"))
|
|
87
|
+
}
|
|
88
|
+
task.addOnCanceledListener(TaskExecutors.immediate()) {
|
|
89
|
+
fix.completeExceptionally(HmsTaskCanceledException().toTrackingException("getCurrentLocation"))
|
|
93
90
|
}
|
|
94
91
|
fix.await()
|
|
95
92
|
}
|
|
@@ -99,18 +96,7 @@ internal class HmsLocationBackend(
|
|
|
99
96
|
}
|
|
100
97
|
|
|
101
98
|
/** Number of listeners with an active request (for tests and diagnostics). */
|
|
102
|
-
internal val activeListenerCount: Int get() =
|
|
103
|
-
|
|
104
|
-
/** Drops [callback] after its request failed, unless it was already replaced or removed. */
|
|
105
|
-
private fun forget(callback: ListenerCallback) {
|
|
106
|
-
callback.active = false
|
|
107
|
-
synchronized(lock) { if (callbacks[callback.listener] === callback) callbacks.remove(callback.listener) }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private fun deactivateAndRemove(callback: ListenerCallback) {
|
|
111
|
-
callback.active = false
|
|
112
|
-
removeQuietly(callback)
|
|
113
|
-
}
|
|
99
|
+
internal val activeListenerCount: Int get() = subscriptions.count
|
|
114
100
|
|
|
115
101
|
private fun removeQuietly(callback: LocationCallback) {
|
|
116
102
|
try {
|
|
@@ -123,21 +109,29 @@ internal class HmsLocationBackend(
|
|
|
123
109
|
}
|
|
124
110
|
}
|
|
125
111
|
|
|
126
|
-
/**
|
|
127
|
-
private class
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Logger.e(TAG, "location listener failed", e)
|
|
112
|
+
/** One `requestLocationUpdates` with its own callback. */
|
|
113
|
+
private inner class Registration(
|
|
114
|
+
private val request: LocationRequest,
|
|
115
|
+
private val events: RegistrationEvents,
|
|
116
|
+
) : OsRegistration {
|
|
117
|
+
private val callback = object : LocationCallback() {
|
|
118
|
+
override fun onLocationResult(result: LocationResult?) {
|
|
119
|
+
events.locations(result?.locations.orEmpty().mapNotNull { location -> location?.let(::toTrackedLocation) })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override fun onLocationAvailability(availability: LocationAvailability?) {
|
|
123
|
+
availability?.let { events.availability(it.isLocationAvailable) }
|
|
139
124
|
}
|
|
140
125
|
}
|
|
126
|
+
|
|
127
|
+
override fun start(done: (Exception?) -> Unit) {
|
|
128
|
+
client.requestLocationUpdates(request, callback, looperProvider())
|
|
129
|
+
.addOnSuccessListener(TaskExecutors.immediate()) { done(null) }
|
|
130
|
+
.addOnFailureListener(TaskExecutors.immediate()) { e -> done(e) }
|
|
131
|
+
.addOnCanceledListener(TaskExecutors.immediate()) { done(HmsTaskCanceledException()) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
override fun remove() = removeQuietly(callback)
|
|
141
135
|
}
|
|
142
136
|
|
|
143
137
|
companion object {
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt
CHANGED
|
@@ -23,6 +23,10 @@ class HmsProviderBundle @JvmOverloads constructor(
|
|
|
23
23
|
private val availabilityCheck: (Context) -> Int = { ctx ->
|
|
24
24
|
HuaweiApiAvailability.getInstance().isHuaweiMobileServicesAvailable(ctx)
|
|
25
25
|
},
|
|
26
|
+
/** `isHuaweiMobileServicesAvailable(context, minimumVersion)`; injectable for tests. */
|
|
27
|
+
private val versionCheck: (Context, Int) -> Int = { ctx, minimum ->
|
|
28
|
+
HuaweiApiAvailability.getInstance().isHuaweiMobileServicesAvailable(ctx, minimum)
|
|
29
|
+
},
|
|
26
30
|
) : ProviderBundle {
|
|
27
31
|
private val appContext: Context = context.applicationContext ?: context
|
|
28
32
|
|
|
@@ -32,24 +36,52 @@ class HmsProviderBundle @JvmOverloads constructor(
|
|
|
32
36
|
private val activityBackend by lazy { HmsActivityBackend(appContext) }
|
|
33
37
|
private val geofenceBackend by lazy { HmsGeofenceBackend(appContext) }
|
|
34
38
|
|
|
39
|
+
@Volatile
|
|
40
|
+
private var lastStatus: Int? = null
|
|
41
|
+
|
|
35
42
|
/** True if HMS Core reports SUCCESS; any failure (including a missing SDK class) counts as unavailable. */
|
|
36
43
|
override fun isAvailable(): Boolean =
|
|
37
44
|
try {
|
|
38
45
|
val result = availabilityCheck(appContext)
|
|
46
|
+
lastStatus = result
|
|
39
47
|
if (result != ConnectionResult.SUCCESS) Logger.d(TAG, "HMS Core unavailable: result $result")
|
|
40
48
|
result == ConnectionResult.SUCCESS
|
|
41
49
|
} catch (t: Throwable) {
|
|
50
|
+
lastStatus = null
|
|
42
51
|
Logger.w(TAG, "HMS availability check failed", t)
|
|
43
52
|
false
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
/** The `HuaweiApiAvailability` result of the last [isAvailable], or null. */
|
|
56
|
+
override fun availabilityStatus(): Int? = lastStatus
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A diagnostic when HMS Core is older than 6.3.0.301 ([MIN_RECOMMENDED_HMS_CORE]), the version Huawei's Location
|
|
60
|
+
* Kit documentation asks for (recommendation 15). It does not change the provider choice. Null when HMS Core is
|
|
61
|
+
* that version or newer, or the check fails.
|
|
62
|
+
*/
|
|
63
|
+
override fun versionDiagnostic(): String? = try {
|
|
64
|
+
val result = versionCheck(appContext, MIN_RECOMMENDED_HMS_CORE)
|
|
65
|
+
if (result == ConnectionResult.SUCCESS) {
|
|
66
|
+
null
|
|
67
|
+
} else {
|
|
68
|
+
"HMS Core is older than 6.3.0.301 or needs an update (isHuaweiMobileServicesAvailable result $result)"
|
|
69
|
+
}
|
|
70
|
+
} catch (t: Throwable) {
|
|
71
|
+
Logger.w(TAG, "HMS Core version check failed", t)
|
|
72
|
+
null
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
override fun location(): LocationBackend = locationBackend
|
|
47
76
|
|
|
48
77
|
override fun activity(): ActivityBackend = activityBackend
|
|
49
78
|
|
|
50
79
|
override fun geofence(): GeofenceBackend = geofenceBackend
|
|
51
80
|
|
|
52
|
-
|
|
53
|
-
const val TAG = "LT.HmsBundle"
|
|
81
|
+
internal companion object {
|
|
82
|
+
private const val TAG = "LT.HmsBundle"
|
|
83
|
+
|
|
84
|
+
/** HMS Core 6.3.0.301, as `isHuaweiMobileServicesAvailable` expects a version (60300301). */
|
|
85
|
+
const val MIN_RECOMMENDED_HMS_CORE = 60300301
|
|
54
86
|
}
|
|
55
87
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.brickssoft.locationtracking.record
|
|
2
2
|
|
|
3
|
+
import android.database.sqlite.SQLiteFullException
|
|
3
4
|
import com.brickssoft.locationtracking.config.ConfigStore
|
|
4
5
|
import com.brickssoft.locationtracking.core.EventBus
|
|
5
6
|
import com.brickssoft.locationtracking.core.Logger
|
|
@@ -29,7 +30,8 @@ interface RecordSink {
|
|
|
29
30
|
* 5. `syncer.onRecordInserted(record)`.
|
|
30
31
|
*
|
|
31
32
|
* If the insert fails, the failure is logged, steps 2-4 still run (live listeners still get the record) and
|
|
32
|
-
* step 5 is skipped because the record is not queued.
|
|
33
|
+
* step 5 is skipped because the record is not queued. A failure because the storage is full (`SQLiteFullException`)
|
|
34
|
+
* also emits a `StorageFull` event (recommendation 16), so the app can tell the user.
|
|
33
35
|
*
|
|
34
36
|
* Round 2: [hooks] receives every record right after step 1, also when the insert failed or the caller was cancelled
|
|
35
37
|
* during the insert (a companion listener keeps its own audit trail, so a local database failure must not hide the
|
|
@@ -43,6 +45,8 @@ class DefaultRecordSink(
|
|
|
43
45
|
private val syncer: HttpSyncer,
|
|
44
46
|
private val events: EventBus,
|
|
45
47
|
private val hooks: RecordHooks = RecordHooks(),
|
|
48
|
+
/** Free bytes of the app's storage, for the `StorageFull` event; null if unknown. */
|
|
49
|
+
private val diskFreeBytes: () -> Long? = { null },
|
|
46
50
|
) : RecordSink {
|
|
47
51
|
override suspend fun submit(record: Record): Record {
|
|
48
52
|
val stored = try {
|
|
@@ -56,6 +60,14 @@ class DefaultRecordSink(
|
|
|
56
60
|
throw e
|
|
57
61
|
} catch (e: Exception) {
|
|
58
62
|
Logger.e(TAG, "failed to persist ${record.event.wire} record ${record.uuid}", e)
|
|
63
|
+
if (isStorageFull(e)) {
|
|
64
|
+
val free = try {
|
|
65
|
+
diskFreeBytes()
|
|
66
|
+
} catch (x: Exception) {
|
|
67
|
+
null
|
|
68
|
+
}
|
|
69
|
+
events.emit(TrackingEvent.StorageFull(record.uuid, record.event, free))
|
|
70
|
+
}
|
|
59
71
|
false
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -91,5 +103,11 @@ class DefaultRecordSink(
|
|
|
91
103
|
|
|
92
104
|
private companion object {
|
|
93
105
|
const val TAG = "LT.RecordSink"
|
|
106
|
+
|
|
107
|
+
/** True if [e] or one of its causes is `SQLiteFullException` (or an "out of disk space" I/O error). */
|
|
108
|
+
fun isStorageFull(e: Throwable): Boolean = generateSequence(e) { it.cause }.take(8).any {
|
|
109
|
+
it is SQLiteFullException || it.message?.contains("SQLITE_FULL") == true ||
|
|
110
|
+
it.message?.contains("ENOSPC") == true
|
|
111
|
+
}
|
|
94
112
|
}
|
|
95
113
|
}
|
|
@@ -37,6 +37,13 @@ internal class BootCountStore(context: Context) {
|
|
|
37
37
|
private fun read(key: String): Int? = if (prefs.contains(key)) prefs.getInt(key, -1) else null
|
|
38
38
|
|
|
39
39
|
companion object {
|
|
40
|
+
/** Forgets every stored boot count (the state was restored onto another installation). */
|
|
41
|
+
@SuppressLint("ApplySharedPref")
|
|
42
|
+
fun clear(context: Context) {
|
|
43
|
+
(context.applicationContext ?: context).getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
44
|
+
.edit().clear().commit()
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
const val PREFS_NAME = "location_tracking_boot"
|
|
41
48
|
private const val KEY_LAST_HANDLED = "last_handled_boot_count"
|
|
42
49
|
private const val KEY_LAST_SERVICE_START = "last_service_start_boot_count"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<paths>
|
|
3
|
-
|
|
3
|
+
<!-- Only the gzipped copy in the cache is shared (emailLog); the daily files are under noBackupFilesDir. -->
|
|
4
4
|
<cache-path name="lt_logs_cache" path="location-tracking-logs/" />
|
|
5
5
|
</paths>
|