@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +107 -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 +60 -4
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import com.brickssoft.locationtracking.core.Constants
|
|
7
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
8
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
9
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
10
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
11
|
+
import com.brickssoft.locationtracking.provider.GeofenceBackend
|
|
12
|
+
import com.brickssoft.locationtracking.provider.OsGeofence
|
|
13
|
+
import com.google.android.gms.location.Geofence
|
|
14
|
+
import com.google.android.gms.location.GeofencingClient
|
|
15
|
+
import com.google.android.gms.location.GeofencingRequest
|
|
16
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
17
|
+
import kotlinx.coroutines.ensureActive
|
|
18
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* OS geofencing through `GeofencingClient`. Transitions arrive at [GmsGeofenceReceiver] through a mutable,
|
|
22
|
+
* explicit broadcast PendingIntent. Adding a region with an existing id replaces it.
|
|
23
|
+
*/
|
|
24
|
+
internal class GmsGeofenceBackend(
|
|
25
|
+
private val context: Context,
|
|
26
|
+
private val client: GeofencingClient,
|
|
27
|
+
) : GeofenceBackend {
|
|
28
|
+
override val kind: ProviderKind = ProviderKind.GMS
|
|
29
|
+
override val supportsDwell: Boolean = true
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Registers [regions], one GMS request per distinct `initialTriggerEntry` value. Regions that watch no
|
|
33
|
+
* transition are skipped (GMS rejects them). If a later request fails, the regions registered by the earlier
|
|
34
|
+
* requests of this call are removed again (best effort) before the error is thrown.
|
|
35
|
+
*
|
|
36
|
+
* @throws TrackingException see [GmsErrors.map].
|
|
37
|
+
*/
|
|
38
|
+
override suspend fun add(regions: List<OsGeofence>) = call("addGeofences") {
|
|
39
|
+
val requests = buildGeofencingRequests(regions)
|
|
40
|
+
val registered = mutableListOf<String>()
|
|
41
|
+
try {
|
|
42
|
+
for (request in requests) {
|
|
43
|
+
client.addGeofences(request, pendingIntent(context)).await()
|
|
44
|
+
registered += request.geofences.map { it.requestId }
|
|
45
|
+
}
|
|
46
|
+
} catch (e: Exception) {
|
|
47
|
+
if (registered.isNotEmpty()) rollBack(registered)
|
|
48
|
+
throw e
|
|
49
|
+
}
|
|
50
|
+
if (registered.isNotEmpty()) Logger.d(TAG, "registered ${registered.size} geofences")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @throws TrackingException see [GmsErrors.map]. */
|
|
54
|
+
override suspend fun remove(ids: List<String>) {
|
|
55
|
+
if (ids.isEmpty()) return
|
|
56
|
+
call("removeGeofences") { client.removeGeofences(ids).await() }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Removes every geofence registered with this backend's PendingIntent; a no-op if it does not exist. */
|
|
60
|
+
override suspend fun removeAll() = call("removeAllGeofences") {
|
|
61
|
+
val pending = existingPendingIntent(context) ?: return@call
|
|
62
|
+
client.removeGeofences(pending).await()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Best effort; never replaces the original error (unless the caller itself is cancelled). */
|
|
66
|
+
private suspend fun rollBack(ids: List<String>) {
|
|
67
|
+
try {
|
|
68
|
+
client.removeGeofences(ids).await()
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
currentCoroutineContext().ensureActive()
|
|
71
|
+
Logger.w(TAG, "rolling back geofences $ids failed", e)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private suspend fun call(operation: String, block: suspend () -> Unit) {
|
|
76
|
+
try {
|
|
77
|
+
block()
|
|
78
|
+
} catch (e: CancellationException) {
|
|
79
|
+
currentCoroutineContext().ensureActive()
|
|
80
|
+
// GMS cancelled the Task itself; the caller is still active and expects a TrackingException.
|
|
81
|
+
throw TrackingException(ErrorCode.UNAVAILABLE, "$operation was cancelled by Google Play services", e)
|
|
82
|
+
} catch (e: Exception) {
|
|
83
|
+
val mapped = GmsErrors.map(e, operation)
|
|
84
|
+
Logger.w(TAG, "${mapped.code}: ${mapped.message}", e)
|
|
85
|
+
throw mapped
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
companion object {
|
|
90
|
+
private const val TAG = "LT.GmsGeofence"
|
|
91
|
+
|
|
92
|
+
/** Initial trigger for `initialTriggerEntry`: report ENTER, and DWELL after the delay, if already inside. */
|
|
93
|
+
private const val INITIAL_TRIGGER_INSIDE =
|
|
94
|
+
GeofencingRequest.INITIAL_TRIGGER_ENTER or GeofencingRequest.INITIAL_TRIGGER_DWELL
|
|
95
|
+
|
|
96
|
+
/** Explicit broadcast to [GmsGeofenceReceiver]; mutable because GMS fills in the transition. */
|
|
97
|
+
fun pendingIntent(context: Context): PendingIntent =
|
|
98
|
+
PendingIntent.getBroadcast(context, Constants.RC_GMS_GEOFENCE, intent(context), Constants.piMutable())
|
|
99
|
+
|
|
100
|
+
/** The PendingIntent of [pendingIntent] if it exists, without creating it. */
|
|
101
|
+
fun existingPendingIntent(context: Context): PendingIntent? = PendingIntent.getBroadcast(
|
|
102
|
+
context,
|
|
103
|
+
Constants.RC_GMS_GEOFENCE,
|
|
104
|
+
intent(context),
|
|
105
|
+
Constants.piMutable() or PendingIntent.FLAG_NO_CREATE,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
private fun intent(context: Context): Intent =
|
|
109
|
+
Intent(context, GmsGeofenceReceiver::class.java).setAction(Constants.ACTION_GEOFENCE)
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Groups [regions] by `initialTriggerEntry`. `true` sets the initial trigger `ENTER | DWELL` (a device that is
|
|
113
|
+
* already inside reports ENTER, and DWELL after the loitering delay, for the transitions it watches); `false`
|
|
114
|
+
* sets no initial trigger.
|
|
115
|
+
*
|
|
116
|
+
* @throws IllegalArgumentException if GMS rejects a region (id longer than 100, bad coordinates or radius).
|
|
117
|
+
*/
|
|
118
|
+
fun buildGeofencingRequests(regions: List<OsGeofence>): List<GeofencingRequest> = regions
|
|
119
|
+
.mapNotNull { region -> toGeofence(region)?.let { region.initialTriggerEntry to it } }
|
|
120
|
+
.groupBy({ it.first }, { it.second })
|
|
121
|
+
.map { (initialTriggerEntry, geofences) ->
|
|
122
|
+
GeofencingRequest.Builder()
|
|
123
|
+
.setInitialTrigger(if (initialTriggerEntry) INITIAL_TRIGGER_INSIDE else 0)
|
|
124
|
+
.addGeofences(geofences)
|
|
125
|
+
.build()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The GMS geofence for [region], or null if it watches no transition. */
|
|
129
|
+
fun toGeofence(region: OsGeofence): Geofence? {
|
|
130
|
+
val transitions = transitionTypes(region)
|
|
131
|
+
if (transitions == 0) {
|
|
132
|
+
Logger.w(TAG, "geofence '${region.id}' watches no transition; not registered")
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
return Geofence.Builder()
|
|
136
|
+
.setRequestId(region.id)
|
|
137
|
+
.setCircularRegion(region.latitude, region.longitude, region.radius)
|
|
138
|
+
.setTransitionTypes(transitions)
|
|
139
|
+
.setLoiteringDelay(region.loiteringDelayMs.coerceAtLeast(0))
|
|
140
|
+
.setExpirationDuration(Geofence.NEVER_EXPIRE)
|
|
141
|
+
.build()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fun transitionTypes(region: OsGeofence): Int {
|
|
145
|
+
var types = 0
|
|
146
|
+
if (region.onEntry) types = types or Geofence.GEOFENCE_TRANSITION_ENTER
|
|
147
|
+
if (region.onExit) types = types or Geofence.GEOFENCE_TRANSITION_EXIT
|
|
148
|
+
if (region.onDwell) types = types or Geofence.GEOFENCE_TRANSITION_DWELL
|
|
149
|
+
return types
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import com.brickssoft.locationtracking.core.Components
|
|
7
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
8
|
+
import com.brickssoft.locationtracking.model.GeofenceAction
|
|
9
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
10
|
+
import com.brickssoft.locationtracking.provider.GeofenceTransitionSink
|
|
11
|
+
import com.brickssoft.locationtracking.provider.OsGeofenceTransition
|
|
12
|
+
import com.google.android.gms.location.Geofence
|
|
13
|
+
import com.google.android.gms.location.GeofenceStatusCodes
|
|
14
|
+
import com.google.android.gms.location.GeofencingEvent
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Receives `GeofencingEvent`s from the PendingIntent of [GmsGeofenceBackend] and delivers one
|
|
18
|
+
* [OsGeofenceTransition] per triggering geofence to the GeofenceManager with `goAsync()` on `Components.scope`.
|
|
19
|
+
*/
|
|
20
|
+
class GmsGeofenceReceiver internal constructor(
|
|
21
|
+
private val target: (Context) -> ReceiverTarget<GeofenceTransitionSink>,
|
|
22
|
+
) : BroadcastReceiver() {
|
|
23
|
+
constructor() : this({ ctx -> Components.get(ctx).let { ReceiverTarget(it.scope, it.geofences) } })
|
|
24
|
+
|
|
25
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
26
|
+
val transitions = parseGeofenceTransitions(intent)
|
|
27
|
+
if (transitions.isEmpty()) return
|
|
28
|
+
Logger.d(TAG, "geofence transitions: ${transitions.joinToString { "${it.id}:${it.action}" }}")
|
|
29
|
+
deliverAsync(context, target, TAG, "geofence delivery") { it.onGeofenceTransitions(transitions) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
internal companion object {
|
|
33
|
+
private const val TAG = "LT.GmsGeofence"
|
|
34
|
+
|
|
35
|
+
fun parseGeofenceTransitions(intent: Intent?): List<OsGeofenceTransition> {
|
|
36
|
+
val event = try {
|
|
37
|
+
intent?.let { GeofencingEvent.fromIntent(it) }
|
|
38
|
+
} catch (e: Exception) {
|
|
39
|
+
Logger.w(TAG, "unreadable geofencing event", e)
|
|
40
|
+
null
|
|
41
|
+
}
|
|
42
|
+
return parseGeofencingEvent(event)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One transition per triggering geofence, all with the event's action and triggering location.
|
|
47
|
+
* Empty (and logged) if [event] is null, carries an error or has an unknown transition.
|
|
48
|
+
*/
|
|
49
|
+
fun parseGeofencingEvent(event: GeofencingEvent?): List<OsGeofenceTransition> {
|
|
50
|
+
if (event == null) {
|
|
51
|
+
Logger.w(TAG, "intent carries no geofencing event")
|
|
52
|
+
return emptyList()
|
|
53
|
+
}
|
|
54
|
+
if (event.hasError()) {
|
|
55
|
+
val code = event.errorCode
|
|
56
|
+
// GEOFENCE_NOT_AVAILABLE also means GMS removed every registered geofence; nothing re-adds them
|
|
57
|
+
// until tracking restarts (see the unit report's contract change request).
|
|
58
|
+
val notAvailable = code == GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE
|
|
59
|
+
val dropped = if (notAvailable) "; all geofences were removed" else ""
|
|
60
|
+
Logger.e(TAG, "geofencing error: ${GmsErrors.describe(code)}$dropped")
|
|
61
|
+
return emptyList()
|
|
62
|
+
}
|
|
63
|
+
val action = mapTransition(event.geofenceTransition) ?: run {
|
|
64
|
+
Logger.w(TAG, "unknown geofence transition ${event.geofenceTransition}")
|
|
65
|
+
return emptyList()
|
|
66
|
+
}
|
|
67
|
+
val location = event.triggeringLocation?.let(TrackedLocation::from)
|
|
68
|
+
return event.triggeringGeofences.orEmpty().map { OsGeofenceTransition(it.requestId, action, location) }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun mapTransition(transition: Int): GeofenceAction? = when (transition) {
|
|
72
|
+
Geofence.GEOFENCE_TRANSITION_ENTER -> GeofenceAction.ENTER
|
|
73
|
+
Geofence.GEOFENCE_TRANSITION_EXIT -> GeofenceAction.EXIT
|
|
74
|
+
Geofence.GEOFENCE_TRANSITION_DWELL -> GeofenceAction.DWELL
|
|
75
|
+
else -> null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import android.os.Looper
|
|
4
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
5
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
6
|
+
import com.brickssoft.locationtracking.model.DesiredAccuracy
|
|
7
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
8
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
9
|
+
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
10
|
+
import com.brickssoft.locationtracking.provider.LocationListener
|
|
11
|
+
import com.brickssoft.locationtracking.provider.LocationRequestSpec
|
|
12
|
+
import com.google.android.gms.location.CurrentLocationRequest
|
|
13
|
+
import com.google.android.gms.location.FusedLocationProviderClient
|
|
14
|
+
import com.google.android.gms.location.LocationAvailability
|
|
15
|
+
import com.google.android.gms.location.LocationCallback
|
|
16
|
+
import com.google.android.gms.location.LocationRequest
|
|
17
|
+
import com.google.android.gms.location.LocationResult
|
|
18
|
+
import com.google.android.gms.location.Priority
|
|
19
|
+
import com.google.android.gms.tasks.CancellationTokenSource
|
|
20
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
21
|
+
import kotlinx.coroutines.ensureActive
|
|
22
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
23
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fused location provider backend.
|
|
27
|
+
*
|
|
28
|
+
* Every [LocationListener] gets its own [LocationCallback] on the main looper. Requesting again with the same
|
|
29
|
+
* listener reuses its callback, which GMS treats as a replacement of the previous request.
|
|
30
|
+
*
|
|
31
|
+
* Only [getCurrentLocation] throws: the [LocationBackend] contract declares no exception for the other methods, so
|
|
32
|
+
* their failures are logged (a missing permission also shows in the provider state).
|
|
33
|
+
*/
|
|
34
|
+
internal class GmsLocationBackend(
|
|
35
|
+
private val client: FusedLocationProviderClient,
|
|
36
|
+
private val looper: () -> Looper = Looper::getMainLooper,
|
|
37
|
+
) : LocationBackend {
|
|
38
|
+
override val kind: ProviderKind = ProviderKind.GMS
|
|
39
|
+
|
|
40
|
+
/** Guarded by itself; GMS registration calls are made under the same lock so add/remove cannot interleave. */
|
|
41
|
+
private val callbacks = HashMap<LocationListener, ListenerCallback>()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Never throws. A synchronous rejection (e.g. `SecurityException` without location permission) is logged and
|
|
45
|
+
* leaves a new listener unregistered, or a replaced listener on its previous request. Asynchronous failures of
|
|
46
|
+
* the GMS Task are logged too.
|
|
47
|
+
*/
|
|
48
|
+
override fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener) {
|
|
49
|
+
val request = try {
|
|
50
|
+
buildLocationRequest(spec)
|
|
51
|
+
} catch (e: Exception) {
|
|
52
|
+
Logger.e(TAG, "invalid location request $spec", e)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
synchronized(callbacks) {
|
|
56
|
+
val existing = callbacks[listener]
|
|
57
|
+
val callback = existing ?: ListenerCallback(listener)
|
|
58
|
+
try {
|
|
59
|
+
client.requestLocationUpdates(request, callback, looper()).logFailure(TAG, "requestLocationUpdates")
|
|
60
|
+
} catch (e: Exception) {
|
|
61
|
+
val mapped = GmsErrors.map(e, "requestLocationUpdates")
|
|
62
|
+
Logger.e(TAG, "${mapped.code}: ${mapped.message}", e)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (existing == null) callbacks[listener] = callback
|
|
66
|
+
Logger.d(TAG, "location updates ${if (existing == null) "requested" else "replaced"}: $spec")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun removeUpdates(listener: LocationListener) {
|
|
71
|
+
synchronized(callbacks) {
|
|
72
|
+
val callback = callbacks.remove(listener) ?: return
|
|
73
|
+
callback.active = false
|
|
74
|
+
try {
|
|
75
|
+
client.removeLocationUpdates(callback).logFailure(TAG, "removeLocationUpdates")
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
Logger.w(TAG, "removeLocationUpdates failed", e)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The cached fused location, or null if there is none or GMS cannot provide it (including a missing permission;
|
|
84
|
+
* logged). Never throws, so callers such as the heartbeat can always fall back to a record without coords.
|
|
85
|
+
*/
|
|
86
|
+
override suspend fun getLastLocation(): TrackedLocation? = try {
|
|
87
|
+
client.lastLocation.await()?.let(TrackedLocation::from)
|
|
88
|
+
} catch (e: CancellationException) {
|
|
89
|
+
currentCoroutineContext().ensureActive()
|
|
90
|
+
Logger.w(TAG, "getLastLocation cancelled by GMS")
|
|
91
|
+
null
|
|
92
|
+
} catch (e: Exception) {
|
|
93
|
+
val mapped = GmsErrors.map(e, "getLastLocation")
|
|
94
|
+
Logger.w(TAG, "${mapped.code}: ${mapped.message}", e)
|
|
95
|
+
null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Requests one fix computed now (a cached fix is accepted only for PASSIVE, which never computes one itself).
|
|
100
|
+
* Returns null if none arrives within [timeoutMs]: GMS gives up after the request duration, and the coroutine
|
|
101
|
+
* timeout is a safety net that also cancels the GMS request.
|
|
102
|
+
*
|
|
103
|
+
* @throws TrackingException PERMISSION_DENIED without location permission, otherwise see [GmsErrors.map]
|
|
104
|
+
* (UNAVAILABLE for GMS API failures).
|
|
105
|
+
*/
|
|
106
|
+
override suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation? {
|
|
107
|
+
if (timeoutMs <= 0) return null
|
|
108
|
+
val cancellation = CancellationTokenSource()
|
|
109
|
+
return try {
|
|
110
|
+
val request = CurrentLocationRequest.Builder()
|
|
111
|
+
.setPriority(accuracy.toGmsPriority())
|
|
112
|
+
.setDurationMillis(timeoutMs)
|
|
113
|
+
.apply { if (accuracy != DesiredAccuracy.PASSIVE) setMaxUpdateAgeMillis(0) }
|
|
114
|
+
.build()
|
|
115
|
+
withTimeoutOrNull(timeoutMs) {
|
|
116
|
+
client.getCurrentLocation(request, cancellation.token).await(cancellation)
|
|
117
|
+
}?.let(TrackedLocation::from)
|
|
118
|
+
} catch (e: CancellationException) {
|
|
119
|
+
// Either the caller was cancelled (rethrown here) or GMS cancelled the task itself (no fix).
|
|
120
|
+
currentCoroutineContext().ensureActive()
|
|
121
|
+
null
|
|
122
|
+
} catch (e: Exception) {
|
|
123
|
+
throw GmsErrors.map(e, "getCurrentLocation")
|
|
124
|
+
} finally {
|
|
125
|
+
cancellation.cancel()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Forwards fixes to one listener until [removeUpdates] deactivates it. Best effort: a batch whose delivery already
|
|
131
|
+
* started on the looper thread may still reach the listener while [removeUpdates] runs on another thread.
|
|
132
|
+
*/
|
|
133
|
+
private class ListenerCallback(private val listener: LocationListener) : LocationCallback() {
|
|
134
|
+
@Volatile
|
|
135
|
+
var active = true
|
|
136
|
+
|
|
137
|
+
override fun onLocationResult(result: LocationResult) {
|
|
138
|
+
if (!active) return
|
|
139
|
+
val locations = result.locations.map(TrackedLocation::from)
|
|
140
|
+
if (locations.isEmpty()) return
|
|
141
|
+
try {
|
|
142
|
+
listener.onLocations(locations)
|
|
143
|
+
} catch (e: Exception) {
|
|
144
|
+
Logger.e(TAG, "location listener failed", e)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
override fun onLocationAvailability(availability: LocationAvailability) {
|
|
149
|
+
Logger.v(TAG, "location available: ${availability.isLocationAvailable}")
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
companion object {
|
|
154
|
+
private const val TAG = "LT.GmsLocation"
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `LocationRequest.Builder(priority, interval)` with the fastest interval (clamped to [0, interval]) and the
|
|
158
|
+
* distance filter (at least 0).
|
|
159
|
+
*/
|
|
160
|
+
fun buildLocationRequest(spec: LocationRequestSpec): LocationRequest {
|
|
161
|
+
val interval = spec.intervalMs.coerceAtLeast(0)
|
|
162
|
+
val distance = spec.distanceFilterM.takeIf { it.isFinite() }?.coerceAtLeast(0f) ?: 0f
|
|
163
|
+
return LocationRequest.Builder(spec.accuracy.toGmsPriority(), interval)
|
|
164
|
+
.setMinUpdateIntervalMillis(spec.fastestIntervalMs.coerceIn(0, interval))
|
|
165
|
+
.setMinUpdateDistanceMeters(distance)
|
|
166
|
+
.build()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fun DesiredAccuracy.toGmsPriority(): Int = when (this) {
|
|
170
|
+
DesiredAccuracy.HIGH -> Priority.PRIORITY_HIGH_ACCURACY
|
|
171
|
+
DesiredAccuracy.BALANCED -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
|
|
172
|
+
DesiredAccuracy.LOW -> Priority.PRIORITY_LOW_POWER
|
|
173
|
+
DesiredAccuracy.PASSIVE -> Priority.PRIORITY_PASSIVE
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
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.ActivityBackend
|
|
7
|
+
import com.brickssoft.locationtracking.provider.GeofenceBackend
|
|
8
|
+
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
9
|
+
import com.brickssoft.locationtracking.provider.ProviderBundle
|
|
10
|
+
import com.google.android.gms.common.ConnectionResult
|
|
11
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
12
|
+
import com.google.android.gms.location.ActivityRecognition
|
|
13
|
+
import com.google.android.gms.location.ActivityRecognitionClient
|
|
14
|
+
import com.google.android.gms.location.FusedLocationProviderClient
|
|
15
|
+
import com.google.android.gms.location.GeofencingClient
|
|
16
|
+
import com.google.android.gms.location.LocationServices
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Google Play services backends: fused location, activity recognition and geofencing.
|
|
20
|
+
*
|
|
21
|
+
* Created reflectively through the public `(Context)` constructor. The other parameters exist so tests can inject
|
|
22
|
+
* the availability check and the GMS clients. Each backend (and its client) is created on first use.
|
|
23
|
+
*/
|
|
24
|
+
class GmsProviderBundle @JvmOverloads constructor(
|
|
25
|
+
context: Context,
|
|
26
|
+
private val availabilityCheck: (Context) -> Int = {
|
|
27
|
+
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(it)
|
|
28
|
+
},
|
|
29
|
+
private val fusedClient: (Context) -> FusedLocationProviderClient = {
|
|
30
|
+
LocationServices.getFusedLocationProviderClient(it)
|
|
31
|
+
},
|
|
32
|
+
private val activityClient: (Context) -> ActivityRecognitionClient = { ActivityRecognition.getClient(it) },
|
|
33
|
+
private val geofencingClient: (Context) -> GeofencingClient = { LocationServices.getGeofencingClient(it) },
|
|
34
|
+
) : ProviderBundle {
|
|
35
|
+
private val appContext: Context = context.applicationContext ?: context
|
|
36
|
+
|
|
37
|
+
override val kind: ProviderKind = ProviderKind.GMS
|
|
38
|
+
|
|
39
|
+
private val locationBackend by lazy { GmsLocationBackend(fusedClient(appContext)) }
|
|
40
|
+
private val activityBackend by lazy { GmsActivityBackend(appContext, activityClient(appContext)) }
|
|
41
|
+
private val geofenceBackend by lazy { GmsGeofenceBackend(appContext, geofencingClient(appContext)) }
|
|
42
|
+
|
|
43
|
+
/** True if `GoogleApiAvailability` reports SUCCESS; false on any other result or failure. */
|
|
44
|
+
override fun isAvailable(): Boolean = try {
|
|
45
|
+
val status = availabilityCheck(appContext)
|
|
46
|
+
if (status != ConnectionResult.SUCCESS) Logger.d(TAG, "Google Play services unavailable: status $status")
|
|
47
|
+
status == ConnectionResult.SUCCESS
|
|
48
|
+
} catch (t: Throwable) {
|
|
49
|
+
Logger.w(TAG, "Google Play services availability check failed", t)
|
|
50
|
+
false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun location(): LocationBackend = locationBackend
|
|
54
|
+
|
|
55
|
+
override fun activity(): ActivityBackend = activityBackend
|
|
56
|
+
|
|
57
|
+
override fun geofence(): GeofenceBackend = geofenceBackend
|
|
58
|
+
|
|
59
|
+
private companion object {
|
|
60
|
+
const val TAG = "LT.Gms"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
6
|
+
import kotlinx.coroutines.CoroutineScope
|
|
7
|
+
import kotlinx.coroutines.Job
|
|
8
|
+
import kotlinx.coroutines.delay
|
|
9
|
+
import kotlinx.coroutines.launch
|
|
10
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
12
|
+
|
|
13
|
+
/** Where a PendingIntent receiver delivers its parsed payload: normally `Components.get(ctx).scope` and a sink. */
|
|
14
|
+
internal class ReceiverTarget<T>(val scope: CoroutineScope, val sink: T)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The system reports a broadcast ANR if `goAsync()` is not finished in time (10 s for foreground broadcasts), so
|
|
18
|
+
* the broadcast is finished after this deadline even if the delivery is still running.
|
|
19
|
+
*/
|
|
20
|
+
internal const val RECEIVER_FINISH_DEADLINE_MS = 9_000L
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Called from `onReceive`: calls `goAsync()`, resolves the target and runs [deliver] with its sink in its scope.
|
|
24
|
+
* The broadcast is always finished, also if the target cannot be resolved.
|
|
25
|
+
*/
|
|
26
|
+
internal fun <T> BroadcastReceiver.deliverAsync(
|
|
27
|
+
context: Context,
|
|
28
|
+
target: (Context) -> ReceiverTarget<T>,
|
|
29
|
+
tag: String,
|
|
30
|
+
what: String,
|
|
31
|
+
deliver: suspend (T) -> Unit,
|
|
32
|
+
) {
|
|
33
|
+
val pending = goAsync()
|
|
34
|
+
try {
|
|
35
|
+
val resolved = target(context)
|
|
36
|
+
resolved.scope.launchReceiverWork(pending, tag, what) { deliver(resolved.sink) }
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
Logger.e(tag, "cannot start $what", e)
|
|
39
|
+
try {
|
|
40
|
+
pending?.finish()
|
|
41
|
+
} catch (finishError: Exception) {
|
|
42
|
+
Logger.w(tag, "$what: finishing the broadcast failed", finishError)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Runs [block] in this scope for a receiver that called `goAsync()`. [pending] is finished exactly once: when the
|
|
49
|
+
* work completes, fails or is cancelled (even if the scope was already cancelled), or after [deadlineMs] while the
|
|
50
|
+
* work keeps running. Failures are logged.
|
|
51
|
+
*/
|
|
52
|
+
internal fun CoroutineScope.launchReceiverWork(
|
|
53
|
+
pending: BroadcastReceiver.PendingResult?,
|
|
54
|
+
tag: String,
|
|
55
|
+
what: String,
|
|
56
|
+
deadlineMs: Long = RECEIVER_FINISH_DEADLINE_MS,
|
|
57
|
+
block: suspend () -> Unit,
|
|
58
|
+
): Job {
|
|
59
|
+
val finished = AtomicBoolean(false)
|
|
60
|
+
val finish = {
|
|
61
|
+
if (finished.compareAndSet(false, true)) {
|
|
62
|
+
try {
|
|
63
|
+
pending?.finish()
|
|
64
|
+
} catch (e: Exception) {
|
|
65
|
+
Logger.w(tag, "$what: finishing the broadcast failed", e)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
val job = launch {
|
|
70
|
+
try {
|
|
71
|
+
block()
|
|
72
|
+
} catch (e: CancellationException) {
|
|
73
|
+
throw e
|
|
74
|
+
} catch (e: Exception) {
|
|
75
|
+
Logger.e(tag, "$what failed", e)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
val watchdog = launch {
|
|
79
|
+
delay(deadlineMs)
|
|
80
|
+
Logger.w(tag, "$what still running after $deadlineMs ms; finishing the broadcast early")
|
|
81
|
+
finish()
|
|
82
|
+
}
|
|
83
|
+
job.invokeOnCompletion {
|
|
84
|
+
watchdog.cancel()
|
|
85
|
+
finish()
|
|
86
|
+
}
|
|
87
|
+
return job
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
4
|
+
import com.google.android.gms.tasks.CancellationTokenSource
|
|
5
|
+
import com.google.android.gms.tasks.Task
|
|
6
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
7
|
+
import java.util.concurrent.Executor
|
|
8
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
9
|
+
import kotlin.coroutines.resume
|
|
10
|
+
import kotlin.coroutines.resumeWithException
|
|
11
|
+
|
|
12
|
+
/** Runs Task listeners on the thread that completes the task; the coroutine then resumes on its own dispatcher. */
|
|
13
|
+
internal object DirectExecutor : Executor {
|
|
14
|
+
override fun execute(command: Runnable) = command.run()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Suspends until this Task completes, without the play-services coroutines dependency.
|
|
19
|
+
*
|
|
20
|
+
* - Success resumes with the result (which may be null, e.g. no last location or `Task<Void>`).
|
|
21
|
+
* - Failure rethrows the Task's exception unchanged.
|
|
22
|
+
* - A cancelled Task throws [CancellationException].
|
|
23
|
+
* - Cancelling the calling coroutine cancels [cancellationTokenSource], if given, so GMS stops the work.
|
|
24
|
+
*/
|
|
25
|
+
internal suspend fun <T> Task<T>.await(cancellationTokenSource: CancellationTokenSource? = null): T {
|
|
26
|
+
if (isComplete) return completedResult()
|
|
27
|
+
return suspendCancellableCoroutine { cont ->
|
|
28
|
+
addOnCompleteListener(DirectExecutor) { task ->
|
|
29
|
+
val error = task.exception
|
|
30
|
+
when {
|
|
31
|
+
error != null -> cont.resumeWithException(error)
|
|
32
|
+
task.isCanceled -> cont.cancel()
|
|
33
|
+
else -> cont.resume(task.result)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (cancellationTokenSource != null) {
|
|
37
|
+
cont.invokeOnCancellation { cancellationTokenSource.cancel() }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun <T> Task<T>.completedResult(): T {
|
|
43
|
+
exception?.let { throw it }
|
|
44
|
+
if (isCanceled) throw CancellationException("Task ${this::class.java.name} was cancelled")
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Adds a listener that only logs a failure of a fire-and-forget Task. Never throws. */
|
|
49
|
+
internal fun Task<*>?.logFailure(tag: String, what: String) {
|
|
50
|
+
try {
|
|
51
|
+
this?.addOnFailureListener(DirectExecutor) { e -> Logger.w(tag, "$what failed", e) }
|
|
52
|
+
} catch (e: Exception) {
|
|
53
|
+
Logger.w(tag, "$what: cannot observe result", e)
|
|
54
|
+
}
|
|
55
|
+
}
|