@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +106 -0
  4. package/android/consumer-rules.pro +42 -0
  5. package/android/src/main/AndroidManifest.xml +52 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
  7. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
  12. package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
  15. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
  17. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
  20. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
  23. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
  30. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
  34. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
  37. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
  39. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
  40. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
  42. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
  43. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
  46. package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
  51. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
  53. package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
  54. package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
  55. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
  56. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
  57. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
  58. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
  59. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
  60. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
  61. package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
  62. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
  63. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
  64. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
  65. package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
  66. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
  67. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
  68. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
  69. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
  70. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
  71. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
  72. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
  73. package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
  74. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
  75. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
  76. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
  77. package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
  78. package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
  79. package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
  80. package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
  81. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
  82. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
  83. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
  84. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
  85. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
  86. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
  87. package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
  88. package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
  89. package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
  90. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
  91. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
  92. package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
  93. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
  94. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
  95. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
  96. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
  97. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
  98. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
  99. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
  100. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
  101. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
  102. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
  103. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
  104. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
  105. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
  106. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
  107. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
  108. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
  109. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
  110. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
  111. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
  112. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
  113. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
  114. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
  115. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
  116. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
  117. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
  118. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
  119. package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
  120. package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
  121. package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
  122. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
  123. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
  124. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
  125. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
  126. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
  127. package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
  128. package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
  129. package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
  130. package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
  131. package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
  132. package/android/src/main/res/values/lt_strings.xml +11 -0
  133. package/android/src/main/res/xml/lt_file_paths.xml +5 -0
  134. package/dist/esm/definitions.d.ts +719 -0
  135. package/dist/esm/definitions.js +2 -0
  136. package/dist/esm/definitions.js.map +1 -0
  137. package/dist/esm/events.d.ts +50 -0
  138. package/dist/esm/events.js +52 -0
  139. package/dist/esm/events.js.map +1 -0
  140. package/dist/esm/index.d.ts +3 -0
  141. package/dist/esm/index.js +5 -0
  142. package/dist/esm/index.js.map +1 -0
  143. package/dist/esm/plugin.d.ts +3 -0
  144. package/dist/esm/plugin.js +10 -0
  145. package/dist/esm/plugin.js.map +1 -0
  146. package/dist/esm/web/config.d.ts +19 -0
  147. package/dist/esm/web/config.js +243 -0
  148. package/dist/esm/web/config.js.map +1 -0
  149. package/dist/esm/web/device.d.ts +9 -0
  150. package/dist/esm/web/device.js +90 -0
  151. package/dist/esm/web/device.js.map +1 -0
  152. package/dist/esm/web/errors.d.ts +14 -0
  153. package/dist/esm/web/errors.js +19 -0
  154. package/dist/esm/web/errors.js.map +1 -0
  155. package/dist/esm/web/geolocation.d.ts +26 -0
  156. package/dist/esm/web/geolocation.js +117 -0
  157. package/dist/esm/web/geolocation.js.map +1 -0
  158. package/dist/esm/web/location.d.ts +7 -0
  159. package/dist/esm/web/location.js +70 -0
  160. package/dist/esm/web/location.js.map +1 -0
  161. package/dist/esm/web.d.ts +164 -0
  162. package/dist/esm/web.js +328 -0
  163. package/dist/esm/web.js.map +1 -0
  164. package/dist/plugin.cjs.js +945 -0
  165. package/dist/plugin.cjs.js.map +1 -0
  166. package/dist/plugin.js +948 -0
  167. package/dist/plugin.js.map +1 -0
  168. package/package.json +59 -4
@@ -0,0 +1,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
+ }