@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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.android
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.BroadcastReceiver
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.IntentFilter
|
|
8
|
+
import android.location.Location
|
|
9
|
+
import android.location.LocationManager
|
|
10
|
+
import android.os.CancellationSignal
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import androidx.core.content.ContextCompat
|
|
13
|
+
import androidx.core.location.LocationListenerCompat
|
|
14
|
+
import androidx.core.location.LocationManagerCompat
|
|
15
|
+
import androidx.core.location.LocationRequestCompat
|
|
16
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
17
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
18
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
19
|
+
import com.brickssoft.locationtracking.model.DesiredAccuracy
|
|
20
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
21
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
22
|
+
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
23
|
+
import com.brickssoft.locationtracking.provider.LocationListener
|
|
24
|
+
import com.brickssoft.locationtracking.provider.LocationRequestSpec
|
|
25
|
+
import kotlinx.coroutines.delay
|
|
26
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
27
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
28
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
29
|
+
import java.util.concurrent.Executor
|
|
30
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
31
|
+
import kotlin.coroutines.resume
|
|
32
|
+
import kotlin.coroutines.resumeWithException
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* [LocationBackend] over the platform [LocationManager] (through `LocationManagerCompat`), used when neither GMS nor
|
|
36
|
+
* HMS is available.
|
|
37
|
+
*
|
|
38
|
+
* - Provider per accuracy: HIGH uses gps, BALANCED/LOW use network, each falling back to whichever is enabled;
|
|
39
|
+
* PASSIVE uses the passive provider (see [LocationProviders.candidates]).
|
|
40
|
+
* - Updates arrive on the main looper. While any listener is registered, `PROVIDERS_CHANGED_ACTION` moves each
|
|
41
|
+
* registration to its best enabled provider.
|
|
42
|
+
* - A [SecurityException] from the platform surfaces as a [TrackingException] with [ErrorCode.PERMISSION_DENIED].
|
|
43
|
+
*/
|
|
44
|
+
class AndroidLocationBackend(
|
|
45
|
+
context: Context,
|
|
46
|
+
private val locationManager: LocationManager? =
|
|
47
|
+
context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager,
|
|
48
|
+
) : LocationBackend {
|
|
49
|
+
private val appContext: Context = context.applicationContext ?: context
|
|
50
|
+
override val kind: ProviderKind = ProviderKind.ANDROID
|
|
51
|
+
|
|
52
|
+
/** Serializes (re-)registrations; [registrations] is also read lock-free by the delivery path. */
|
|
53
|
+
private val lock = Any()
|
|
54
|
+
private val registrations = ConcurrentHashMap<LocationListener, Registration>()
|
|
55
|
+
private var providersReceiver: BroadcastReceiver? = null
|
|
56
|
+
|
|
57
|
+
override fun requestUpdates(spec: LocationRequestSpec, listener: LocationListener) {
|
|
58
|
+
val lm = locationManager ?: return Logger.e(TAG, "no LocationManager: cannot request location updates")
|
|
59
|
+
synchronized(lock) {
|
|
60
|
+
val providers = LocationProviders.candidates(lm, spec.accuracy)
|
|
61
|
+
val registration = register(lm, spec, listener, providers)
|
|
62
|
+
?: return Logger.e(TAG, "no usable location provider for ${spec.accuracy.wire} (tried $providers)")
|
|
63
|
+
registrations.put(listener, registration)?.let { unregister(lm, it) }
|
|
64
|
+
observeProviderChanges()
|
|
65
|
+
Logger.d(TAG, "requested ${registration.provider} updates: $spec")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun removeUpdates(listener: LocationListener) {
|
|
70
|
+
val lm = locationManager ?: return
|
|
71
|
+
synchronized(lock) {
|
|
72
|
+
val registration = registrations.remove(listener) ?: return
|
|
73
|
+
unregister(lm, registration)
|
|
74
|
+
if (registrations.isEmpty()) stopObservingProviderChanges()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override suspend fun getLastLocation(): TrackedLocation? {
|
|
79
|
+
val lm = locationManager ?: return null
|
|
80
|
+
return LocationProviders.freshestLastKnown(lm)?.let(TrackedLocation::from)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Requests one fix from the best enabled provider for [accuracy] (PASSIVE is treated as LOW). The platform gives
|
|
85
|
+
* up after about 30 s, so the request is repeated until [timeoutMs] elapses. Returns null on timeout or when no
|
|
86
|
+
* provider is enabled.
|
|
87
|
+
*/
|
|
88
|
+
override suspend fun getCurrentLocation(accuracy: DesiredAccuracy, timeoutMs: Long): TrackedLocation? {
|
|
89
|
+
val lm = locationManager ?: return null
|
|
90
|
+
val effective = if (accuracy == DesiredAccuracy.PASSIVE) DesiredAccuracy.LOW else accuracy
|
|
91
|
+
return withTimeoutOrNull(timeoutMs.coerceAtLeast(0)) { awaitCurrentLocation(lm, effective) }
|
|
92
|
+
?.let(TrackedLocation::from)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private suspend fun awaitCurrentLocation(lm: LocationManager, accuracy: DesiredAccuracy): Location? {
|
|
96
|
+
while (true) {
|
|
97
|
+
val enabled = LocationProviders.candidates(lm, accuracy, enabledOnly = true)
|
|
98
|
+
if (enabled.isEmpty()) return null
|
|
99
|
+
currentLocationFromFirst(lm, enabled)?.let { return it }
|
|
100
|
+
delay(CURRENT_LOCATION_RETRY_DELAY_MS)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Asks the first provider of [providers] that does not refuse with a [SecurityException]. */
|
|
105
|
+
private suspend fun currentLocationFromFirst(lm: LocationManager, providers: List<String>): Location? {
|
|
106
|
+
var denied: SecurityException? = null
|
|
107
|
+
for (provider in providers) {
|
|
108
|
+
try {
|
|
109
|
+
return currentLocation(lm, provider)
|
|
110
|
+
} catch (e: SecurityException) {
|
|
111
|
+
denied = e
|
|
112
|
+
} catch (e: IllegalArgumentException) {
|
|
113
|
+
Logger.w(TAG, "provider $provider rejected getCurrentLocation", e)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (denied != null) throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", denied)
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@SuppressLint("MissingPermission")
|
|
121
|
+
private suspend fun currentLocation(lm: LocationManager, provider: String): Location? =
|
|
122
|
+
suspendCancellableCoroutine { cont ->
|
|
123
|
+
val signal = CancellationSignal()
|
|
124
|
+
val resumed = AtomicBoolean(false)
|
|
125
|
+
cont.invokeOnCancellation { signal.cancel() }
|
|
126
|
+
try {
|
|
127
|
+
LocationManagerCompat.getCurrentLocation(lm, provider, signal, DIRECT_EXECUTOR) { location ->
|
|
128
|
+
if (resumed.compareAndSet(false, true)) cont.resume(location)
|
|
129
|
+
}
|
|
130
|
+
} catch (e: Exception) {
|
|
131
|
+
if (resumed.compareAndSet(false, true)) cont.resumeWithException(e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Registers [listener] on the first of [providers] that accepts the request.
|
|
137
|
+
*
|
|
138
|
+
* @return null if no provider accepted it for a reason other than permissions.
|
|
139
|
+
* @throws TrackingException [ErrorCode.PERMISSION_DENIED] if a provider refused with a [SecurityException] and
|
|
140
|
+
* no other accepted.
|
|
141
|
+
*/
|
|
142
|
+
@SuppressLint("MissingPermission")
|
|
143
|
+
private fun register(
|
|
144
|
+
lm: LocationManager,
|
|
145
|
+
spec: LocationRequestSpec,
|
|
146
|
+
listener: LocationListener,
|
|
147
|
+
providers: List<String>,
|
|
148
|
+
): Registration? {
|
|
149
|
+
var denied: SecurityException? = null
|
|
150
|
+
for (provider in providers) {
|
|
151
|
+
val registration = Registration(listener, spec, provider)
|
|
152
|
+
try {
|
|
153
|
+
LocationManagerCompat.requestLocationUpdates(
|
|
154
|
+
lm, provider, requestFor(spec), registration, Looper.getMainLooper(),
|
|
155
|
+
)
|
|
156
|
+
return registration
|
|
157
|
+
} catch (e: SecurityException) {
|
|
158
|
+
Logger.w(TAG, "provider $provider refused location updates", e)
|
|
159
|
+
denied = e
|
|
160
|
+
} catch (e: IllegalArgumentException) {
|
|
161
|
+
Logger.w(TAG, "provider $provider rejected location updates", e)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (denied != null) throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", denied)
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private fun unregister(lm: LocationManager, registration: Registration) {
|
|
169
|
+
try {
|
|
170
|
+
LocationManagerCompat.removeUpdates(lm, registration)
|
|
171
|
+
} catch (e: Exception) {
|
|
172
|
+
Logger.w(TAG, "removeUpdates failed for ${registration.provider}", e)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Moves every registration whose best enabled provider changed to that provider. Runs on the main thread. */
|
|
177
|
+
private fun onProvidersChanged() {
|
|
178
|
+
val lm = locationManager ?: return
|
|
179
|
+
synchronized(lock) {
|
|
180
|
+
for ((listener, current) in registrations.entries.toList()) {
|
|
181
|
+
val best = LocationProviders.candidates(lm, current.spec.accuracy, enabledOnly = true).firstOrNull()
|
|
182
|
+
if (best == null || best == current.provider) continue
|
|
183
|
+
val next = try {
|
|
184
|
+
register(lm, current.spec, listener, listOf(best))
|
|
185
|
+
} catch (e: TrackingException) {
|
|
186
|
+
null
|
|
187
|
+
} ?: continue
|
|
188
|
+
registrations[listener] = next
|
|
189
|
+
unregister(lm, current)
|
|
190
|
+
Logger.i(TAG, "location provider changed: ${current.provider} -> $best")
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private fun observeProviderChanges() {
|
|
196
|
+
if (providersReceiver != null) return
|
|
197
|
+
val receiver = object : BroadcastReceiver() {
|
|
198
|
+
override fun onReceive(context: Context, intent: Intent) = onProvidersChanged()
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
ContextCompat.registerReceiver(
|
|
202
|
+
appContext, receiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION),
|
|
203
|
+
ContextCompat.RECEIVER_NOT_EXPORTED,
|
|
204
|
+
)
|
|
205
|
+
providersReceiver = receiver
|
|
206
|
+
} catch (e: Exception) {
|
|
207
|
+
Logger.w(TAG, "cannot observe location provider changes", e)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun stopObservingProviderChanges() {
|
|
212
|
+
val receiver = providersReceiver ?: return
|
|
213
|
+
providersReceiver = null
|
|
214
|
+
try {
|
|
215
|
+
appContext.unregisterReceiver(receiver)
|
|
216
|
+
} catch (e: Exception) {
|
|
217
|
+
Logger.w(TAG, "unregisterReceiver failed", e)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** One platform registration of [listener] on [provider]; stale ones (replaced or removed) drop their fixes. */
|
|
222
|
+
private inner class Registration(
|
|
223
|
+
val listener: LocationListener,
|
|
224
|
+
val spec: LocationRequestSpec,
|
|
225
|
+
val provider: String,
|
|
226
|
+
) : LocationListenerCompat {
|
|
227
|
+
override fun onLocationChanged(location: Location) = deliver(listOf(location))
|
|
228
|
+
|
|
229
|
+
override fun onLocationChanged(locations: MutableList<Location>) = deliver(locations)
|
|
230
|
+
|
|
231
|
+
private fun deliver(locations: List<Location>) {
|
|
232
|
+
if (locations.isEmpty() || registrations[listener] !== this) return
|
|
233
|
+
try {
|
|
234
|
+
listener.onLocations(locations.map(TrackedLocation::from))
|
|
235
|
+
} catch (e: Exception) {
|
|
236
|
+
Logger.e(TAG, "location listener failed", e)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
internal companion object {
|
|
242
|
+
private const val TAG = "LT.AndroidLocation"
|
|
243
|
+
|
|
244
|
+
/** Pause between two platform `getCurrentLocation` attempts that returned no fix. */
|
|
245
|
+
const val CURRENT_LOCATION_RETRY_DELAY_MS = 1_000L
|
|
246
|
+
|
|
247
|
+
private val DIRECT_EXECUTOR = Executor { it.run() }
|
|
248
|
+
|
|
249
|
+
/** Maps [spec] to a platform request; values are clamped to what `LocationRequestCompat` accepts. */
|
|
250
|
+
fun requestFor(spec: LocationRequestSpec): LocationRequestCompat {
|
|
251
|
+
val interval = spec.intervalMs.coerceAtLeast(0)
|
|
252
|
+
val distance = spec.distanceFilterM.takeIf { it.isFinite() && it > 0f } ?: 0f
|
|
253
|
+
val quality = when (spec.accuracy) {
|
|
254
|
+
DesiredAccuracy.HIGH -> LocationRequestCompat.QUALITY_HIGH_ACCURACY
|
|
255
|
+
DesiredAccuracy.BALANCED -> LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY
|
|
256
|
+
DesiredAccuracy.LOW, DesiredAccuracy.PASSIVE -> LocationRequestCompat.QUALITY_LOW_POWER
|
|
257
|
+
}
|
|
258
|
+
return LocationRequestCompat.Builder(interval)
|
|
259
|
+
.setQuality(quality)
|
|
260
|
+
.setMinUpdateIntervalMillis(spec.fastestIntervalMs.coerceIn(0, interval))
|
|
261
|
+
.setMinUpdateDistanceMeters(distance)
|
|
262
|
+
.build()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.android
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.location.LocationManager
|
|
5
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
6
|
+
import com.brickssoft.locationtracking.provider.ActivityBackend
|
|
7
|
+
import com.brickssoft.locationtracking.provider.GeofenceBackend
|
|
8
|
+
import com.brickssoft.locationtracking.provider.LocationBackend
|
|
9
|
+
import com.brickssoft.locationtracking.provider.ProviderBundle
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The platform `LocationManager` backends, the fallback when neither GMS nor HMS is usable. Created reflectively via
|
|
13
|
+
* its public (Context) constructor. Each backend is created once, on first use.
|
|
14
|
+
*/
|
|
15
|
+
class AndroidProviderBundle(context: Context) : ProviderBundle {
|
|
16
|
+
private val appContext: Context = context.applicationContext ?: context
|
|
17
|
+
override val kind: ProviderKind = ProviderKind.ANDROID
|
|
18
|
+
|
|
19
|
+
private val location by lazy { AndroidLocationBackend(appContext) }
|
|
20
|
+
private val activity by lazy { AndroidActivityBackend() }
|
|
21
|
+
private val geofence by lazy { AndroidGeofenceBackend(appContext) }
|
|
22
|
+
|
|
23
|
+
/** True if the device has a [LocationManager]. */
|
|
24
|
+
override fun isAvailable(): Boolean = appContext.getSystemService(Context.LOCATION_SERVICE) is LocationManager
|
|
25
|
+
|
|
26
|
+
override fun location(): LocationBackend = location
|
|
27
|
+
|
|
28
|
+
override fun activity(): ActivityBackend = activity
|
|
29
|
+
|
|
30
|
+
override fun geofence(): GeofenceBackend = geofence
|
|
31
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.android
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.location.Location
|
|
5
|
+
import android.location.LocationManager
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import androidx.core.location.LocationManagerCompat
|
|
8
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
9
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
10
|
+
import com.brickssoft.locationtracking.model.DesiredAccuracy
|
|
11
|
+
|
|
12
|
+
/** Provider choice and last-known lookups over the platform [LocationManager]. */
|
|
13
|
+
internal object LocationProviders {
|
|
14
|
+
/** `LocationManager.FUSED_PROVIDER` (public since API 31). */
|
|
15
|
+
private const val FUSED_PROVIDER = "fused"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Providers for [accuracy], most preferred first: HIGH prefers gps, BALANCED/LOW prefer network, each falling back
|
|
19
|
+
* to the other (then `fused` on API 31+); PASSIVE uses only the passive provider.
|
|
20
|
+
*/
|
|
21
|
+
fun preferred(accuracy: DesiredAccuracy): List<String> {
|
|
22
|
+
val fused = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) listOf(FUSED_PROVIDER) else emptyList()
|
|
23
|
+
return when (accuracy) {
|
|
24
|
+
DesiredAccuracy.HIGH -> listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER) + fused
|
|
25
|
+
DesiredAccuracy.BALANCED, DesiredAccuracy.LOW ->
|
|
26
|
+
listOf(LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER) + fused
|
|
27
|
+
DesiredAccuracy.PASSIVE -> listOf(LocationManager.PASSIVE_PROVIDER)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The providers to try for [accuracy], in order: the enabled ones of [preferred], then (unless [enabledOnly]) the
|
|
33
|
+
* existing but disabled ones; a request on a disabled provider starts delivering once it is enabled. If none of
|
|
34
|
+
* them exists on the device, the passive provider is the last resort.
|
|
35
|
+
*/
|
|
36
|
+
fun candidates(lm: LocationManager, accuracy: DesiredAccuracy, enabledOnly: Boolean = false): List<String> {
|
|
37
|
+
val existing = preferred(accuracy).filter { exists(lm, it) }
|
|
38
|
+
if (existing.isEmpty()) {
|
|
39
|
+
val passive = LocationManager.PASSIVE_PROVIDER
|
|
40
|
+
return if (accuracy != DesiredAccuracy.PASSIVE && exists(lm, passive)) listOf(passive) else emptyList()
|
|
41
|
+
}
|
|
42
|
+
val (enabled, disabled) = existing.partition { isEnabled(lm, it) }
|
|
43
|
+
return if (enabledOnly) enabled else enabled + disabled
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun exists(lm: LocationManager, provider: String): Boolean =
|
|
47
|
+
try {
|
|
48
|
+
LocationManagerCompat.hasProvider(lm, provider)
|
|
49
|
+
} catch (e: Exception) {
|
|
50
|
+
false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun isEnabled(lm: LocationManager, provider: String): Boolean =
|
|
54
|
+
try {
|
|
55
|
+
lm.isProviderEnabled(provider)
|
|
56
|
+
} catch (e: Exception) {
|
|
57
|
+
false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The freshest `getLastKnownLocation` across the enabled providers, or null if none has a fix.
|
|
62
|
+
*
|
|
63
|
+
* @throws TrackingException [ErrorCode.PERMISSION_DENIED] if every provider refused with a [SecurityException].
|
|
64
|
+
*/
|
|
65
|
+
@SuppressLint("MissingPermission")
|
|
66
|
+
fun freshestLastKnown(lm: LocationManager): Location? {
|
|
67
|
+
val providers = try {
|
|
68
|
+
lm.getProviders(true)
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
emptyList<String>()
|
|
71
|
+
}
|
|
72
|
+
var best: Location? = null
|
|
73
|
+
var denied: SecurityException? = null
|
|
74
|
+
var permitted = false
|
|
75
|
+
for (provider in providers) {
|
|
76
|
+
val location = try {
|
|
77
|
+
lm.getLastKnownLocation(provider).also { permitted = true }
|
|
78
|
+
} catch (e: SecurityException) {
|
|
79
|
+
denied = e
|
|
80
|
+
continue
|
|
81
|
+
} catch (e: IllegalArgumentException) {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (location != null && (best == null || isNewer(location, best))) best = location
|
|
85
|
+
}
|
|
86
|
+
if (!permitted && denied != null) {
|
|
87
|
+
throw TrackingException(ErrorCode.PERMISSION_DENIED, "Location permission denied", denied)
|
|
88
|
+
}
|
|
89
|
+
return best
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Compares on the elapsed-realtime clock when both fixes carry it, else on wall time. */
|
|
93
|
+
fun isNewer(a: Location, b: Location): Boolean {
|
|
94
|
+
val an = a.elapsedRealtimeNanos
|
|
95
|
+
val bn = b.elapsedRealtimeNanos
|
|
96
|
+
return if (an > 0 && bn > 0) an > bn else a.time > b.time
|
|
97
|
+
}
|
|
98
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.PendingIntent
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.pm.PackageManager
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import androidx.core.content.ContextCompat
|
|
10
|
+
import com.brickssoft.locationtracking.core.Constants
|
|
11
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
12
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
13
|
+
import com.brickssoft.locationtracking.provider.ActivityBackend
|
|
14
|
+
import com.google.android.gms.location.ActivityRecognitionClient
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Activity recognition through `ActivityRecognitionClient.requestActivityUpdates`. Results arrive at
|
|
18
|
+
* [GmsActivityReceiver] through a mutable, explicit broadcast PendingIntent.
|
|
19
|
+
*/
|
|
20
|
+
internal class GmsActivityBackend(
|
|
21
|
+
private val context: Context,
|
|
22
|
+
private val client: ActivityRecognitionClient,
|
|
23
|
+
) : ActivityBackend {
|
|
24
|
+
override val kind: ProviderKind = ProviderKind.GMS
|
|
25
|
+
override val isSupported: Boolean = true
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns false if the ACTIVITY_RECOGNITION runtime permission is missing (API 29+) or GMS rejects the call
|
|
29
|
+
* synchronously. The GMS Task completes later, so an asynchronous rejection can only be logged.
|
|
30
|
+
*/
|
|
31
|
+
override fun start(intervalMs: Long): Boolean {
|
|
32
|
+
if (!hasPermission()) {
|
|
33
|
+
Logger.w(TAG, "activity updates not requested: ACTIVITY_RECOGNITION permission denied")
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return try {
|
|
37
|
+
client.requestActivityUpdates(intervalMs.coerceAtLeast(0), pendingIntent(context))
|
|
38
|
+
.logFailure(TAG, "requestActivityUpdates")
|
|
39
|
+
Logger.d(TAG, "activity updates requested every $intervalMs ms")
|
|
40
|
+
true
|
|
41
|
+
} catch (e: Exception) {
|
|
42
|
+
Logger.w(TAG, "requestActivityUpdates failed", e)
|
|
43
|
+
false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Removes the updates if their PendingIntent exists (GMS keeps it alive while registered, across restarts). */
|
|
48
|
+
override fun stop() {
|
|
49
|
+
try {
|
|
50
|
+
val pending = existingPendingIntent(context) ?: return
|
|
51
|
+
client.removeActivityUpdates(pending).logFailure(TAG, "removeActivityUpdates")
|
|
52
|
+
} catch (e: Exception) {
|
|
53
|
+
Logger.w(TAG, "removeActivityUpdates failed", e)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fun hasPermission(): Boolean =
|
|
58
|
+
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
|
|
59
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
|
|
60
|
+
PackageManager.PERMISSION_GRANTED
|
|
61
|
+
|
|
62
|
+
companion object {
|
|
63
|
+
private const val TAG = "LT.GmsActivity"
|
|
64
|
+
|
|
65
|
+
/** Explicit broadcast to [GmsActivityReceiver]; mutable because GMS fills in the result. */
|
|
66
|
+
fun pendingIntent(context: Context): PendingIntent =
|
|
67
|
+
PendingIntent.getBroadcast(context, Constants.RC_GMS_ACTIVITY, intent(context), Constants.piMutable())
|
|
68
|
+
|
|
69
|
+
/** The PendingIntent of [pendingIntent] if it exists, without creating it. */
|
|
70
|
+
fun existingPendingIntent(context: Context): PendingIntent? = PendingIntent.getBroadcast(
|
|
71
|
+
context,
|
|
72
|
+
Constants.RC_GMS_ACTIVITY,
|
|
73
|
+
intent(context),
|
|
74
|
+
Constants.piMutable() or PendingIntent.FLAG_NO_CREATE,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
private fun intent(context: Context): Intent =
|
|
78
|
+
Intent(context, GmsActivityReceiver::class.java).setAction(Constants.ACTION_ACTIVITY)
|
|
79
|
+
}
|
|
80
|
+
}
|
package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import com.brickssoft.locationtracking.core.Components
|
|
7
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
8
|
+
import com.brickssoft.locationtracking.model.ActivitySample
|
|
9
|
+
import com.brickssoft.locationtracking.model.ActivityType
|
|
10
|
+
import com.brickssoft.locationtracking.provider.ActivitySink
|
|
11
|
+
import com.google.android.gms.location.ActivityRecognitionResult
|
|
12
|
+
import com.google.android.gms.location.DetectedActivity
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Receives `ActivityRecognitionResult`s from the PendingIntent of [GmsActivityBackend] and delivers them to the
|
|
16
|
+
* engine ([ActivitySink]) with `goAsync()` on `Components.scope`.
|
|
17
|
+
*/
|
|
18
|
+
class GmsActivityReceiver internal constructor(
|
|
19
|
+
private val target: (Context) -> ReceiverTarget<ActivitySink>,
|
|
20
|
+
) : BroadcastReceiver() {
|
|
21
|
+
constructor() : this({ ctx -> Components.get(ctx).let { ReceiverTarget(it.scope, it.engine) } })
|
|
22
|
+
|
|
23
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
24
|
+
val samples = parseActivitySamples(intent)
|
|
25
|
+
if (samples.isEmpty()) return
|
|
26
|
+
Logger.v(TAG, "activity: $samples")
|
|
27
|
+
deliverAsync(context, target, TAG, "activity delivery") { it.onActivitySamples(samples) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
internal companion object {
|
|
31
|
+
private const val TAG = "LT.GmsActivity"
|
|
32
|
+
|
|
33
|
+
/** The samples in [intent], most probable first; empty if it carries no result. */
|
|
34
|
+
fun parseActivitySamples(intent: Intent?): List<ActivitySample> {
|
|
35
|
+
val result = try {
|
|
36
|
+
intent?.let { ActivityRecognitionResult.extractResult(it) }
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
Logger.w(TAG, "unreadable activity result", e)
|
|
39
|
+
null
|
|
40
|
+
} ?: return emptyList()
|
|
41
|
+
return toActivitySamples(result.probableActivities.orEmpty())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Maps GMS activities, keeping the highest confidence per [ActivityType], sorted by confidence descending. */
|
|
45
|
+
fun toActivitySamples(activities: List<DetectedActivity>): List<ActivitySample> = activities
|
|
46
|
+
.map { ActivitySample(mapActivityType(it.type), it.confidence.coerceIn(0, 100)) }
|
|
47
|
+
.groupBy { it.type }
|
|
48
|
+
.map { (_, samples) -> samples.maxBy { it.confidence } }
|
|
49
|
+
.sortedByDescending { it.confidence }
|
|
50
|
+
|
|
51
|
+
/** TILTING, UNKNOWN and any future type map to [ActivityType.UNKNOWN]. */
|
|
52
|
+
fun mapActivityType(type: Int): ActivityType = when (type) {
|
|
53
|
+
DetectedActivity.IN_VEHICLE -> ActivityType.IN_VEHICLE
|
|
54
|
+
DetectedActivity.ON_BICYCLE -> ActivityType.ON_BICYCLE
|
|
55
|
+
DetectedActivity.ON_FOOT -> ActivityType.ON_FOOT
|
|
56
|
+
DetectedActivity.WALKING -> ActivityType.WALKING
|
|
57
|
+
DetectedActivity.RUNNING -> ActivityType.RUNNING
|
|
58
|
+
DetectedActivity.STILL -> ActivityType.STILL
|
|
59
|
+
else -> ActivityType.UNKNOWN
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.gms
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
4
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
5
|
+
import com.google.android.gms.common.api.ApiException
|
|
6
|
+
import com.google.android.gms.common.api.CommonStatusCodes
|
|
7
|
+
import com.google.android.gms.location.GeofenceStatusCodes
|
|
8
|
+
|
|
9
|
+
/** Maps Google Play services failures to [TrackingException]. */
|
|
10
|
+
internal object GmsErrors {
|
|
11
|
+
/**
|
|
12
|
+
* - [TrackingException] is returned unchanged.
|
|
13
|
+
* - [SecurityException] and `GEOFENCE_INSUFFICIENT_LOCATION_PERMISSION` become PERMISSION_DENIED.
|
|
14
|
+
* - `GEOFENCE_TOO_MANY_GEOFENCES` and `GEOFENCE_TOO_MANY_PENDING_INTENTS` become TOO_MANY_GEOFENCES.
|
|
15
|
+
* - `TIMEOUT` becomes TIMEOUT; `DEVELOPER_ERROR` and `INTERNAL_ERROR` become INTERNAL.
|
|
16
|
+
* - Every other [ApiException] (e.g. `GEOFENCE_NOT_AVAILABLE`, `API_NOT_CONNECTED`) and [IllegalStateException]
|
|
17
|
+
* (GMS "not connected") become UNAVAILABLE.
|
|
18
|
+
* - [IllegalArgumentException] (rejected by a GMS builder) becomes INVALID_ARGUMENT; anything else INTERNAL.
|
|
19
|
+
*/
|
|
20
|
+
fun map(error: Throwable, operation: String): TrackingException = when (error) {
|
|
21
|
+
is TrackingException -> error
|
|
22
|
+
is SecurityException -> TrackingException(
|
|
23
|
+
ErrorCode.PERMISSION_DENIED,
|
|
24
|
+
"$operation: location permission denied",
|
|
25
|
+
error,
|
|
26
|
+
)
|
|
27
|
+
is ApiException -> TrackingException(
|
|
28
|
+
codeFor(error.statusCode),
|
|
29
|
+
"$operation failed: ${describe(error.statusCode)}",
|
|
30
|
+
error,
|
|
31
|
+
)
|
|
32
|
+
is IllegalArgumentException -> TrackingException(
|
|
33
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
34
|
+
"$operation: ${error.message ?: "invalid argument"}",
|
|
35
|
+
error,
|
|
36
|
+
)
|
|
37
|
+
is IllegalStateException -> TrackingException(
|
|
38
|
+
ErrorCode.UNAVAILABLE,
|
|
39
|
+
"$operation failed: ${reason(error)}",
|
|
40
|
+
error,
|
|
41
|
+
)
|
|
42
|
+
else -> TrackingException(ErrorCode.INTERNAL, "$operation failed: ${reason(error)}", error)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The [ErrorCode] for a GMS status code (see [map]). */
|
|
46
|
+
fun codeFor(statusCode: Int): ErrorCode = when (statusCode) {
|
|
47
|
+
GeofenceStatusCodes.GEOFENCE_INSUFFICIENT_LOCATION_PERMISSION -> ErrorCode.PERMISSION_DENIED
|
|
48
|
+
GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES,
|
|
49
|
+
GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS,
|
|
50
|
+
-> ErrorCode.TOO_MANY_GEOFENCES
|
|
51
|
+
CommonStatusCodes.TIMEOUT -> ErrorCode.TIMEOUT
|
|
52
|
+
CommonStatusCodes.DEVELOPER_ERROR, CommonStatusCodes.INTERNAL_ERROR -> ErrorCode.INTERNAL
|
|
53
|
+
else -> ErrorCode.UNAVAILABLE
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** E.g. `GEOFENCE_TOO_MANY_GEOFENCES (1001)`, with a hint for `GEOFENCE_NOT_AVAILABLE`. */
|
|
57
|
+
fun describe(statusCode: Int): String {
|
|
58
|
+
val name = try {
|
|
59
|
+
GeofenceStatusCodes.getStatusCodeString(statusCode)
|
|
60
|
+
} catch (e: Exception) {
|
|
61
|
+
"status"
|
|
62
|
+
}
|
|
63
|
+
val hint = if (statusCode == GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE) {
|
|
64
|
+
"; location is off or Google Location Accuracy is disabled"
|
|
65
|
+
} else {
|
|
66
|
+
""
|
|
67
|
+
}
|
|
68
|
+
return "$name ($statusCode)$hint"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun reason(error: Throwable): String = error.message ?: error::class.java.name
|
|
72
|
+
}
|