@bricks-soft/capacitor-location-tracking 8.0.1 → 8.1.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/README.md +375 -124
- 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 +107 -4
- 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/package.json +2 -2
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>
|
|
@@ -121,6 +121,12 @@ export interface HeartbeatConfig {
|
|
|
121
121
|
* @default 300
|
|
122
122
|
*/
|
|
123
123
|
maxInterval?: number;
|
|
124
|
+
/**
|
|
125
|
+
* seconds, 0..60. After a heartbeat is queued, keep the phone awake until its upload has finished, or at most this
|
|
126
|
+
* long, so it reaches the server on time while tracking is on. 0 = do not keep the phone awake.
|
|
127
|
+
* @default 10
|
|
128
|
+
*/
|
|
129
|
+
uploadKeepAwake?: number;
|
|
124
130
|
}
|
|
125
131
|
export interface AuthorizationConfig {
|
|
126
132
|
/** @default 'JWT' */
|
|
@@ -186,6 +192,11 @@ export interface HttpConfig {
|
|
|
186
192
|
* @default 60000
|
|
187
193
|
*/
|
|
188
194
|
timeout?: number;
|
|
195
|
+
/**
|
|
196
|
+
* seconds, >= 0. The longest wait that a `Retry-After` header of a 429 or 503 answer can impose on automatic uploads.
|
|
197
|
+
* @default 3600
|
|
198
|
+
*/
|
|
199
|
+
maxRetryAfter?: number;
|
|
189
200
|
authorization?: AuthorizationConfig | null;
|
|
190
201
|
}
|
|
191
202
|
export interface PersistenceConfig {
|
|
@@ -301,8 +312,22 @@ export interface State {
|
|
|
301
312
|
odometer: number;
|
|
302
313
|
backend: LocationBackend;
|
|
303
314
|
lastRecordAt: string | null;
|
|
304
|
-
/**
|
|
315
|
+
/** reason of the last `tracking_stop` record (the `tracking_stop` reasons of `Location.reason`); null if none */
|
|
316
|
+
lastStopReason: string | null;
|
|
317
|
+
/** when tracking stopped for `lastStopReason` (ISO-8601 UTC ms); null if none */
|
|
318
|
+
lastStopAt: string | null;
|
|
319
|
+
/**
|
|
320
|
+
* fully populated with defaults. On Android the `http.authorization` secrets (`accessToken`, `refreshToken`, the
|
|
321
|
+
* values of `refreshPayload` and `refreshHeaders`) are masked: `***`, or `***` plus the last 4 characters for values
|
|
322
|
+
* longer than 8 characters; a `refreshPayload` value that contains `{refreshToken}` is shown. Passing a masked value
|
|
323
|
+
* back to `setConfig` keeps the stored one. The web stub returns them as given.
|
|
324
|
+
*/
|
|
305
325
|
config: Config;
|
|
326
|
+
/**
|
|
327
|
+
* `ready()`, `setConfig()` and `reset()` only: what the given config contains that was accepted but is probably
|
|
328
|
+
* wrong (for example an `http://` upload URL).
|
|
329
|
+
*/
|
|
330
|
+
warnings?: string[];
|
|
306
331
|
}
|
|
307
332
|
export interface Coords {
|
|
308
333
|
latitude: number;
|
|
@@ -343,10 +368,16 @@ export interface Location {
|
|
|
343
368
|
type: ActivityType;
|
|
344
369
|
confidence: number;
|
|
345
370
|
};
|
|
346
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* level 0..1, -1 unknown. Heartbeat records also carry `temperature` (°C), `health` and `plugged` when the device
|
|
373
|
+
* reports them.
|
|
374
|
+
*/
|
|
347
375
|
battery: {
|
|
348
376
|
level: number;
|
|
349
377
|
is_charging: boolean;
|
|
378
|
+
temperature?: number;
|
|
379
|
+
health?: 'good' | 'overheat' | 'dead' | 'over_voltage' | 'cold' | 'failure' | 'unknown';
|
|
380
|
+
plugged?: 'ac' | 'usb' | 'wireless' | 'dock' | 'none' | 'other';
|
|
350
381
|
};
|
|
351
382
|
backend: LocationBackend | null;
|
|
352
383
|
extras?: Record<string, unknown>;
|
|
@@ -360,11 +391,45 @@ export interface Location {
|
|
|
360
391
|
provider?: ProviderState;
|
|
361
392
|
/**
|
|
362
393
|
* tracking_start: start|start_geofences|boot|restore|package_replaced|resume_notification;
|
|
363
|
-
* tracking_stop: stop|stop_on_stationary|stop_after_elapsed|terminate|permission_denied|service_start_failed|reboot|package_replaced
|
|
394
|
+
* tracking_stop: stop|stop_on_stationary|stop_after_elapsed|terminate|permission_denied|service_start_failed|reboot|package_replaced|user_stopped
|
|
364
395
|
*/
|
|
365
396
|
reason?: string;
|
|
397
|
+
/** event 'tracking_stop' with reason 'user_stopped' only: when the user stopped the app (ISO-8601 UTC ms) */
|
|
398
|
+
stopped_at?: string;
|
|
366
399
|
/** event 'heartbeat' only, optional: how the heartbeat is scheduled */
|
|
367
400
|
heartbeat?: HeartbeatMeta;
|
|
401
|
+
/** connection type when the record was created */
|
|
402
|
+
network?: ConnectivityType;
|
|
403
|
+
/** the device id on every record; more readings on 'heartbeat' and 'tracking_start' records (see `DeviceReadings`) */
|
|
404
|
+
device?: DeviceReadings;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The `device` object of records; a reading is present only when the device provides it.
|
|
408
|
+
* Every record: `device_id`.
|
|
409
|
+
* Heartbeat records: `background_restricted`, `standby_bucket`, `disk_free_bytes`, `thermal_status`.
|
|
410
|
+
* `tracking_start` records: `manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes`.
|
|
411
|
+
*/
|
|
412
|
+
export interface DeviceReadings {
|
|
413
|
+
/**
|
|
414
|
+
* the device id: on Android, `Settings.Secure.ANDROID_ID`; other platforms will fill it with their own id.
|
|
415
|
+
* Absent in records queued by an older version. Declare "Device or other IDs" in Google Play's Data safety form.
|
|
416
|
+
*/
|
|
417
|
+
device_id?: string;
|
|
418
|
+
/** the user set the app's battery use to "Restricted" (Android 9+) */
|
|
419
|
+
background_restricted?: boolean;
|
|
420
|
+
/** App Standby bucket (Android 9+) */
|
|
421
|
+
standby_bucket?: 'exempted' | 'active' | 'working_set' | 'frequent' | 'rare' | 'restricted';
|
|
422
|
+
/** free space on the storage that holds the app's files */
|
|
423
|
+
disk_free_bytes?: number;
|
|
424
|
+
/** Android 10+ */
|
|
425
|
+
thermal_status?: 'none' | 'light' | 'moderate' | 'severe' | 'critical' | 'emergency' | 'shutdown' | 'unknown';
|
|
426
|
+
manufacturer?: string;
|
|
427
|
+
model?: string;
|
|
428
|
+
/** Android version, e.g. "14" */
|
|
429
|
+
os_version?: string;
|
|
430
|
+
sdk_int?: number;
|
|
431
|
+
low_ram_device?: boolean;
|
|
432
|
+
ram_total_bytes?: number;
|
|
368
433
|
}
|
|
369
434
|
/** Scheduling metadata of a `heartbeat` record (snake_case = wire format). */
|
|
370
435
|
export interface HeartbeatMeta {
|
|
@@ -382,7 +447,8 @@ export interface HeartbeatMeta {
|
|
|
382
447
|
}
|
|
383
448
|
export type LocationRecord = Location;
|
|
384
449
|
export interface InsertLocationInput {
|
|
385
|
-
|
|
450
|
+
/** `accuracy` (m) is required and must be greater than 0; otherwise the call rejects with `INVALID_ARGUMENT`. */
|
|
451
|
+
coords: Pick<Coords, 'latitude' | 'longitude' | 'accuracy'> & Partial<Coords>;
|
|
386
452
|
timestamp?: string;
|
|
387
453
|
event?: RecordEvent;
|
|
388
454
|
is_moving?: boolean;
|
|
@@ -449,6 +515,25 @@ export interface PowerSaveChangeEvent {
|
|
|
449
515
|
}
|
|
450
516
|
export interface EnabledChangeEvent {
|
|
451
517
|
enabled: boolean;
|
|
518
|
+
/**
|
|
519
|
+
* why: a `tracking_start` reason when tracking was switched on (start, start_geofences, resume_notification), a
|
|
520
|
+
* `tracking_stop` reason when it was switched off; the same values as `Location.reason`
|
|
521
|
+
*/
|
|
522
|
+
reason: string | null;
|
|
523
|
+
}
|
|
524
|
+
/** The location request failed (no permission, Play services or HMS Core refused it, no usable provider). */
|
|
525
|
+
export interface LocationErrorEvent {
|
|
526
|
+
code: ErrorCode;
|
|
527
|
+
message: string;
|
|
528
|
+
backend: LocationBackend;
|
|
529
|
+
/** the plugin requests location updates again after this many ms; null if it does not */
|
|
530
|
+
retryInMs: number | null;
|
|
531
|
+
}
|
|
532
|
+
/** A record could not be stored because the device storage is full. */
|
|
533
|
+
export interface StorageFullEvent {
|
|
534
|
+
uuid: string;
|
|
535
|
+
event: RecordEvent;
|
|
536
|
+
diskFreeBytes: number | null;
|
|
452
537
|
}
|
|
453
538
|
export interface NotificationActionEvent {
|
|
454
539
|
id: string;
|
|
@@ -473,6 +558,8 @@ export interface LocationTrackingEventMap {
|
|
|
473
558
|
enabledchange: EnabledChangeEvent;
|
|
474
559
|
notificationaction: NotificationActionEvent;
|
|
475
560
|
authorization: AuthorizationEvent;
|
|
561
|
+
locationerror: LocationErrorEvent;
|
|
562
|
+
storagefull: StorageFullEvent;
|
|
476
563
|
}
|
|
477
564
|
export type LocationTrackingEventName = keyof LocationTrackingEventMap;
|
|
478
565
|
export interface CurrentPositionOptions {
|
|
@@ -545,6 +632,20 @@ export interface DeviceInfo {
|
|
|
545
632
|
hmsAvailable: boolean;
|
|
546
633
|
backend: LocationBackend;
|
|
547
634
|
packagedProviders: string[];
|
|
635
|
+
/** why a better location backend is not used (for example "gms: unavailable (status 2)"); null when none is better */
|
|
636
|
+
backendFallbackReason: string | null;
|
|
637
|
+
/** raw GoogleApiAvailability result (0 = success); null if GMS is not packaged or not checked yet */
|
|
638
|
+
gmsStatus: number | null;
|
|
639
|
+
/** raw HuaweiApiAvailability result (0 = success); null if HMS is not packaged or not checked yet */
|
|
640
|
+
hmsStatus: number | null;
|
|
641
|
+
/** true when HMS Core is older than 6.3.0.301; null if HMS is not packaged */
|
|
642
|
+
hmsCoreOutdated: boolean | null;
|
|
643
|
+
/** last LocationAvailability reported by GMS or HMS; only a hint (it can be wrong both ways); null if none */
|
|
644
|
+
locationAvailableHint: boolean | null;
|
|
645
|
+
/** number of CPU cores */
|
|
646
|
+
cpuCores: number;
|
|
647
|
+
/** CPU types (Build.SUPPORTED_ABIS), e.g. ["arm64-v8a"] */
|
|
648
|
+
supportedAbis: string[];
|
|
548
649
|
}
|
|
549
650
|
export interface Sensors {
|
|
550
651
|
accelerometer: boolean;
|
|
@@ -715,5 +816,7 @@ export interface LocationTrackingPlugin {
|
|
|
715
816
|
addListener(eventName: 'enabledchange', listenerFunc: (e: EnabledChangeEvent) => void): Promise<PluginListenerHandle>;
|
|
716
817
|
addListener(eventName: 'notificationaction', listenerFunc: (e: NotificationActionEvent) => void): Promise<PluginListenerHandle>;
|
|
717
818
|
addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) => void): Promise<PluginListenerHandle>;
|
|
819
|
+
addListener(eventName: 'locationerror', listenerFunc: (e: LocationErrorEvent) => void): Promise<PluginListenerHandle>;
|
|
820
|
+
addListener(eventName: 'storagefull', listenerFunc: (e: StorageFullEvent) => void): Promise<PluginListenerHandle>;
|
|
718
821
|
removeAllListeners(): Promise<void>;
|
|
719
822
|
}
|