@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.1

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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +107 -0
  4. package/android/consumer-rules.pro +42 -0
  5. package/android/src/main/AndroidManifest.xml +52 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
  7. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
  12. package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
  15. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
  17. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
  20. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
  23. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
  30. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
  34. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
  37. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
  39. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
  40. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
  42. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
  43. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
  46. package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
  51. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
  53. package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
  54. package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
  55. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
  56. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
  57. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
  58. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
  59. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
  60. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
  61. package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
  62. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
  63. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
  64. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
  65. package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
  66. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
  67. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
  68. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
  69. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
  70. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
  71. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
  72. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
  73. package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
  74. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
  75. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
  76. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
  77. package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
  78. package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
  79. package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
  80. package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
  81. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
  82. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
  83. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
  84. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
  85. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
  86. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
  87. package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
  88. package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
  89. package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
  90. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
  91. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
  92. package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
  93. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
  94. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
  95. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
  96. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
  97. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
  98. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
  99. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
  100. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
  101. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
  102. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
  103. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
  104. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
  105. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
  106. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
  107. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
  108. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
  109. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
  110. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
  111. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
  112. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
  113. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
  114. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
  115. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
  116. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
  117. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
  118. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
  119. package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
  120. package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
  121. package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
  122. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
  123. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
  124. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
  125. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
  126. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
  127. package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
  128. package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
  129. package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
  130. package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
  131. package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
  132. package/android/src/main/res/values/lt_strings.xml +11 -0
  133. package/android/src/main/res/xml/lt_file_paths.xml +5 -0
  134. package/dist/esm/definitions.d.ts +719 -0
  135. package/dist/esm/definitions.js +2 -0
  136. package/dist/esm/definitions.js.map +1 -0
  137. package/dist/esm/events.d.ts +50 -0
  138. package/dist/esm/events.js +52 -0
  139. package/dist/esm/events.js.map +1 -0
  140. package/dist/esm/index.d.ts +3 -0
  141. package/dist/esm/index.js +5 -0
  142. package/dist/esm/index.js.map +1 -0
  143. package/dist/esm/plugin.d.ts +3 -0
  144. package/dist/esm/plugin.js +10 -0
  145. package/dist/esm/plugin.js.map +1 -0
  146. package/dist/esm/web/config.d.ts +19 -0
  147. package/dist/esm/web/config.js +243 -0
  148. package/dist/esm/web/config.js.map +1 -0
  149. package/dist/esm/web/device.d.ts +9 -0
  150. package/dist/esm/web/device.js +90 -0
  151. package/dist/esm/web/device.js.map +1 -0
  152. package/dist/esm/web/errors.d.ts +14 -0
  153. package/dist/esm/web/errors.js +19 -0
  154. package/dist/esm/web/errors.js.map +1 -0
  155. package/dist/esm/web/geolocation.d.ts +26 -0
  156. package/dist/esm/web/geolocation.js +117 -0
  157. package/dist/esm/web/geolocation.js.map +1 -0
  158. package/dist/esm/web/location.d.ts +7 -0
  159. package/dist/esm/web/location.js +70 -0
  160. package/dist/esm/web/location.js.map +1 -0
  161. package/dist/esm/web.d.ts +164 -0
  162. package/dist/esm/web.js +328 -0
  163. package/dist/esm/web.js.map +1 -0
  164. package/dist/plugin.cjs.js +945 -0
  165. package/dist/plugin.cjs.js.map +1 -0
  166. package/dist/plugin.js +948 -0
  167. package/dist/plugin.js.map +1 -0
  168. package/package.json +60 -4
@@ -0,0 +1,265 @@
1
+ package com.brickssoft.locationtracking.provider.android
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.BroadcastReceiver
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.IntentFilter
8
+ import android.location.Location
9
+ import android.location.LocationManager
10
+ import android.os.CancellationSignal
11
+ import android.os.Looper
12
+ import androidx.core.content.ContextCompat
13
+ import androidx.core.location.LocationListenerCompat
14
+ import androidx.core.location.LocationManagerCompat
15
+ import androidx.core.location.LocationRequestCompat
16
+ import com.brickssoft.locationtracking.core.ErrorCode
17
+ import com.brickssoft.locationtracking.core.Logger
18
+ import com.brickssoft.locationtracking.core.TrackingException
19
+ import com.brickssoft.locationtracking.model.DesiredAccuracy
20
+ import com.brickssoft.locationtracking.model.ProviderKind
21
+ import com.brickssoft.locationtracking.model.TrackedLocation
22
+ import com.brickssoft.locationtracking.provider.LocationBackend
23
+ import com.brickssoft.locationtracking.provider.LocationListener
24
+ import com.brickssoft.locationtracking.provider.LocationRequestSpec
25
+ import kotlinx.coroutines.delay
26
+ import kotlinx.coroutines.suspendCancellableCoroutine
27
+ import kotlinx.coroutines.withTimeoutOrNull
28
+ import java.util.concurrent.ConcurrentHashMap
29
+ import java.util.concurrent.Executor
30
+ import java.util.concurrent.atomic.AtomicBoolean
31
+ import kotlin.coroutines.resume
32
+ import kotlin.coroutines.resumeWithException
33
+
34
+ /**
35
+ * [LocationBackend] over the platform [LocationManager] (through `LocationManagerCompat`), used when neither GMS nor
36
+ * HMS is available.
37
+ *
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].
43
+ */
44
+ class AndroidLocationBackend(
45
+ context: Context,
46
+ private val locationManager: LocationManager? =
47
+ context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager,
48
+ ) : LocationBackend {
49
+ private val appContext: Context = context.applicationContext ?: context
50
+ override val kind: ProviderKind = ProviderKind.ANDROID
51
+
52
+ /** Serializes (re-)registrations; [registrations] is also read lock-free by the delivery path. */
53
+ private val lock = Any()
54
+ private val registrations = ConcurrentHashMap<LocationListener, Registration>()
55
+ private var providersReceiver: BroadcastReceiver? = null
56
+
57
+ 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")
66
+ }
67
+ }
68
+
69
+ 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
+ }
76
+ }
77
+
78
+ override suspend fun getLastLocation(): TrackedLocation? {
79
+ val lm = locationManager ?: return null
80
+ return LocationProviders.freshestLastKnown(lm)?.let(TrackedLocation::from)
81
+ }
82
+
83
+ /**
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.
87
+ */
88
+ override suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation? {
89
+ val lm = locationManager ?: return null
90
+ val effective = if (accuracy == DesiredAccuracy.PASSIVE) DesiredAccuracy.LOW else accuracy
91
+ return withTimeoutOrNull(timeoutMs.coerceAtLeast(0)) { awaitCurrentLocation(lm, effective) }
92
+ ?.let(TrackedLocation::from)
93
+ }
94
+
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) {
108
+ 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)
114
+ }
115
+ }
116
+ if (denied != null) throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", denied)
117
+ return null
118
+ }
119
+
120
+ @SuppressLint("MissingPermission")
121
+ private suspend fun currentLocation(lm: LocationManager, provider: String): Location? =
122
+ suspendCancellableCoroutine { cont ->
123
+ val signal = CancellationSignal()
124
+ val resumed = AtomicBoolean(false)
125
+ cont.invokeOnCancellation { signal.cancel() }
126
+ try {
127
+ LocationManagerCompat.getCurrentLocation(lm, provider, signal, DIRECT_EXECUTOR) { location ->
128
+ if (resumed.compareAndSet(false, true)) cont.resume(location)
129
+ }
130
+ } catch (e: Exception) {
131
+ if (resumed.compareAndSet(false, true)) cont.resumeWithException(e)
132
+ }
133
+ }
134
+
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. */
177
+ private fun onProvidersChanged() {
178
+ 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
+ }
192
+ }
193
+ }
194
+
195
+ 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)
208
+ }
209
+ }
210
+
211
+ private fun stopObservingProviderChanges() {
212
+ val receiver = providersReceiver ?: return
213
+ providersReceiver = null
214
+ try {
215
+ appContext.unregisterReceiver(receiver)
216
+ } catch (e: Exception) {
217
+ Logger.w(TAG, "unregisterReceiver failed", e)
218
+ }
219
+ }
220
+
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))
228
+
229
+ override fun onLocationChanged(locations: MutableList<Location>) = deliver(locations)
230
+
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)
237
+ }
238
+ }
239
+ }
240
+
241
+ internal companion object {
242
+ private const val TAG = "LT.AndroidLocation"
243
+
244
+ /** Pause between two platform `getCurrentLocation` attempts that returned no fix. */
245
+ const val CURRENT_LOCATION_RETRY_DELAY_MS = 1_000L
246
+
247
+ private val DIRECT_EXECUTOR = Executor { it.run() }
248
+
249
+ /** Maps [spec] to a platform request; values are clamped to what `LocationRequestCompat` accepts. */
250
+ fun requestFor(spec: LocationRequestSpec): LocationRequestCompat {
251
+ val interval = spec.intervalMs.coerceAtLeast(0)
252
+ val distance = spec.distanceFilterM.takeIf { it.isFinite() && it > 0f } ?: 0f
253
+ val quality = when (spec.accuracy) {
254
+ DesiredAccuracy.HIGH -> LocationRequestCompat.QUALITY_HIGH_ACCURACY
255
+ DesiredAccuracy.BALANCED -> LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY
256
+ DesiredAccuracy.LOW, DesiredAccuracy.PASSIVE -> LocationRequestCompat.QUALITY_LOW_POWER
257
+ }
258
+ return LocationRequestCompat.Builder(interval)
259
+ .setQuality(quality)
260
+ .setMinUpdateIntervalMillis(spec.fastestIntervalMs.coerceIn(0, interval))
261
+ .setMinUpdateDistanceMeters(distance)
262
+ .build()
263
+ }
264
+ }
265
+ }
@@ -0,0 +1,31 @@
1
+ package com.brickssoft.locationtracking.provider.android
2
+
3
+ import android.content.Context
4
+ import android.location.LocationManager
5
+ import com.brickssoft.locationtracking.model.ProviderKind
6
+ import com.brickssoft.locationtracking.provider.ActivityBackend
7
+ import com.brickssoft.locationtracking.provider.GeofenceBackend
8
+ import com.brickssoft.locationtracking.provider.LocationBackend
9
+ import com.brickssoft.locationtracking.provider.ProviderBundle
10
+
11
+ /**
12
+ * The platform `LocationManager` backends, the fallback when neither GMS nor HMS is usable. Created reflectively via
13
+ * its public (Context) constructor. Each backend is created once, on first use.
14
+ */
15
+ class AndroidProviderBundle(context: Context) : ProviderBundle {
16
+ private val appContext: Context = context.applicationContext ?: context
17
+ override val kind: ProviderKind = ProviderKind.ANDROID
18
+
19
+ private val location by lazy { AndroidLocationBackend(appContext) }
20
+ private val activity by lazy { AndroidActivityBackend() }
21
+ private val geofence by lazy { AndroidGeofenceBackend(appContext) }
22
+
23
+ /** True if the device has a [LocationManager]. */
24
+ override fun isAvailable(): Boolean = appContext.getSystemService(Context.LOCATION_SERVICE) is LocationManager
25
+
26
+ override fun location(): LocationBackend = location
27
+
28
+ override fun activity(): ActivityBackend = activity
29
+
30
+ override fun geofence(): GeofenceBackend = geofence
31
+ }
@@ -0,0 +1,98 @@
1
+ package com.brickssoft.locationtracking.provider.android
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.location.Location
5
+ import android.location.LocationManager
6
+ import android.os.Build
7
+ import androidx.core.location.LocationManagerCompat
8
+ import com.brickssoft.locationtracking.core.ErrorCode
9
+ import com.brickssoft.locationtracking.core.TrackingException
10
+ import com.brickssoft.locationtracking.model.DesiredAccuracy
11
+
12
+ /** Provider choice and last-known lookups over the platform [LocationManager]. */
13
+ internal object LocationProviders {
14
+ /** `LocationManager.FUSED_PROVIDER` (public since API 31). */
15
+ private const val FUSED_PROVIDER = "fused"
16
+
17
+ /**
18
+ * Providers for [accuracy], most preferred first: HIGH prefers gps, BALANCED/LOW prefer network, each falling back
19
+ * to the other (then `fused` on API 31+); PASSIVE uses only the passive provider.
20
+ */
21
+ fun preferred(accuracy: DesiredAccuracy): List<String> {
22
+ val fused = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) listOf(FUSED_PROVIDER) else emptyList()
23
+ return when (accuracy) {
24
+ DesiredAccuracy.HIGH -> listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER) + fused
25
+ DesiredAccuracy.BALANCED, DesiredAccuracy.LOW ->
26
+ listOf(LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER) + fused
27
+ DesiredAccuracy.PASSIVE -> listOf(LocationManager.PASSIVE_PROVIDER)
28
+ }
29
+ }
30
+
31
+ /**
32
+ * The providers to try for [accuracy], in order: the enabled ones of [preferred], then (unless [enabledOnly]) the
33
+ * existing but disabled ones; a request on a disabled provider starts delivering once it is enabled. If none of
34
+ * them exists on the device, the passive provider is the last resort.
35
+ */
36
+ fun candidates(lm: LocationManager, accuracy: DesiredAccuracy, enabledOnly: Boolean = false): List<String> {
37
+ val existing = preferred(accuracy).filter { exists(lm, it) }
38
+ if (existing.isEmpty()) {
39
+ val passive = LocationManager.PASSIVE_PROVIDER
40
+ return if (accuracy != DesiredAccuracy.PASSIVE && exists(lm, passive)) listOf(passive) else emptyList()
41
+ }
42
+ val (enabled, disabled) = existing.partition { isEnabled(lm, it) }
43
+ return if (enabledOnly) enabled else enabled + disabled
44
+ }
45
+
46
+ fun exists(lm: LocationManager, provider: String): Boolean =
47
+ try {
48
+ LocationManagerCompat.hasProvider(lm, provider)
49
+ } catch (e: Exception) {
50
+ false
51
+ }
52
+
53
+ fun isEnabled(lm: LocationManager, provider: String): Boolean =
54
+ try {
55
+ lm.isProviderEnabled(provider)
56
+ } catch (e: Exception) {
57
+ false
58
+ }
59
+
60
+ /**
61
+ * The freshest `getLastKnownLocation` across the enabled providers, or null if none has a fix.
62
+ *
63
+ * @throws TrackingException [ErrorCode.PERMISSION_DENIED] if every provider refused with a [SecurityException].
64
+ */
65
+ @SuppressLint("MissingPermission")
66
+ fun freshestLastKnown(lm: LocationManager): Location? {
67
+ val providers = try {
68
+ lm.getProviders(true)
69
+ } catch (e: Exception) {
70
+ emptyList<String>()
71
+ }
72
+ var best: Location? = null
73
+ var denied: SecurityException? = null
74
+ var permitted = false
75
+ for (provider in providers) {
76
+ val location = try {
77
+ lm.getLastKnownLocation(provider).also { permitted = true }
78
+ } catch (e: SecurityException) {
79
+ denied = e
80
+ continue
81
+ } catch (e: IllegalArgumentException) {
82
+ continue
83
+ }
84
+ if (location != null && (best == null || isNewer(location, best))) best = location
85
+ }
86
+ if (!permitted && denied != null) {
87
+ throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", denied)
88
+ }
89
+ return best
90
+ }
91
+
92
+ /** Compares on the elapsed-realtime clock when both fixes carry it, else on wall time. */
93
+ fun isNewer(a: Location, b: Location): Boolean {
94
+ val an = a.elapsedRealtimeNanos
95
+ val bn = b.elapsedRealtimeNanos
96
+ return if (an > 0 && bn > 0) an > bn else a.time > b.time
97
+ }
98
+ }
@@ -0,0 +1,80 @@
1
+ package com.brickssoft.locationtracking.provider.gms
2
+
3
+ import android.Manifest
4
+ import android.app.PendingIntent
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.pm.PackageManager
8
+ import android.os.Build
9
+ import androidx.core.content.ContextCompat
10
+ import com.brickssoft.locationtracking.core.Constants
11
+ import com.brickssoft.locationtracking.core.Logger
12
+ import com.brickssoft.locationtracking.model.ProviderKind
13
+ import com.brickssoft.locationtracking.provider.ActivityBackend
14
+ import com.google.android.gms.location.ActivityRecognitionClient
15
+
16
+ /**
17
+ * Activity recognition through `ActivityRecognitionClient.requestActivityUpdates`. Results arrive at
18
+ * [GmsActivityReceiver] through a mutable, explicit broadcast PendingIntent.
19
+ */
20
+ internal class GmsActivityBackend(
21
+ private val context: Context,
22
+ private val client: ActivityRecognitionClient,
23
+ ) : ActivityBackend {
24
+ override val kind: ProviderKind = ProviderKind.GMS
25
+ override val isSupported: Boolean = true
26
+
27
+ /**
28
+ * Returns false if the ACTIVITY_RECOGNITION runtime permission is missing (API 29+) or GMS rejects the call
29
+ * synchronously. The GMS Task completes later, so an asynchronous rejection can only be logged.
30
+ */
31
+ override fun start(intervalMs: Long): Boolean {
32
+ if (!hasPermission()) {
33
+ Logger.w(TAG, "activity updates not requested: ACTIVITY_RECOGNITION permission denied")
34
+ return false
35
+ }
36
+ return try {
37
+ client.requestActivityUpdates(intervalMs.coerceAtLeast(0), pendingIntent(context))
38
+ .logFailure(TAG, "requestActivityUpdates")
39
+ Logger.d(TAG, "activity updates requested every $intervalMs ms")
40
+ true
41
+ } catch (e: Exception) {
42
+ Logger.w(TAG, "requestActivityUpdates failed", e)
43
+ false
44
+ }
45
+ }
46
+
47
+ /** Removes the updates if their PendingIntent exists (GMS keeps it alive while registered, across restarts). */
48
+ override fun stop() {
49
+ try {
50
+ val pending = existingPendingIntent(context) ?: return
51
+ client.removeActivityUpdates(pending).logFailure(TAG, "removeActivityUpdates")
52
+ } catch (e: Exception) {
53
+ Logger.w(TAG, "removeActivityUpdates failed", e)
54
+ }
55
+ }
56
+
57
+ private fun hasPermission(): Boolean =
58
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
59
+ ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
60
+ PackageManager.PERMISSION_GRANTED
61
+
62
+ companion object {
63
+ private const val TAG = "LT.GmsActivity"
64
+
65
+ /** Explicit broadcast to [GmsActivityReceiver]; mutable because GMS fills in the result. */
66
+ fun pendingIntent(context: Context): PendingIntent =
67
+ PendingIntent.getBroadcast(context, Constants.RC_GMS_ACTIVITY, intent(context), Constants.piMutable())
68
+
69
+ /** The PendingIntent of [pendingIntent] if it exists, without creating it. */
70
+ fun existingPendingIntent(context: Context): PendingIntent? = PendingIntent.getBroadcast(
71
+ context,
72
+ Constants.RC_GMS_ACTIVITY,
73
+ intent(context),
74
+ Constants.piMutable() or PendingIntent.FLAG_NO_CREATE,
75
+ )
76
+
77
+ private fun intent(context: Context): Intent =
78
+ Intent(context, GmsActivityReceiver::class.java).setAction(Constants.ACTION_ACTIVITY)
79
+ }
80
+ }
@@ -0,0 +1,62 @@
1
+ package com.brickssoft.locationtracking.provider.gms
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import com.brickssoft.locationtracking.core.Components
7
+ import com.brickssoft.locationtracking.core.Logger
8
+ import com.brickssoft.locationtracking.model.ActivitySample
9
+ import com.brickssoft.locationtracking.model.ActivityType
10
+ import com.brickssoft.locationtracking.provider.ActivitySink
11
+ import com.google.android.gms.location.ActivityRecognitionResult
12
+ import com.google.android.gms.location.DetectedActivity
13
+
14
+ /**
15
+ * Receives `ActivityRecognitionResult`s from the PendingIntent of [GmsActivityBackend] and delivers them to the
16
+ * engine ([ActivitySink]) with `goAsync()` on `Components.scope`.
17
+ */
18
+ class GmsActivityReceiver internal constructor(
19
+ private val target: (Context) -> ReceiverTarget<ActivitySink>,
20
+ ) : BroadcastReceiver() {
21
+ constructor() : this({ ctx -> Components.get(ctx).let { ReceiverTarget(it.scope, it.engine) } })
22
+
23
+ override fun onReceive(context: Context, intent: Intent) {
24
+ val samples = parseActivitySamples(intent)
25
+ if (samples.isEmpty()) return
26
+ Logger.v(TAG, "activity: $samples")
27
+ deliverAsync(context, target, TAG, "activity delivery") { it.onActivitySamples(samples) }
28
+ }
29
+
30
+ internal companion object {
31
+ private const val TAG = "LT.GmsActivity"
32
+
33
+ /** The samples in [intent], most probable first; empty if it carries no result. */
34
+ fun parseActivitySamples(intent: Intent?): List<ActivitySample> {
35
+ val result = try {
36
+ intent?.let { ActivityRecognitionResult.extractResult(it) }
37
+ } catch (e: Exception) {
38
+ Logger.w(TAG, "unreadable activity result", e)
39
+ null
40
+ } ?: return emptyList()
41
+ return toActivitySamples(result.probableActivities.orEmpty())
42
+ }
43
+
44
+ /** Maps GMS activities, keeping the highest confidence per [ActivityType], sorted by confidence descending. */
45
+ fun toActivitySamples(activities: List<DetectedActivity>): List<ActivitySample> = activities
46
+ .map { ActivitySample(mapActivityType(it.type), it.confidence.coerceIn(0, 100)) }
47
+ .groupBy { it.type }
48
+ .map { (_, samples) -> samples.maxBy { it.confidence } }
49
+ .sortedByDescending { it.confidence }
50
+
51
+ /** TILTING, UNKNOWN and any future type map to [ActivityType.UNKNOWN]. */
52
+ fun mapActivityType(type: Int): ActivityType = when (type) {
53
+ DetectedActivity.IN_VEHICLE -> ActivityType.IN_VEHICLE
54
+ DetectedActivity.ON_BICYCLE -> ActivityType.ON_BICYCLE
55
+ DetectedActivity.ON_FOOT -> ActivityType.ON_FOOT
56
+ DetectedActivity.WALKING -> ActivityType.WALKING
57
+ DetectedActivity.RUNNING -> ActivityType.RUNNING
58
+ DetectedActivity.STILL -> ActivityType.STILL
59
+ else -> ActivityType.UNKNOWN
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,72 @@
1
+ package com.brickssoft.locationtracking.provider.gms
2
+
3
+ import com.brickssoft.locationtracking.core.ErrorCode
4
+ import com.brickssoft.locationtracking.core.TrackingException
5
+ import com.google.android.gms.common.api.ApiException
6
+ import com.google.android.gms.common.api.CommonStatusCodes
7
+ import com.google.android.gms.location.GeofenceStatusCodes
8
+
9
+ /** Maps Google Play services failures to [TrackingException]. */
10
+ internal object GmsErrors {
11
+ /**
12
+ * - [TrackingException] is returned unchanged.
13
+ * - [SecurityException] and `GEOFENCE_INSUFFICIENT_LOCATION_PERMISSION` become PERMISSION_DENIED.
14
+ * - `GEOFENCE_TOO_MANY_GEOFENCES` and `GEOFENCE_TOO_MANY_PENDING_INTENTS` become TOO_MANY_GEOFENCES.
15
+ * - `TIMEOUT` becomes TIMEOUT; `DEVELOPER_ERROR` and `INTERNAL_ERROR` become INTERNAL.
16
+ * - Every other [ApiException] (e.g. `GEOFENCE_NOT_AVAILABLE`, `API_NOT_CONNECTED`) and [IllegalStateException]
17
+ * (GMS "not connected") become UNAVAILABLE.
18
+ * - [IllegalArgumentException] (rejected by a GMS builder) becomes INVALID_ARGUMENT; anything else INTERNAL.
19
+ */
20
+ fun map(error: Throwable, operation: String): TrackingException = when (error) {
21
+ is TrackingException -> error
22
+ is SecurityException -> TrackingException(
23
+ ErrorCode.PERMISSION_DENIED,
24
+ "$operation: location permission denied",
25
+ error,
26
+ )
27
+ is ApiException -> TrackingException(
28
+ codeFor(error.statusCode),
29
+ "$operation failed: ${describe(error.statusCode)}",
30
+ error,
31
+ )
32
+ is IllegalArgumentException -> TrackingException(
33
+ ErrorCode.INVALID_ARGUMENT,
34
+ "$operation: ${error.message ?: "invalid argument"}",
35
+ error,
36
+ )
37
+ is IllegalStateException -> TrackingException(
38
+ ErrorCode.UNAVAILABLE,
39
+ "$operation failed: ${reason(error)}",
40
+ error,
41
+ )
42
+ else -> TrackingException(ErrorCode.INTERNAL, "$operation failed: ${reason(error)}", error)
43
+ }
44
+
45
+ /** The [ErrorCode] for a GMS status code (see [map]). */
46
+ fun codeFor(statusCode: Int): ErrorCode = when (statusCode) {
47
+ GeofenceStatusCodes.GEOFENCE_INSUFFICIENT_LOCATION_PERMISSION -> ErrorCode.PERMISSION_DENIED
48
+ GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES,
49
+ GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS,
50
+ -> ErrorCode.TOO_MANY_GEOFENCES
51
+ CommonStatusCodes.TIMEOUT -> ErrorCode.TIMEOUT
52
+ CommonStatusCodes.DEVELOPER_ERROR, CommonStatusCodes.INTERNAL_ERROR -> ErrorCode.INTERNAL
53
+ else -> ErrorCode.UNAVAILABLE
54
+ }
55
+
56
+ /** E.g. `GEOFENCE_TOO_MANY_GEOFENCES (1001)`, with a hint for `GEOFENCE_NOT_AVAILABLE`. */
57
+ fun describe(statusCode: Int): String {
58
+ val name = try {
59
+ GeofenceStatusCodes.getStatusCodeString(statusCode)
60
+ } catch (e: Exception) {
61
+ "status"
62
+ }
63
+ val hint = if (statusCode == GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE) {
64
+ "; location is off or Google Location Accuracy is disabled"
65
+ } else {
66
+ ""
67
+ }
68
+ return "$name ($statusCode)$hint"
69
+ }
70
+
71
+ private fun reason(error: Throwable): String = error.message ?: error::class.java.name
72
+ }