@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/LocationSubscriptions.kt
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
4
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
5
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One OS location registration made by a backend for one listener. [start] registers it and reports the outcome
|
|
9
|
+
* through `done` (null = registered, else the failure), on any thread, possibly before [start] returns. [remove]
|
|
10
|
+
* unregisters it; removing a registration that never completed is harmless. Neither may throw.
|
|
11
|
+
*/
|
|
12
|
+
internal interface OsRegistration {
|
|
13
|
+
fun start(done: (Exception?) -> Unit)
|
|
14
|
+
|
|
15
|
+
fun remove()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What the OS callbacks of one [OsRegistration] report. Any thread. */
|
|
19
|
+
internal interface RegistrationEvents {
|
|
20
|
+
fun locations(locations: List<TrackedLocation>)
|
|
21
|
+
|
|
22
|
+
/** `LocationAvailability` from GMS or HMS: a hint, which Google documents can be wrong in both directions. */
|
|
23
|
+
fun availability(available: Boolean)
|
|
24
|
+
|
|
25
|
+
/** A failure reported after the registration completed (the platform backend's providers, for example). */
|
|
26
|
+
fun error(error: Exception)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Listener bookkeeping shared by the GMS, HMS and Android location backends (recommendation 3), in place of three
|
|
31
|
+
* copies of it. One subscription per [LocationListener]:
|
|
32
|
+
* - [request] creates a new OS registration; a request for a listener that has one replaces it (the previous one is
|
|
33
|
+
* removed first, and its late fixes are dropped).
|
|
34
|
+
* - **Registration result.** A failure (synchronous or asynchronous) forgets the subscription, removes the OS
|
|
35
|
+
* registration and reports the error to the listener once ([LocationListener.onError], mapped by [mapError]); the
|
|
36
|
+
* engine then retries. A delivered fix counts as a successful registration: Huawei does not guarantee that the
|
|
37
|
+
* success callback comes before the first result, and without AppGallery Connect setup it can come late or never. A
|
|
38
|
+
* result reported after that (or a second one) is ignored.
|
|
39
|
+
* - **Removal.** [remove] stops deliveries at once. If the OS registration has not completed yet, it is removed now
|
|
40
|
+
* (best effort) and again once it completes, so a late registration cannot keep running unseen.
|
|
41
|
+
* - Listener exceptions are logged, never thrown into the OS callback.
|
|
42
|
+
*
|
|
43
|
+
* Unlike the design it follows, an error does not stop tracking: the listener decides what to do.
|
|
44
|
+
*/
|
|
45
|
+
internal class LocationSubscriptions(
|
|
46
|
+
private val tag: String,
|
|
47
|
+
private val mapError: (Exception) -> TrackingException,
|
|
48
|
+
) {
|
|
49
|
+
private val lock = Any()
|
|
50
|
+
private val subscriptions = HashMap<LocationListener, Subscription>()
|
|
51
|
+
|
|
52
|
+
/** The last `LocationAvailability` reported by any registration, or null if none was reported yet. */
|
|
53
|
+
@Volatile
|
|
54
|
+
var availabilityHint: Boolean? = null
|
|
55
|
+
private set
|
|
56
|
+
|
|
57
|
+
/** Number of listeners with a subscription (registered or pending). */
|
|
58
|
+
val count: Int get() = synchronized(lock) { subscriptions.size }
|
|
59
|
+
|
|
60
|
+
/** The listeners that have a subscription, and the request each was made with. */
|
|
61
|
+
fun requests(): Map<LocationListener, LocationRequestSpec> =
|
|
62
|
+
synchronized(lock) { subscriptions.mapValues { it.value.spec } }
|
|
63
|
+
|
|
64
|
+
/** Registers [listener] for [spec] through the registration [create] builds (see the class docs). */
|
|
65
|
+
fun request(listener: LocationListener, spec: LocationRequestSpec, create: (RegistrationEvents) -> OsRegistration) {
|
|
66
|
+
val subscription = Subscription(listener, spec)
|
|
67
|
+
val previous = synchronized(lock) { subscriptions.put(listener, subscription) }
|
|
68
|
+
previous?.cancel()
|
|
69
|
+
val registration = try {
|
|
70
|
+
create(subscription)
|
|
71
|
+
} catch (e: Exception) {
|
|
72
|
+
subscription.registered(e)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
subscription.begin(registration)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Removes [listener]'s subscription; unknown listeners are ignored. */
|
|
79
|
+
fun remove(listener: LocationListener) {
|
|
80
|
+
synchronized(lock) { subscriptions.remove(listener) }?.cancel()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun forget(subscription: Subscription) {
|
|
84
|
+
synchronized(lock) {
|
|
85
|
+
if (subscriptions[subscription.listener] === subscription) subscriptions.remove(subscription.listener)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private inner class Subscription(val listener: LocationListener, val spec: LocationRequestSpec) : RegistrationEvents {
|
|
90
|
+
private val subscriptionLock = Any()
|
|
91
|
+
private var registration: OsRegistration? = null
|
|
92
|
+
private var active = true
|
|
93
|
+
|
|
94
|
+
/** The outcome of the OS registration is known (completed, failed, or a fix was delivered). */
|
|
95
|
+
private var finished = false
|
|
96
|
+
|
|
97
|
+
fun begin(registration: OsRegistration) {
|
|
98
|
+
synchronized(subscriptionLock) { this.registration = registration }
|
|
99
|
+
try {
|
|
100
|
+
registration.start(::registered)
|
|
101
|
+
} catch (e: Exception) {
|
|
102
|
+
registered(e)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun registered(error: Exception?) {
|
|
107
|
+
val report: TrackingException?
|
|
108
|
+
val removeNow: Boolean
|
|
109
|
+
synchronized(subscriptionLock) {
|
|
110
|
+
if (finished) return
|
|
111
|
+
finished = true
|
|
112
|
+
report = if (error != null && active) mapError(error) else null
|
|
113
|
+
if (error != null) active = false
|
|
114
|
+
// Removed while the registration was pending: now that it exists, remove it for good.
|
|
115
|
+
removeNow = error != null || !active
|
|
116
|
+
}
|
|
117
|
+
if (error != null) forget(this)
|
|
118
|
+
if (removeNow) removeRegistration()
|
|
119
|
+
if (report != null) {
|
|
120
|
+
Logger.e(tag, "location request failed: ${report.code}: ${report.message}", error)
|
|
121
|
+
notifyError(report)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override fun locations(locations: List<TrackedLocation>) {
|
|
126
|
+
if (locations.isEmpty()) return
|
|
127
|
+
synchronized(subscriptionLock) {
|
|
128
|
+
if (!active) return
|
|
129
|
+
finished = true // a delivered fix proves the registration (HMS may never confirm it)
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
listener.onLocations(locations)
|
|
133
|
+
} catch (e: Exception) {
|
|
134
|
+
Logger.e(tag, "location listener failed", e)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override fun availability(available: Boolean) {
|
|
139
|
+
synchronized(subscriptionLock) { if (!active) return }
|
|
140
|
+
if (availabilityHint != available) Logger.i(tag, "location availability (hint): $available")
|
|
141
|
+
availabilityHint = available
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun error(error: Exception) {
|
|
145
|
+
synchronized(subscriptionLock) {
|
|
146
|
+
if (!active) return
|
|
147
|
+
active = false
|
|
148
|
+
finished = true
|
|
149
|
+
}
|
|
150
|
+
forget(this)
|
|
151
|
+
removeRegistration()
|
|
152
|
+
val mapped = mapError(error)
|
|
153
|
+
Logger.e(tag, "location updates failed: ${mapped.code}: ${mapped.message}", error)
|
|
154
|
+
notifyError(mapped)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Stops deliveries; removes the OS registration now and, if it is still pending, again when it completes. */
|
|
158
|
+
fun cancel() {
|
|
159
|
+
synchronized(subscriptionLock) { active = false }
|
|
160
|
+
removeRegistration()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun removeRegistration() {
|
|
164
|
+
val target = synchronized(subscriptionLock) { registration } ?: return
|
|
165
|
+
try {
|
|
166
|
+
target.remove()
|
|
167
|
+
} catch (e: Exception) {
|
|
168
|
+
Logger.w(tag, "removing a location registration failed", e)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun notifyError(error: TrackingException) {
|
|
173
|
+
try {
|
|
174
|
+
listener.onError(error)
|
|
175
|
+
} catch (e: Exception) {
|
|
176
|
+
Logger.e(tag, "location listener failed on an error", e)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -9,6 +9,7 @@ import android.location.Location
|
|
|
9
9
|
import android.location.LocationManager
|
|
10
10
|
import android.os.CancellationSignal
|
|
11
11
|
import android.os.Looper
|
|
12
|
+
import android.os.SystemClock
|
|
12
13
|
import androidx.core.content.ContextCompat
|
|
13
14
|
import androidx.core.location.LocationListenerCompat
|
|
14
15
|
import androidx.core.location.LocationManagerCompat
|
|
@@ -22,12 +23,22 @@ import com.brickssoft.locationtracking.model.TrackedLocation
|
|
|
22
23
|
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
23
24
|
import com.brickssoft.locationtracking.provider.LocationListener
|
|
24
25
|
import com.brickssoft.locationtracking.provider.LocationRequestSpec
|
|
26
|
+
import com.brickssoft.locationtracking.provider.LocationSubscriptions
|
|
27
|
+
import com.brickssoft.locationtracking.provider.OsRegistration
|
|
28
|
+
import com.brickssoft.locationtracking.provider.RegistrationEvents
|
|
29
|
+
import kotlinx.coroutines.cancelChildren
|
|
30
|
+
import kotlinx.coroutines.channels.Channel
|
|
31
|
+
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
|
32
|
+
import kotlinx.coroutines.coroutineScope
|
|
25
33
|
import kotlinx.coroutines.delay
|
|
34
|
+
import kotlinx.coroutines.joinAll
|
|
35
|
+
import kotlinx.coroutines.launch
|
|
26
36
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
27
37
|
import kotlinx.coroutines.withTimeoutOrNull
|
|
28
38
|
import java.util.concurrent.ConcurrentHashMap
|
|
29
39
|
import java.util.concurrent.Executor
|
|
30
40
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
41
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
31
42
|
import kotlin.coroutines.resume
|
|
32
43
|
import kotlin.coroutines.resumeWithException
|
|
33
44
|
|
|
@@ -35,11 +46,18 @@ import kotlin.coroutines.resumeWithException
|
|
|
35
46
|
* [LocationBackend] over the platform [LocationManager] (through `LocationManagerCompat`), used when neither GMS nor
|
|
36
47
|
* HMS is available.
|
|
37
48
|
*
|
|
38
|
-
* -
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
49
|
+
* - Providers per request ([LocationProviders.forRequest]): HIGH and BALANCED use `gps` and `network` together, as two
|
|
50
|
+
* platform listeners (recommendation 12), so a phone indoors without satellites still gets network fixes. While a GPS
|
|
51
|
+
* fix arrived in the last [gpsPriorityMs], network fixes are dropped, so GPS and network points do not alternate
|
|
52
|
+
* (a network fix can be 1–2 km off). LOW uses one provider (network, else gps); PASSIVE the passive provider. When
|
|
53
|
+
* neither gps nor network is enabled, `fused` (Android 12+) is used; when nothing is enabled, the existing providers
|
|
54
|
+
* are registered anyway and start delivering once enabled.
|
|
55
|
+
* - Listener bookkeeping is [LocationSubscriptions]; a request that no provider accepts reaches the listener's
|
|
56
|
+
* `onError` (PERMISSION_DENIED when every provider refused with a [SecurityException], else UNAVAILABLE).
|
|
57
|
+
* - Updates arrive on the main looper. While any listener is registered, `PROVIDERS_CHANGED_ACTION` registers each
|
|
58
|
+
* request again when its provider set changed.
|
|
59
|
+
* - [getCurrentLocation] asks the enabled providers in parallel and returns the most accurate fix that arrived within
|
|
60
|
+
* [CURRENT_LOCATION_GRACE_MS] of the first one.
|
|
43
61
|
*/
|
|
44
62
|
class AndroidLocationBackend(
|
|
45
63
|
context: Context,
|
|
@@ -49,30 +67,32 @@ class AndroidLocationBackend(
|
|
|
49
67
|
private val appContext: Context = context.applicationContext ?: context
|
|
50
68
|
override val kind: ProviderKind = ProviderKind.ANDROID
|
|
51
69
|
|
|
52
|
-
|
|
70
|
+
private val subscriptions = LocationSubscriptions(TAG, ::mapError)
|
|
71
|
+
|
|
72
|
+
/** The providers each listener's current registration uses. */
|
|
73
|
+
private val registeredProviders = ConcurrentHashMap<LocationListener, List<String>>()
|
|
74
|
+
|
|
75
|
+
/** Guards [providersReceiver]. */
|
|
53
76
|
private val lock = Any()
|
|
54
|
-
private val registrations = ConcurrentHashMap<LocationListener, Registration>()
|
|
55
77
|
private var providersReceiver: BroadcastReceiver? = null
|
|
56
78
|
|
|
57
79
|
override fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener) {
|
|
58
|
-
val lm = locationManager ?:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
?: return Logger.e(TAG, "no usable location provider for ${spec.accuracy.wire} (tried $providers)")
|
|
63
|
-
registrations.put(listener, registration)?.let { unregister(lm, it) }
|
|
64
|
-
observeProviderChanges()
|
|
65
|
-
Logger.d(TAG, "requested ${registration.provider} updates: $spec")
|
|
80
|
+
val lm = locationManager ?: run {
|
|
81
|
+
Logger.e(TAG, "no LocationManager: cannot request location updates")
|
|
82
|
+
listener.onError(TrackingException(ErrorCode.UNAVAILABLE, "This device has no LocationManager"))
|
|
83
|
+
return
|
|
66
84
|
}
|
|
85
|
+
val providers = LocationProviders.forRequest(lm, spec.accuracy)
|
|
86
|
+
registeredProviders[listener] = providers
|
|
87
|
+
subscriptions.request(listener, spec) { events -> PlatformRegistration(lm, spec, providers, events) }
|
|
88
|
+
observeProviderChanges()
|
|
89
|
+
Logger.d(TAG, "requested $providers updates: $spec")
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
override fun removeUpdates(listener: LocationListener) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
unregister(lm, registration)
|
|
74
|
-
if (registrations.isEmpty()) stopObservingProviderChanges()
|
|
75
|
-
}
|
|
93
|
+
subscriptions.remove(listener)
|
|
94
|
+
registeredProviders.remove(listener)
|
|
95
|
+
if (subscriptions.count == 0) stopObservingProviderChanges()
|
|
76
96
|
}
|
|
77
97
|
|
|
78
98
|
override suspend fun getLastLocation(): TrackedLocation? {
|
|
@@ -81,9 +101,12 @@ class AndroidLocationBackend(
|
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
/**
|
|
84
|
-
* Requests one fix from
|
|
85
|
-
* up after about 30 s, so
|
|
86
|
-
*
|
|
104
|
+
* Requests one fix from every enabled provider for [accuracy] in parallel (PASSIVE is treated as LOW). The platform
|
|
105
|
+
* gives up after about 30 s, so each request is repeated until [timeoutMs] elapses. After the first fix, the
|
|
106
|
+
* others get [CURRENT_LOCATION_GRACE_MS] to deliver a more accurate one. Returns null on timeout or when no provider
|
|
107
|
+
* is enabled.
|
|
108
|
+
*
|
|
109
|
+
* @throws TrackingException PERMISSION_DENIED if every provider refused with a [SecurityException].
|
|
87
110
|
*/
|
|
88
111
|
override suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation? {
|
|
89
112
|
val lm = locationManager ?: return null
|
|
@@ -92,28 +115,53 @@ class AndroidLocationBackend(
|
|
|
92
115
|
?.let(TrackedLocation::from)
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
private suspend fun awaitCurrentLocation(lm: LocationManager, accuracy: DesiredAccuracy): Location?
|
|
96
|
-
|
|
97
|
-
val
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
private suspend fun awaitCurrentLocation(lm: LocationManager, accuracy: DesiredAccuracy): Location? =
|
|
119
|
+
coroutineScope {
|
|
120
|
+
val providers = LocationProviders.candidates(lm, accuracy, enabledOnly = true)
|
|
121
|
+
if (providers.isEmpty()) return@coroutineScope null
|
|
122
|
+
val fixes = Channel<Location>(Channel.UNLIMITED)
|
|
123
|
+
val denied = AtomicInteger(0)
|
|
124
|
+
val requests = providers.map { provider ->
|
|
125
|
+
launch {
|
|
126
|
+
try {
|
|
127
|
+
repeatCurrentLocation(lm, provider)?.let { fixes.send(it) }
|
|
128
|
+
} catch (e: SecurityException) {
|
|
129
|
+
denied.incrementAndGet()
|
|
130
|
+
} catch (e: IllegalArgumentException) {
|
|
131
|
+
Logger.w(TAG, "provider $provider rejected getCurrentLocation", e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
launch {
|
|
136
|
+
requests.joinAll()
|
|
137
|
+
fixes.close(
|
|
138
|
+
if (denied.get() == providers.size) {
|
|
139
|
+
TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied")
|
|
140
|
+
} else {
|
|
141
|
+
null
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
}
|
|
108
145
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
var best = fixes.receive()
|
|
147
|
+
withTimeoutOrNull(CURRENT_LOCATION_GRACE_MS) {
|
|
148
|
+
for (next in fixes) if (next.accuracy < best.accuracy) best = next
|
|
149
|
+
}
|
|
150
|
+
best
|
|
151
|
+
} catch (e: ClosedReceiveChannelException) {
|
|
152
|
+
null
|
|
153
|
+
} finally {
|
|
154
|
+
requests.forEach { it.cancel() }
|
|
155
|
+
coroutineContext.cancelChildren()
|
|
114
156
|
}
|
|
115
157
|
}
|
|
116
|
-
|
|
158
|
+
|
|
159
|
+
/** One fix from [provider], asking again while it is enabled and gives up without one. */
|
|
160
|
+
private suspend fun repeatCurrentLocation(lm: LocationManager, provider: String): Location? {
|
|
161
|
+
while (LocationProviders.isEnabled(lm, provider)) {
|
|
162
|
+
currentLocation(lm, provider)?.let { return it }
|
|
163
|
+
delay(CURRENT_LOCATION_RETRY_DELAY_MS)
|
|
164
|
+
}
|
|
117
165
|
return null
|
|
118
166
|
}
|
|
119
167
|
|
|
@@ -132,85 +180,39 @@ class AndroidLocationBackend(
|
|
|
132
180
|
}
|
|
133
181
|
}
|
|
134
182
|
|
|
135
|
-
/**
|
|
136
|
-
* Registers [listener] on the first of [providers] that accepts the request.
|
|
137
|
-
*
|
|
138
|
-
* @return null if no provider accepted it for a reason other than permissions.
|
|
139
|
-
* @throws TrackingException [ErrorCode.PERMISSION_DENIED] if a provider refused with a [SecurityException] and
|
|
140
|
-
* no other accepted.
|
|
141
|
-
*/
|
|
142
|
-
@SuppressLint("MissingPermission")
|
|
143
|
-
private fun register(
|
|
144
|
-
lm: LocationManager,
|
|
145
|
-
spec: LocationRequestSpec,
|
|
146
|
-
listener: LocationListener,
|
|
147
|
-
providers: List<String>,
|
|
148
|
-
): Registration? {
|
|
149
|
-
var denied: SecurityException? = null
|
|
150
|
-
for (provider in providers) {
|
|
151
|
-
val registration = Registration(listener, spec, provider)
|
|
152
|
-
try {
|
|
153
|
-
LocationManagerCompat.requestLocationUpdates(
|
|
154
|
-
lm, provider, requestFor(spec), registration, Looper.getMainLooper(),
|
|
155
|
-
)
|
|
156
|
-
return registration
|
|
157
|
-
} catch (e: SecurityException) {
|
|
158
|
-
Logger.w(TAG, "provider $provider refused location updates", e)
|
|
159
|
-
denied = e
|
|
160
|
-
} catch (e: IllegalArgumentException) {
|
|
161
|
-
Logger.w(TAG, "provider $provider rejected location updates", e)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (denied != null) throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", denied)
|
|
165
|
-
return null
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private fun unregister(lm: LocationManager, registration: Registration) {
|
|
169
|
-
try {
|
|
170
|
-
LocationManagerCompat.removeUpdates(lm, registration)
|
|
171
|
-
} catch (e: Exception) {
|
|
172
|
-
Logger.w(TAG, "removeUpdates failed for ${registration.provider}", e)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Moves every registration whose best enabled provider changed to that provider. Runs on the main thread. */
|
|
183
|
+
/** Registers each request again whose provider set changed. Runs on the main thread. */
|
|
177
184
|
private fun onProvidersChanged() {
|
|
178
185
|
val lm = locationManager ?: return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
null
|
|
187
|
-
} ?: continue
|
|
188
|
-
registrations[listener] = next
|
|
189
|
-
unregister(lm, current)
|
|
190
|
-
Logger.i(TAG, "location provider changed: ${current.provider} -> $best")
|
|
191
|
-
}
|
|
186
|
+
for ((listener, spec) in subscriptions.requests()) {
|
|
187
|
+
val wanted = LocationProviders.forRequest(lm, spec.accuracy)
|
|
188
|
+
val current = registeredProviders[listener]
|
|
189
|
+
if (wanted == current) continue
|
|
190
|
+
Logger.i(TAG, "location providers changed: $current -> $wanted")
|
|
191
|
+
registeredProviders[listener] = wanted
|
|
192
|
+
subscriptions.request(listener, spec) { events -> PlatformRegistration(lm, spec, wanted, events) }
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
private fun observeProviderChanges() {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
synchronized(lock) {
|
|
198
|
+
if (providersReceiver != null) return
|
|
199
|
+
val receiver = object : BroadcastReceiver() {
|
|
200
|
+
override fun onReceive(context: Context, intent: Intent) = onProvidersChanged()
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
ContextCompat.registerReceiver(
|
|
204
|
+
appContext, receiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION),
|
|
205
|
+
ContextCompat.RECEIVER_NOT_EXPORTED,
|
|
206
|
+
)
|
|
207
|
+
providersReceiver = receiver
|
|
208
|
+
} catch (e: Exception) {
|
|
209
|
+
Logger.w(TAG, "cannot observe location provider changes", e)
|
|
210
|
+
}
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
private fun stopObservingProviderChanges() {
|
|
212
|
-
val receiver = providersReceiver ?: return
|
|
213
|
-
providersReceiver = null
|
|
215
|
+
val receiver = synchronized(lock) { providersReceiver.also { providersReceiver = null } } ?: return
|
|
214
216
|
try {
|
|
215
217
|
appContext.unregisterReceiver(receiver)
|
|
216
218
|
} catch (e: Exception) {
|
|
@@ -218,24 +220,87 @@ class AndroidLocationBackend(
|
|
|
218
220
|
}
|
|
219
221
|
}
|
|
220
222
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
private fun mapError(e: Exception): TrackingException = when (e) {
|
|
224
|
+
is TrackingException -> e
|
|
225
|
+
is SecurityException -> TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", e)
|
|
226
|
+
else -> TrackingException(ErrorCode.UNAVAILABLE, "Location updates failed: ${e.message}", e)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* One platform listener per provider of [providers]. Registration succeeds if at least one provider accepts the
|
|
231
|
+
* request. Network fixes are dropped while a GPS fix is recent (see the class docs).
|
|
232
|
+
*/
|
|
233
|
+
private inner class PlatformRegistration(
|
|
234
|
+
private val lm: LocationManager,
|
|
235
|
+
private val spec: LocationRequestSpec,
|
|
236
|
+
private val providers: List<String>,
|
|
237
|
+
private val events: RegistrationEvents,
|
|
238
|
+
) : OsRegistration {
|
|
239
|
+
private val listeners = ArrayList<ProviderListener>()
|
|
228
240
|
|
|
229
|
-
|
|
241
|
+
/** `SystemClock.elapsedRealtime()` of the last GPS fix delivered, or null. */
|
|
242
|
+
@Volatile
|
|
243
|
+
private var lastGpsFixAt: Long? = null
|
|
244
|
+
private val gpsPriorityMs = gpsPriorityMs(spec)
|
|
230
245
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
@SuppressLint("MissingPermission")
|
|
247
|
+
override fun start(done: (Exception?) -> Unit) {
|
|
248
|
+
var denied: SecurityException? = null
|
|
249
|
+
for (provider in providers) {
|
|
250
|
+
val listener = ProviderListener(provider)
|
|
251
|
+
try {
|
|
252
|
+
LocationManagerCompat.requestLocationUpdates(
|
|
253
|
+
lm, provider, requestFor(spec), listener, Looper.getMainLooper(),
|
|
254
|
+
)
|
|
255
|
+
synchronized(listeners) { listeners += listener }
|
|
256
|
+
} catch (e: SecurityException) {
|
|
257
|
+
Logger.w(TAG, "provider $provider refused location updates", e)
|
|
258
|
+
denied = e
|
|
259
|
+
} catch (e: IllegalArgumentException) {
|
|
260
|
+
Logger.w(TAG, "provider $provider rejected location updates", e)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
val accepted = synchronized(listeners) { listeners.isNotEmpty() }
|
|
264
|
+
when {
|
|
265
|
+
accepted -> done(null)
|
|
266
|
+
denied != null -> done(denied)
|
|
267
|
+
else -> done(
|
|
268
|
+
TrackingException(ErrorCode.UNAVAILABLE, "No usable location provider for ${spec.accuracy.wire} (tried $providers)"),
|
|
269
|
+
)
|
|
237
270
|
}
|
|
238
271
|
}
|
|
272
|
+
|
|
273
|
+
override fun remove() {
|
|
274
|
+
val registered = synchronized(listeners) { listeners.toList().also { listeners.clear() } }
|
|
275
|
+
for (listener in registered) {
|
|
276
|
+
try {
|
|
277
|
+
LocationManagerCompat.removeUpdates(lm, listener)
|
|
278
|
+
} catch (e: Exception) {
|
|
279
|
+
Logger.w(TAG, "removeUpdates failed for ${listener.provider}", e)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private fun deliver(provider: String, locations: List<Location>) {
|
|
285
|
+
if (locations.isEmpty()) return
|
|
286
|
+
val now = SystemClock.elapsedRealtime()
|
|
287
|
+
if (provider == LocationManager.GPS_PROVIDER) {
|
|
288
|
+
lastGpsFixAt = now
|
|
289
|
+
} else if (provider == LocationManager.NETWORK_PROVIDER && LocationManager.GPS_PROVIDER in providers) {
|
|
290
|
+
val gps = lastGpsFixAt
|
|
291
|
+
if (gps != null && now - gps < gpsPriorityMs) {
|
|
292
|
+
Logger.v(TAG, "network fix dropped: a GPS fix arrived ${(now - gps) / 1_000} s ago")
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
events.locations(locations.map(TrackedLocation::from))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private inner class ProviderListener(val provider: String) : LocationListenerCompat {
|
|
300
|
+
override fun onLocationChanged(location: Location) = deliver(provider, listOf(location))
|
|
301
|
+
|
|
302
|
+
override fun onLocationChanged(locations: MutableList<Location>) = deliver(provider, locations)
|
|
303
|
+
}
|
|
239
304
|
}
|
|
240
305
|
|
|
241
306
|
internal companion object {
|
|
@@ -244,6 +309,15 @@ class AndroidLocationBackend(
|
|
|
244
309
|
/** Pause between two platform `getCurrentLocation` attempts that returned no fix. */
|
|
245
310
|
const val CURRENT_LOCATION_RETRY_DELAY_MS = 1_000L
|
|
246
311
|
|
|
312
|
+
/** After the first current-location fix, how long the other providers may still deliver a better one. */
|
|
313
|
+
const val CURRENT_LOCATION_GRACE_MS = 3_000L
|
|
314
|
+
|
|
315
|
+
/** Network fixes are dropped for at least this long after a GPS fix. */
|
|
316
|
+
const val GPS_PRIORITY_MIN_MS = 10_000L
|
|
317
|
+
|
|
318
|
+
/** How long after a GPS fix network fixes are dropped: twice the request interval, at least 10 s. */
|
|
319
|
+
fun gpsPriorityMs(spec: LocationRequestSpec): Long = maxOf(GPS_PRIORITY_MIN_MS, 2 * spec.intervalMs)
|
|
320
|
+
|
|
247
321
|
private val DIRECT_EXECUTOR = Executor { it.run() }
|
|
248
322
|
|
|
249
323
|
/** Maps [spec] to a platform request; values are clamped to what `LocationRequestCompat` accepts. */
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt
CHANGED
|
@@ -43,6 +43,26 @@ internal object LocationProviders {
|
|
|
43
43
|
return if (enabledOnly) enabled else enabled + disabled
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The providers one continuous request for [accuracy] registers (recommendation 12):
|
|
48
|
+
* - HIGH and BALANCED: the enabled ones of `gps` and `network`, both when both are enabled; if neither is enabled,
|
|
49
|
+
* `fused` (API 31+) when it is; if nothing is enabled, the existing ones of `gps` and `network`, which start
|
|
50
|
+
* delivering once enabled.
|
|
51
|
+
* - LOW: the first of [candidates] (network preferred, a single provider).
|
|
52
|
+
* - PASSIVE: the passive provider.
|
|
53
|
+
*/
|
|
54
|
+
fun forRequest(lm: LocationManager, accuracy: DesiredAccuracy): List<String> {
|
|
55
|
+
if (accuracy == DesiredAccuracy.LOW || accuracy == DesiredAccuracy.PASSIVE) {
|
|
56
|
+
return candidates(lm, accuracy).take(1)
|
|
57
|
+
}
|
|
58
|
+
val pair = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER).filter { exists(lm, it) }
|
|
59
|
+
val enabled = pair.filter { isEnabled(lm, it) }
|
|
60
|
+
if (enabled.isNotEmpty()) return enabled
|
|
61
|
+
val fused = candidates(lm, accuracy, enabledOnly = true).firstOrNull()
|
|
62
|
+
if (fused != null) return listOf(fused)
|
|
63
|
+
return pair.ifEmpty { candidates(lm, accuracy).take(1) }
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
fun exists(lm: LocationManager, provider: String): Boolean =
|
|
47
67
|
try {
|
|
48
68
|
LocationManagerCompat.hasProvider(lm, provider)
|