@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,267 @@
1
+ package com.brickssoft.locationtracking.api
2
+
3
+ import android.content.Context
4
+ import androidx.annotation.VisibleForTesting
5
+ import com.brickssoft.locationtracking.bridge.BridgeServices
6
+ import com.brickssoft.locationtracking.bridge.ComponentServices
7
+ import com.brickssoft.locationtracking.bridge.PluginHandlers
8
+ import com.brickssoft.locationtracking.bridge.Rejection
9
+ import com.brickssoft.locationtracking.core.Components
10
+ import com.brickssoft.locationtracking.core.ErrorCode
11
+ import com.brickssoft.locationtracking.core.Logger
12
+ import com.brickssoft.locationtracking.core.TrackingException
13
+ import kotlinx.coroutines.CoroutineScope
14
+ import kotlinx.coroutines.isActive
15
+ import kotlinx.coroutines.launch
16
+ import org.json.JSONArray
17
+ import org.json.JSONObject
18
+ import java.util.concurrent.atomic.AtomicBoolean
19
+ import kotlin.coroutines.cancellation.CancellationException
20
+
21
+ /**
22
+ * Public native API for companion plugins and native app code (round 2; guide: `docs/native-api.md`). Each method
23
+ * mirrors the JS method of the same name: it takes the same JSON shapes, runs the same code as the JS bridge
24
+ * (`PluginHandlers`, launched on `Components.scope`) and delivers the JS result shape to a [NativeCallback].
25
+ *
26
+ * - Every callback is invoked exactly once, on the `LT-native` thread ([THREAD_NAME]), when its call completes. Calls
27
+ * run concurrently on the plugin's scope: a call that suspends (for example `start()`) can complete after a later
28
+ * call, so callbacks arrive in completion order, not in call order.
29
+ * - A failure is a `Result.failure` holding a [TrackingException] whose `code` is the JS `ErrorCode` (for example
30
+ * `PERMISSION_DENIED`, `INVALID_ARGUMENT`, `NO_URL`); any other exception is wrapped as `INTERNAL` with the original
31
+ * exception as its cause.
32
+ * - Native calls are not subject to the JS bridge's NOT_READY rule and do not set the bridge's ready flag: JS must
33
+ * still call `ready()` itself. A native caller that needs a configuration calls [ready] first.
34
+ * - JSON arguments are copied when the method is called, so the caller may change or reuse its JSONObject afterwards.
35
+ */
36
+ object LocationTrackingNative {
37
+ /**
38
+ * Name (or name prefix, followed by `.`) of the `<meta-data>` that declares a [LocationTrackingListener] class:
39
+ * `<meta-data android:name="com.brickssoft.locationtracking.LISTENER" android:value="com.example.MyListener"/>`.
40
+ */
41
+ const val LISTENER_META_DATA = "com.brickssoft.locationtracking.LISTENER"
42
+
43
+ /** Name of the single background thread that runs listeners and callbacks. */
44
+ const val THREAD_NAME = "LT-native"
45
+
46
+ private const val TAG = "LT.Native"
47
+
48
+ /**
49
+ * Subscribes [listener] to every record and event emitted after this call (see [LocationTrackingListener]). The
50
+ * listener's methods receive the application context of [context]. The subscription is process-wide and does not
51
+ * create the plugin's components (nothing is emitted before they exist); [NativeSubscription.remove] ends it.
52
+ */
53
+ @JvmStatic
54
+ fun addListener(context: Context, listener: LocationTrackingListener): NativeSubscription =
55
+ NativeListeners.add(context, listener)
56
+
57
+ /** JS `ready({config, reset})`; result: `State` JSON (`ConfigJson.stateToJson`). */
58
+ @JvmStatic
59
+ fun ready(context: Context, config: JSONObject?, reset: Boolean, callback: NativeCallback<JSONObject>) {
60
+ val copy = if (config == null) null else copyOf(config, "ready", callback) ?: return
61
+ call(context, "ready", callback) {
62
+ val options = JSONObject().put("reset", reset)
63
+ if (copy != null) options.put("config", copy)
64
+ ready(options)
65
+ }
66
+ }
67
+
68
+ /** JS `setConfig({config})`; result: `State` JSON. */
69
+ @JvmStatic
70
+ fun setConfig(context: Context, config: JSONObject, callback: NativeCallback<JSONObject>) {
71
+ val copy = copyOf(config, "setConfig", callback) ?: return
72
+ call(context, "setConfig", callback) { setConfig(JSONObject().put("config", copy)) }
73
+ }
74
+
75
+ /** JS `start()`; result: `State` JSON. */
76
+ @JvmStatic
77
+ fun start(context: Context, callback: NativeCallback<JSONObject>) {
78
+ call(context, "start", callback) { start() }
79
+ }
80
+
81
+ /** JS `startGeofences()`; result: `State` JSON. */
82
+ @JvmStatic
83
+ fun startGeofences(context: Context, callback: NativeCallback<JSONObject>) {
84
+ call(context, "startGeofences", callback) { startGeofences() }
85
+ }
86
+
87
+ /** JS `stop()`; result: `State` JSON. */
88
+ @JvmStatic
89
+ fun stop(context: Context, callback: NativeCallback<JSONObject>) {
90
+ call(context, "stop", callback) { stop() }
91
+ }
92
+
93
+ /** JS `changePace({isMoving})`. */
94
+ @JvmStatic
95
+ fun changePace(context: Context, isMoving: Boolean, callback: NativeCallback<Unit>) {
96
+ call(context, "changePace", callback) {
97
+ changePace(JSONObject().put("isMoving", isMoving))
98
+ Unit
99
+ }
100
+ }
101
+
102
+ /** JS `getState()`; result: `State` JSON. */
103
+ @JvmStatic
104
+ fun getState(context: Context, callback: NativeCallback<JSONObject>) {
105
+ call(context, "getState", callback) { getState() }
106
+ }
107
+
108
+ /** JS `getHeartbeatStatus()`; result: `HeartbeatStatus` JSON. */
109
+ @JvmStatic
110
+ fun getHeartbeatStatus(context: Context, callback: NativeCallback<JSONObject>) {
111
+ call(context, "getHeartbeatStatus", callback) { getHeartbeatStatus() }
112
+ }
113
+
114
+ /** JS `sync()`; result: the uploaded records (wire JSON, the JS `locations` array). */
115
+ @JvmStatic
116
+ fun sync(context: Context, callback: NativeCallback<JSONArray>) {
117
+ call(context, "sync", callback) { sync().getJSONArray("locations") }
118
+ }
119
+
120
+ /**
121
+ * JS `insertLocation({location})` (`InsertLocationInput` JSON); result: the new record's uuid. The record is
122
+ * queued for upload and delivered to every [LocationTrackingListener.onRecord], like the JS method.
123
+ */
124
+ @JvmStatic
125
+ fun insertLocation(context: Context, location: JSONObject, callback: NativeCallback<String>) {
126
+ val copy = copyOf(location, "insertLocation", callback) ?: return
127
+ call(context, "insertLocation", callback) {
128
+ insertLocation(JSONObject().put("location", copy)).getString("uuid")
129
+ }
130
+ }
131
+
132
+ /** JS `addGeofence({geofence})` (JS `Geofence` JSON, parsed by `GeofenceJson`). */
133
+ @JvmStatic
134
+ fun addGeofence(context: Context, geofence: JSONObject, callback: NativeCallback<Unit>) {
135
+ val copy = copyOf(geofence, "addGeofence", callback) ?: return
136
+ call(context, "addGeofence", callback) {
137
+ addGeofence(JSONObject().put("geofence", copy))
138
+ Unit
139
+ }
140
+ }
141
+
142
+ /** JS `removeGeofence({identifier})`. */
143
+ @JvmStatic
144
+ fun removeGeofence(context: Context, identifier: String, callback: NativeCallback<Unit>) {
145
+ call(context, "removeGeofence", callback) {
146
+ removeGeofence(JSONObject().put("identifier", identifier))
147
+ Unit
148
+ }
149
+ }
150
+
151
+ /** JS `getGeofences()`; result: the JS `geofences` array. */
152
+ @JvmStatic
153
+ fun getGeofences(context: Context, callback: NativeCallback<JSONArray>) {
154
+ call(context, "getGeofences", callback) { getGeofences().getJSONArray("geofences") }
155
+ }
156
+
157
+ // ---- plumbing
158
+
159
+ /** Test seam: replaces the components the calls run on. */
160
+ @VisibleForTesting
161
+ @Volatile
162
+ internal var backendOverride: ((Context) -> NativeBackend)? = null
163
+
164
+ @Volatile
165
+ private var cached: NativeBackend? = null
166
+
167
+ private fun backend(context: Context): NativeBackend {
168
+ backendOverride?.let { return it(context) }
169
+ val components = Components.get(context)
170
+ // Components.get() publishes a new instance before its bootstrap() has run NativeListeners.install, and runs
171
+ // bootstrap() while it holds the lock of the Components companion object. If another thread is still inside
172
+ // that bootstrap, taking the same lock once waits until it has finished, so a native call can never emit a
173
+ // record before the manifest listeners are connected. (Uncontended, the lock costs nothing measurable.)
174
+ synchronized(Components) { }
175
+ cached?.takeIf { it.owner === components }?.let { return it }
176
+ return NativeBackend(ComponentServices(components), components.scope, owner = components).also { cached = it }
177
+ }
178
+
179
+ /**
180
+ * Runs [block] on the backend's scope and delivers its result, or its failure mapped by [failureOf], to
181
+ * [callback] on the `LT-native` thread. The callback is invoked exactly once: also when the scope is already
182
+ * cancelled or is cancelled while the call runs (then the failure is INTERNAL).
183
+ */
184
+ private fun <T> call(
185
+ context: Context,
186
+ method: String,
187
+ callback: NativeCallback<T>,
188
+ block: suspend PluginHandlers.() -> T,
189
+ ) {
190
+ val settled = AtomicBoolean(false)
191
+ val settle = { result: Result<T> -> if (settled.compareAndSet(false, true)) deliver(method, callback, result) }
192
+ try {
193
+ val backend = backend(context)
194
+ backend.scope.launch {
195
+ val result = try {
196
+ Result.success(backend.handlers.block())
197
+ } catch (t: Throwable) { // also Errors and our own cancellation: the callback must always run
198
+ Result.failure(failureOf(method, t, shutDown = t is CancellationException && !isActive))
199
+ }
200
+ settle(result)
201
+ }.invokeOnCompletion { cause ->
202
+ // Cancelled before the block ran (the scope was already cancelled): nothing settled the call yet.
203
+ if (cause != null && !settled.get()) settle(Result.failure(failureOf(method, cause, shutDown = true)))
204
+ }
205
+ } catch (t: Throwable) {
206
+ settle(Result.failure(failureOf(method, t, shutDown = false)))
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Copies [json] now, so a caller that changes it afterwards does not affect the call. A JSONObject that cannot be
212
+ * serialized (for example one that contains itself) fails the call with INVALID_ARGUMENT.
213
+ */
214
+ private fun <T> copyOf(json: JSONObject, method: String, callback: NativeCallback<T>): JSONObject? = try {
215
+ JSONObject(json.toString())
216
+ } catch (t: Throwable) { // also StackOverflowError: never throw to the caller, always invoke the callback
217
+ Logger.d(TAG, "LocationTrackingNative.$method: the JSON argument cannot be serialized ($t)")
218
+ deliver(method, callback, Result.failure(TrackingException(ErrorCode.INVALID_ARGUMENT, "invalid JSON argument: $t", t)))
219
+ null
220
+ }
221
+
222
+ /**
223
+ * The failure delivered for [t], with the JS bridge's mapping ([Rejection.of]): a [TrackingException] keeps its
224
+ * code, anything else is INTERNAL. [shutDown] is true when the plugin's own scope was cancelled (the components
225
+ * were shut down), which gets its own message. Logged like the bridge logs a rejection.
226
+ */
227
+ private fun failureOf(method: String, t: Throwable, shutDown: Boolean): TrackingException {
228
+ val failure = when {
229
+ t is TrackingException -> t
230
+ shutDown -> TrackingException(
231
+ ErrorCode.INTERNAL,
232
+ "LocationTrackingNative.$method did not complete: the plugin's components were shut down",
233
+ t,
234
+ )
235
+ else -> Rejection.of(t).let { TrackingException(it.code, it.message, t) }
236
+ }
237
+ if (failure.code == ErrorCode.INTERNAL) {
238
+ Logger.e(TAG, "LocationTrackingNative.$method failed", t)
239
+ } else {
240
+ Logger.d(TAG, "LocationTrackingNative.$method failed: ${failure.code} ${failure.message}")
241
+ }
242
+ return failure
243
+ }
244
+
245
+ private fun <T> deliver(method: String, callback: NativeCallback<T>, result: Result<T>) {
246
+ NativeThread.post {
247
+ try {
248
+ callback.onResult(result)
249
+ } catch (t: Throwable) {
250
+ Logger.e(TAG, "the callback of LocationTrackingNative.$method threw", t)
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * What [LocationTrackingNative] calls run on: the bridge's handlers over [services] with their own ready flag (always
258
+ * true, so the NOT_READY rule does not apply and the JS bridge's flag is never set), launched on [scope]. [owner] is
259
+ * the [Components] instance the backend was built from (the facade builds a new backend after a reset).
260
+ */
261
+ internal class NativeBackend(
262
+ services: BridgeServices,
263
+ val scope: CoroutineScope,
264
+ val owner: Any? = null,
265
+ ) {
266
+ val handlers = PluginHandlers(services, readyFlag = AtomicBoolean(true))
267
+ }
@@ -0,0 +1,270 @@
1
+ package com.brickssoft.locationtracking.api
2
+
3
+ import android.content.Context
4
+ import android.content.pm.ApplicationInfo
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import android.os.Bundle
8
+ import androidx.annotation.VisibleForTesting
9
+ import com.brickssoft.locationtracking.core.Components
10
+ import com.brickssoft.locationtracking.core.EventBus
11
+ import com.brickssoft.locationtracking.core.Logger
12
+ import com.brickssoft.locationtracking.core.RecordHooks
13
+ import com.brickssoft.locationtracking.core.Subscription
14
+ import com.brickssoft.locationtracking.core.TrackingEvent
15
+ import com.brickssoft.locationtracking.model.EventJson
16
+ import com.brickssoft.locationtracking.model.Record
17
+ import com.brickssoft.locationtracking.model.RecordJson
18
+ import org.json.JSONObject
19
+ import java.util.concurrent.CopyOnWriteArrayList
20
+
21
+ /**
22
+ * Installs the manifest-declared [LocationTrackingListener]s and connects every listener (manifest and programmatic)
23
+ * to the plugin: `components.recordHooks` for records, `components.events` for events. Delivery runs on the
24
+ * `LT-native` thread ([NativeThread]).
25
+ *
26
+ * How a record or an event reaches a listener:
27
+ * 1. [RecordHooks.dispatch] or [EventBus.emit] calls this object synchronously on the emitting thread (the engine,
28
+ * a receiver, an I/O coroutine).
29
+ * 2. This object takes a snapshot of the registered listeners (if there is any) and queues one task on the `LT-native`
30
+ * thread. It does not build any JSON on the emitting thread, so the engine is never slowed down by a listener.
31
+ * 3. The task builds the JSON once (`RecordJson.toJson(record)` without `sent_at`, or `EventJson.name/payload`), gives
32
+ * each listener its own JSONObject (a listener that changes its object does not change what the next listener
33
+ * receives) and calls each listener that is still subscribed. A listener that throws is logged; the other
34
+ * listeners still run.
35
+ *
36
+ * Programmatic listeners are process-wide: they stay registered when a new [Components] instance is installed (only
37
+ * tests create more than one). Manifest listeners are created again by every [install].
38
+ */
39
+ internal object NativeListeners {
40
+ private const val TAG = "LT.Native"
41
+
42
+ /** One registered listener. [active] turns false when it is removed; queued deliveries then skip it. */
43
+ private class Entry(
44
+ val listener: LocationTrackingListener,
45
+ val context: Context,
46
+ val label: String,
47
+ ) {
48
+ @Volatile
49
+ var active = true
50
+ }
51
+
52
+ private val lock = Any()
53
+
54
+ /** Every listener, manifest listeners first (in manifest order), then programmatic ones in subscription order. */
55
+ private val entries = CopyOnWriteArrayList<Entry>()
56
+
57
+ /** The manifest listeners of the current installation. Guarded by [lock]. */
58
+ private var manifestEntries: List<Entry> = emptyList()
59
+
60
+ /** The record hook and event bus subscriptions of the current installation. Guarded by [lock]. */
61
+ private var connection: List<Subscription> = emptyList()
62
+
63
+ /**
64
+ * Called once per [Components] instance from `Components.bootstrap()`: reads the manifest meta-data, creates the
65
+ * listener classes and subscribes to the instance's record hooks and event bus, replacing the subscriptions and
66
+ * the manifest listeners of a previous instance. A failure while reading or creating manifest listeners is logged
67
+ * and never prevents the subscriptions.
68
+ */
69
+ fun install(context: Context, components: Components) {
70
+ val appContext = context.applicationContext ?: context
71
+ val manifest = try {
72
+ createManifestListeners(appContext)
73
+ } catch (e: Exception) {
74
+ Logger.e(TAG, "cannot read the listener meta-data; no manifest listener is installed", e)
75
+ emptyList()
76
+ }
77
+ connect(appContext, components.recordHooks, components.events, manifest)
78
+ }
79
+
80
+ /**
81
+ * Subscribes to [hooks] and [events] and makes [manifest] the manifest listeners (called with [context]).
82
+ * Cancels the subscriptions of the previous call and deactivates its manifest listeners.
83
+ * [install] uses it; tests call it directly with the full-stack harness or fakes.
84
+ */
85
+ fun connect(
86
+ context: Context,
87
+ hooks: RecordHooks,
88
+ events: EventBus,
89
+ manifest: List<Pair<String, LocationTrackingListener>>,
90
+ ) {
91
+ val appContext = context.applicationContext ?: context
92
+ synchronized(lock) {
93
+ connection.forEach { it.cancel() }
94
+ for (entry in manifestEntries) {
95
+ entry.active = false
96
+ entries.remove(entry)
97
+ }
98
+ val created = manifest.map { (key, listener) ->
99
+ Entry(listener, appContext, "manifest listener ${listener.javaClass.name} ($key)")
100
+ }
101
+ entries.addAll(0, created)
102
+ manifestEntries = created
103
+ connection = listOf(
104
+ hooks.add { record -> onRecordQueued(record) },
105
+ events.subscribe { event -> onEvent(event) },
106
+ )
107
+ }
108
+ if (manifest.isNotEmpty()) {
109
+ Logger.i(TAG, "installed ${manifest.size} manifest listener(s): ${manifest.joinToString { it.second.javaClass.name }}")
110
+ }
111
+ }
112
+
113
+ /** Programmatic subscription ([LocationTrackingNative.addListener]). */
114
+ fun add(context: Context, listener: LocationTrackingListener): NativeSubscription {
115
+ val entry = Entry(listener, context.applicationContext ?: context, "listener ${listener.javaClass.name}")
116
+ entries.add(entry)
117
+ return NativeSubscription {
118
+ entry.active = false
119
+ entries.remove(entry)
120
+ }
121
+ }
122
+
123
+ // ---- delivery
124
+
125
+ private fun onRecordQueued(record: Record) {
126
+ if (entries.isEmpty()) return
127
+ // The iterator of a CopyOnWriteArrayList is a snapshot of the listeners registered now; taking it copies nothing.
128
+ val targets = entries.iterator()
129
+ deliver({ "record ${record.event.wire} ${record.uuid}" }, targets, { RecordJson.toJson(record) }) { entry, json ->
130
+ entry.listener.onRecord(entry.context, json)
131
+ }
132
+ }
133
+
134
+ private fun onEvent(event: TrackingEvent) {
135
+ if (entries.isEmpty()) return
136
+ val targets = entries.iterator()
137
+ deliver({ "event ${event::class.simpleName}" }, targets, { EventJson.payload(event) }) { entry, json ->
138
+ entry.listener.onEvent(entry.context, EventJson.name(event), json)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Queues one task on the `LT-native` thread that builds the JSON once and calls [call] for every listener of
144
+ * [targets] that is still subscribed. Each listener gets its own JSONObject: the first one the built object, the
145
+ * others a copy parsed from its text (made before any listener runs).
146
+ *
147
+ * Every [Throwable] is caught, also an Error such as NoClassDefFoundError or StackOverflowError: an exception that
148
+ * escapes the `LT-native` thread would crash the process, and with it the tracking service.
149
+ */
150
+ private fun deliver(
151
+ what: () -> String,
152
+ targets: Iterator<Entry>,
153
+ build: () -> JSONObject,
154
+ call: (Entry, JSONObject) -> Unit,
155
+ ) {
156
+ NativeThread.post {
157
+ try {
158
+ val active = targets.asSequence().filter { it.active }.toList()
159
+ if (active.isEmpty()) return@post
160
+ val json = build()
161
+ val text = if (active.size > 1) json.toString() else null
162
+ for ((index, entry) in active.withIndex()) {
163
+ if (!entry.active) continue // removed while an earlier listener ran
164
+ try {
165
+ call(entry, if (index == 0) json else JSONObject(text!!))
166
+ } catch (t: Throwable) {
167
+ Logger.e(TAG, "${entry.label} threw while receiving ${what()}", t)
168
+ }
169
+ }
170
+ } catch (t: Throwable) {
171
+ Logger.e(TAG, "cannot deliver ${what()} to the native listeners", t)
172
+ }
173
+ }
174
+ }
175
+
176
+ // ---- manifest
177
+
178
+ /**
179
+ * The listener class names declared in the manifest, as (meta-data name, class name) pairs: the entry named
180
+ * [LocationTrackingNative.LISTENER_META_DATA] first, then the entries whose name starts with it followed by `.`,
181
+ * sorted by name. A class named twice is returned once (the first occurrence).
182
+ */
183
+ fun declaredListenerClasses(context: Context): List<Pair<String, String>> {
184
+ val metaData = applicationInfo(context).metaData ?: return emptyList()
185
+ val base = LocationTrackingNative.LISTENER_META_DATA
186
+ val keys = metaData.keySet()
187
+ .filter { it == base || it.startsWith("$base.") }
188
+ .sortedWith(compareBy<String>({ it != base }, { it }))
189
+ val seen = HashSet<String>()
190
+ val result = ArrayList<Pair<String, String>>()
191
+ for (key in keys) {
192
+ val className = classNameOf(metaData, key)
193
+ if (className == null) {
194
+ Logger.e(TAG, "meta-data $key must be a class name (android:value=\"com.example.MyListener\")")
195
+ continue
196
+ }
197
+ if (seen.add(className)) {
198
+ result += key to className
199
+ } else {
200
+ Logger.w(TAG, "meta-data $key names $className again; it is created once")
201
+ }
202
+ }
203
+ return result
204
+ }
205
+
206
+ /**
207
+ * Creates one instance of each declared listener class with its public no-arg constructor. A class that cannot
208
+ * be loaded or created, or that does not implement [LocationTrackingListener], is logged with its [Throwable]
209
+ * and skipped.
210
+ */
211
+ fun createManifestListeners(context: Context): List<Pair<String, LocationTrackingListener>> =
212
+ declaredListenerClasses(context).mapNotNull { (key, className) -> instantiate(context, key, className) }
213
+
214
+ private fun instantiate(context: Context, key: String, className: String): Pair<String, LocationTrackingListener>? =
215
+ try {
216
+ val type = Class.forName(className, true, context.classLoader)
217
+ if (!LocationTrackingListener::class.java.isAssignableFrom(type)) {
218
+ Logger.e(TAG, "$className ($key) does not implement ${LocationTrackingListener::class.java.name}; skipped")
219
+ null
220
+ } else {
221
+ key to (type.getConstructor().newInstance() as LocationTrackingListener)
222
+ }
223
+ } catch (e: Exception) {
224
+ // ClassNotFoundException, NoSuchMethodException (no public no-arg constructor), InstantiationException,
225
+ // IllegalAccessException, InvocationTargetException (the constructor threw).
226
+ Logger.e(TAG, "cannot create the listener $className ($key); skipped", e)
227
+ null
228
+ } catch (e: LinkageError) {
229
+ // NoClassDefFoundError (a missing dependency), ExceptionInInitializerError (a static initializer threw).
230
+ Logger.e(TAG, "cannot load the listener $className ($key); skipped", e)
231
+ null
232
+ }
233
+
234
+ private fun classNameOf(metaData: Bundle, key: String): String? {
235
+ @Suppress("DEPRECATION") // Bundle.get(String): the value type is not known in advance.
236
+ val value = metaData.get(key)
237
+ return (value as? String)?.trim()?.takeIf { it.isNotEmpty() }
238
+ }
239
+
240
+ private fun applicationInfo(context: Context): ApplicationInfo {
241
+ val pm = context.packageManager
242
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
243
+ pm.getApplicationInfo(
244
+ context.packageName,
245
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()),
246
+ )
247
+ } else {
248
+ @Suppress("DEPRECATION")
249
+ pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
250
+ }
251
+ }
252
+
253
+ // ---- tests
254
+
255
+ /** Test hook: cancels the subscriptions and forgets every listener (manifest and programmatic). */
256
+ @VisibleForTesting
257
+ fun resetForTests() {
258
+ synchronized(lock) {
259
+ connection.forEach { it.cancel() }
260
+ connection = emptyList()
261
+ manifestEntries = emptyList()
262
+ entries.forEach { it.active = false }
263
+ entries.clear()
264
+ }
265
+ }
266
+
267
+ /** Test hook: the number of registered listeners (manifest and programmatic). */
268
+ @VisibleForTesting
269
+ val listenerCount: Int get() = entries.size
270
+ }
@@ -0,0 +1,57 @@
1
+ package com.brickssoft.locationtracking.api
2
+
3
+ import androidx.annotation.VisibleForTesting
4
+ import com.brickssoft.locationtracking.core.Logger
5
+ import java.util.concurrent.CountDownLatch
6
+ import java.util.concurrent.LinkedBlockingQueue
7
+ import java.util.concurrent.RejectedExecutionException
8
+ import java.util.concurrent.ThreadPoolExecutor
9
+ import java.util.concurrent.TimeUnit
10
+
11
+ /**
12
+ * The one background thread of the companion native API, named [LocationTrackingNative.THREAD_NAME] (`LT-native`).
13
+ *
14
+ * Every [LocationTrackingListener] call and every [NativeCallback] runs here, one task at a time, in the order the
15
+ * tasks were posted. Because there is only one thread for all listeners and all callbacks, the order is global: a
16
+ * record posted before an event is delivered before that event, to every listener.
17
+ *
18
+ * The thread is a daemon thread (it never keeps the JVM alive) and exists for the whole process once created. The
19
+ * queue has no size limit, so nothing is dropped; a listener that blocks this thread delays every later delivery.
20
+ */
21
+ internal object NativeThread {
22
+ private const val TAG = "LT.Native"
23
+
24
+ private val executor = ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue()) { runnable ->
25
+ Thread(runnable, LocationTrackingNative.THREAD_NAME).apply { isDaemon = true }
26
+ }
27
+
28
+ /** Queue length at which (and at every further multiple of which) a backlog warning is logged. */
29
+ const val BACKLOG_WARNING = 1_000
30
+
31
+ /** Queues [task] for the `LT-native` thread. [task] must catch its own exceptions. */
32
+ fun post(task: Runnable) {
33
+ try {
34
+ executor.execute(task)
35
+ } catch (e: RejectedExecutionException) {
36
+ // Only possible if the executor was shut down, which this object never does.
37
+ Logger.e(TAG, "the ${LocationTrackingNative.THREAD_NAME} executor rejected a task", e)
38
+ return
39
+ }
40
+ val queued = executor.queue.size
41
+ if (queued >= BACKLOG_WARNING && queued % BACKLOG_WARNING == 0) {
42
+ // Nothing is dropped; this only makes a blocked listener or callback visible in the log.
43
+ Logger.w(TAG, "$queued tasks wait for the ${LocationTrackingNative.THREAD_NAME} thread: a native listener or callback is blocking it")
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Test hook: waits until every task queued before this call has run. Returns false on timeout. Must not be
49
+ * called from the `LT-native` thread itself (it would wait for itself).
50
+ */
51
+ @VisibleForTesting
52
+ fun awaitIdle(timeoutMs: Long = 15_000L): Boolean {
53
+ val done = CountDownLatch(1)
54
+ post { done.countDown() }
55
+ return done.await(timeoutMs, TimeUnit.MILLISECONDS)
56
+ }
57
+ }
@@ -0,0 +1,64 @@
1
+ package com.brickssoft.locationtracking.bridge
2
+
3
+ import com.brickssoft.locationtracking.config.ConfigStore
4
+ import com.brickssoft.locationtracking.core.AppDispatchers
5
+ import com.brickssoft.locationtracking.core.Components
6
+ import com.brickssoft.locationtracking.core.EventBus
7
+ import com.brickssoft.locationtracking.core.RecordHooks
8
+ import com.brickssoft.locationtracking.data.LocationStore
9
+ import com.brickssoft.locationtracking.device.DeviceInfoProvider
10
+ import com.brickssoft.locationtracking.device.DeviceMonitor
11
+ import com.brickssoft.locationtracking.engine.TrackingEngine
12
+ import com.brickssoft.locationtracking.geofence.GeofenceManager
13
+ import com.brickssoft.locationtracking.heartbeat.HeartbeatScheduler
14
+ import com.brickssoft.locationtracking.http.HttpSyncer
15
+ import com.brickssoft.locationtracking.logging.LogStore
16
+ import com.brickssoft.locationtracking.permission.PermissionManager
17
+ import com.brickssoft.locationtracking.position.PositionService
18
+ import com.brickssoft.locationtracking.processing.Odometer
19
+ import com.brickssoft.locationtracking.processing.RecordFactory
20
+ import com.brickssoft.locationtracking.settings.DeviceSettings
21
+
22
+ /** The components [PluginHandlers] delegates to. Tests implement it with the scaffold fakes. */
23
+ internal interface BridgeServices {
24
+ val engine: TrackingEngine
25
+ val positions: PositionService
26
+ val locationStore: LocationStore
27
+ val recordFactory: RecordFactory
28
+ val syncer: HttpSyncer
29
+ val geofences: GeofenceManager
30
+ val heartbeat: HeartbeatScheduler
31
+ val device: DeviceMonitor
32
+ val deviceInfo: DeviceInfoProvider
33
+ val deviceSettings: DeviceSettings
34
+ val permissions: PermissionManager
35
+ val logStore: LogStore
36
+ val odometer: Odometer
37
+ val configStore: ConfigStore
38
+ val events: EventBus
39
+ val dispatchers: AppDispatchers
40
+
41
+ /** Round 2: `insertLocation` dispatches its record here (unit 5). */
42
+ val recordHooks: RecordHooks
43
+ }
44
+
45
+ /** [BridgeServices] backed by [Components]; each component is resolved (and created) on first use only. */
46
+ internal class ComponentServices(private val components: Components) : BridgeServices {
47
+ override val engine: TrackingEngine get() = components.engine
48
+ override val positions: PositionService get() = components.positions
49
+ override val locationStore: LocationStore get() = components.locationStore
50
+ override val recordFactory: RecordFactory get() = components.recordFactory
51
+ override val syncer: HttpSyncer get() = components.syncer
52
+ override val geofences: GeofenceManager get() = components.geofences
53
+ override val heartbeat: HeartbeatScheduler get() = components.heartbeat
54
+ override val device: DeviceMonitor get() = components.device
55
+ override val deviceInfo: DeviceInfoProvider get() = components.deviceInfo
56
+ override val deviceSettings: DeviceSettings get() = components.deviceSettings
57
+ override val permissions: PermissionManager get() = components.permissions
58
+ override val logStore: LogStore get() = components.logStore
59
+ override val odometer: Odometer get() = components.odometer
60
+ override val configStore: ConfigStore get() = components.configStore
61
+ override val events: EventBus get() = components.events
62
+ override val dispatchers: AppDispatchers get() = components.dispatchers
63
+ override val recordHooks: RecordHooks get() = components.recordHooks
64
+ }