@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.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +106 -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 +59 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import androidx.annotation.VisibleForTesting
|
|
6
|
+
import com.brickssoft.locationtracking.api.NativeListeners
|
|
7
|
+
import com.brickssoft.locationtracking.config.ConfigStore
|
|
8
|
+
import com.brickssoft.locationtracking.config.SharedPrefsConfigStore
|
|
9
|
+
import com.brickssoft.locationtracking.data.GeofenceStore
|
|
10
|
+
import com.brickssoft.locationtracking.data.LocationStore
|
|
11
|
+
import com.brickssoft.locationtracking.data.SqliteGeofenceStore
|
|
12
|
+
import com.brickssoft.locationtracking.data.SqliteLocationStore
|
|
13
|
+
import com.brickssoft.locationtracking.data.TrackingDatabase
|
|
14
|
+
import com.brickssoft.locationtracking.device.DefaultDeviceInfoProvider
|
|
15
|
+
import com.brickssoft.locationtracking.device.DefaultDeviceMonitor
|
|
16
|
+
import com.brickssoft.locationtracking.device.DeviceInfoProvider
|
|
17
|
+
import com.brickssoft.locationtracking.device.DeviceMonitor
|
|
18
|
+
import com.brickssoft.locationtracking.engine.DefaultTrackingEngine
|
|
19
|
+
import com.brickssoft.locationtracking.engine.TrackingEngine
|
|
20
|
+
import com.brickssoft.locationtracking.geofence.DefaultGeofenceManager
|
|
21
|
+
import com.brickssoft.locationtracking.geofence.GeofenceManager
|
|
22
|
+
import com.brickssoft.locationtracking.heartbeat.DefaultHeartbeatScheduler
|
|
23
|
+
import com.brickssoft.locationtracking.heartbeat.HeartbeatScheduler
|
|
24
|
+
import com.brickssoft.locationtracking.http.HttpSyncer
|
|
25
|
+
import com.brickssoft.locationtracking.http.OkHttpSyncer
|
|
26
|
+
import com.brickssoft.locationtracking.logging.FileLogger
|
|
27
|
+
import com.brickssoft.locationtracking.logging.LogStore
|
|
28
|
+
import com.brickssoft.locationtracking.permission.DefaultPermissionManager
|
|
29
|
+
import com.brickssoft.locationtracking.permission.PermissionManager
|
|
30
|
+
import com.brickssoft.locationtracking.position.DefaultPositionService
|
|
31
|
+
import com.brickssoft.locationtracking.position.PositionService
|
|
32
|
+
import com.brickssoft.locationtracking.processing.DefaultLocationProcessor
|
|
33
|
+
import com.brickssoft.locationtracking.processing.DefaultOdometer
|
|
34
|
+
import com.brickssoft.locationtracking.processing.DefaultRecordFactory
|
|
35
|
+
import com.brickssoft.locationtracking.processing.LocationProcessor
|
|
36
|
+
import com.brickssoft.locationtracking.processing.Odometer
|
|
37
|
+
import com.brickssoft.locationtracking.processing.RecordFactory
|
|
38
|
+
import com.brickssoft.locationtracking.provider.DefaultProviderFactory
|
|
39
|
+
import com.brickssoft.locationtracking.provider.ProviderFactory
|
|
40
|
+
import com.brickssoft.locationtracking.provider.StationaryRegionSink
|
|
41
|
+
import com.brickssoft.locationtracking.record.DefaultRecordSink
|
|
42
|
+
import com.brickssoft.locationtracking.record.RecordSink
|
|
43
|
+
import com.brickssoft.locationtracking.service.DefaultServiceController
|
|
44
|
+
import com.brickssoft.locationtracking.service.ServiceController
|
|
45
|
+
import com.brickssoft.locationtracking.settings.DefaultDeviceSettings
|
|
46
|
+
import com.brickssoft.locationtracking.settings.DeviceSettings
|
|
47
|
+
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
48
|
+
import kotlinx.coroutines.CoroutineScope
|
|
49
|
+
import kotlinx.coroutines.SupervisorJob
|
|
50
|
+
import kotlinx.coroutines.cancel
|
|
51
|
+
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
52
|
+
import kotlinx.coroutines.flow.map
|
|
53
|
+
import kotlinx.coroutines.launch
|
|
54
|
+
import okhttp3.OkHttpClient
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lazy service locator: the single place where every component is constructed. It fixes each unit's constructor
|
|
58
|
+
* signature. Dependency cycles are broken with [Lazy] parameters, which constructors must not touch.
|
|
59
|
+
*
|
|
60
|
+
* Obtain it with [Components.get] from any context (plugin, service, receivers).
|
|
61
|
+
*/
|
|
62
|
+
class Components private constructor(context: Context) {
|
|
63
|
+
/** The application context. */
|
|
64
|
+
val context: Context = context.applicationContext ?: context
|
|
65
|
+
|
|
66
|
+
val clock: Clock = SystemClockImpl(this.context)
|
|
67
|
+
val dispatchers: AppDispatchers = AppDispatchers.DEFAULT
|
|
68
|
+
val scope: CoroutineScope = CoroutineScope(
|
|
69
|
+
SupervisorJob() + dispatchers.engine + CoroutineExceptionHandler { _, t -> Logger.e("LT", "uncaught", t) },
|
|
70
|
+
)
|
|
71
|
+
val events: EventBus = SimpleEventBus()
|
|
72
|
+
|
|
73
|
+
/** Round 2: every queued record (sink and insertLocation) for the companion native API (unit 5). */
|
|
74
|
+
val recordHooks: RecordHooks = RecordHooks()
|
|
75
|
+
val http: OkHttpClient by lazy { OkHttpClient() }
|
|
76
|
+
|
|
77
|
+
// U17
|
|
78
|
+
val logStore: LogStore by lazy { FileLogger(this.context, clock, dispatchers, lazy { http }) }
|
|
79
|
+
|
|
80
|
+
// U3
|
|
81
|
+
val configStore: ConfigStore by lazy { SharedPrefsConfigStore(this.context, clock) }
|
|
82
|
+
|
|
83
|
+
// U15
|
|
84
|
+
val permissions: PermissionManager by lazy { DefaultPermissionManager(this.context) }
|
|
85
|
+
val deviceSettings: DeviceSettings by lazy { DefaultDeviceSettings(this.context) }
|
|
86
|
+
|
|
87
|
+
// U6
|
|
88
|
+
val providers: ProviderFactory by lazy { DefaultProviderFactory(this.context, configStore) }
|
|
89
|
+
|
|
90
|
+
// U14
|
|
91
|
+
val device: DeviceMonitor by lazy {
|
|
92
|
+
DefaultDeviceMonitor(
|
|
93
|
+
this.context, configStore, permissions, lazy { providers }, events, clock,
|
|
94
|
+
lazy { recordFactory }, lazy { recordSink }, scope,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
val deviceInfo: DeviceInfoProvider by lazy { DefaultDeviceInfoProvider(this.context, lazy { providers }) }
|
|
98
|
+
|
|
99
|
+
// U10
|
|
100
|
+
private val database by lazy { TrackingDatabase(this.context) }
|
|
101
|
+
val locationStore: LocationStore by lazy { SqliteLocationStore(database, configStore, clock, dispatchers) }
|
|
102
|
+
val geofenceStore: GeofenceStore by lazy { SqliteGeofenceStore(database, dispatchers) }
|
|
103
|
+
|
|
104
|
+
// U9
|
|
105
|
+
val processor: LocationProcessor by lazy { DefaultLocationProcessor(configStore) }
|
|
106
|
+
val odometer: Odometer by lazy { DefaultOdometer(configStore) }
|
|
107
|
+
val recordFactory: RecordFactory by lazy { DefaultRecordFactory(configStore, device, providers, clock) }
|
|
108
|
+
|
|
109
|
+
// U11
|
|
110
|
+
val syncer: HttpSyncer by lazy {
|
|
111
|
+
OkHttpSyncer(configStore, locationStore, device, events, clock, http, dispatchers, scope)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// U12
|
|
115
|
+
val heartbeat: HeartbeatScheduler by lazy {
|
|
116
|
+
DefaultHeartbeatScheduler(
|
|
117
|
+
this.context, configStore, device, providers, locationStore, recordFactory, lazy { recordSink }, clock, scope,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// SCAFFOLD
|
|
122
|
+
val recordSink: RecordSink by lazy {
|
|
123
|
+
DefaultRecordSink(locationStore, configStore, heartbeat, syncer, events, recordHooks)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// U13 (round 2: STATIONARY_REGION_ID transitions go to the engine)
|
|
127
|
+
val geofences: GeofenceManager by lazy {
|
|
128
|
+
DefaultGeofenceManager(
|
|
129
|
+
geofenceStore, providers, configStore, recordFactory, recordSink, events, clock, scope,
|
|
130
|
+
lazy { stationarySink },
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// U16
|
|
135
|
+
val positions: PositionService by lazy {
|
|
136
|
+
DefaultPositionService(providers, configStore, permissions, device, recordFactory, recordSink, clock, scope)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// U8
|
|
140
|
+
val serviceController: ServiceController by lazy { DefaultServiceController(this.context, configStore, events, clock) }
|
|
141
|
+
|
|
142
|
+
// U7
|
|
143
|
+
val engine: TrackingEngine by lazy {
|
|
144
|
+
DefaultTrackingEngine(
|
|
145
|
+
configStore, providers, processor, odometer, recordFactory, recordSink,
|
|
146
|
+
heartbeat, geofences, serviceController, device, syncer, permissions, events, clock, scope,
|
|
147
|
+
ForceStopProbe.system(this.context),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Round 2: the engine receives the transitions of its stationary region (unit 2). */
|
|
152
|
+
val stationarySink: StationaryRegionSink get() = engine
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Runs once, right after construction: installs the file logger, keeps its level in sync with config and (round 2)
|
|
156
|
+
* installs the native companion listeners before any component can emit.
|
|
157
|
+
*/
|
|
158
|
+
private fun bootstrap() {
|
|
159
|
+
Logger.sink = logStore
|
|
160
|
+
val store = configStore
|
|
161
|
+
val initial = store.config.value.logger
|
|
162
|
+
logStore.configure(initial.logLevel, initial.logMaxDays)
|
|
163
|
+
scope.launch {
|
|
164
|
+
store.config
|
|
165
|
+
.map { it.logger }
|
|
166
|
+
.distinctUntilChanged()
|
|
167
|
+
.collect { logStore.configure(it.logLevel, it.logMaxDays) }
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
NativeListeners.install(context, this)
|
|
171
|
+
} catch (e: Exception) {
|
|
172
|
+
Logger.e(Constants.TAG, "failed to install native listeners", e)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
companion object {
|
|
177
|
+
@SuppressLint("StaticFieldLeak") // holds the application context only
|
|
178
|
+
@Volatile
|
|
179
|
+
private var instance: Components? = null
|
|
180
|
+
|
|
181
|
+
/** The instance whose [bootstrap] is running; only read and written while holding the lock. */
|
|
182
|
+
@SuppressLint("StaticFieldLeak") // holds the application context only
|
|
183
|
+
private var constructing: Components? = null
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Thread-safe; always uses the application context.
|
|
187
|
+
*
|
|
188
|
+
* The instance is published only after [bootstrap] has run, so no other thread can use it (and emit a record)
|
|
189
|
+
* before the manifest-declared native listeners are installed. Other threads wait on the lock meanwhile; a
|
|
190
|
+
* nested call on the constructing thread (the lock is reentrant) gets the instance under construction.
|
|
191
|
+
*/
|
|
192
|
+
fun get(context: Context): Components {
|
|
193
|
+
instance?.let { return it }
|
|
194
|
+
return synchronized(this) {
|
|
195
|
+
instance ?: constructing ?: Components(context).also {
|
|
196
|
+
constructing = it
|
|
197
|
+
try {
|
|
198
|
+
it.bootstrap()
|
|
199
|
+
} finally {
|
|
200
|
+
constructing = null
|
|
201
|
+
instance = it
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Test hook: cancels the scope, detaches the log sink and forgets the instance. */
|
|
208
|
+
@VisibleForTesting
|
|
209
|
+
fun reset() {
|
|
210
|
+
synchronized(this) {
|
|
211
|
+
instance?.scope?.cancel()
|
|
212
|
+
instance = null
|
|
213
|
+
Logger.sink = null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.os.Build
|
|
5
|
+
|
|
6
|
+
/** Shared names, ids and PendingIntent request codes. */
|
|
7
|
+
object Constants {
|
|
8
|
+
/** Tag prefix for [Logger]. */
|
|
9
|
+
const val TAG = "LT"
|
|
10
|
+
|
|
11
|
+
const val PREFS_NAME = "location_tracking_prefs"
|
|
12
|
+
const val DATABASE_NAME = "location_tracking.db"
|
|
13
|
+
const val LOG_DIR = "location-tracking-logs"
|
|
14
|
+
|
|
15
|
+
/** Authority suffix of `LogFileProvider`: `${applicationId}.locationtracking.logs`. */
|
|
16
|
+
const val LOG_FILE_PROVIDER_SUFFIX = ".locationtracking.logs"
|
|
17
|
+
|
|
18
|
+
/** Maximum number of registered geofences. */
|
|
19
|
+
const val MAX_GEOFENCES = 100
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Id of the engine's own OS geofence while STATIONARY (GPS off): a circle around the stationary anchor whose EXIT
|
|
23
|
+
* wakes the engine. It is never stored, never counted against [MAX_GEOFENCES], never recorded and never emitted;
|
|
24
|
+
* `DefaultGeofenceManager` routes its transitions to `StationaryRegionSink` (architecture round 2, §3).
|
|
25
|
+
*/
|
|
26
|
+
const val STATIONARY_REGION_ID = "__lt_stationary__"
|
|
27
|
+
|
|
28
|
+
/** Maximum number of notification action buttons. */
|
|
29
|
+
const val MAX_NOTIFICATION_ACTIONS = 3
|
|
30
|
+
|
|
31
|
+
const val NOTIFICATION_ID = 7301
|
|
32
|
+
|
|
33
|
+
/** The resume notification (`notification.resume`) and its channel. */
|
|
34
|
+
const val RESUME_NOTIFICATION_ID = 7302
|
|
35
|
+
const val RESUME_CHANNEL_ID = "location_tracking_resume"
|
|
36
|
+
|
|
37
|
+
// PendingIntent request codes
|
|
38
|
+
const val RC_HEARTBEAT = 7310
|
|
39
|
+
const val RC_GMS_ACTIVITY = 7320
|
|
40
|
+
const val RC_GMS_GEOFENCE = 7321
|
|
41
|
+
const val RC_HMS_ACTIVITY = 7330
|
|
42
|
+
const val RC_HMS_GEOFENCE = 7331
|
|
43
|
+
const val RC_NOTIFICATION_CONTENT = 7349
|
|
44
|
+
const val RC_RESUME_NOTIFICATION = 7348
|
|
45
|
+
|
|
46
|
+
/** Notification action `i` uses `RC_NOTIFICATION_ACTION_BASE + i`. */
|
|
47
|
+
const val RC_NOTIFICATION_ACTION_BASE = 7350
|
|
48
|
+
|
|
49
|
+
/** Android proximity alerts; told apart by the data URI [geofenceUri]. */
|
|
50
|
+
const val RC_ANDROID_PROXIMITY = 7400
|
|
51
|
+
|
|
52
|
+
/** Scheme of the data URI that identifies an Android proximity-alert PendingIntent. */
|
|
53
|
+
const val GEOFENCE_URI_SCHEME = "lt-geofence"
|
|
54
|
+
|
|
55
|
+
// Intent actions / extras
|
|
56
|
+
const val ACTION_HEARTBEAT = "com.brickssoft.locationtracking.HEARTBEAT"
|
|
57
|
+
const val ACTION_NOTIFICATION_ACTION = "com.brickssoft.locationtracking.NOTIFICATION_ACTION"
|
|
58
|
+
const val ACTION_GEOFENCE = "com.brickssoft.locationtracking.GEOFENCE"
|
|
59
|
+
const val ACTION_ACTIVITY = "com.brickssoft.locationtracking.ACTIVITY"
|
|
60
|
+
const val EXTRA_ACTION_ID = "com.brickssoft.locationtracking.EXTRA_ACTION_ID"
|
|
61
|
+
|
|
62
|
+
/** `lt-geofence://<id>` */
|
|
63
|
+
fun geofenceUri(id: String): String = "$GEOFENCE_URI_SCHEME://${android.net.Uri.encode(id)}"
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Flags for PendingIntents the OS must fill in (GMS/HMS activity and geofence, proximity alerts):
|
|
67
|
+
* `FLAG_UPDATE_CURRENT | FLAG_MUTABLE` on API 31+.
|
|
68
|
+
*/
|
|
69
|
+
fun piMutable(): Int =
|
|
70
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
71
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
72
|
+
} else {
|
|
73
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Flags for alarm, notification and action PendingIntents: `FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE`. */
|
|
77
|
+
fun piImmutable(): Int = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
/** Rejection codes; `name` is the JS `code` of a failed promise. */
|
|
4
|
+
enum class ErrorCode {
|
|
5
|
+
NOT_READY,
|
|
6
|
+
PERMISSION_DENIED,
|
|
7
|
+
LOCATION_DISABLED,
|
|
8
|
+
TIMEOUT,
|
|
9
|
+
UNAVAILABLE,
|
|
10
|
+
INVALID_ARGUMENT,
|
|
11
|
+
NOT_FOUND,
|
|
12
|
+
NO_URL,
|
|
13
|
+
HTTP_ERROR,
|
|
14
|
+
NETWORK_ERROR,
|
|
15
|
+
TOO_MANY_GEOFENCES,
|
|
16
|
+
NO_ACTIVITY,
|
|
17
|
+
IO_ERROR,
|
|
18
|
+
UNIMPLEMENTED,
|
|
19
|
+
INTERNAL,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The only exception type that crosses component boundaries; the bridge maps it to `call.reject(message, code.name)`. */
|
|
23
|
+
class TrackingException(
|
|
24
|
+
val code: ErrorCode,
|
|
25
|
+
message: String,
|
|
26
|
+
cause: Throwable? = null,
|
|
27
|
+
) : Exception(message, cause)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
4
|
+
|
|
5
|
+
/** Handle returned by [EventBus.subscribe]. */
|
|
6
|
+
fun interface Subscription {
|
|
7
|
+
fun cancel()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** In-process event bus. Delivery is synchronous, on the emitter's thread. */
|
|
11
|
+
interface EventBus {
|
|
12
|
+
fun emit(event: TrackingEvent)
|
|
13
|
+
|
|
14
|
+
fun subscribe(listener: (TrackingEvent) -> Unit): Subscription
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Default [EventBus]. A throwing listener is logged and does not affect other listeners. */
|
|
18
|
+
class SimpleEventBus : EventBus {
|
|
19
|
+
private val listeners = CopyOnWriteArrayList<(TrackingEvent) -> Unit>()
|
|
20
|
+
|
|
21
|
+
override fun emit(event: TrackingEvent) {
|
|
22
|
+
for (listener in listeners) {
|
|
23
|
+
try {
|
|
24
|
+
listener(event)
|
|
25
|
+
} catch (e: Exception) {
|
|
26
|
+
Logger.e(TAG, "event listener failed for ${event::class.simpleName}", e)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun subscribe(listener: (TrackingEvent) -> Unit): Subscription {
|
|
32
|
+
listeners.add(listener)
|
|
33
|
+
return Subscription { listeners.remove(listener) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private companion object {
|
|
37
|
+
const val TAG = "LT.EventBus"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import android.app.ActivityManager
|
|
4
|
+
import android.app.ApplicationExitInfo
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.os.Build
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tells whether this process started after the user force-stopped the app (Settings → Force stop, `am force-stop`).
|
|
10
|
+
*
|
|
11
|
+
* Android cancels the app's alarms and does not restart its services after a force stop, so tracking stays off until
|
|
12
|
+
* the user opens the app again. A broadcast that was already on its way can still start the process (on the CI
|
|
13
|
+
* emulator a Play services activity update arrived 0.5 s after `am force-stop`, scenario P-L04). The engine asks this
|
|
14
|
+
* probe before a background trigger (activity update, stationary region EXIT) restores tracking.
|
|
15
|
+
*/
|
|
16
|
+
fun interface ForceStopProbe {
|
|
17
|
+
/** True when the previous process of this app ended with a user force stop. */
|
|
18
|
+
fun startedAfterForceStop(): Boolean
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
private const val TAG = "LT.ForceStop"
|
|
22
|
+
|
|
23
|
+
/** Exit records read to find the main process's newest one (other processes of the app leave records too). */
|
|
24
|
+
private const val EXIT_RECORDS = 16
|
|
25
|
+
|
|
26
|
+
/** Never reports a force stop (tests, and Android 10 and older). */
|
|
27
|
+
val NEVER: ForceStopProbe = ForceStopProbe { false }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads `ActivityManager.getHistoricalProcessExitReasons` (Android 11+, no permission needed for the own
|
|
31
|
+
* package) once per process: the newest exit record of the app's main process is the previous process.
|
|
32
|
+
* Records of other processes are skipped: the WebView's isolated renderer runs under the app, and a force stop
|
|
33
|
+
* kills it a few milliseconds after the main process ("isolated not needed"), so its record can be the newest
|
|
34
|
+
* one (CI emulator, API 34, scenario P-L04: reading only the newest record missed the force stop).
|
|
35
|
+
*/
|
|
36
|
+
fun system(context: Context): ForceStopProbe {
|
|
37
|
+
val app = context.applicationContext ?: context
|
|
38
|
+
val result by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { lastExitWasForceStop(app) }
|
|
39
|
+
return ForceStopProbe { result }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun lastExitWasForceStop(context: Context): Boolean {
|
|
43
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false
|
|
44
|
+
return try {
|
|
45
|
+
val manager = context.getSystemService(ActivityManager::class.java) ?: return false
|
|
46
|
+
val mainProcess = context.applicationInfo?.processName ?: context.packageName
|
|
47
|
+
val previous = manager.getHistoricalProcessExitReasons(context.packageName, 0, EXIT_RECORDS)
|
|
48
|
+
.firstOrNull { it.processName == mainProcess }
|
|
49
|
+
?: return false
|
|
50
|
+
val forceStopped = previous.reason == ApplicationExitInfo.REASON_USER_REQUESTED
|
|
51
|
+
Logger.i(
|
|
52
|
+
TAG,
|
|
53
|
+
"previous process ${previous.pid} exited with reason ${previous.reason} " +
|
|
54
|
+
"(${previous.description ?: "no description"}); force stop: $forceStopped",
|
|
55
|
+
)
|
|
56
|
+
forceStopped
|
|
57
|
+
} catch (e: Exception) {
|
|
58
|
+
Logger.w(TAG, "the previous process's exit reason is unknown", e)
|
|
59
|
+
false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import java.text.ParsePosition
|
|
4
|
+
import java.text.SimpleDateFormat
|
|
5
|
+
import java.util.Date
|
|
6
|
+
import java.util.Locale
|
|
7
|
+
import java.util.TimeZone
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ISO-8601 formatting and parsing for API 24+ (no `java.time`).
|
|
11
|
+
* The output format is always `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` in UTC.
|
|
12
|
+
*/
|
|
13
|
+
object Iso8601 {
|
|
14
|
+
private const val PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
|
15
|
+
|
|
16
|
+
private val formatter = threadLocalFormat(PATTERN)
|
|
17
|
+
|
|
18
|
+
/** Accepted input patterns, tried in order (UTC 'Z' with/without millis, numeric offsets). */
|
|
19
|
+
private val parsers = listOf(
|
|
20
|
+
threadLocalFormat(PATTERN),
|
|
21
|
+
threadLocalFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
|
22
|
+
threadLocalFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"),
|
|
23
|
+
threadLocalFormat("yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
private fun threadLocalFormat(pattern: String): ThreadLocal<SimpleDateFormat> =
|
|
27
|
+
object : ThreadLocal<SimpleDateFormat>() {
|
|
28
|
+
override fun initialValue(): SimpleDateFormat = SimpleDateFormat(pattern, Locale.US).apply {
|
|
29
|
+
timeZone = TimeZone.getTimeZone("UTC")
|
|
30
|
+
isLenient = false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Formats epoch ms as `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`. */
|
|
35
|
+
fun format(epochMs: Long): String = formatter.get()!!.format(Date(epochMs))
|
|
36
|
+
|
|
37
|
+
/** [format], or null for null input. */
|
|
38
|
+
fun formatOrNull(epochMs: Long?): String? = epochMs?.let { format(it) }
|
|
39
|
+
|
|
40
|
+
/** Parses an ISO-8601 timestamp to epoch ms, or returns null if it is not valid. */
|
|
41
|
+
fun parse(text: String?): Long? {
|
|
42
|
+
if (text.isNullOrBlank()) return null
|
|
43
|
+
val trimmed = normalizeFraction(text.trim())
|
|
44
|
+
for (parser in parsers) {
|
|
45
|
+
val position = ParsePosition(0)
|
|
46
|
+
val date = parser.get()!!.parse(trimmed, position)
|
|
47
|
+
if (date != null && position.index == trimmed.length) return date.time
|
|
48
|
+
}
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Pads or truncates the fractional seconds to exactly 3 digits so SimpleDateFormat parses them as ms. */
|
|
53
|
+
private fun normalizeFraction(text: String): String {
|
|
54
|
+
val dot = text.indexOf('.', startIndex = 19)
|
|
55
|
+
if (dot != 19) return text
|
|
56
|
+
var end = dot + 1
|
|
57
|
+
while (end < text.length && text[end].isDigit()) end++
|
|
58
|
+
val digits = text.substring(dot + 1, end)
|
|
59
|
+
if (digits.isEmpty()) return text
|
|
60
|
+
val ms = digits.padEnd(3, '0').substring(0, 3)
|
|
61
|
+
return text.substring(0, dot + 1) + ms + text.substring(end)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/** Log levels, ordered from least to most verbose. [wire] is the JS `LogLevel` value. */
|
|
6
|
+
enum class LogLevel {
|
|
7
|
+
OFF,
|
|
8
|
+
ERROR,
|
|
9
|
+
WARN,
|
|
10
|
+
INFO,
|
|
11
|
+
DEBUG,
|
|
12
|
+
VERBOSE,
|
|
13
|
+
;
|
|
14
|
+
|
|
15
|
+
val wire: String get() = name.lowercase()
|
|
16
|
+
|
|
17
|
+
/** True if a message at [level] passes a filter set to this level. */
|
|
18
|
+
fun allows(level: LogLevel): Boolean = level != OFF && this != OFF && level.ordinal <= ordinal
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
fun fromWire(value: String?): LogLevel? = entries.firstOrNull { it.wire.equals(value, ignoreCase = true) }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Destination for log lines besides logcat (the file logger). */
|
|
26
|
+
interface LogSink {
|
|
27
|
+
fun write(level: LogLevel, tag: String, message: String, error: Throwable?)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Global logger. Writes every message to logcat and to [sink] (set by `Components.bootstrap()`).
|
|
32
|
+
* Never throws: failures of the sink or of logcat are swallowed.
|
|
33
|
+
*/
|
|
34
|
+
object Logger {
|
|
35
|
+
@Volatile
|
|
36
|
+
var sink: LogSink? = null
|
|
37
|
+
|
|
38
|
+
fun e(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.ERROR, tag, msg, t)
|
|
39
|
+
|
|
40
|
+
fun w(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.WARN, tag, msg, t)
|
|
41
|
+
|
|
42
|
+
fun i(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.INFO, tag, msg, t)
|
|
43
|
+
|
|
44
|
+
fun d(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.DEBUG, tag, msg, t)
|
|
45
|
+
|
|
46
|
+
fun v(tag: String, msg: String, t: Throwable? = null) = log(LogLevel.VERBOSE, tag, msg, t)
|
|
47
|
+
|
|
48
|
+
fun log(level: LogLevel, tag: String, msg: String, t: Throwable? = null) {
|
|
49
|
+
if (level == LogLevel.OFF) return
|
|
50
|
+
try {
|
|
51
|
+
when (level) {
|
|
52
|
+
LogLevel.ERROR -> Log.e(tag, msg, t)
|
|
53
|
+
LogLevel.WARN -> Log.w(tag, msg, t)
|
|
54
|
+
LogLevel.INFO -> Log.i(tag, msg, t)
|
|
55
|
+
LogLevel.DEBUG -> Log.d(tag, msg, t)
|
|
56
|
+
LogLevel.VERBOSE -> Log.v(tag, msg, t)
|
|
57
|
+
LogLevel.OFF -> Unit
|
|
58
|
+
}
|
|
59
|
+
} catch (_: Throwable) {
|
|
60
|
+
// logcat unavailable (plain JVM); ignore
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
sink?.write(level, tag, msg, t)
|
|
64
|
+
} catch (_: Throwable) {
|
|
65
|
+
// a broken sink must never break the caller
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.model.Record
|
|
4
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
5
|
+
|
|
6
|
+
/** Observes every record queued for upload. See [RecordHooks]. */
|
|
7
|
+
fun interface RecordObserver {
|
|
8
|
+
fun onRecordQueued(record: Record)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fan-out of every queued record to in-process observers (SCAFFOLD, round 2). The companion native API
|
|
13
|
+
* (`api/NativeListeners`, unit 5) registers the observer that forwards records to `LocationTrackingListener.onRecord`.
|
|
14
|
+
*
|
|
15
|
+
* Callers (unit 5 adds the calls):
|
|
16
|
+
* - `DefaultRecordSink.submit`, right after the insert (also when the insert failed: a companion listener keeps its
|
|
17
|
+
* own audit trail, so a local database failure must not hide the record from it);
|
|
18
|
+
* - the bridge's `insertLocation`, right after its insert (inserted records bypass the sink).
|
|
19
|
+
*
|
|
20
|
+
* [dispatch] is synchronous on the caller's thread (the engine or an I/O coroutine), so observers must not block: they
|
|
21
|
+
* hand the record off to their own thread. A throwing observer is logged and does not affect the others.
|
|
22
|
+
*/
|
|
23
|
+
class RecordHooks {
|
|
24
|
+
private val observers = CopyOnWriteArrayList<RecordObserver>()
|
|
25
|
+
|
|
26
|
+
fun add(observer: RecordObserver): Subscription {
|
|
27
|
+
observers.add(observer)
|
|
28
|
+
return Subscription { observers.remove(observer) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when at least one observer is registered (lets callers skip work). */
|
|
32
|
+
val hasObservers: Boolean get() = observers.isNotEmpty()
|
|
33
|
+
|
|
34
|
+
fun dispatch(record: Record) {
|
|
35
|
+
for (observer in observers) {
|
|
36
|
+
try {
|
|
37
|
+
observer.onRecordQueued(record)
|
|
38
|
+
} catch (e: Exception) {
|
|
39
|
+
Logger.e(TAG, "record observer failed for ${record.event.wire} ${record.uuid}", e)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private companion object {
|
|
45
|
+
const val TAG = "LT.RecordHooks"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package com.brickssoft.locationtracking.core
|
|
2
|
+
|
|
3
|
+
import com.brickssoft.locationtracking.model.ActivitySample
|
|
4
|
+
import com.brickssoft.locationtracking.model.Connectivity
|
|
5
|
+
import com.brickssoft.locationtracking.model.GeofenceAction
|
|
6
|
+
import com.brickssoft.locationtracking.model.GeofenceSpec
|
|
7
|
+
import com.brickssoft.locationtracking.model.HttpResult
|
|
8
|
+
import com.brickssoft.locationtracking.model.ProviderState
|
|
9
|
+
import com.brickssoft.locationtracking.model.Record
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Events forwarded to JS. The JS event name and payload are built by
|
|
13
|
+
* [com.brickssoft.locationtracking.model.EventJson].
|
|
14
|
+
*/
|
|
15
|
+
sealed interface TrackingEvent {
|
|
16
|
+
/** "location" */
|
|
17
|
+
data class Location(val record: Record) : TrackingEvent
|
|
18
|
+
|
|
19
|
+
/** "motionchange" */
|
|
20
|
+
data class MotionChange(val isMoving: Boolean, val record: Record) : TrackingEvent
|
|
21
|
+
|
|
22
|
+
/** "activitychange" */
|
|
23
|
+
data class ActivityChange(val activity: ActivitySample) : TrackingEvent
|
|
24
|
+
|
|
25
|
+
/** "providerchange" */
|
|
26
|
+
data class ProviderChange(val state: ProviderState) : TrackingEvent
|
|
27
|
+
|
|
28
|
+
/** "heartbeat" */
|
|
29
|
+
data class Heartbeat(val record: Record) : TrackingEvent
|
|
30
|
+
|
|
31
|
+
/** "geofence"; [extras] is JSON object text. */
|
|
32
|
+
data class Geofence(
|
|
33
|
+
val identifier: String,
|
|
34
|
+
val action: GeofenceAction,
|
|
35
|
+
val record: Record,
|
|
36
|
+
val extras: String?,
|
|
37
|
+
) : TrackingEvent
|
|
38
|
+
|
|
39
|
+
/** "geofenceschange" */
|
|
40
|
+
data class GeofencesChange(val on: List<GeofenceSpec>, val off: List<String>) : TrackingEvent
|
|
41
|
+
|
|
42
|
+
/** "http" */
|
|
43
|
+
data class Http(val result: HttpResult) : TrackingEvent
|
|
44
|
+
|
|
45
|
+
/** "connectivitychange" */
|
|
46
|
+
data class ConnectivityChange(val connectivity: Connectivity) : TrackingEvent
|
|
47
|
+
|
|
48
|
+
/** "powersavechange" */
|
|
49
|
+
data class PowerSaveChange(val isPowerSaveMode: Boolean) : TrackingEvent
|
|
50
|
+
|
|
51
|
+
/** "enabledchange" */
|
|
52
|
+
data class EnabledChange(val enabled: Boolean) : TrackingEvent
|
|
53
|
+
|
|
54
|
+
/** "notificationaction" */
|
|
55
|
+
data class NotificationAction(val id: String) : TrackingEvent
|
|
56
|
+
|
|
57
|
+
/** "authorization"; [responseJson] is JSON object text. */
|
|
58
|
+
data class Authorization(
|
|
59
|
+
val success: Boolean,
|
|
60
|
+
val status: Int,
|
|
61
|
+
val error: String?,
|
|
62
|
+
val responseJson: String?,
|
|
63
|
+
) : TrackingEvent
|
|
64
|
+
}
|