@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/DefaultProviderFactory.kt
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.brickssoft.locationtracking.config.ConfigStore
|
|
5
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
6
|
+
import com.brickssoft.locationtracking.model.LocationProviderSetting
|
|
7
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
8
|
+
import com.brickssoft.locationtracking.provider.android.AndroidProviderBundle
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Selects GMS, HMS or the platform `LocationManager` from config `locationProvider`:
|
|
12
|
+
* - `auto`: GMS if it is packaged ([ProviderPackaging]: allowed by the app's `PROVIDERS` meta-data, SDK class present,
|
|
13
|
+
* receivers declared) and its bundle is available; else HMS likewise; else ANDROID.
|
|
14
|
+
* - `gms` / `hms`: that backend, or ANDROID (with a warning) if it is not packaged or not available.
|
|
15
|
+
* - `android`: always ANDROID.
|
|
16
|
+
*
|
|
17
|
+
* Bundles are created only by reflection ([ProviderBundles]) and cached; any [Throwable] while probing or creating
|
|
18
|
+
* one (e.g. [NoClassDefFoundError]) marks it unavailable. Selection is lazy (first access), thread-safe, and
|
|
19
|
+
* re-evaluated only by [reselect].
|
|
20
|
+
*
|
|
21
|
+
* @param classPresent whether a class can be loaded; defaults to `Class.forName(name, false, loader)`.
|
|
22
|
+
* @param createBundle instantiates a bundle class by name; defaults to its public (Context) constructor.
|
|
23
|
+
* @param receiverDeclared whether a receiver is declared in the merged manifest; defaults to `getReceiverInfo`.
|
|
24
|
+
* @param providersMetaData the app's `PROVIDERS` meta-data value, or null; defaults to the application's meta-data.
|
|
25
|
+
*/
|
|
26
|
+
class DefaultProviderFactory(
|
|
27
|
+
context: Context,
|
|
28
|
+
private val configStore: ConfigStore,
|
|
29
|
+
classPresent: (String) -> Boolean = reflectiveClassPresent(context.classLoader),
|
|
30
|
+
private val createBundle: (String) -> ProviderBundle = reflectiveBundleCreator(context),
|
|
31
|
+
receiverDeclared: (String) -> Boolean = ProviderPackaging.receiverDeclaredIn(context.applicationContext ?: context),
|
|
32
|
+
providersMetaData: () -> String? = ProviderPackaging.providersMetaDataIn(context.applicationContext ?: context),
|
|
33
|
+
) : ProviderFactory {
|
|
34
|
+
private val appContext: Context = context.applicationContext ?: context
|
|
35
|
+
|
|
36
|
+
private val packaging = ProviderPackaging(classPresent, receiverDeclared, providersMetaData)
|
|
37
|
+
|
|
38
|
+
private val lock = Any()
|
|
39
|
+
|
|
40
|
+
@Volatile
|
|
41
|
+
private var selection: Selection? = null
|
|
42
|
+
|
|
43
|
+
/** Created bundles; a null value means the bundle could not be loaded. Guarded by [lock]. */
|
|
44
|
+
private val bundles = HashMap<ProviderKind, ProviderBundle?>()
|
|
45
|
+
|
|
46
|
+
override val kind: ProviderKind get() = current().kind
|
|
47
|
+
|
|
48
|
+
override fun location(): LocationBackend = current().bundle.location()
|
|
49
|
+
|
|
50
|
+
override fun activity(): ActivityBackend = current().bundle.activity()
|
|
51
|
+
|
|
52
|
+
override fun geofence(): GeofenceBackend = current().bundle.geofence()
|
|
53
|
+
|
|
54
|
+
override fun isAvailable(kind: ProviderKind): Boolean {
|
|
55
|
+
val bundle = synchronized(lock) { bundleLocked(kind) } ?: return false
|
|
56
|
+
return isUsable(bundle)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun reselect(): Boolean = synchronized(lock) {
|
|
60
|
+
val previous = selection
|
|
61
|
+
val next = selectLocked()
|
|
62
|
+
selection = next
|
|
63
|
+
previous != null && previous.kind != next.kind
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun current(): Selection =
|
|
67
|
+
selection ?: synchronized(lock) { selection ?: selectLocked().also { selection = it } }
|
|
68
|
+
|
|
69
|
+
private fun selectLocked(): Selection {
|
|
70
|
+
val setting = configStore.config.value.locationProvider
|
|
71
|
+
val kind = when (setting) {
|
|
72
|
+
LocationProviderSetting.AUTO -> when {
|
|
73
|
+
availableLocked(ProviderKind.GMS) -> ProviderKind.GMS
|
|
74
|
+
availableLocked(ProviderKind.HMS) -> ProviderKind.HMS
|
|
75
|
+
else -> ProviderKind.ANDROID
|
|
76
|
+
}
|
|
77
|
+
LocationProviderSetting.GMS -> explicitLocked(ProviderKind.GMS)
|
|
78
|
+
LocationProviderSetting.HMS -> explicitLocked(ProviderKind.HMS)
|
|
79
|
+
LocationProviderSetting.ANDROID -> ProviderKind.ANDROID
|
|
80
|
+
}
|
|
81
|
+
val bundle = if (kind == ProviderKind.ANDROID) androidBundleLocked() else checkNotNull(bundles[kind])
|
|
82
|
+
if (selection?.kind != kind) Logger.i(TAG, "location backend: ${kind.wire} (locationProvider=${setting.wire})")
|
|
83
|
+
return Selection(kind, bundle)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private fun explicitLocked(kind: ProviderKind): ProviderKind {
|
|
87
|
+
if (availableLocked(kind)) return kind
|
|
88
|
+
Logger.w(TAG, "locationProvider=${kind.wire} is not packaged or not available; falling back to android")
|
|
89
|
+
return ProviderKind.ANDROID
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private fun availableLocked(kind: ProviderKind): Boolean = bundleLocked(kind)?.let(::isUsable) ?: false
|
|
93
|
+
|
|
94
|
+
/** The cached bundle of [kind], loading it on first use; null if its SDK or bundle class cannot be loaded. */
|
|
95
|
+
private fun bundleLocked(kind: ProviderKind): ProviderBundle? {
|
|
96
|
+
if (bundles.containsKey(kind)) return bundles[kind]
|
|
97
|
+
val bundle = loadBundle(kind)
|
|
98
|
+
bundles[kind] = bundle
|
|
99
|
+
return bundle
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun loadBundle(kind: ProviderKind): ProviderBundle? {
|
|
103
|
+
val reason = when (packaging.status(kind)) {
|
|
104
|
+
ProviderPackaging.Status.PACKAGED -> null
|
|
105
|
+
ProviderPackaging.Status.NOT_LISTED -> "not listed in ${ProviderPackaging.PROVIDERS_META_DATA}"
|
|
106
|
+
ProviderPackaging.Status.SDK_MISSING -> "SDK is not packaged"
|
|
107
|
+
ProviderPackaging.Status.RECEIVERS_REMOVED -> "SDK is present but its receivers are not in the manifest"
|
|
108
|
+
}
|
|
109
|
+
if (reason != null) {
|
|
110
|
+
// Listed in the app's PROVIDERS meta-data but unusable is a setup error; otherwise it is expected.
|
|
111
|
+
if (packaging.listedExplicitly(kind)) {
|
|
112
|
+
Logger.w(TAG, "${kind.wire} is listed in ${ProviderPackaging.PROVIDERS_META_DATA} but not used: $reason")
|
|
113
|
+
} else {
|
|
114
|
+
Logger.i(TAG, "${kind.wire} is not used: $reason")
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
val name = ProviderBundles.bundleClassName(kind)
|
|
119
|
+
val bundle = guard("create $name") { createBundle(name) }
|
|
120
|
+
if (bundle == null && kind == ProviderKind.ANDROID) {
|
|
121
|
+
// The last resort must always exist; it lives in this module, so it can be constructed directly.
|
|
122
|
+
Logger.e(TAG, "reflective creation of the android bundle failed; constructing it directly")
|
|
123
|
+
return AndroidProviderBundle(appContext)
|
|
124
|
+
}
|
|
125
|
+
return bundle
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun androidBundleLocked(): ProviderBundle = checkNotNull(bundleLocked(ProviderKind.ANDROID))
|
|
129
|
+
|
|
130
|
+
private fun isUsable(bundle: ProviderBundle): Boolean =
|
|
131
|
+
guard("${bundle.javaClass.name}.isAvailable") { bundle.isAvailable() } == true
|
|
132
|
+
|
|
133
|
+
/** Runs [block]; any failure except a VM error (out of memory, stack overflow) is logged and yields null. */
|
|
134
|
+
private inline fun <T> guard(what: String, block: () -> T): T? =
|
|
135
|
+
try {
|
|
136
|
+
block()
|
|
137
|
+
} catch (e: VirtualMachineError) {
|
|
138
|
+
throw e
|
|
139
|
+
} catch (t: Throwable) {
|
|
140
|
+
Logger.w(TAG, "$what failed: ${t.javaClass.name}: ${t.message}", t)
|
|
141
|
+
null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private data class Selection(val kind: ProviderKind, val bundle: ProviderBundle)
|
|
145
|
+
|
|
146
|
+
companion object {
|
|
147
|
+
private const val TAG = "LT.ProviderFactory"
|
|
148
|
+
|
|
149
|
+
/** `Class.forName(name, false, loader)`: true if the class exists, without initializing it. */
|
|
150
|
+
fun reflectiveClassPresent(loader: ClassLoader?): (String) -> Boolean = { name ->
|
|
151
|
+
try {
|
|
152
|
+
Class.forName(name, false, loader ?: DefaultProviderFactory::class.java.classLoader)
|
|
153
|
+
true
|
|
154
|
+
} catch (e: ClassNotFoundException) {
|
|
155
|
+
false
|
|
156
|
+
} catch (e: LinkageError) {
|
|
157
|
+
false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Instantiates a [ProviderBundle] class through its public (Context) constructor, with the app context. */
|
|
162
|
+
fun reflectiveBundleCreator(context: Context): (String) -> ProviderBundle = { name ->
|
|
163
|
+
val appContext = context.applicationContext ?: context
|
|
164
|
+
val loader = appContext.classLoader ?: DefaultProviderFactory::class.java.classLoader
|
|
165
|
+
Class.forName(name, true, loader).getConstructor(Context::class.java).newInstance(appContext)
|
|
166
|
+
as ProviderBundle
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider
|
|
2
|
+
|
|
3
|
+
import android.content.ComponentName
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.ApplicationInfo
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
9
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether the GMS or HMS backend may be used in this APK. ANDROID always may. GMS or HMS is packaged when:
|
|
13
|
+
* 1. the app's `<meta-data android:name="com.brickssoft.locationtracking.PROVIDERS">` ([PROVIDERS_META_DATA]) lists
|
|
14
|
+
* it, or the app declares no such entry;
|
|
15
|
+
* 2. its SDK class is present; and
|
|
16
|
+
* 3. the receivers its activity and geofence results are delivered to are declared in the app's merged manifest.
|
|
17
|
+
*
|
|
18
|
+
* An app that publishes one APK per store declares the entry in each flavor's manifest (`gms` or `hms`) and removes
|
|
19
|
+
* the other provider's receivers with `tools:node="remove"` (README, "Separate Play and AppGallery APKs"). The other
|
|
20
|
+
* provider's SDK classes can still reach the APK through other libraries; the entry and the missing receivers both
|
|
21
|
+
* keep the plugin from using it.
|
|
22
|
+
*
|
|
23
|
+
* The entry's value is a comma-separated list of `gms`, `hms` and `android` (case and spaces are ignored); `android`
|
|
24
|
+
* alone means neither GMS nor HMS. Unknown names are logged and ignored. A value with no known name is logged as an
|
|
25
|
+
* error and the entry is ignored.
|
|
26
|
+
*
|
|
27
|
+
* A check that throws (anything except a VM error) reads as not packaged and is logged.
|
|
28
|
+
*
|
|
29
|
+
* @param classPresent whether a class can be loaded (see [DefaultProviderFactory.reflectiveClassPresent]).
|
|
30
|
+
* @param receiverDeclared whether a receiver class is declared in the merged manifest (see [receiverDeclaredIn]).
|
|
31
|
+
* @param providersMetaData the raw value of the [PROVIDERS_META_DATA] entry, or null if the app declares none (see
|
|
32
|
+
* [providersMetaDataIn]).
|
|
33
|
+
*/
|
|
34
|
+
class ProviderPackaging(
|
|
35
|
+
private val classPresent: (String) -> Boolean,
|
|
36
|
+
private val receiverDeclared: (String) -> Boolean,
|
|
37
|
+
providersMetaData: () -> String? = { null },
|
|
38
|
+
) {
|
|
39
|
+
enum class Status { PACKAGED, NOT_LISTED, SDK_MISSING, RECEIVERS_REMOVED }
|
|
40
|
+
|
|
41
|
+
/** The providers the app's entry lists, or null if there is no usable entry. Read once. */
|
|
42
|
+
private val listed: Set<ProviderKind>? by lazy {
|
|
43
|
+
val raw = passesOrNull("read meta-data $PROVIDERS_META_DATA") { providersMetaData() }
|
|
44
|
+
raw?.let(::parseProviders)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True if the app's [PROVIDERS_META_DATA] entry names [kind]; false without an entry. */
|
|
48
|
+
fun listedExplicitly(kind: ProviderKind): Boolean = listed?.contains(kind) == true
|
|
49
|
+
|
|
50
|
+
fun status(kind: ProviderKind): Status {
|
|
51
|
+
val sdkClass = ProviderBundles.sdkClassName(kind) ?: return Status.PACKAGED
|
|
52
|
+
listed?.let { if (kind !in it) return Status.NOT_LISTED }
|
|
53
|
+
if (!passes("probe $sdkClass") { classPresent(sdkClass) }) return Status.SDK_MISSING
|
|
54
|
+
val receivers = ProviderBundles.receiverClassNames(kind)
|
|
55
|
+
if (!receivers.all { passes("look up receiver $it") { receiverDeclared(it) } }) return Status.RECEIVERS_REMOVED
|
|
56
|
+
return Status.PACKAGED
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun isPackaged(kind: ProviderKind): Boolean = status(kind) == Status.PACKAGED
|
|
60
|
+
|
|
61
|
+
private fun parseProviders(raw: String): Set<ProviderKind>? {
|
|
62
|
+
val names = raw.split(',').map { it.trim().lowercase() }.filter { it.isNotEmpty() }
|
|
63
|
+
val kinds = names.mapNotNull { name ->
|
|
64
|
+
ProviderKind.fromWire(name) ?: null.also {
|
|
65
|
+
Logger.w(TAG, "$PROVIDERS_META_DATA: unknown provider '$name' ignored (use gms, hms or android)")
|
|
66
|
+
}
|
|
67
|
+
}.toSet()
|
|
68
|
+
if (kinds.isEmpty()) {
|
|
69
|
+
Logger.e(TAG, "$PROVIDERS_META_DATA='$raw' names no provider (use gms, hms or android); entry ignored")
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
return kinds
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private inline fun passes(what: String, block: () -> Boolean): Boolean = passesOrNull(what, block) ?: false
|
|
76
|
+
|
|
77
|
+
private inline fun <T> passesOrNull(what: String, block: () -> T): T? =
|
|
78
|
+
try {
|
|
79
|
+
block()
|
|
80
|
+
} catch (e: VirtualMachineError) {
|
|
81
|
+
throw e
|
|
82
|
+
} catch (t: Throwable) {
|
|
83
|
+
Logger.w(TAG, "$what failed: ${t.javaClass.name}: ${t.message}", t)
|
|
84
|
+
null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
companion object {
|
|
88
|
+
private const val TAG = "LT.ProviderPackaging"
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Name of the `<meta-data>` in the app's `<application>` that lists the providers the plugin may use:
|
|
92
|
+
* `<meta-data android:name="com.brickssoft.locationtracking.PROVIDERS" android:value="hms"/>`.
|
|
93
|
+
*/
|
|
94
|
+
const val PROVIDERS_META_DATA = "com.brickssoft.locationtracking.PROVIDERS"
|
|
95
|
+
|
|
96
|
+
/** Flags of the receiver lookup: disabled receivers and both direct-boot states count as declared. */
|
|
97
|
+
const val RECEIVER_LOOKUP_FLAGS = PackageManager.MATCH_DISABLED_COMPONENTS or
|
|
98
|
+
PackageManager.MATCH_DIRECT_BOOT_AWARE or
|
|
99
|
+
PackageManager.MATCH_DIRECT_BOOT_UNAWARE
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `PackageManager.getReceiverInfo` for a receiver of this app: true if the merged manifest declares it.
|
|
103
|
+
* [RECEIVER_LOOKUP_FLAGS] make the answer independent of whether the receiver is enabled and of whether the
|
|
104
|
+
* user has unlocked the phone since boot (before that, Android hides components that are not direct-boot
|
|
105
|
+
* aware, which the plugin's receivers are not).
|
|
106
|
+
*/
|
|
107
|
+
fun receiverDeclaredIn(context: Context): (String) -> Boolean = { name ->
|
|
108
|
+
try {
|
|
109
|
+
@Suppress("DEPRECATION") // the ComponentInfoFlags overload needs API 33
|
|
110
|
+
context.packageManager.getReceiverInfo(ComponentName(context.packageName, name), RECEIVER_LOOKUP_FLAGS)
|
|
111
|
+
true
|
|
112
|
+
} catch (e: PackageManager.NameNotFoundException) {
|
|
113
|
+
false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The string value of the app's [PROVIDERS_META_DATA] entry, or null if it declares none. */
|
|
118
|
+
fun providersMetaDataIn(context: Context): () -> String? = {
|
|
119
|
+
applicationInfo(context).metaData?.getString(PROVIDERS_META_DATA)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun applicationInfo(context: Context): ApplicationInfo {
|
|
123
|
+
val pm = context.packageManager
|
|
124
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
125
|
+
pm.getApplicationInfo(
|
|
126
|
+
context.packageName,
|
|
127
|
+
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()),
|
|
128
|
+
)
|
|
129
|
+
} else {
|
|
130
|
+
@Suppress("DEPRECATION")
|
|
131
|
+
pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.android
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
4
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
5
|
+
import com.brickssoft.locationtracking.provider.ActivityBackend
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The platform has no activity recognition without GMS or HMS: [isSupported] is false and [start] always returns
|
|
9
|
+
* false, so motion detection relies on location distance (`stationaryRadius`) alone.
|
|
10
|
+
*/
|
|
11
|
+
class AndroidActivityBackend : ActivityBackend {
|
|
12
|
+
override val kind: ProviderKind = ProviderKind.ANDROID
|
|
13
|
+
override val isSupported: Boolean = false
|
|
14
|
+
|
|
15
|
+
override fun start(intervalMs: Long): Boolean {
|
|
16
|
+
Logger.d(TAG, "activity recognition is not supported by the android backend")
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun stop() = Unit
|
|
21
|
+
|
|
22
|
+
private companion object {
|
|
23
|
+
const val TAG = "LT.AndroidActivity"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.android
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.PendingIntent
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.SharedPreferences
|
|
8
|
+
import android.location.LocationManager
|
|
9
|
+
import android.net.Uri
|
|
10
|
+
import com.brickssoft.locationtracking.core.Constants
|
|
11
|
+
import com.brickssoft.locationtracking.core.ErrorCode
|
|
12
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
13
|
+
import com.brickssoft.locationtracking.core.TrackingException
|
|
14
|
+
import com.brickssoft.locationtracking.model.ProviderKind
|
|
15
|
+
import com.brickssoft.locationtracking.provider.GeofenceBackend
|
|
16
|
+
import com.brickssoft.locationtracking.provider.OsGeofence
|
|
17
|
+
|
|
18
|
+
/** Seam over the platform proximity-alert API (`LocationManager.addProximityAlert` / `removeProximityAlert`). */
|
|
19
|
+
interface ProximityAlerts {
|
|
20
|
+
/** Registers a circular alert that never expires. May throw [SecurityException] without fine location. */
|
|
21
|
+
fun add(latitude: Double, longitude: Double, radius: Float, intent: PendingIntent)
|
|
22
|
+
|
|
23
|
+
fun remove(intent: PendingIntent)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** [ProximityAlerts] backed by the platform [LocationManager]. */
|
|
27
|
+
internal class LocationManagerProximityAlerts(context: Context) : ProximityAlerts {
|
|
28
|
+
private val lm = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
|
|
29
|
+
|
|
30
|
+
@SuppressLint("MissingPermission")
|
|
31
|
+
override fun add(latitude: Double, longitude: Double, radius: Float, intent: PendingIntent) {
|
|
32
|
+
val manager = lm ?: throw TrackingException(ErrorCode.UNAVAILABLE, "LocationManager is unavailable")
|
|
33
|
+
manager.addProximityAlert(latitude, longitude, radius, NO_EXPIRATION, intent)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun remove(intent: PendingIntent) {
|
|
37
|
+
lm?.removeProximityAlert(intent)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private companion object {
|
|
41
|
+
const val NO_EXPIRATION = -1L
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* [GeofenceBackend] over `LocationManager` proximity alerts.
|
|
47
|
+
*
|
|
48
|
+
* - One mutable broadcast PendingIntent per region: explicit [AndroidGeofenceReceiver] component, action
|
|
49
|
+
* [Constants.ACTION_GEOFENCE], data URI [Constants.geofenceUri] and request code [Constants.RC_ANDROID_PROXIMITY].
|
|
50
|
+
* The region's `onEntry`/`onExit` flags travel as extras; the receiver drops unwanted transitions.
|
|
51
|
+
* - No dwell ([supportsDwell] is false). The platform always reports ENTER when the device is already inside, so
|
|
52
|
+
* `initialTriggerEntry` is left to the GeofenceManager.
|
|
53
|
+
* - Registered ids are kept in memory and mirrored to SharedPreferences, so [removeAll] also clears alerts that were
|
|
54
|
+
* registered before a process restart. [remove] re-creates each PendingIntent from its id.
|
|
55
|
+
* - [add] validates every region before touching the OS ([ErrorCode.INVALID_ARGUMENT]). If the platform then fails,
|
|
56
|
+
* the regions that call introduced are removed again; a region that was already registered keeps its new circle if
|
|
57
|
+
* it was re-added, or is dropped if re-adding it failed (its previous circle is already gone).
|
|
58
|
+
* - Calls run on the caller's thread: a few binder calls per region, no disk I/O after the first preferences load
|
|
59
|
+
* (started in the background at construction).
|
|
60
|
+
*/
|
|
61
|
+
class AndroidGeofenceBackend(
|
|
62
|
+
context: Context,
|
|
63
|
+
private val alerts: ProximityAlerts = LocationManagerProximityAlerts(context),
|
|
64
|
+
) : GeofenceBackend {
|
|
65
|
+
private val appContext: Context = context.applicationContext ?: context
|
|
66
|
+
override val kind: ProviderKind = ProviderKind.ANDROID
|
|
67
|
+
override val supportsDwell: Boolean = false
|
|
68
|
+
|
|
69
|
+
private val lock = Any()
|
|
70
|
+
private val prefs: SharedPreferences = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
71
|
+
private var ids: MutableSet<String>? = null
|
|
72
|
+
|
|
73
|
+
/** Ids currently registered with the OS (as far as this backend knows). */
|
|
74
|
+
internal val registeredIds: Set<String> get() = synchronized(lock) { registered().toSet() }
|
|
75
|
+
|
|
76
|
+
override suspend fun add(regions: List<OsGeofence>) {
|
|
77
|
+
for (region in regions) {
|
|
78
|
+
invalidReason(region)?.let {
|
|
79
|
+
throw TrackingException(ErrorCode.INVALID_ARGUMENT, "Invalid geofence '${region.id}': $it")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
synchronized(lock) {
|
|
83
|
+
val before = registered().toSet()
|
|
84
|
+
val added = ArrayList<String>(regions.size)
|
|
85
|
+
for (region in regions) {
|
|
86
|
+
val intent = pendingIntentFor(region)
|
|
87
|
+
try {
|
|
88
|
+
// The platform keys alerts by (PendingIntent, circle): drop the previous circle of this id first.
|
|
89
|
+
alerts.remove(intent)
|
|
90
|
+
alerts.add(region.latitude, region.longitude, region.radius, intent)
|
|
91
|
+
} catch (e: Exception) {
|
|
92
|
+
(added.filter { it !in before } + region.id).forEach(::removeLocked)
|
|
93
|
+
persist()
|
|
94
|
+
throw toTrackingException(e)
|
|
95
|
+
}
|
|
96
|
+
added += region.id
|
|
97
|
+
registered() += region.id
|
|
98
|
+
}
|
|
99
|
+
persist()
|
|
100
|
+
if (regions.isNotEmpty()) Logger.d(TAG, "added ${regions.size} proximity alert(s)")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override suspend fun remove(ids: List<String>) {
|
|
105
|
+
synchronized(lock) {
|
|
106
|
+
ids.forEach(::removeLocked)
|
|
107
|
+
persist()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override suspend fun removeAll() {
|
|
112
|
+
synchronized(lock) {
|
|
113
|
+
registered().toList().forEach(::removeLocked)
|
|
114
|
+
persist()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private fun removeLocked(id: String) {
|
|
119
|
+
existingPendingIntent(id)?.let { intent ->
|
|
120
|
+
try {
|
|
121
|
+
alerts.remove(intent)
|
|
122
|
+
} catch (e: Exception) {
|
|
123
|
+
Logger.w(TAG, "removeProximityAlert failed for $id", e)
|
|
124
|
+
}
|
|
125
|
+
intent.cancel()
|
|
126
|
+
}
|
|
127
|
+
registered() -= id
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun registered(): MutableSet<String> =
|
|
131
|
+
ids ?: HashSet(prefs.getStringSet(KEY_IDS, null).orEmpty()).also { ids = it }
|
|
132
|
+
|
|
133
|
+
private fun persist() {
|
|
134
|
+
prefs.edit().putStringSet(KEY_IDS, HashSet(registered())).apply()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun pendingIntentFor(region: OsGeofence): PendingIntent {
|
|
138
|
+
val intent = baseIntent(region.id)
|
|
139
|
+
.putExtra(EXTRA_NOTIFY_ENTRY, region.onEntry)
|
|
140
|
+
.putExtra(EXTRA_NOTIFY_EXIT, region.onExit)
|
|
141
|
+
return PendingIntent.getBroadcast(appContext, Constants.RC_ANDROID_PROXIMITY, intent, Constants.piMutable())
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The PendingIntent registered for [id], if any; same identity (intent, request code, mutability) as created. */
|
|
145
|
+
private fun existingPendingIntent(id: String): PendingIntent? {
|
|
146
|
+
val flags = PendingIntent.FLAG_NO_CREATE or (Constants.piMutable() and PendingIntent.FLAG_UPDATE_CURRENT.inv())
|
|
147
|
+
return PendingIntent.getBroadcast(appContext, Constants.RC_ANDROID_PROXIMITY, baseIntent(id), flags)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private fun baseIntent(id: String): Intent =
|
|
151
|
+
Intent(appContext, AndroidGeofenceReceiver::class.java)
|
|
152
|
+
.setAction(Constants.ACTION_GEOFENCE)
|
|
153
|
+
.setData(Uri.parse(Constants.geofenceUri(id)))
|
|
154
|
+
|
|
155
|
+
private fun invalidReason(region: OsGeofence): String? = when {
|
|
156
|
+
region.id.isEmpty() -> "empty identifier"
|
|
157
|
+
region.latitude !in -90.0..90.0 -> "latitude ${region.latitude} out of range"
|
|
158
|
+
region.longitude !in -180.0..180.0 -> "longitude ${region.longitude} out of range"
|
|
159
|
+
!region.radius.isFinite() || region.radius <= 0f -> "radius ${region.radius} must be > 0"
|
|
160
|
+
else -> null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun toTrackingException(e: Exception): TrackingException = when (e) {
|
|
164
|
+
is TrackingException -> e
|
|
165
|
+
is SecurityException ->
|
|
166
|
+
TrackingException(ErrorCode.PERMISSION_DENIED, "Proximity alerts need fine location permission", e)
|
|
167
|
+
is IllegalArgumentException ->
|
|
168
|
+
TrackingException(ErrorCode.INVALID_ARGUMENT, "Invalid geofence: ${e.message}", e)
|
|
169
|
+
else -> TrackingException(ErrorCode.UNAVAILABLE, "Cannot add proximity alert: ${e.message}", e)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
internal companion object {
|
|
173
|
+
private const val TAG = "LT.AndroidGeofence"
|
|
174
|
+
private const val PREFS_NAME = "location_tracking_android_geofences"
|
|
175
|
+
private const val KEY_IDS = "registered_ids"
|
|
176
|
+
|
|
177
|
+
/** Boolean extras of the proximity PendingIntent: whether ENTER / EXIT should be delivered (default true). */
|
|
178
|
+
const val EXTRA_NOTIFY_ENTRY = "com.brickssoft.locationtracking.provider.android.NOTIFY_ENTRY"
|
|
179
|
+
const val EXTRA_NOTIFY_EXIT = "com.brickssoft.locationtracking.provider.android.NOTIFY_EXIT"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.provider.android
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.location.LocationManager
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import com.brickssoft.locationtracking.core.Clock
|
|
9
|
+
import com.brickssoft.locationtracking.core.Components
|
|
10
|
+
import com.brickssoft.locationtracking.core.Constants
|
|
11
|
+
import com.brickssoft.locationtracking.core.Logger
|
|
12
|
+
import com.brickssoft.locationtracking.model.GeofenceAction
|
|
13
|
+
import com.brickssoft.locationtracking.model.TrackedLocation
|
|
14
|
+
import com.brickssoft.locationtracking.provider.GeofenceTransitionSink
|
|
15
|
+
import com.brickssoft.locationtracking.provider.OsGeofenceTransition
|
|
16
|
+
import kotlinx.coroutines.CoroutineScope
|
|
17
|
+
import kotlinx.coroutines.Job
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Delivers `LocationManager` proximity alerts (see [AndroidGeofenceBackend]) to the GeofenceManager.
|
|
23
|
+
*
|
|
24
|
+
* The id comes from the data URI `lt-geofence://<id>` and the action from [LocationManager.KEY_PROXIMITY_ENTERING].
|
|
25
|
+
* The alert carries no fix, so the transition gets the freshest last known location if it is at most
|
|
26
|
+
* [MAX_TRIGGER_FIX_AGE_MS] old, else null.
|
|
27
|
+
*/
|
|
28
|
+
class AndroidGeofenceReceiver : BroadcastReceiver() {
|
|
29
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
30
|
+
val transition = parse(intent) ?: return
|
|
31
|
+
val components = Components.get(context)
|
|
32
|
+
val sink = components.geofences
|
|
33
|
+
val pending = goAsync()
|
|
34
|
+
deliver(transition, components.scope, sink, { recentLastKnown(components.context, components.clock) }) {
|
|
35
|
+
pending.finish()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
internal companion object {
|
|
40
|
+
private const val TAG = "LT.AndroidGeofenceRx"
|
|
41
|
+
|
|
42
|
+
/** A last known fix older than this is not attached to a transition. */
|
|
43
|
+
const val MAX_TRIGGER_FIX_AGE_MS = 2 * 60_000L
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The transition described by a proximity-alert [intent] (location = null), or null if the intent is not one
|
|
47
|
+
* of ours or its action was not requested for the region.
|
|
48
|
+
*/
|
|
49
|
+
fun parse(intent: Intent): OsGeofenceTransition? {
|
|
50
|
+
val id = idFrom(intent.data) ?: return null
|
|
51
|
+
if (!intent.hasExtra(LocationManager.KEY_PROXIMITY_ENTERING)) return null
|
|
52
|
+
val entering = intent.getBooleanExtra(LocationManager.KEY_PROXIMITY_ENTERING, false)
|
|
53
|
+
val wantedExtra = if (entering) {
|
|
54
|
+
AndroidGeofenceBackend.EXTRA_NOTIFY_ENTRY
|
|
55
|
+
} else {
|
|
56
|
+
AndroidGeofenceBackend.EXTRA_NOTIFY_EXIT
|
|
57
|
+
}
|
|
58
|
+
if (!intent.getBooleanExtra(wantedExtra, true)) return null
|
|
59
|
+
return OsGeofenceTransition(id, if (entering) GeofenceAction.ENTER else GeofenceAction.EXIT, null)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Decodes `<id>` of [Constants.geofenceUri]. */
|
|
63
|
+
fun idFrom(uri: Uri?): String? {
|
|
64
|
+
if (uri == null || uri.scheme != Constants.GEOFENCE_URI_SCHEME) return null
|
|
65
|
+
val encoded = uri.encodedSchemeSpecificPart?.removePrefix("//") ?: return null
|
|
66
|
+
return Uri.decode(encoded).takeIf { it.isNotEmpty() }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Adds the location from [locate] (null if it fails) and hands [transition] to [sink] on [scope].
|
|
71
|
+
* [finish] runs exactly once when the job completes, even if [scope] is already cancelled.
|
|
72
|
+
*/
|
|
73
|
+
fun deliver(
|
|
74
|
+
transition: OsGeofenceTransition,
|
|
75
|
+
scope: CoroutineScope,
|
|
76
|
+
sink: GeofenceTransitionSink,
|
|
77
|
+
locate: () -> TrackedLocation?,
|
|
78
|
+
finish: () -> Unit,
|
|
79
|
+
): Job {
|
|
80
|
+
val job = scope.launch {
|
|
81
|
+
try {
|
|
82
|
+
val location = try {
|
|
83
|
+
locate()
|
|
84
|
+
} catch (e: Exception) {
|
|
85
|
+
Logger.w(TAG, "no last known location for geofence ${transition.id}", e)
|
|
86
|
+
null
|
|
87
|
+
}
|
|
88
|
+
sink.onGeofenceTransitions(listOf(transition.copy(location = location)))
|
|
89
|
+
} catch (e: CancellationException) {
|
|
90
|
+
throw e
|
|
91
|
+
} catch (e: Exception) {
|
|
92
|
+
Logger.e(TAG, "failed to deliver ${transition.action} of geofence ${transition.id}", e)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
job.invokeOnCompletion { finish() }
|
|
96
|
+
return job
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True if [fix] is at most [MAX_TRIGGER_FIX_AGE_MS] old (elapsed-realtime clock when the fix carries it). */
|
|
100
|
+
fun isRecent(fix: TrackedLocation, clock: Clock): Boolean {
|
|
101
|
+
val ageMs = if (fix.elapsedRealtimeNanos > 0) {
|
|
102
|
+
clock.elapsedRealtime() - fix.elapsedRealtimeNanos / 1_000_000
|
|
103
|
+
} else {
|
|
104
|
+
clock.now() - fix.time
|
|
105
|
+
}
|
|
106
|
+
return ageMs <= MAX_TRIGGER_FIX_AGE_MS
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun recentLastKnown(context: Context, clock: Clock): TrackedLocation? {
|
|
110
|
+
val lm = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return null
|
|
111
|
+
val fix = LocationProviders.freshestLastKnown(lm)?.let(TrackedLocation::from) ?: return null
|
|
112
|
+
return fix.takeIf { isRecent(it, clock) }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|