@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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +107 -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 +60 -4
@@ -0,0 +1,104 @@
1
+ package com.brickssoft.locationtracking.device
2
+
3
+ import android.content.Context
4
+ import android.hardware.Sensor
5
+ import android.hardware.SensorManager
6
+ import android.os.Build
7
+ import androidx.core.content.ContextCompat
8
+ import com.brickssoft.locationtracking.BuildConfig
9
+ import com.brickssoft.locationtracking.core.Logger
10
+ import com.brickssoft.locationtracking.model.DeviceInfo
11
+ import com.brickssoft.locationtracking.model.ProviderKind
12
+ import com.brickssoft.locationtracking.model.Sensors
13
+ import com.brickssoft.locationtracking.provider.DefaultProviderFactory
14
+ import com.brickssoft.locationtracking.provider.ProviderFactory
15
+ import com.brickssoft.locationtracking.provider.ProviderPackaging
16
+
17
+ /**
18
+ * Default [DeviceInfoProvider]: build properties, plugin version, GMS/HMS availability and the active backend
19
+ * (from [providers]), the providers packaged in the APK ([ProviderPackaging]), and which motion sensors the device has.
20
+ * If the provider factory fails, both SDKs read as unavailable and the backend as `android` (the fallback).
21
+ *
22
+ * The constructor does not touch [providers].
23
+ */
24
+ class DefaultDeviceInfoProvider(
25
+ private val context: Context,
26
+ private val providers: Lazy<ProviderFactory>,
27
+ classPresent: (String) -> Boolean = DefaultProviderFactory.reflectiveClassPresent(context.classLoader),
28
+ receiverDeclared: (String) -> Boolean = ProviderPackaging.receiverDeclaredIn(context),
29
+ providersMetaData: () -> String? = ProviderPackaging.providersMetaDataIn(context),
30
+ ) : DeviceInfoProvider {
31
+ private val packaging = ProviderPackaging(classPresent, receiverDeclared, providersMetaData)
32
+
33
+ override fun deviceInfo(): DeviceInfo {
34
+ // DeviceInfo is diagnostic: a broken provider factory must not hide the build properties.
35
+ val factory = try {
36
+ providers.value
37
+ } catch (e: Exception) {
38
+ Logger.e(TAG, "provider factory unavailable", e)
39
+ null
40
+ }
41
+ val backend = try {
42
+ factory?.kind
43
+ } catch (e: Exception) {
44
+ Logger.e(TAG, "provider kind unavailable", e)
45
+ null
46
+ }
47
+ return DeviceInfo(
48
+ platform = "android",
49
+ manufacturer = Build.MANUFACTURER.orEmpty(),
50
+ model = Build.MODEL.orEmpty(),
51
+ brand = Build.BRAND.orEmpty(),
52
+ osVersion = Build.VERSION.RELEASE.orEmpty(),
53
+ sdkInt = Build.VERSION.SDK_INT,
54
+ pluginVersion = BuildConfig.PLUGIN_VERSION,
55
+ gmsAvailable = factory != null && isAvailable(factory, ProviderKind.GMS),
56
+ hmsAvailable = factory != null && isAvailable(factory, ProviderKind.HMS),
57
+ backend = backend ?: ProviderKind.ANDROID,
58
+ packagedProviders = packagedProviders(),
59
+ )
60
+ }
61
+
62
+ override fun sensors(): Sensors {
63
+ val manager = try {
64
+ ContextCompat.getSystemService(context, SensorManager::class.java)
65
+ } catch (e: Exception) {
66
+ Logger.w(TAG, "SensorManager unavailable", e)
67
+ null
68
+ }
69
+ fun has(type: Int): Boolean = try {
70
+ manager?.getDefaultSensor(type) != null
71
+ } catch (e: Exception) {
72
+ Logger.w(TAG, "getDefaultSensor($type) failed", e)
73
+ false
74
+ }
75
+ return Sensors(
76
+ accelerometer = has(Sensor.TYPE_ACCELEROMETER),
77
+ gyroscope = has(Sensor.TYPE_GYROSCOPE),
78
+ magnetometer = has(Sensor.TYPE_MAGNETIC_FIELD),
79
+ significantMotion = has(Sensor.TYPE_SIGNIFICANT_MOTION),
80
+ stepCounter = has(Sensor.TYPE_STEP_COUNTER),
81
+ stepDetector = has(Sensor.TYPE_STEP_DETECTOR),
82
+ barometer = has(Sensor.TYPE_PRESSURE),
83
+ )
84
+ }
85
+
86
+ private fun isAvailable(factory: ProviderFactory, kind: ProviderKind): Boolean = try {
87
+ factory.isAvailable(kind)
88
+ } catch (e: Exception) {
89
+ Logger.w(TAG, "isAvailable(${kind.wire}) failed", e)
90
+ false
91
+ }
92
+
93
+ /**
94
+ * The providers packaged in the APK ([ProviderPackaging]: allowed by the app's `PROVIDERS` meta-data, SDK classes
95
+ * present, receivers declared), in the order `gms`, `hms`. Read at runtime, so a flavored app reports only the
96
+ * provider its flavor declares, even when another library brings the other SDK's classes.
97
+ */
98
+ private fun packagedProviders(): List<String> =
99
+ listOf(ProviderKind.GMS, ProviderKind.HMS).filter(packaging::isPackaged).map { it.wire }
100
+
101
+ internal companion object {
102
+ private const val TAG = "LT.DeviceInfo"
103
+ }
104
+ }
@@ -0,0 +1,335 @@
1
+ package com.brickssoft.locationtracking.device
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.app.AlarmManager
6
+ import android.content.BroadcastReceiver
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.IntentFilter
10
+ import android.content.pm.PackageManager
11
+ import android.location.LocationManager
12
+ import android.net.ConnectivityManager
13
+ import android.net.Network
14
+ import android.net.NetworkCapabilities
15
+ import android.os.Build
16
+ import android.os.PowerManager
17
+ import androidx.core.content.ContextCompat
18
+ import androidx.core.location.LocationManagerCompat
19
+ import com.brickssoft.locationtracking.config.ConfigStore
20
+ import com.brickssoft.locationtracking.core.Clock
21
+ import com.brickssoft.locationtracking.core.EventBus
22
+ import com.brickssoft.locationtracking.core.Logger
23
+ import com.brickssoft.locationtracking.core.TrackingEvent
24
+ import com.brickssoft.locationtracking.model.BatterySnapshot
25
+ import com.brickssoft.locationtracking.model.Connectivity
26
+ import com.brickssoft.locationtracking.model.ProviderState
27
+ import com.brickssoft.locationtracking.model.RecordEvent
28
+ import com.brickssoft.locationtracking.permission.PermissionManager
29
+ import com.brickssoft.locationtracking.processing.RecordFactory
30
+ import com.brickssoft.locationtracking.provider.ProviderFactory
31
+ import com.brickssoft.locationtracking.record.RecordSink
32
+ import kotlinx.coroutines.CoroutineScope
33
+ import kotlinx.coroutines.delay
34
+ import kotlinx.coroutines.launch
35
+ import kotlinx.coroutines.sync.Mutex
36
+ import kotlinx.coroutines.sync.withLock
37
+ import java.util.concurrent.atomic.AtomicBoolean
38
+ import java.util.concurrent.atomic.AtomicReference
39
+ import kotlin.coroutines.cancellation.CancellationException
40
+
41
+ /**
42
+ * Default [DeviceMonitor].
43
+ *
44
+ * [start] registers (once) a receiver for location provider/mode changes, power-save and device-idle changes, and
45
+ * a default-network callback, then schedules a provider check. Provider broadcasts schedule a debounced
46
+ * [checkProviderState] on [scope]. Connectivity and power-save changes are emitted as
47
+ * [TrackingEvent.ConnectivityChange] / [TrackingEvent.PowerSaveChange], deduplicated against the last published
48
+ * value (seeded at registration, so registering alone emits nothing). Device-idle changes are only logged; the
49
+ * heartbeat scheduler listens for them itself.
50
+ *
51
+ * [checkProviderState] diffs the current [ProviderState] against the persisted `runtime.providerState`. The first
52
+ * observation is persisted silently. A later difference is emitted as [TrackingEvent.ProviderChange] and, while
53
+ * tracking is enabled, submitted as a `providerchange` record with the last known location; the new state is
54
+ * persisted after the record was handed to the sink, so a failed submit is retried by the next check.
55
+ *
56
+ * The constructor does not touch the lazy parameters or any system service.
57
+ */
58
+ class DefaultDeviceMonitor(
59
+ private val context: Context,
60
+ private val configStore: ConfigStore,
61
+ private val permissions: PermissionManager,
62
+ private val providers: Lazy<ProviderFactory>,
63
+ private val events: EventBus,
64
+ @Suppress("unused") private val clock: Clock,
65
+ private val recordFactory: Lazy<RecordFactory>,
66
+ private val recordSink: Lazy<RecordSink>,
67
+ private val scope: CoroutineScope,
68
+ ) : DeviceMonitor {
69
+ private val receiverRegistered = AtomicBoolean(false)
70
+ private val networkCallbackRegistered = AtomicBoolean(false)
71
+ private val providerCheckPending = AtomicBoolean(false)
72
+ private val providerMutex = Mutex()
73
+ private val lastConnectivity = AtomicReference<Connectivity?>(null)
74
+ private val lastPowerSave = AtomicReference<Boolean?>(null)
75
+
76
+ private val locationManager: LocationManager? by lazy { systemService(LocationManager::class.java) }
77
+ private val connectivityManager: ConnectivityManager? by lazy { systemService(ConnectivityManager::class.java) }
78
+ private val powerManager: PowerManager? by lazy { systemService(PowerManager::class.java) }
79
+ private val alarmManager: AlarmManager? by lazy { systemService(AlarmManager::class.java) }
80
+
81
+ private val receiver = object : BroadcastReceiver() {
82
+ override fun onReceive(context: Context, intent: Intent) {
83
+ when (intent.action) {
84
+ LocationManager.PROVIDERS_CHANGED_ACTION,
85
+ LocationManager.MODE_CHANGED_ACTION,
86
+ -> scheduleProviderCheck(REASON_PROVIDERS_CHANGED)
87
+ PowerManager.ACTION_POWER_SAVE_MODE_CHANGED -> publishPowerSave(isPowerSaveMode())
88
+ PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED -> Logger.d(TAG, "device idle mode: ${isDeviceIdleMode()}")
89
+ }
90
+ }
91
+ }
92
+
93
+ /** Tracks the default network. Callbacks of one registration arrive serially on one thread. */
94
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
95
+ @Volatile
96
+ private var current: Network? = null
97
+
98
+ /** Connectivity of [current] from its capabilities, before [isBlocked] is applied. */
99
+ @Volatile
100
+ private var fromCapabilities: Connectivity? = null
101
+
102
+ /**
103
+ * Whether this app's traffic is blocked (Data Saver, background restrictions, Doze); reported on API 29+.
104
+ * Kept across network switches: API 29+ reports the new network's status right after its capabilities.
105
+ */
106
+ @Volatile
107
+ private var isBlocked = false
108
+
109
+ override fun onAvailable(network: Network) {
110
+ if (network != current) fromCapabilities = null
111
+ current = network
112
+ // From API 26 onAvailable is always followed by onCapabilitiesChanged, and querying the capabilities
113
+ // here is racy. Before that, the query is the only way to learn the new network's state.
114
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
115
+ capabilitiesOf(network)?.let { update(DeviceStateMapping.connectivityOf(it)) }
116
+ }
117
+ }
118
+
119
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
120
+ current = network
121
+ update(DeviceStateMapping.connectivityOf(networkCapabilities))
122
+ }
123
+
124
+ override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
125
+ val active = current
126
+ if (active != null && active != network) return
127
+ isBlocked = blocked
128
+ fromCapabilities?.let { publishConnectivity(effective(it)) }
129
+ }
130
+
131
+ override fun onLost(network: Network) {
132
+ val active = current
133
+ // A default-network callback can report the old network as lost after the new one became available.
134
+ if (active != null && active != network) return
135
+ current = null
136
+ fromCapabilities = null
137
+ publishConnectivity(DeviceStateMapping.DISCONNECTED)
138
+ }
139
+
140
+ private fun update(value: Connectivity) {
141
+ fromCapabilities = value
142
+ publishConnectivity(effective(value))
143
+ }
144
+
145
+ private fun effective(value: Connectivity): Connectivity =
146
+ if (isBlocked && value.connected) value.copy(connected = false) else value
147
+ }
148
+
149
+ override fun start() {
150
+ if (receiverRegistered.compareAndSet(false, true)) {
151
+ lastPowerSave.set(isPowerSaveMode())
152
+ val filter = IntentFilter().apply {
153
+ addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
154
+ addAction(LocationManager.MODE_CHANGED_ACTION)
155
+ addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
156
+ addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
157
+ }
158
+ try {
159
+ registerNotExported(receiver, filter)
160
+ // Catches changes made while no receiver was listening (service start, cold start).
161
+ scheduleProviderCheck(REASON_START)
162
+ } catch (e: Exception) {
163
+ receiverRegistered.set(false)
164
+ Logger.e(TAG, "failed to register the device receiver", e)
165
+ }
166
+ }
167
+ if (networkCallbackRegistered.compareAndSet(false, true)) {
168
+ lastConnectivity.set(connectivity())
169
+ val cm = connectivityManager
170
+ try {
171
+ checkNotNull(cm) { "no ConnectivityManager" }.registerDefaultNetworkCallback(networkCallback)
172
+ } catch (e: Exception) {
173
+ networkCallbackRegistered.set(false)
174
+ Logger.e(TAG, "failed to register the network callback", e)
175
+ }
176
+ }
177
+ Logger.d(TAG, "started (receiver=${receiverRegistered.get()}, network=${networkCallbackRegistered.get()})")
178
+ }
179
+
180
+ override suspend fun checkProviderState(reason: String) {
181
+ providerMutex.withLock {
182
+ try {
183
+ val current = providerState()
184
+ val runtime = configStore.runtime.value
185
+ val previous = runtime.providerState
186
+ if (current == previous) {
187
+ Logger.v(TAG, "provider state unchanged ($reason)")
188
+ return
189
+ }
190
+ if (previous == null) {
191
+ configStore.updateRuntime { it.copy(providerState = current) }
192
+ Logger.i(TAG, "provider state first observed ($reason): $current")
193
+ return
194
+ }
195
+ Logger.i(TAG, "providerchange ($reason): $previous -> $current")
196
+ events.emit(TrackingEvent.ProviderChange(current))
197
+ if (runtime.enabled) {
198
+ val record = recordFactory.value.create(
199
+ RecordEvent.PROVIDERCHANGE,
200
+ runtime.lastLocation,
201
+ provider = current,
202
+ )
203
+ recordSink.value.submit(record)
204
+ }
205
+ configStore.updateRuntime { it.copy(providerState = current) }
206
+ } catch (e: CancellationException) {
207
+ throw e
208
+ } catch (e: Exception) {
209
+ // The state stays unpersisted, so the next check reports the change again.
210
+ Logger.e(TAG, "provider state check failed ($reason)", e)
211
+ }
212
+ }
213
+ }
214
+
215
+ override fun providerState(): ProviderState {
216
+ val lm = locationManager
217
+ val foreground = permissions.hasForegroundLocation()
218
+ val background = foreground && permissions.hasBackgroundLocation()
219
+ val fine = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
220
+ PackageManager.PERMISSION_GRANTED
221
+ return ProviderState(
222
+ enabled = lm != null && safely("isLocationEnabled", false) { LocationManagerCompat.isLocationEnabled(lm) },
223
+ gps = isProviderEnabled(lm, LocationManager.GPS_PROVIDER),
224
+ network = isProviderEnabled(lm, LocationManager.NETWORK_PROVIDER),
225
+ permission = DeviceStateMapping.permissionLevel(foreground, background),
226
+ accuracy = DeviceStateMapping.accuracyLevel(foreground, fine),
227
+ backend = providers.value.kind,
228
+ )
229
+ }
230
+
231
+ override fun connectivity(): Connectivity {
232
+ val cm = connectivityManager ?: return DeviceStateMapping.DISCONNECTED
233
+ return safely("connectivity", DeviceStateMapping.DISCONNECTED) {
234
+ val network = cm.activeNetwork
235
+ if (network == null) {
236
+ DeviceStateMapping.DISCONNECTED
237
+ } else {
238
+ DeviceStateMapping.connectivityOf(capabilitiesOf(network))
239
+ }
240
+ }
241
+ }
242
+
243
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
244
+ override fun battery(): BatterySnapshot = safely("battery", BatterySnapshot.UNKNOWN) {
245
+ // A null receiver only reads the sticky broadcast and registers nothing, so no export flag is needed
246
+ // (ACTION_BATTERY_CHANGED is a protected broadcast).
247
+ DeviceStateMapping.batteryOf(context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)))
248
+ }
249
+
250
+ override fun isPowerSaveMode(): Boolean = safely("isPowerSaveMode", false) { powerManager?.isPowerSaveMode == true }
251
+
252
+ override fun isDeviceIdleMode(): Boolean =
253
+ safely("isDeviceIdleMode", false) { powerManager?.isDeviceIdleMode == true }
254
+
255
+ override fun isIgnoringBatteryOptimizations(): Boolean = safely("isIgnoringBatteryOptimizations", false) {
256
+ powerManager?.isIgnoringBatteryOptimizations(context.packageName) == true
257
+ }
258
+
259
+ override fun canScheduleExactAlarms(): Boolean {
260
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
261
+ return safely("canScheduleExactAlarms", false) { alarmManager?.canScheduleExactAlarms() == true }
262
+ }
263
+
264
+ /**
265
+ * Runs one [checkProviderState] [PROVIDER_CHECK_DEBOUNCE_MS] after the first of a burst of triggers. One user
266
+ * toggle can send several broadcasts (one per provider, plus MODE_CHANGED) with intermediate states in between;
267
+ * the delay lets them settle so a single toggle yields a single providerchange. A trigger that arrives after
268
+ * the check has begun schedules another one, so the final state is always observed.
269
+ */
270
+ private fun scheduleProviderCheck(reason: String) {
271
+ if (!providerCheckPending.compareAndSet(false, true)) return
272
+ val job = scope.launch {
273
+ delay(PROVIDER_CHECK_DEBOUNCE_MS)
274
+ providerCheckPending.set(false)
275
+ checkProviderState(reason)
276
+ }
277
+ // If the scope is no longer active the body never runs; don't leave the flag stuck.
278
+ job.invokeOnCompletion { cause -> if (cause != null) providerCheckPending.set(false) }
279
+ }
280
+
281
+ private fun publishConnectivity(value: Connectivity) {
282
+ if (lastConnectivity.getAndSet(value) == value) return
283
+ Logger.i(TAG, "connectivity: connected=${value.connected}, type=${value.type.wire}")
284
+ events.emit(TrackingEvent.ConnectivityChange(value))
285
+ }
286
+
287
+ private fun publishPowerSave(value: Boolean) {
288
+ if (lastPowerSave.getAndSet(value) == value) return
289
+ Logger.i(TAG, "power save mode: $value")
290
+ events.emit(TrackingEvent.PowerSaveChange(value))
291
+ }
292
+
293
+ /**
294
+ * Registers [receiver] with RECEIVER_NOT_EXPORTED; system broadcasts still reach it. Below API 33 androidx
295
+ * emulates the flag with the `<applicationId>.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION` its manifest declares.
296
+ * If an app strips that permission, fall back to a plain registration: safe, because every action this
297
+ * monitor listens to is a protected broadcast that only the system can send.
298
+ */
299
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
300
+ private fun registerNotExported(receiver: BroadcastReceiver, filter: IntentFilter) {
301
+ try {
302
+ ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
303
+ } catch (e: RuntimeException) {
304
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) throw e
305
+ Logger.w(TAG, "RECEIVER_NOT_EXPORTED emulation unavailable; registering without it", e)
306
+ context.registerReceiver(receiver, filter)
307
+ }
308
+ }
309
+
310
+ private fun capabilitiesOf(network: Network): NetworkCapabilities? =
311
+ safely("getNetworkCapabilities", null) { connectivityManager?.getNetworkCapabilities(network) }
312
+
313
+ private fun isProviderEnabled(lm: LocationManager?, provider: String): Boolean =
314
+ lm != null && safely("isProviderEnabled($provider)", false) { lm.isProviderEnabled(provider) }
315
+
316
+ private fun <T> systemService(type: Class<T>): T? = safely("getSystemService(${type.simpleName})", null) {
317
+ ContextCompat.getSystemService(context, type)
318
+ }
319
+
320
+ private inline fun <T> safely(what: String, fallback: T, block: () -> T): T = try {
321
+ block()
322
+ } catch (e: Exception) {
323
+ Logger.w(TAG, "$what failed", e)
324
+ fallback
325
+ }
326
+
327
+ internal companion object {
328
+ private const val TAG = "LT.DeviceMonitor"
329
+ private const val REASON_PROVIDERS_CHANGED = "providers_changed"
330
+ private const val REASON_START = "start"
331
+
332
+ /** Settle time between the first provider trigger of a burst and the check. */
333
+ const val PROVIDER_CHECK_DEBOUNCE_MS = 1_000L
334
+ }
335
+ }
@@ -0,0 +1,36 @@
1
+ package com.brickssoft.locationtracking.device
2
+
3
+ import com.brickssoft.locationtracking.model.BatterySnapshot
4
+ import com.brickssoft.locationtracking.model.Connectivity
5
+ import com.brickssoft.locationtracking.model.DeviceInfo
6
+ import com.brickssoft.locationtracking.model.ProviderState
7
+ import com.brickssoft.locationtracking.model.Sensors
8
+
9
+ /** Device state: location providers, connectivity, battery, power and idle modes. */
10
+ interface DeviceMonitor {
11
+ /** Idempotent: PROVIDERS_CHANGED, NetworkCallback, POWER_SAVE_MODE_CHANGED, DEVICE_IDLE_MODE_CHANGED. */
12
+ fun start()
13
+
14
+ /** Diffs against runtime.providerState -> ProviderChange event (+ providerchange record if enabled). */
15
+ suspend fun checkProviderState(reason: String)
16
+
17
+ fun providerState(): ProviderState
18
+
19
+ fun connectivity(): Connectivity
20
+
21
+ fun battery(): BatterySnapshot
22
+
23
+ fun isPowerSaveMode(): Boolean
24
+
25
+ fun isDeviceIdleMode(): Boolean
26
+
27
+ fun isIgnoringBatteryOptimizations(): Boolean
28
+
29
+ fun canScheduleExactAlarms(): Boolean
30
+ }
31
+
32
+ interface DeviceInfoProvider {
33
+ fun deviceInfo(): DeviceInfo
34
+
35
+ fun sensors(): Sensors
36
+ }
@@ -0,0 +1,69 @@
1
+ package com.brickssoft.locationtracking.device
2
+
3
+ import android.content.Intent
4
+ import android.net.NetworkCapabilities
5
+ import android.os.BatteryManager
6
+ import com.brickssoft.locationtracking.model.AccuracyLevel
7
+ import com.brickssoft.locationtracking.model.BatterySnapshot
8
+ import com.brickssoft.locationtracking.model.Connectivity
9
+ import com.brickssoft.locationtracking.model.ConnectivityType
10
+ import com.brickssoft.locationtracking.model.PermissionLevel
11
+
12
+ /** Pure mappings from Android system values to the plugin's device models. */
13
+ internal object DeviceStateMapping {
14
+ /** No network at all. */
15
+ val DISCONNECTED = Connectivity(connected = false, type = ConnectivityType.NONE)
16
+
17
+ /**
18
+ * Maps a network's capabilities to [Connectivity].
19
+ *
20
+ * The type comes from the first matching transport: Wi-Fi, cellular, ethernet, otherwise OTHER (VPN,
21
+ * Bluetooth, ...). `connected` needs NET_CAPABILITY_INTERNET; VALIDATED is not required because validation
22
+ * can lag or be blocked (for example where the platform's connectivity check host is unreachable).
23
+ * A network behind a captive portal counts as not connected, so that logging in later reads as a change.
24
+ */
25
+ fun connectivityOf(caps: NetworkCapabilities?): Connectivity {
26
+ if (caps == null) return DISCONNECTED
27
+ val type = when {
28
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> ConnectivityType.WIFI
29
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> ConnectivityType.CELLULAR
30
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> ConnectivityType.ETHERNET
31
+ else -> ConnectivityType.OTHER
32
+ }
33
+ val connected = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
34
+ !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
35
+ return Connectivity(connected, type)
36
+ }
37
+
38
+ /**
39
+ * Parses an `ACTION_BATTERY_CHANGED` intent. Level is `level / scale` in 0..1, or -1 when unknown.
40
+ * Charging means plugged in (AC, USB, wireless, dock) or reporting CHARGING; FULL counts as charging only
41
+ * when the plug state is missing from the intent.
42
+ */
43
+ fun batteryOf(intent: Intent?): BatterySnapshot {
44
+ if (intent == null) return BatterySnapshot.UNKNOWN
45
+ val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
46
+ val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
47
+ val fraction = if (level >= 0 && scale > 0) (level.toFloat() / scale).coerceIn(0f, 1f) else -1f
48
+ val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
49
+ val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
50
+ val isCharging = plugged > 0 ||
51
+ status == BatteryManager.BATTERY_STATUS_CHARGING ||
52
+ (plugged < 0 && status == BatteryManager.BATTERY_STATUS_FULL)
53
+ return BatterySnapshot(fraction, isCharging)
54
+ }
55
+
56
+ /** Foreground access is required for any level; background access on top of it means ALWAYS. */
57
+ fun permissionLevel(foreground: Boolean, background: Boolean): PermissionLevel = when {
58
+ !foreground -> PermissionLevel.DENIED
59
+ background -> PermissionLevel.ALWAYS
60
+ else -> PermissionLevel.WHEN_IN_USE
61
+ }
62
+
63
+ /** PRECISE with fine location, APPROXIMATE with foreground access but no fine location, else NONE. */
64
+ fun accuracyLevel(foreground: Boolean, fine: Boolean): AccuracyLevel = when {
65
+ !foreground -> AccuracyLevel.NONE
66
+ fine -> AccuracyLevel.PRECISE
67
+ else -> AccuracyLevel.APPROXIMATE
68
+ }
69
+ }