@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +106 -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 +59 -4
@@ -0,0 +1,71 @@
1
+ package com.brickssoft.locationtracking.provider.hms
2
+
3
+ import android.Manifest
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import androidx.core.content.ContextCompat
8
+ import com.brickssoft.locationtracking.core.Logger
9
+ import com.brickssoft.locationtracking.model.ProviderKind
10
+ import com.brickssoft.locationtracking.provider.ActivityBackend
11
+ import com.huawei.hmf.tasks.TaskExecutors
12
+ import com.huawei.hms.location.ActivityIdentification
13
+ import com.huawei.hms.location.ActivityIdentificationService
14
+
15
+ /**
16
+ * [ActivityBackend] over HMS activity identification. Results arrive as broadcasts to [HmsActivityReceiver] through
17
+ * [HmsPendingIntents.activity].
18
+ *
19
+ * `start` returns false when `ACTIVITY_RECOGNITION` is missing (API 29+) or the request cannot be submitted; an
20
+ * asynchronous HMS failure is only logged. The service is created lazily, on first use.
21
+ */
22
+ internal class HmsActivityBackend(
23
+ private val context: Context,
24
+ serviceProvider: () -> ActivityIdentificationService = { ActivityIdentification.getService(context) },
25
+ ) : ActivityBackend {
26
+ override val kind: ProviderKind = ProviderKind.HMS
27
+ override val isSupported: Boolean = true
28
+
29
+ private val service: ActivityIdentificationService by lazy(serviceProvider)
30
+
31
+ override fun start(intervalMs: Long): Boolean {
32
+ if (!hasActivityPermission()) {
33
+ Logger.w(TAG, "activity identification not started: ACTIVITY_RECOGNITION permission is missing")
34
+ return false
35
+ }
36
+ return try {
37
+ service.createActivityIdentificationUpdates(intervalMs.coerceAtLeast(0L), HmsPendingIntents.activity(context))
38
+ .addOnFailureListener(TaskExecutors.immediate()) { e ->
39
+ Logger.w(TAG, "createActivityIdentificationUpdates failed: ${describeHmsError(e)}", e)
40
+ }
41
+ true
42
+ } catch (e: Exception) {
43
+ Logger.e(TAG, "createActivityIdentificationUpdates failed: ${describeHmsError(e)}", e)
44
+ false
45
+ }
46
+ }
47
+
48
+ override fun stop() {
49
+ try {
50
+ service.deleteActivityIdentificationUpdates(HmsPendingIntents.activity(context))
51
+ .addOnFailureListener(TaskExecutors.immediate()) { e ->
52
+ Logger.d(TAG, "deleteActivityIdentificationUpdates failed: ${describeHmsError(e)}")
53
+ }
54
+ } catch (e: Exception) {
55
+ Logger.w(TAG, "deleteActivityIdentificationUpdates failed: ${describeHmsError(e)}", e)
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Below API 29 the permission is `com.huawei.hms.permission.ACTIVITY_RECOGNITION`, which the plugin treats as
61
+ * granted (architecture §7.6); HMS reports a real denial through the task, which is logged.
62
+ */
63
+ private fun hasActivityPermission(): Boolean =
64
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
65
+ ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
66
+ PackageManager.PERMISSION_GRANTED
67
+
68
+ private companion object {
69
+ const val TAG = "LT.HmsActivity"
70
+ }
71
+ }
@@ -0,0 +1,57 @@
1
+ package com.brickssoft.locationtracking.provider.hms
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.huawei.hms.location.ActivityIdentificationData
11
+ import com.huawei.hms.location.ActivityIdentificationResponse
12
+
13
+ /**
14
+ * Delivers HMS activity-identification results to the engine (`Components.get(ctx).engine`, an `ActivitySink`).
15
+ * Declared in the manifest; it only fires for the PendingIntent registered by [HmsActivityBackend].
16
+ */
17
+ class HmsActivityReceiver : BroadcastReceiver() {
18
+ override fun onReceive(context: Context, intent: Intent) {
19
+ val samples = parseActivitySamples(intent)
20
+ if (samples.isEmpty()) return
21
+ val components = Components.get(context)
22
+ deliverAsync(goAsync(), components.scope, TAG, "activity samples") {
23
+ components.engine.onActivitySamples(samples)
24
+ }
25
+ }
26
+
27
+ internal companion object {
28
+ private const val TAG = "LT.HmsActivityReceiver"
29
+
30
+ /** Samples in [intent], most probable first; empty if it carries no HMS activity result. */
31
+ fun parseActivitySamples(intent: Intent?): List<ActivitySample> {
32
+ val response = try {
33
+ if (intent == null) null else ActivityIdentificationResponse.getDataFromIntent(intent)
34
+ } catch (e: Exception) {
35
+ Logger.w(TAG, "unreadable activity identification result", e)
36
+ null
37
+ } ?: return emptyList()
38
+ return response.activityIdentificationDatas.orEmpty()
39
+ .mapNotNull { data -> data?.let(::toActivitySample) }
40
+ .sortedByDescending { it.confidence }
41
+ }
42
+
43
+ /** Maps one HMS identification; unknown types become [ActivityType.UNKNOWN]. Confidence is clamped to 0..100. */
44
+ fun toActivitySample(data: ActivityIdentificationData): ActivitySample =
45
+ ActivitySample(activityType(data.identificationActivity), data.possibility.coerceIn(0, 100))
46
+
47
+ fun activityType(hmsType: Int): ActivityType = when (hmsType) {
48
+ ActivityIdentificationData.VEHICLE -> ActivityType.IN_VEHICLE
49
+ ActivityIdentificationData.BIKE -> ActivityType.ON_BICYCLE
50
+ ActivityIdentificationData.FOOT -> ActivityType.ON_FOOT
51
+ ActivityIdentificationData.WALKING -> ActivityType.WALKING
52
+ ActivityIdentificationData.RUNNING -> ActivityType.RUNNING
53
+ ActivityIdentificationData.STILL -> ActivityType.STILL
54
+ else -> ActivityType.UNKNOWN // OTHERS and types added by newer HMS versions
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,76 @@
1
+ package com.brickssoft.locationtracking.provider.hms
2
+
3
+ import com.brickssoft.locationtracking.core.ErrorCode
4
+ import com.brickssoft.locationtracking.core.TrackingException
5
+ import com.huawei.hms.common.ApiException
6
+ import com.huawei.hms.location.GeofenceErrorCodes
7
+
8
+ /**
9
+ * HMS Location Kit status codes used for error mapping. The values mirror
10
+ * `com.huawei.hms.support.api.location.common.exception.LocationStatusCode` (shipped in a transitive artifact, so
11
+ * they are copied here) and [GeofenceErrorCodes].
12
+ */
13
+ internal object HmsStatusCodes {
14
+ const val ARGUMENTS_EMPTY = 10100
15
+ const val ARGUMENTS_INVALID = 10101
16
+ const val NETWORK_LOCATION_SERVICES_DISABLED = 10105
17
+ const val LOCATION_ENABLE_OFF = 10106
18
+ const val PARAM_ERROR_EMPTY = 10801
19
+ const val PARAM_ERROR_INVALID = 10802
20
+ const val PERMISSION_DENIED = 10803
21
+ const val NO_PRECISE_LOCATION_PERMISSION = 10809
22
+
23
+ /** Like GMS `GEOFENCE_NOT_AVAILABLE`: typically location was switched off. */
24
+ const val GEOFENCE_UNAVAILABLE = GeofenceErrorCodes.GEOFENCE_UNAVAILABLE
25
+ const val GEOFENCE_NUMBER_OVER_LIMIT = GeofenceErrorCodes.GEOFENCE_NUMBER_OVER_LIMIT
26
+ const val GEOFENCE_PENDINGINTENT_OVER_LIMIT = GeofenceErrorCodes.GEOFENCE_PENDINGINTENT_OVER_LIMIT
27
+ const val GEOFENCE_INSUFFICIENT_PERMISSION = GeofenceErrorCodes.GEOFENCE_INSUFFICIENT_PERMISSION
28
+ }
29
+
30
+ /**
31
+ * Maps an HMS status code to the plugin's [ErrorCode]:
32
+ * permission errors → [ErrorCode.PERMISSION_DENIED], geofence limits → [ErrorCode.TOO_MANY_GEOFENCES],
33
+ * location (or network location, which geofencing needs) switched off → [ErrorCode.LOCATION_DISABLED],
34
+ * argument errors → [ErrorCode.INVALID_ARGUMENT],
35
+ * everything else (HMS Core missing or outdated, AppGallery Connect errors, …) → [ErrorCode.UNAVAILABLE].
36
+ */
37
+ internal fun errorCodeForStatus(statusCode: Int): ErrorCode = when (statusCode) {
38
+ HmsStatusCodes.PERMISSION_DENIED,
39
+ HmsStatusCodes.NO_PRECISE_LOCATION_PERMISSION,
40
+ HmsStatusCodes.GEOFENCE_INSUFFICIENT_PERMISSION,
41
+ -> ErrorCode.PERMISSION_DENIED
42
+
43
+ HmsStatusCodes.GEOFENCE_NUMBER_OVER_LIMIT,
44
+ HmsStatusCodes.GEOFENCE_PENDINGINTENT_OVER_LIMIT,
45
+ -> ErrorCode.TOO_MANY_GEOFENCES
46
+
47
+ HmsStatusCodes.LOCATION_ENABLE_OFF,
48
+ HmsStatusCodes.NETWORK_LOCATION_SERVICES_DISABLED,
49
+ HmsStatusCodes.GEOFENCE_UNAVAILABLE,
50
+ -> ErrorCode.LOCATION_DISABLED
51
+
52
+ HmsStatusCodes.ARGUMENTS_EMPTY,
53
+ HmsStatusCodes.ARGUMENTS_INVALID,
54
+ HmsStatusCodes.PARAM_ERROR_EMPTY,
55
+ HmsStatusCodes.PARAM_ERROR_INVALID,
56
+ -> ErrorCode.INVALID_ARGUMENT
57
+
58
+ else -> ErrorCode.UNAVAILABLE
59
+ }
60
+
61
+ /** Converts a failure of an HMS call into the only exception type that crosses component boundaries. */
62
+ internal fun Throwable.toTrackingException(operation: String): TrackingException = when (this) {
63
+ is TrackingException -> this
64
+ is SecurityException -> TrackingException(ErrorCode.PERMISSION_DENIED, "$operation: location permission denied", this)
65
+ is ApiException -> TrackingException(errorCodeForStatus(statusCode), "$operation failed: ${describeHmsError(this)}", this)
66
+ is IllegalArgumentException -> TrackingException(ErrorCode.INVALID_ARGUMENT, "$operation: ${message ?: "invalid argument"}", this)
67
+ else -> TrackingException(ErrorCode.UNAVAILABLE, "$operation failed: ${describeHmsError(this)}", this)
68
+ }
69
+
70
+ /** Short description for logs and messages, including the HMS status code when there is one. */
71
+ internal fun describeHmsError(t: Throwable): String =
72
+ if (t is ApiException) {
73
+ "HMS status ${t.message ?: t.statusCode}" // ApiException's message is "<code>:<status message>"
74
+ } else {
75
+ "${t.javaClass.simpleName}: ${t.message}"
76
+ }
@@ -0,0 +1,116 @@
1
+ package com.brickssoft.locationtracking.provider.hms
2
+
3
+ import android.content.Context
4
+ import com.brickssoft.locationtracking.core.Logger
5
+ import com.brickssoft.locationtracking.model.ProviderKind
6
+ import com.brickssoft.locationtracking.provider.GeofenceBackend
7
+ import com.brickssoft.locationtracking.provider.OsGeofence
8
+ import com.huawei.hmf.tasks.TaskExecutors
9
+ import com.huawei.hms.location.Geofence
10
+ import com.huawei.hms.location.GeofenceRequest
11
+ import com.huawei.hms.location.GeofenceService
12
+ import com.huawei.hms.location.LocationServices
13
+
14
+ /**
15
+ * [GeofenceBackend] over the HMS geofence service. Transitions arrive as broadcasts to [HmsGeofenceReceiver]
16
+ * through [HmsPendingIntents.geofence].
17
+ *
18
+ * - Regions are circles that never expire; ENTER/EXIT/DWELL map to HMS conversions, and DWELL uses the region's
19
+ * loitering delay.
20
+ * - `initialTriggerEntry` maps to the request's init conversions (ENTER and DWELL), so regions are sent in one
21
+ * request per distinct value; if a later request fails, the earlier ones of the same call are deleted again.
22
+ * Duplicate ids within one call keep the last region.
23
+ * - A region with no transition to report is skipped: HMS rejects geofences without conversions.
24
+ * - Every failure is thrown as a `TrackingException` (see [toTrackingException]).
25
+ *
26
+ * The service is created lazily, on first use.
27
+ */
28
+ internal class HmsGeofenceBackend(
29
+ private val context: Context,
30
+ serviceProvider: () -> GeofenceService = { LocationServices.getGeofenceService(context) },
31
+ ) : GeofenceBackend {
32
+ override val kind: ProviderKind = ProviderKind.HMS
33
+ override val supportsDwell: Boolean = true
34
+
35
+ private val service: GeofenceService by lazy(serviceProvider)
36
+
37
+ override suspend fun add(regions: List<OsGeofence>) {
38
+ val (unique, silent) = regions.associateBy { it.id }.values.partition { hmsConversions(it) != 0 }
39
+ for (region in silent) Logger.w(TAG, "geofence '${region.id}' has no transition to report; not registered")
40
+ if (unique.isEmpty()) return
41
+ val registered = ArrayList<String>(unique.size)
42
+ for ((initConversions, group) in unique.groupBy(::hmsInitConversions)) {
43
+ try {
44
+ hmsCall("createGeofenceList") {
45
+ val request = GeofenceRequest.Builder()
46
+ .createGeofenceList(group.map(::buildHmsGeofence))
47
+ .setInitConversions(initConversions)
48
+ .setCoordinateType(GeofenceRequest.COORDINATE_TYPE_WGS_84)
49
+ .build()
50
+ service.createGeofenceList(request, HmsPendingIntents.geofence(context)).await()
51
+ }
52
+ } catch (e: Exception) {
53
+ rollBack(registered)
54
+ throw e
55
+ }
56
+ group.mapTo(registered) { it.id }
57
+ }
58
+ Logger.d(TAG, "registered ${registered.size} geofence(s)")
59
+ }
60
+
61
+ /** Best effort: a failed `add` must not leave the earlier requests of the same call registered. */
62
+ private fun rollBack(ids: List<String>) {
63
+ if (ids.isEmpty()) return
64
+ try {
65
+ service.deleteGeofenceList(ids).addOnFailureListener(TaskExecutors.immediate()) { e ->
66
+ Logger.w(TAG, "rolling back ${ids.size} geofence(s) failed: ${describeHmsError(e)}", e)
67
+ }
68
+ } catch (e: Exception) {
69
+ Logger.w(TAG, "rolling back ${ids.size} geofence(s) failed: ${describeHmsError(e)}", e)
70
+ }
71
+ }
72
+
73
+ override suspend fun remove(ids: List<String>) {
74
+ val unique = ids.distinct()
75
+ if (unique.isEmpty()) return
76
+ hmsCall("deleteGeofenceList") { service.deleteGeofenceList(unique).await() }
77
+ }
78
+
79
+ override suspend fun removeAll() {
80
+ hmsCall("deleteGeofenceList") { service.deleteGeofenceList(HmsPendingIntents.geofence(context)).await() }
81
+ }
82
+
83
+ private companion object {
84
+ const val TAG = "LT.HmsGeofence"
85
+ }
86
+ }
87
+
88
+ /** HMS conversion flags of a region (0 if it reports nothing). */
89
+ internal fun hmsConversions(region: OsGeofence): Int {
90
+ var flags = 0
91
+ if (region.onEntry) flags = flags or Geofence.ENTER_GEOFENCE_CONVERSION
92
+ if (region.onExit) flags = flags or Geofence.EXIT_GEOFENCE_CONVERSION
93
+ if (region.onDwell) flags = flags or Geofence.DWELL_GEOFENCE_CONVERSION
94
+ return flags
95
+ }
96
+
97
+ /**
98
+ * Init conversions of the request that carries [region]: fire ENTER (and DWELL after the loitering delay) if the
99
+ * device is already inside when the region is registered, or nothing when `initialTriggerEntry` is off.
100
+ */
101
+ internal fun hmsInitConversions(region: OsGeofence): Int =
102
+ if (region.initialTriggerEntry) {
103
+ GeofenceRequest.ENTER_INIT_CONVERSION or GeofenceRequest.DWELL_INIT_CONVERSION
104
+ } else {
105
+ 0
106
+ }
107
+
108
+ /** Builds the HMS geofence for [region]. */
109
+ internal fun buildHmsGeofence(region: OsGeofence): Geofence =
110
+ Geofence.Builder()
111
+ .setUniqueId(region.id)
112
+ .setRoundArea(region.latitude, region.longitude, region.radius)
113
+ .setConversions(hmsConversions(region))
114
+ .setDwellDelayTime(region.loiteringDelayMs.coerceAtLeast(0))
115
+ .setValidContinueTime(Geofence.GEOFENCE_NEVER_EXPIRE)
116
+ .build()
@@ -0,0 +1,67 @@
1
+ package com.brickssoft.locationtracking.provider.hms
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.GeofenceAction
9
+ import com.brickssoft.locationtracking.provider.OsGeofenceTransition
10
+ import com.huawei.hms.location.Geofence
11
+ import com.huawei.hms.location.GeofenceData
12
+
13
+ /**
14
+ * Delivers HMS geofence transitions to the GeofenceManager (`Components.get(ctx).geofences`, a
15
+ * `GeofenceTransitionSink`). Declared in the manifest; it only fires for the PendingIntent registered by
16
+ * [HmsGeofenceBackend].
17
+ */
18
+ class HmsGeofenceReceiver : BroadcastReceiver() {
19
+ override fun onReceive(context: Context, intent: Intent) {
20
+ val transitions = parseGeofenceTransitions(intent)
21
+ if (transitions.isEmpty()) return
22
+ val components = Components.get(context)
23
+ deliverAsync(goAsync(), components.scope, TAG, "geofence transitions") {
24
+ components.geofences.onGeofenceTransitions(transitions)
25
+ }
26
+ }
27
+
28
+ internal companion object {
29
+ private const val TAG = "LT.HmsGeofenceReceiver"
30
+
31
+ /** Transitions in [intent]; empty if it carries no HMS geofence event or reports an error. */
32
+ fun parseGeofenceTransitions(intent: Intent?): List<OsGeofenceTransition> {
33
+ val data = try {
34
+ GeofenceData.getDataFromIntent(intent)
35
+ } catch (e: Exception) {
36
+ Logger.w(TAG, "unreadable geofence event", e)
37
+ null
38
+ } ?: return emptyList()
39
+ return toTransitions(data)
40
+ }
41
+
42
+ /** One transition per distinct geofence id, all with the event's action and triggering fix. */
43
+ fun toTransitions(data: GeofenceData): List<OsGeofenceTransition> {
44
+ if (data.isFailure) {
45
+ Logger.w(TAG, "HMS geofence error ${data.errorCode}")
46
+ return emptyList()
47
+ }
48
+ val action = geofenceAction(data.conversion)
49
+ if (action == null) {
50
+ Logger.w(TAG, "ignoring geofence event with conversion ${data.conversion}")
51
+ return emptyList()
52
+ }
53
+ val location = data.convertingLocation?.let(::toTrackedLocation)
54
+ return data.convertingGeofenceList.orEmpty()
55
+ .mapNotNull { it?.uniqueId }
56
+ .distinct()
57
+ .map { id -> OsGeofenceTransition(id, action, location) }
58
+ }
59
+
60
+ fun geofenceAction(conversion: Int): GeofenceAction? = when (conversion) {
61
+ Geofence.ENTER_GEOFENCE_CONVERSION -> GeofenceAction.ENTER
62
+ Geofence.EXIT_GEOFENCE_CONVERSION -> GeofenceAction.EXIT
63
+ Geofence.DWELL_GEOFENCE_CONVERSION -> GeofenceAction.DWELL
64
+ else -> null
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,83 @@
1
+ package com.brickssoft.locationtracking.provider.hms
2
+
3
+ import android.app.PendingIntent
4
+ import android.content.BroadcastReceiver
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import com.brickssoft.locationtracking.core.Constants
8
+ import com.brickssoft.locationtracking.core.Logger
9
+ import kotlinx.coroutines.CoroutineScope
10
+ import kotlinx.coroutines.Job
11
+ import kotlinx.coroutines.delay
12
+ import kotlinx.coroutines.launch
13
+ import java.util.concurrent.atomic.AtomicBoolean
14
+ import kotlin.coroutines.cancellation.CancellationException
15
+
16
+ /**
17
+ * PendingIntents handed to HMS. They are explicit broadcasts to our receivers and mutable (API 31+), because HMS
18
+ * fills in the result extras. Creating one again returns the same PendingIntent, which is what the delete calls need.
19
+ */
20
+ internal object HmsPendingIntents {
21
+ fun activity(context: Context): PendingIntent = PendingIntent.getBroadcast(
22
+ context,
23
+ Constants.RC_HMS_ACTIVITY,
24
+ Intent(context, HmsActivityReceiver::class.java).setAction(Constants.ACTION_ACTIVITY),
25
+ Constants.piMutable(),
26
+ )
27
+
28
+ fun geofence(context: Context): PendingIntent = PendingIntent.getBroadcast(
29
+ context,
30
+ Constants.RC_HMS_GEOFENCE,
31
+ Intent(context, HmsGeofenceReceiver::class.java).setAction(Constants.ACTION_GEOFENCE),
32
+ Constants.piMutable(),
33
+ )
34
+ }
35
+
36
+ /** Receivers must finish within about 10 s (architecture §7.12); the broadcast is released before that. */
37
+ internal const val BROADCAST_FINISH_DEADLINE_MS = 9_000L
38
+
39
+ /**
40
+ * Runs [block] on [scope] for a broadcast kept alive with `goAsync()`. Failures are logged, never rethrown.
41
+ *
42
+ * [pending] is finished exactly once: when the job completes (including when it fails or is cancelled before it
43
+ * starts), or after [finishAfterMs] if it is still running. In that case the work goes on without holding the
44
+ * broadcast, so a slow sink cannot trigger a broadcast timeout.
45
+ */
46
+ internal fun deliverAsync(
47
+ pending: BroadcastReceiver.PendingResult?,
48
+ scope: CoroutineScope,
49
+ tag: String,
50
+ what: String,
51
+ finishAfterMs: Long = BROADCAST_FINISH_DEADLINE_MS,
52
+ block: suspend () -> Unit,
53
+ ): Job {
54
+ val finished = AtomicBoolean(false)
55
+ fun finishOnce() {
56
+ if (!finished.compareAndSet(false, true)) return
57
+ try {
58
+ pending?.finish()
59
+ } catch (e: Exception) {
60
+ Logger.w(tag, "finishing the broadcast failed", e)
61
+ }
62
+ }
63
+
64
+ val job = scope.launch {
65
+ try {
66
+ block()
67
+ } catch (e: CancellationException) {
68
+ throw e
69
+ } catch (e: Exception) {
70
+ Logger.e(tag, "failed to deliver $what", e)
71
+ }
72
+ }
73
+ val deadline = scope.launch {
74
+ delay(finishAfterMs)
75
+ Logger.w(tag, "delivering $what takes longer than $finishAfterMs ms; releasing the broadcast")
76
+ finishOnce()
77
+ }
78
+ job.invokeOnCompletion {
79
+ deadline.cancel()
80
+ finishOnce()
81
+ }
82
+ return job
83
+ }
@@ -0,0 +1,197 @@
1
+ package com.brickssoft.locationtracking.provider.hms
2
+
3
+ import android.content.Context
4
+ import android.location.Location
5
+ import android.os.Looper
6
+ import com.brickssoft.locationtracking.core.Logger
7
+ import com.brickssoft.locationtracking.model.DesiredAccuracy
8
+ import com.brickssoft.locationtracking.model.ProviderKind
9
+ import com.brickssoft.locationtracking.model.TrackedLocation
10
+ import com.brickssoft.locationtracking.provider.LocationBackend
11
+ import com.brickssoft.locationtracking.provider.LocationListener
12
+ import com.brickssoft.locationtracking.provider.LocationRequestSpec
13
+ import com.huawei.hmf.tasks.TaskExecutors
14
+ import com.huawei.hms.location.FusedLocationProviderClient
15
+ import com.huawei.hms.location.LocationCallback
16
+ import com.huawei.hms.location.LocationRequest
17
+ import com.huawei.hms.location.LocationResult
18
+ import com.huawei.hms.location.LocationServices
19
+ import kotlinx.coroutines.CompletableDeferred
20
+ import kotlinx.coroutines.withTimeoutOrNull
21
+
22
+ /**
23
+ * [LocationBackend] over the HMS fused location client.
24
+ *
25
+ * - Each listener gets its own [LocationCallback] on the main looper; requesting again with the same listener
26
+ * replaces its request, and late fixes from a replaced callback are dropped.
27
+ * - `getCurrentLocation` is a one-shot request (`numUpdates = 1`) whose callback is always removed.
28
+ * - Permission failures surface as `TrackingException(PERMISSION_DENIED)`; other HMS failures are mapped by
29
+ * [toTrackingException].
30
+ *
31
+ * The client is created lazily, on first use.
32
+ */
33
+ internal class HmsLocationBackend(
34
+ clientProvider: () -> FusedLocationProviderClient,
35
+ private val looperProvider: () -> Looper = { Looper.getMainLooper() },
36
+ ) : LocationBackend {
37
+ constructor(context: Context) : this({ LocationServices.getFusedLocationProviderClient(context) })
38
+
39
+ override val kind: ProviderKind = ProviderKind.HMS
40
+
41
+ private val client: FusedLocationProviderClient by lazy(clientProvider)
42
+
43
+ /**
44
+ * Active callback per listener. Guarded by [lock], which is also held across the HMS request/remove calls so a
45
+ * concurrent request/remove for the same listener cannot leave an orphaned HMS registration. Those calls only
46
+ * submit work and return a task; callbacks never take the lock (they check [ListenerCallback.active]).
47
+ */
48
+ private val callbacks = HashMap<LocationListener, ListenerCallback>()
49
+ private val lock = Any()
50
+
51
+ override fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener) {
52
+ val request = buildLocationRequest(spec)
53
+ synchronized(lock) {
54
+ callbacks.remove(listener)?.let(::deactivateAndRemove)
55
+ val callback = ListenerCallback(listener)
56
+ callbacks[listener] = callback
57
+ try {
58
+ client.requestLocationUpdates(request, callback, looperProvider())
59
+ .addOnFailureListener(TaskExecutors.immediate()) { e ->
60
+ Logger.e(TAG, "requestLocationUpdates failed: ${describeHmsError(e)}", e)
61
+ forget(callback)
62
+ }
63
+ } catch (e: Exception) {
64
+ Logger.e(TAG, "requestLocationUpdates failed: ${describeHmsError(e)}", e)
65
+ forget(callback)
66
+ }
67
+ }
68
+ }
69
+
70
+ override fun removeUpdates(listener: LocationListener) {
71
+ synchronized(lock) { callbacks.remove(listener)?.let(::deactivateAndRemove) }
72
+ }
73
+
74
+ override suspend fun getLastLocation(): TrackedLocation? = hmsCall("getLastLocation") {
75
+ val location: Location? = client.lastLocation.await()
76
+ location?.let(::toTrackedLocation)
77
+ }
78
+
79
+ override suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation? {
80
+ val fix = CompletableDeferred<TrackedLocation>()
81
+ val callback = object : LocationCallback() {
82
+ override fun onLocationResult(result: LocationResult?) {
83
+ val location = result?.locations.orEmpty().lastOrNull { it != null } ?: return
84
+ fix.complete(toTrackedLocation(location))
85
+ }
86
+ }
87
+ var requested = false
88
+ try {
89
+ return withTimeoutOrNull(timeoutMs.coerceAtLeast(0)) {
90
+ hmsCall("getCurrentLocation") {
91
+ requested = true
92
+ client.requestLocationUpdates(buildOneShotRequest(accuracy), callback, looperProvider()).await()
93
+ }
94
+ fix.await()
95
+ }
96
+ } finally {
97
+ if (requested) removeQuietly(callback)
98
+ }
99
+ }
100
+
101
+ /** Number of listeners with an active request (for tests and diagnostics). */
102
+ internal val activeListenerCount: Int get() = synchronized(lock) { callbacks.size }
103
+
104
+ /** Drops [callback] after its request failed, unless it was already replaced or removed. */
105
+ private fun forget(callback: ListenerCallback) {
106
+ callback.active = false
107
+ synchronized(lock) { if (callbacks[callback.listener] === callback) callbacks.remove(callback.listener) }
108
+ }
109
+
110
+ private fun deactivateAndRemove(callback: ListenerCallback) {
111
+ callback.active = false
112
+ removeQuietly(callback)
113
+ }
114
+
115
+ private fun removeQuietly(callback: LocationCallback) {
116
+ try {
117
+ client.removeLocationUpdates(callback)
118
+ .addOnFailureListener(TaskExecutors.immediate()) { e ->
119
+ Logger.d(TAG, "removeLocationUpdates failed: ${describeHmsError(e)}")
120
+ }
121
+ } catch (e: Exception) {
122
+ Logger.w(TAG, "removeLocationUpdates failed: ${describeHmsError(e)}", e)
123
+ }
124
+ }
125
+
126
+ /** Delivers to [listener] until replaced or removed; late fixes after that are dropped. */
127
+ private class ListenerCallback(val listener: LocationListener) : LocationCallback() {
128
+ @Volatile
129
+ var active = true
130
+
131
+ override fun onLocationResult(result: LocationResult?) {
132
+ if (!active) return
133
+ val fixes = result?.locations.orEmpty().mapNotNull { location -> location?.let(::toTrackedLocation) }
134
+ if (fixes.isEmpty()) return
135
+ try {
136
+ listener.onLocations(fixes)
137
+ } catch (e: Exception) {
138
+ Logger.e(TAG, "location listener failed", e)
139
+ }
140
+ }
141
+ }
142
+
143
+ companion object {
144
+ private const val TAG = "LT.HmsLocation"
145
+
146
+ /** Interval of the one-shot request behind `getCurrentLocation`. */
147
+ internal const val ONE_SHOT_INTERVAL_MS = 1_000L
148
+ }
149
+ }
150
+
151
+ /** HMS priority for a desired accuracy. */
152
+ internal fun hmsPriority(accuracy: DesiredAccuracy): Int = when (accuracy) {
153
+ DesiredAccuracy.HIGH -> LocationRequest.PRIORITY_HIGH_ACCURACY
154
+ DesiredAccuracy.BALANCED -> LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
155
+ DesiredAccuracy.LOW -> LocationRequest.PRIORITY_LOW_POWER
156
+ DesiredAccuracy.PASSIVE -> LocationRequest.PRIORITY_NO_POWER
157
+ }
158
+
159
+ /** Continuous request: negative values are clamped to 0 and the fastest interval never exceeds the interval. */
160
+ internal fun buildLocationRequest(spec: LocationRequestSpec): LocationRequest {
161
+ val interval = spec.intervalMs.coerceAtLeast(0L)
162
+ return LocationRequest.create()
163
+ .setPriority(hmsPriority(spec.accuracy))
164
+ .setInterval(interval)
165
+ .setFastestInterval(spec.fastestIntervalMs.coerceIn(0L, interval))
166
+ .setSmallestDisplacement(spec.distanceFilterM.coerceAtLeast(0f))
167
+ }
168
+
169
+ /** One-shot request for `getCurrentLocation`. */
170
+ internal fun buildOneShotRequest(accuracy: DesiredAccuracy): LocationRequest =
171
+ LocationRequest.create()
172
+ .setPriority(hmsPriority(accuracy))
173
+ .setInterval(HmsLocationBackend.ONE_SHOT_INTERVAL_MS)
174
+ .setFastestInterval(HmsLocationBackend.ONE_SHOT_INTERVAL_MS / 2)
175
+ .setNumUpdates(1)
176
+
177
+ /**
178
+ * [TrackedLocation.from] plus the extras HMS uses on some devices and API levels: `mockLocation` marks a mock fix
179
+ * and `verticalAccuracy` fills a missing altitude accuracy.
180
+ */
181
+ @Suppress("DEPRECATION") // Bundle.get: the value types of these extras vary between HMS versions
182
+ internal fun toTrackedLocation(location: Location): TrackedLocation {
183
+ val base = TrackedLocation.from(location)
184
+ val extras = try {
185
+ location.extras?.takeUnless { it.isEmpty }
186
+ } catch (_: RuntimeException) {
187
+ null // a bundle that cannot be unparcelled carries nothing we need
188
+ } ?: return base
189
+ val mock = extras.get(FusedLocationProviderClient.KEY_MOCK_LOCATION) as? Boolean ?: false
190
+ val verticalAccuracy = (extras.get(FusedLocationProviderClient.KEY_VERTICAL_ACCURACY) as? Number)
191
+ ?.toFloat()
192
+ ?.takeIf { it > 0f && it.isFinite() }
193
+ return base.copy(
194
+ isMock = base.isMock || mock,
195
+ altitudeAccuracy = base.altitudeAccuracy ?: verticalAccuracy,
196
+ )
197
+ }