@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.
Files changed (124) hide show
  1. package/BricksSoftCapacitorLocationTracking.podspec +18 -0
  2. package/Package.swift +13 -0
  3. package/README.md +476 -147
  4. package/android/build.gradle +3 -1
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +4 -1
  7. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +14 -5
  8. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +10 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +70 -15
  10. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +6 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +8 -3
  12. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +10 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/config/SecretStore.kt +188 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +188 -17
  15. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +11 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +21 -5
  17. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +20 -9
  18. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +24 -2
  19. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +43 -1
  20. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +17 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceDetails.kt +167 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +191 -34
  23. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +6 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +3 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +52 -8
  26. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +22 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +78 -27
  28. package/android/src/main/java/com/brickssoft/locationtracking/http/ConfigChecks.kt +140 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +50 -12
  30. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +17 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +507 -92
  32. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +6 -6
  33. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +23 -1
  34. package/android/src/main/java/com/brickssoft/locationtracking/http/UploadJobs.kt +217 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/http/UploadWait.kt +184 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +66 -7
  37. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogRedaction.kt +31 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +44 -1
  39. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +79 -8
  40. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +9 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +12 -2
  42. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +10 -2
  43. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +24 -1
  44. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +31 -10
  45. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +48 -3
  46. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +60 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/provider/LocationSubscriptions.kt +180 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +200 -126
  49. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +20 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +39 -51
  51. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +8 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +47 -53
  53. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +34 -2
  54. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +19 -1
  55. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +7 -0
  56. package/android/src/main/res/xml/lt_file_paths.xml +1 -1
  57. package/dist/esm/definitions.d.ts +117 -8
  58. package/dist/esm/definitions.js.map +1 -1
  59. package/dist/esm/events.d.ts +8 -2
  60. package/dist/esm/events.js +7 -1
  61. package/dist/esm/events.js.map +1 -1
  62. package/dist/esm/index.d.ts +1 -1
  63. package/dist/esm/index.js +1 -1
  64. package/dist/esm/index.js.map +1 -1
  65. package/dist/esm/web/config.js +10 -0
  66. package/dist/esm/web/config.js.map +1 -1
  67. package/dist/esm/web/device.d.ts +1 -1
  68. package/dist/esm/web/device.js +8 -1
  69. package/dist/esm/web/device.js.map +1 -1
  70. package/dist/esm/web.d.ts +0 -8
  71. package/dist/esm/web.js +12 -3
  72. package/dist/esm/web.js.map +1 -1
  73. package/dist/plugin.cjs.js +39 -5
  74. package/dist/plugin.cjs.js.map +1 -1
  75. package/dist/plugin.js +39 -5
  76. package/dist/plugin.js.map +1 -1
  77. package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
  78. package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
  79. package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
  80. package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
  81. package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
  82. package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
  83. package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
  84. package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
  85. package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
  86. package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
  87. package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
  88. package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
  89. package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
  90. package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
  91. package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
  92. package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
  93. package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
  94. package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
  95. package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
  96. package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
  97. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
  98. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
  99. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
  100. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
  101. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
  102. package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
  103. package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
  104. package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
  105. package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
  106. package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
  107. package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
  108. package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
  109. package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
  110. package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
  111. package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
  112. package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
  113. package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
  114. package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
  115. package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
  116. package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
  117. package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
  118. package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
  119. package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
  120. package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
  121. package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
  122. package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
  123. package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
  124. package/package.json +10 -4
@@ -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
- * - Provider per accuracy: HIGH uses gps, BALANCED/LOW use network, each falling back to whichever is enabled;
39
- * PASSIVE uses the passive provider (see [LocationProviders.candidates]).
40
- * - Updates arrive on the main looper. While any listener is registered, `PROVIDERS_CHANGED_ACTION` moves each
41
- * registration to its best enabled provider.
42
- * - A [SecurityException] from the platform surfaces as a [TrackingException] with [ErrorCode.PERMISSION_DENIED].
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
- /** Serializes (re-)registrations; [registrations] is also read lock-free by the delivery path. */
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 ?: return Logger.e(TAG, "no LocationManager: cannot request location updates")
59
- synchronized(lock) {
60
- val providers = LocationProviders.candidates(lm, spec.accuracy)
61
- val registration = register(lm, spec, listener, providers)
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
- val lm = locationManager ?: return
71
- synchronized(lock) {
72
- val registration = registrations.remove(listener) ?: return
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 the best enabled provider for [accuracy] (PASSIVE is treated as LOW). The platform gives
85
- * up after about 30 s, so the request is repeated until [timeoutMs] elapses. Returns null on timeout or when no
86
- * provider is enabled.
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
- while (true) {
97
- val enabled = LocationProviders.candidates(lm, accuracy, enabledOnly = true)
98
- if (enabled.isEmpty()) return null
99
- currentLocationFromFirst(lm, enabled)?.let { return it }
100
- delay(CURRENT_LOCATION_RETRY_DELAY_MS)
101
- }
102
- }
103
-
104
- /** Asks the first provider of [providers] that does not refuse with a [SecurityException]. */
105
- private suspend fun currentLocationFromFirst(lm: LocationManager, providers: List<String>): Location? {
106
- var denied: SecurityException? = null
107
- for (provider in providers) {
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
- return currentLocation(lm, provider)
110
- } catch (e: SecurityException) {
111
- denied = e
112
- } catch (e: IllegalArgumentException) {
113
- Logger.w(TAG, "provider $provider rejected getCurrentLocation", e)
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
- if (denied != null) throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", denied)
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
- synchronized(lock) {
180
- for ((listener, current) in registrations.entries.toList()) {
181
- val best = LocationProviders.candidates(lm, current.spec.accuracy, enabledOnly = true).firstOrNull()
182
- if (best == null || best == current.provider) continue
183
- val next = try {
184
- register(lm, current.spec, listener, listOf(best))
185
- } catch (e: TrackingException) {
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
- if (providersReceiver != null) return
197
- val receiver = object : BroadcastReceiver() {
198
- override fun onReceive(context: Context, intent: Intent) = onProvidersChanged()
199
- }
200
- try {
201
- ContextCompat.registerReceiver(
202
- appContext, receiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION),
203
- ContextCompat.RECEIVER_NOT_EXPORTED,
204
- )
205
- providersReceiver = receiver
206
- } catch (e: Exception) {
207
- Logger.w(TAG, "cannot observe location provider changes", e)
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
- /** One platform registration of [listener] on [provider]; stale ones (replaced or removed) drop their fixes. */
222
- private inner class Registration(
223
- val listener: LocationListener,
224
- val spec: LocationRequestSpec,
225
- val provider: String,
226
- ) : LocationListenerCompat {
227
- override fun onLocationChanged(location: Location) = deliver(listOf(location))
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
- override fun onLocationChanged(locations: MutableList<Location>) = deliver(locations)
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
- private fun deliver(locations: List<Location>) {
232
- if (locations.isEmpty() || registrations[listener] !== this) return
233
- try {
234
- listener.onLocations(locations.map(TrackedLocation::from))
235
- } catch (e: Exception) {
236
- Logger.e(TAG, "location listener failed", e)
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. */
@@ -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)