@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,541 @@
1
+ package com.brickssoft.locationtracking.bridge
2
+
3
+ import android.app.Activity
4
+ import android.content.ActivityNotFoundException
5
+ import android.content.Intent
6
+ import com.brickssoft.locationtracking.config.ConfigJson
7
+ import com.brickssoft.locationtracking.config.State
8
+ import com.brickssoft.locationtracking.core.Components
9
+ import com.brickssoft.locationtracking.core.ErrorCode
10
+ import com.brickssoft.locationtracking.core.Logger
11
+ import com.brickssoft.locationtracking.core.Subscription
12
+ import com.brickssoft.locationtracking.core.TrackingException
13
+ import com.brickssoft.locationtracking.model.BatteryOptimizationStatus
14
+ import com.brickssoft.locationtracking.model.BatteryOptimizationStatusJson
15
+ import com.brickssoft.locationtracking.model.DeviceInfoJson
16
+ import com.brickssoft.locationtracking.model.EventJson
17
+ import com.brickssoft.locationtracking.model.GeofenceJson
18
+ import com.brickssoft.locationtracking.model.HeartbeatStatusJson
19
+ import com.brickssoft.locationtracking.model.PermissionStatusJson
20
+ import com.brickssoft.locationtracking.model.PowerManagerInfoJson
21
+ import com.brickssoft.locationtracking.model.ProviderStateJson
22
+ import com.brickssoft.locationtracking.model.Record
23
+ import com.brickssoft.locationtracking.model.RecordJson
24
+ import com.brickssoft.locationtracking.model.SensorsJson
25
+ import com.brickssoft.locationtracking.permission.PermissionHost
26
+ import com.brickssoft.locationtracking.permission.PermissionState
27
+ import com.brickssoft.locationtracking.permission.PermissionType
28
+ import kotlinx.coroutines.CancellationException
29
+ import kotlinx.coroutines.suspendCancellableCoroutine
30
+ import kotlinx.coroutines.sync.Mutex
31
+ import kotlinx.coroutines.sync.withLock
32
+ import kotlinx.coroutines.withContext
33
+ import org.json.JSONArray
34
+ import org.json.JSONException
35
+ import org.json.JSONObject
36
+ import java.util.concurrent.atomic.AtomicBoolean
37
+ import kotlin.coroutines.resume
38
+
39
+ /**
40
+ * The logic of every `LocationTracking` plugin method, testable without a Capacitor bridge: options come in as the
41
+ * call's JSONObject, results go out as the JS shape (null = `void`), and failures are thrown as
42
+ * [TrackingException] (the plugin maps them to `call.reject(message, code.name)`).
43
+ *
44
+ * Every method except [ALLOWED_BEFORE_READY] throws NOT_READY until [ready] has resolved in this process.
45
+ * Suspend methods run on the caller's coroutine (the plugin uses `Components.scope`); UI work (settings screens,
46
+ * permission requests, the email chooser) is moved to `dispatchers.main`.
47
+ *
48
+ * @param activityProvider the plugin's current activity, if any.
49
+ * @param readyFlag set once `ready()` resolved; process-wide by default.
50
+ */
51
+ internal class PluginHandlers(
52
+ private val services: BridgeServices,
53
+ private val activityProvider: () -> Activity? = { null },
54
+ private val readyFlag: AtomicBoolean = processReady,
55
+ val watches: WatchRegistry = WatchRegistry(),
56
+ ) {
57
+ constructor(components: Components) : this(ComponentServices(components))
58
+
59
+ /** Serializes permission requests (see [requestPermissions]). */
60
+ private val permissionRequests = Mutex()
61
+
62
+ /** True once `ready()` has resolved (in this process, unless a custom flag was injected). */
63
+ val isReady: Boolean get() = readyFlag.get()
64
+
65
+ // ---- lifecycle and config
66
+
67
+ /** `ready({config?, reset = true})` -> State. */
68
+ suspend fun ready(options: JSONObject): JSONObject {
69
+ gate("ready")
70
+ val config = OptionParsers.optObject(options, "config")
71
+ val reset = OptionParsers.optBoolean(options, "reset") ?: true
72
+ val state = services.engine.ready(config, reset)
73
+ readyFlag.set(true)
74
+ return stateJson(state)
75
+ }
76
+
77
+ /** `setConfig({config})` -> State. */
78
+ suspend fun setConfig(options: JSONObject): JSONObject {
79
+ gate("setConfig")
80
+ return stateJson(services.engine.setConfig(OptionParsers.requireObject(options, "config")))
81
+ }
82
+
83
+ /** `reset({config?})` -> State. */
84
+ suspend fun reset(options: JSONObject): JSONObject {
85
+ gate("reset")
86
+ return stateJson(services.engine.reset(OptionParsers.optObject(options, "config")))
87
+ }
88
+
89
+ fun getState(): JSONObject {
90
+ gate("getState")
91
+ return stateJson(services.engine.state())
92
+ }
93
+
94
+ suspend fun start(): JSONObject {
95
+ gate("start")
96
+ return stateJson(services.engine.start())
97
+ }
98
+
99
+ suspend fun startGeofences(): JSONObject {
100
+ gate("startGeofences")
101
+ return stateJson(services.engine.startGeofences())
102
+ }
103
+
104
+ suspend fun stop(): JSONObject {
105
+ gate("stop")
106
+ return stateJson(services.engine.stop())
107
+ }
108
+
109
+ /** `changePace({isMoving})` -> void. */
110
+ suspend fun changePace(options: JSONObject): JSONObject? {
111
+ gate("changePace")
112
+ services.engine.changePace(OptionParsers.requireBoolean(options, "isMoving"))
113
+ return null
114
+ }
115
+
116
+ // ---- positions
117
+
118
+ /** `getCurrentPosition(options?)` -> Location. */
119
+ suspend fun getCurrentPosition(options: JSONObject): JSONObject {
120
+ gate("getCurrentPosition")
121
+ val record = services.positions.getCurrentPosition(OptionParsers.currentPositionOptions(options))
122
+ return RecordJson.toJson(record)
123
+ }
124
+
125
+ /**
126
+ * `watchPosition(options, callback)`: registers [target] under [id] (the call's callback id) and starts the
127
+ * watch. Locations and errors go to [target] until [clearWatch] or [releaseAllWatches].
128
+ */
129
+ fun watchPosition(id: String, options: JSONObject, target: WatchTarget) {
130
+ gate("watchPosition")
131
+ if (id.isBlank()) OptionParsers.invalid("watchPosition needs a callback id")
132
+ val watchOptions = OptionParsers.watchPositionOptions(options)
133
+ watches.add(id, target)
134
+ try {
135
+ services.positions.watchPosition(id, watchOptions) { record, error -> deliverWatch(id, record, error) }
136
+ } catch (e: Exception) {
137
+ watches.release(id)
138
+ throw e
139
+ }
140
+ }
141
+
142
+ /**
143
+ * `clearWatch({id})` -> void; unknown ids are ignored. The position watch is stopped first; the JS callback is
144
+ * released even if that fails.
145
+ */
146
+ fun clearWatch(options: JSONObject): JSONObject? {
147
+ gate("clearWatch")
148
+ val id = OptionParsers.requireString(options, "id")
149
+ try {
150
+ services.positions.clearWatch(id)
151
+ } finally {
152
+ watches.release(id)
153
+ }
154
+ return null
155
+ }
156
+
157
+ /** Releases every watch of this plugin instance (handleOnDestroy). */
158
+ fun releaseAllWatches() {
159
+ val ids = watches.releaseAll()
160
+ if (ids.isEmpty()) return
161
+ val positions = services.positions
162
+ for (id in ids) {
163
+ try {
164
+ positions.clearWatch(id)
165
+ } catch (e: Exception) {
166
+ Logger.w(TAG, "clearWatch($id) failed", e)
167
+ }
168
+ }
169
+ }
170
+
171
+ private fun deliverWatch(id: String, record: Record?, error: TrackingException?) {
172
+ val target = watches[id] ?: return
173
+ try {
174
+ when {
175
+ record != null -> target.deliver(RecordJson.toJson(record))
176
+ error != null -> target.fail(error.code, error.message ?: error.code.name)
177
+ }
178
+ } catch (e: Exception) {
179
+ Logger.e(TAG, "watch $id delivery failed", e)
180
+ }
181
+ }
182
+
183
+ // ---- odometer
184
+
185
+ fun getOdometer(): JSONObject {
186
+ gate("getOdometer")
187
+ return odometerJson()
188
+ }
189
+
190
+ /** `setOdometer({odometer})`: meters, >= 0. */
191
+ fun setOdometer(options: JSONObject): JSONObject {
192
+ gate("setOdometer")
193
+ val value = OptionParsers.requireNumber(options, "odometer")
194
+ if (value < 0) OptionParsers.invalid("'odometer' must be >= 0")
195
+ services.odometer.set(value)
196
+ return odometerJson()
197
+ }
198
+
199
+ fun resetOdometer(): JSONObject {
200
+ gate("resetOdometer")
201
+ services.odometer.reset()
202
+ return odometerJson()
203
+ }
204
+
205
+ private fun odometerJson(): JSONObject = JSONObject().put("odometer", services.odometer.value)
206
+
207
+ // ---- records
208
+
209
+ /** `getLocations({limit?})` -> `{locations}`, oldest first; no limit = all. */
210
+ suspend fun getLocations(options: JSONObject): JSONObject {
211
+ gate("getLocations")
212
+ val limit = OptionParsers.optInt(options, "limit", min = 0) ?: -1
213
+ return JSONObject().put("locations", RecordJson.toJsonArray(services.locationStore.list(limit)))
214
+ }
215
+
216
+ suspend fun getCount(): JSONObject {
217
+ gate("getCount")
218
+ return JSONObject().put("count", services.locationStore.count())
219
+ }
220
+
221
+ /**
222
+ * `insertLocation({location})` -> `{uuid}`. The record is queued (and handed to the upload policy) like any
223
+ * other record, but it does not count as tracking activity: no event, no runtime or heartbeat update.
224
+ * Round 2: after a successful insert the record goes to `recordHooks` (native companion listeners receive it
225
+ * in `onRecord`), because inserted records do not pass through the record sink. A failed insert rejects the
226
+ * call and is not dispatched: the caller receives the error and may retry.
227
+ */
228
+ suspend fun insertLocation(options: JSONObject): JSONObject {
229
+ gate("insertLocation")
230
+ val input = OptionParsers.insertLocationInput(options)
231
+ val record = try {
232
+ services.recordFactory.fromExternal(input)
233
+ } catch (e: JSONException) {
234
+ throw TrackingException(ErrorCode.INVALID_ARGUMENT, e.message ?: "invalid location", e)
235
+ } catch (e: IllegalArgumentException) {
236
+ throw TrackingException(ErrorCode.INVALID_ARGUMENT, e.message ?: "invalid location", e)
237
+ }
238
+ services.locationStore.insert(record)
239
+ services.recordHooks.dispatch(record)
240
+ services.syncer.onRecordInserted(record)
241
+ return JSONObject().put("uuid", record.uuid)
242
+ }
243
+
244
+ suspend fun destroyLocations(): JSONObject {
245
+ gate("destroyLocations")
246
+ return JSONObject().put("count", services.locationStore.deleteAll())
247
+ }
248
+
249
+ /** `destroyLocation({uuid})` -> `{deleted}`. */
250
+ suspend fun destroyLocation(options: JSONObject): JSONObject {
251
+ gate("destroyLocation")
252
+ val uuid = OptionParsers.requireString(options, "uuid")
253
+ return JSONObject().put("deleted", services.locationStore.delete(listOf(uuid)) > 0)
254
+ }
255
+
256
+ /** `sync()` -> `{locations}`: the uploaded records. */
257
+ suspend fun sync(): JSONObject {
258
+ gate("sync")
259
+ return JSONObject().put("locations", RecordJson.toJsonArray(services.syncer.sync()))
260
+ }
261
+
262
+ // ---- geofences
263
+
264
+ suspend fun addGeofence(options: JSONObject): JSONObject? {
265
+ gate("addGeofence")
266
+ val spec = OptionParsers.geofence(OptionParsers.requireObject(options, "geofence"))
267
+ services.geofences.add(listOf(spec))
268
+ return null
269
+ }
270
+
271
+ /** `addGeofences({geofences})`; an empty array is a no-op. */
272
+ suspend fun addGeofences(options: JSONObject): JSONObject? {
273
+ gate("addGeofences")
274
+ val specs = OptionParsers.geofences(options)
275
+ if (specs.isNotEmpty()) services.geofences.add(specs)
276
+ return null
277
+ }
278
+
279
+ suspend fun removeGeofence(options: JSONObject): JSONObject? {
280
+ gate("removeGeofence")
281
+ services.geofences.remove(listOf(OptionParsers.requireString(options, "identifier")))
282
+ return null
283
+ }
284
+
285
+ /**
286
+ * `removeGeofences({identifiers?})`: no `identifiers` (missing or null) removes all; an empty array removes
287
+ * nothing, so a computed list that happens to be empty never wipes every geofence.
288
+ */
289
+ suspend fun removeGeofences(options: JSONObject): JSONObject? {
290
+ gate("removeGeofences")
291
+ val array = OptionParsers.optArray(options, "identifiers")
292
+ when {
293
+ array == null -> services.geofences.removeAll()
294
+ array.length() > 0 -> services.geofences.remove(identifiers(array))
295
+ }
296
+ return null
297
+ }
298
+
299
+ suspend fun getGeofences(): JSONObject {
300
+ gate("getGeofences")
301
+ return JSONObject().put("geofences", GeofenceJson.toJsonArray(services.geofences.list()))
302
+ }
303
+
304
+ /** `getGeofence({identifier})` -> `{geofence}` (null if unknown). */
305
+ suspend fun getGeofence(options: JSONObject): JSONObject {
306
+ gate("getGeofence")
307
+ val spec = services.geofences.get(OptionParsers.requireString(options, "identifier"))
308
+ return JSONObject().put("geofence", spec?.let { GeofenceJson.toJson(it) } ?: JSONObject.NULL)
309
+ }
310
+
311
+ suspend fun geofenceExists(options: JSONObject): JSONObject {
312
+ gate("geofenceExists")
313
+ val spec = services.geofences.get(OptionParsers.requireString(options, "identifier"))
314
+ return JSONObject().put("exists", spec != null)
315
+ }
316
+
317
+ private fun identifiers(array: JSONArray): List<String> = (0 until array.length()).map { i ->
318
+ (array.opt(i) as? String)?.takeIf { it.isNotBlank() }
319
+ ?: OptionParsers.invalid("identifiers[$i] must be a non-empty string")
320
+ }
321
+
322
+ // ---- heartbeat and device
323
+
324
+ suspend fun getHeartbeatStatus(): JSONObject {
325
+ gate("getHeartbeatStatus")
326
+ return HeartbeatStatusJson.toJson(services.heartbeat.status())
327
+ }
328
+
329
+ fun getProviderState(): JSONObject {
330
+ gate("getProviderState")
331
+ return ProviderStateJson.toJson(services.device.providerState())
332
+ }
333
+
334
+ fun isPowerSaveMode(): JSONObject {
335
+ gate("isPowerSaveMode")
336
+ return JSONObject().put("isPowerSaveMode", services.device.isPowerSaveMode())
337
+ }
338
+
339
+ fun getBatteryOptimizationStatus(): JSONObject {
340
+ gate("getBatteryOptimizationStatus")
341
+ val device = services.device
342
+ return BatteryOptimizationStatusJson.toJson(
343
+ BatteryOptimizationStatus(
344
+ isIgnoringBatteryOptimizations = device.isIgnoringBatteryOptimizations(),
345
+ canScheduleExactAlarms = device.canScheduleExactAlarms(),
346
+ isDeviceIdleMode = device.isDeviceIdleMode(),
347
+ ),
348
+ )
349
+ }
350
+
351
+ fun getPowerManagerInfo(): JSONObject {
352
+ gate("getPowerManagerInfo")
353
+ return PowerManagerInfoJson.toJson(services.deviceSettings.powerManagerInfo())
354
+ }
355
+
356
+ fun getDeviceInfo(): JSONObject {
357
+ gate("getDeviceInfo")
358
+ return DeviceInfoJson.toJson(services.deviceInfo.deviceInfo())
359
+ }
360
+
361
+ fun getSensors(): JSONObject {
362
+ gate("getSensors")
363
+ return SensorsJson.toJson(services.deviceInfo.sensors())
364
+ }
365
+
366
+ // ---- settings screens (main thread) -> {opened}
367
+
368
+ suspend fun openBatteryOptimizationSettings(): JSONObject =
369
+ openSettings("openBatteryOptimizationSettings") { services.deviceSettings.openBatteryOptimizationSettings(it) }
370
+
371
+ suspend fun openPowerManagerSettings(): JSONObject =
372
+ openSettings("openPowerManagerSettings") { services.deviceSettings.openPowerManagerSettings(it) }
373
+
374
+ suspend fun openLocationSettings(): JSONObject =
375
+ openSettings("openLocationSettings") { services.deviceSettings.openLocationSettings(it) }
376
+
377
+ suspend fun openAppSettings(): JSONObject =
378
+ openSettings("openAppSettings") { services.deviceSettings.openAppSettings(it) }
379
+
380
+ private suspend fun openSettings(method: String, open: (Activity?) -> Boolean): JSONObject {
381
+ gate(method)
382
+ val opened = withContext(services.dispatchers.main) { open(activity()) }
383
+ return JSONObject().put("opened", opened)
384
+ }
385
+
386
+ // ---- permissions -> PermissionStatus
387
+
388
+ fun checkPermissions(): JSONObject {
389
+ gate("checkPermissions")
390
+ return permissionStatusJson(emptyMap())
391
+ }
392
+
393
+ /**
394
+ * `requestPermissions({permissions?})`: runs `PermissionManager.request` on the main thread through [host] and
395
+ * resolves with the resulting status (entries reported by the request win over `permissions.status()`).
396
+ * Requests are serialized: Android answers an overlapping request at once with an empty result, and Capacitor
397
+ * matches results to calls in FIFO order, so concurrent requests would receive each other's results.
398
+ */
399
+ suspend fun requestPermissions(options: JSONObject, host: PermissionHost): JSONObject {
400
+ gate("requestPermissions")
401
+ val types = OptionParsers.permissionTypes(options)
402
+ if (types.isEmpty()) return permissionStatusJson(emptyMap())
403
+ val result = permissionRequests.withLock {
404
+ val rationale = services.configStore.config.value.backgroundPermissionRationale
405
+ withContext(services.dispatchers.main) {
406
+ suspendCancellableCoroutine { continuation ->
407
+ val settled = AtomicBoolean(false)
408
+ services.permissions.request(host, types, rationale) { status ->
409
+ if (settled.compareAndSet(false, true)) continuation.resume(status)
410
+ }
411
+ }
412
+ }
413
+ }
414
+ return permissionStatusJson(result)
415
+ }
416
+
417
+ private fun permissionStatusJson(overrides: Map<PermissionType, PermissionState>): JSONObject =
418
+ PermissionStatusJson.toJson(services.permissions.status() + overrides)
419
+
420
+ // ---- log
421
+
422
+ /** `log({level, message})` -> void; written through [Logger] (logcat and the log file). */
423
+ fun log(options: JSONObject): JSONObject? {
424
+ gate("log")
425
+ val level = OptionParsers.logLevel(options)
426
+ val message = OptionParsers.optString(options, "message") ?: OptionParsers.invalid("'message' is required")
427
+ Logger.log(level, JS_TAG, message)
428
+ return null
429
+ }
430
+
431
+ suspend fun getLog(options: JSONObject): JSONObject {
432
+ gate("getLog")
433
+ return JSONObject().put("log", services.logStore.read(OptionParsers.logQuery(options)))
434
+ }
435
+
436
+ suspend fun destroyLog(): JSONObject? {
437
+ gate("destroyLog")
438
+ services.logStore.destroy()
439
+ return null
440
+ }
441
+
442
+ /** `uploadLog({url, headers?, params?})` -> `{success, status}`. */
443
+ suspend fun uploadLog(options: JSONObject): JSONObject {
444
+ gate("uploadLog")
445
+ val o = OptionParsers.uploadLogOptions(options)
446
+ val result = services.logStore.upload(o.url, o.headers, o.paramsJson)
447
+ return JSONObject().put("success", result.success).put("status", result.status)
448
+ }
449
+
450
+ /** `emailLog({email, subject?})` -> void: opens a chooser with the log attached; NO_ACTIVITY without UI. */
451
+ suspend fun emailLog(options: JSONObject): JSONObject? {
452
+ gate("emailLog")
453
+ val email = OptionParsers.requireString(options, "email")
454
+ val subject = OptionParsers.optString(options, "subject")
455
+ requireActivity()
456
+ val intent = services.logStore.prepareEmail(email, subject)
457
+ withContext(services.dispatchers.main) {
458
+ val chooser = Intent.createChooser(intent, subject?.takeIf { it.isNotBlank() } ?: EMAIL_CHOOSER_TITLE)
459
+ try {
460
+ requireActivity().startActivity(chooser)
461
+ } catch (e: ActivityNotFoundException) {
462
+ throw TrackingException(ErrorCode.UNAVAILABLE, "No app can send the log", e)
463
+ }
464
+ }
465
+ return null
466
+ }
467
+
468
+ // ---- events and lifecycle
469
+
470
+ /**
471
+ * Forwards every bus event to [notify] as (JS event name, payload builder); cancel the result to stop. The
472
+ * payload is built only when [notify] calls the builder, so an event without JS listeners costs nothing and the
473
+ * emitting (engine) thread never serializes records.
474
+ */
475
+ fun forwardEvents(notify: (name: String, payload: () -> JSONObject) -> Unit): Subscription =
476
+ services.events.subscribe { event ->
477
+ try {
478
+ notify(EventJson.name(event)) { EventJson.payload(event) }
479
+ } catch (e: Exception) {
480
+ Logger.e(TAG, "failed to forward ${event::class.simpleName}", e)
481
+ }
482
+ }
483
+
484
+ /** handleOnResume: re-checks the provider state (catches changes made while the app was in the background). */
485
+ suspend fun onResume() {
486
+ try {
487
+ services.device.checkProviderState(RESUME_REASON)
488
+ } catch (e: CancellationException) {
489
+ throw e
490
+ } catch (e: Exception) {
491
+ Logger.w(TAG, "provider state check on resume failed", e)
492
+ }
493
+ }
494
+
495
+ // ---- helpers
496
+
497
+ private fun gate(method: String) {
498
+ if (method !in ALLOWED_BEFORE_READY && !readyFlag.get()) {
499
+ throw TrackingException(ErrorCode.NOT_READY, "$method() requires ready() to have resolved first")
500
+ }
501
+ }
502
+
503
+ private fun stateJson(state: State): JSONObject = ConfigJson.stateToJson(state)
504
+
505
+ private fun activity(): Activity? = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed }
506
+
507
+ private fun requireActivity(): Activity =
508
+ activity() ?: throw TrackingException(ErrorCode.NO_ACTIVITY, "No activity is attached to the plugin")
509
+
510
+ companion object {
511
+ private const val TAG = "LT.Bridge"
512
+
513
+ /** Log tag of messages written by JS `log()`. */
514
+ const val JS_TAG = "LT.JS"
515
+
516
+ /** The `checkProviderState` reason used on handleOnResume. */
517
+ const val RESUME_REASON = "resume"
518
+
519
+ const val EMAIL_CHOOSER_TITLE = "Send location tracking log"
520
+
521
+ /** Methods that work before `ready()` (architecture §1, NOT_READY rule). */
522
+ val ALLOWED_BEFORE_READY: Set<String> = setOf(
523
+ "ready",
524
+ "getState",
525
+ "checkPermissions",
526
+ "requestPermissions",
527
+ "getDeviceInfo",
528
+ "getSensors",
529
+ "log",
530
+ "getLog",
531
+ "getProviderState",
532
+ "openBatteryOptimizationSettings",
533
+ "openPowerManagerSettings",
534
+ "openLocationSettings",
535
+ "openAppSettings",
536
+ )
537
+
538
+ /** Process-wide "ready() resolved" flag. */
539
+ internal val processReady = AtomicBoolean(false)
540
+ }
541
+ }
@@ -0,0 +1,76 @@
1
+ package com.brickssoft.locationtracking.bridge
2
+
3
+ import android.app.Activity
4
+ import com.brickssoft.locationtracking.core.Logger
5
+ import com.brickssoft.locationtracking.permission.PermissionHost
6
+ import com.brickssoft.locationtracking.permission.PermissionType
7
+ import com.getcapacitor.Plugin
8
+ import com.getcapacitor.annotation.CapacitorPlugin
9
+ import com.getcapacitor.util.PermissionHelper
10
+
11
+ /**
12
+ * Starts a Capacitor permission request for plugin permission aliases and calls `onDone` from the plugin's
13
+ * `@PermissionCallback`. Implemented by `LocationTrackingPlugin` per call, because `requestPermissionForAliases`
14
+ * is protected.
15
+ */
16
+ fun interface PermissionRequestLauncher {
17
+ fun launch(aliases: List<String>, onDone: () -> Unit)
18
+ }
19
+
20
+ /**
21
+ * [PermissionHost] on top of the Capacitor plugin.
22
+ *
23
+ * [requestAliases] forwards the known aliases whose Android permissions are declared in the app manifest to
24
+ * [launcher]. It completes immediately (nothing to ask) when no alias is left, when there is no [launcher], or when
25
+ * the launch fails; otherwise [launcher] calls `onDone` from the permission callback, on the main thread.
26
+ * Undeclared permissions are skipped because Capacitor would reject the call without ever calling back.
27
+ *
28
+ * @param isRequestable whether an alias can be requested; default: all its permissions are in the app manifest.
29
+ */
30
+ class PluginPermissionHost(
31
+ private val plugin: Plugin,
32
+ private val launcher: PermissionRequestLauncher? = null,
33
+ private val isRequestable: (alias: String) -> Boolean = { declaredInManifest(plugin, it) },
34
+ ) : PermissionHost {
35
+ /** The plugin's activity, or null without a bridge or while it is finishing or destroyed. */
36
+ override val activity: Activity? get() = usableActivity(plugin)
37
+
38
+ override fun requestAliases(aliases: List<String>, onDone: () -> Unit) {
39
+ val known = aliases.filter { PermissionType.fromAlias(it) != null }.distinct()
40
+ if (known.size != aliases.distinct().size) Logger.w(TAG, "ignoring unknown permission aliases in $aliases")
41
+ val requestable = known.filter(isRequestable)
42
+ if (requestable.size != known.size) {
43
+ Logger.w(TAG, "not requesting ${known - requestable.toSet()}: permissions missing from the app manifest")
44
+ }
45
+ val l = launcher
46
+ if (requestable.isEmpty() || l == null) {
47
+ onDone()
48
+ return
49
+ }
50
+ try {
51
+ l.launch(requestable, onDone)
52
+ } catch (e: Exception) {
53
+ Logger.e(TAG, "permission request for $requestable failed", e)
54
+ onDone()
55
+ }
56
+ }
57
+
58
+ internal companion object {
59
+ private const val TAG = "LT.PermissionHost"
60
+
61
+ /** The plugin's activity, or null without a bridge or while it is finishing or destroyed. */
62
+ fun usableActivity(plugin: Plugin): Activity? =
63
+ runCatching { plugin.activity }.getOrNull()?.takeUnless { it.isFinishing || it.isDestroyed }
64
+
65
+ /** True if every Android permission of [alias] (per the `@CapacitorPlugin` annotation) is in the manifest. */
66
+ fun declaredInManifest(plugin: Plugin, alias: String): Boolean {
67
+ val strings = plugin.javaClass.getAnnotation(CapacitorPlugin::class.java)
68
+ ?.permissions
69
+ ?.firstOrNull { it.alias == alias }
70
+ ?.strings
71
+ ?: return false
72
+ val context = runCatching { plugin.context }.getOrNull() ?: return false
73
+ return PermissionHelper.hasDefinedPermissions(context, strings)
74
+ }
75
+ }
76
+ }