@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.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +107 -0
- package/android/consumer-rules.pro +42 -0
- package/android/src/main/AndroidManifest.xml +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
- package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
- package/android/src/main/res/values/lt_strings.xml +11 -0
- package/android/src/main/res/xml/lt_file_paths.xml +5 -0
- package/dist/esm/definitions.d.ts +719 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/events.d.ts +50 -0
- package/dist/esm/events.js +52 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +10 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/web/config.d.ts +19 -0
- package/dist/esm/web/config.js +243 -0
- package/dist/esm/web/config.js.map +1 -0
- package/dist/esm/web/device.d.ts +9 -0
- package/dist/esm/web/device.js +90 -0
- package/dist/esm/web/device.js.map +1 -0
- package/dist/esm/web/errors.d.ts +14 -0
- package/dist/esm/web/errors.js +19 -0
- package/dist/esm/web/errors.js.map +1 -0
- package/dist/esm/web/geolocation.d.ts +26 -0
- package/dist/esm/web/geolocation.js +117 -0
- package/dist/esm/web/geolocation.js.map +1 -0
- package/dist/esm/web/location.d.ts +7 -0
- package/dist/esm/web/location.js +70 -0
- package/dist/esm/web/location.js.map +1 -0
- package/dist/esm/web.d.ts +164 -0
- package/dist/esm/web.js +328 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +945 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +948 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +60 -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
|
+
}
|