@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.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +106 -0
- package/android/consumer-rules.pro +42 -0
- package/android/src/main/AndroidManifest.xml +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
- package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
- package/android/src/main/res/values/lt_strings.xml +11 -0
- package/android/src/main/res/xml/lt_file_paths.xml +5 -0
- package/dist/esm/definitions.d.ts +719 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/events.d.ts +50 -0
- package/dist/esm/events.js +52 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +10 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/web/config.d.ts +19 -0
- package/dist/esm/web/config.js +243 -0
- package/dist/esm/web/config.js.map +1 -0
- package/dist/esm/web/device.d.ts +9 -0
- package/dist/esm/web/device.js +90 -0
- package/dist/esm/web/device.js.map +1 -0
- package/dist/esm/web/errors.d.ts +14 -0
- package/dist/esm/web/errors.js +19 -0
- package/dist/esm/web/errors.js.map +1 -0
- package/dist/esm/web/geolocation.d.ts +26 -0
- package/dist/esm/web/geolocation.js +117 -0
- package/dist/esm/web/geolocation.js.map +1 -0
- package/dist/esm/web/location.d.ts +7 -0
- package/dist/esm/web/location.js +70 -0
- package/dist/esm/web/location.js.map +1 -0
- package/dist/esm/web.d.ts +164 -0
- package/dist/esm/web.js +328 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +945 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +948 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +59 -4
package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt
ADDED
|
@@ -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()
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt
ADDED
|
@@ -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
|
+
}
|