@kafitra/react-native-live-tracking 0.1.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 +396 -0
- package/android/build.gradle +71 -0
- package/android/gradle.properties +7 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/livetracking/LiveTrackingModuleImpl.kt +728 -0
- package/android/src/main/java/com/livetracking/LiveTrackingPackage.kt +16 -0
- package/android/src/main/java/com/livetracking/location/LocationEngine.kt +93 -0
- package/android/src/main/java/com/livetracking/network/NetworkListener.kt +127 -0
- package/android/src/main/java/com/livetracking/optimizer/ActivityRecognitionHandler.kt +248 -0
- package/android/src/main/java/com/livetracking/optimizer/MotionSleepManager.kt +130 -0
- package/android/src/main/java/com/livetracking/permissions/PermissionHandler.kt +145 -0
- package/android/src/main/java/com/livetracking/queue/QueueEngine.kt +167 -0
- package/android/src/main/java/com/livetracking/queue/QueuedLocation.kt +16 -0
- package/android/src/main/java/com/livetracking/queue/TrackingDatabase.kt +239 -0
- package/android/src/main/java/com/livetracking/receiver/BootReceiver.kt +53 -0
- package/android/src/main/java/com/livetracking/service/TrackingForegroundService.kt +145 -0
- package/android/src/main/java/com/livetracking/sync/FirebaseSyncEngine.kt +277 -0
- package/android/src/main/java/com/livetracking/sync/LocationDataPoint.kt +31 -0
- package/android/src/main/java/com/livetracking/sync/SyncEngineController.kt +220 -0
- package/android/src/main/java/com/livetracking/sync/SyncTargetConfig.kt +20 -0
- package/android/src/main/java/com/livetracking/sync/TargetHandler.kt +601 -0
- package/android/src/newarch/java/com/livetracking/LiveTrackingModule.kt +64 -0
- package/android/src/oldarch/java/com/livetracking/LiveTrackingModule.kt +70 -0
- package/android/src/test/java/com/livetracking/BackoffCalculationTest.kt +216 -0
- package/android/src/test/java/com/livetracking/BatchAccumulatorTest.kt +391 -0
- package/android/src/test/java/com/livetracking/BootReceiverTest.kt +247 -0
- package/android/src/test/java/com/livetracking/FirebaseSyncEngineTest.kt +337 -0
- package/android/src/test/java/com/livetracking/LocationEngineTest.kt +202 -0
- package/android/src/test/java/com/livetracking/MotionSleepManagerTest.kt +420 -0
- package/android/src/test/java/com/livetracking/OfflineQueueTest.kt +462 -0
- package/android/src/test/java/com/livetracking/PermissionHandlerTest.kt +200 -0
- package/android/src/test/java/com/livetracking/QueueEngineTest.kt +335 -0
- package/android/src/test/java/com/livetracking/SyncEngineControllerTest.kt +855 -0
- package/ios/ActivityRecognitionHandler.swift +196 -0
- package/ios/BackgroundModeHelper.swift +132 -0
- package/ios/FirebaseSyncEngine.swift +276 -0
- package/ios/LiveTracking-Bridging-Header.h +2 -0
- package/ios/LiveTracking.m +37 -0
- package/ios/LiveTracking.swift +773 -0
- package/ios/LocationDataPoint.swift +56 -0
- package/ios/LocationEngine.swift +160 -0
- package/ios/MotionSleepManager.swift +151 -0
- package/ios/NetworkListener.swift +105 -0
- package/ios/OfflineQueueManager.swift +503 -0
- package/ios/PermissionHandler.swift +148 -0
- package/ios/QueueEngine.swift +249 -0
- package/ios/SyncEngineController.swift +396 -0
- package/ios/SyncTargetConfig.swift +36 -0
- package/ios/TargetHandler.swift +715 -0
- package/ios/Tests/ActivityRecognitionHandlerTests.swift +259 -0
- package/ios/Tests/FirebaseSyncEngineTests.swift +303 -0
- package/ios/Tests/LocationEngineTests.swift +244 -0
- package/ios/Tests/MotionSleepManagerTests.swift +355 -0
- package/ios/Tests/NetworkListenerTests.swift +188 -0
- package/ios/Tests/OfflineQueueFlushTests.swift +375 -0
- package/ios/Tests/PermissionHandlerTests.swift +238 -0
- package/ios/Tests/QueueEngineTests.swift +346 -0
- package/ios/TrackingCleanup.swift +93 -0
- package/ios/TrackingNotificationManager.swift +187 -0
- package/lib/commonjs/EventEmitter.js +113 -0
- package/lib/commonjs/EventEmitter.js.map +1 -0
- package/lib/commonjs/LiveTracking.js +134 -0
- package/lib/commonjs/LiveTracking.js.map +1 -0
- package/lib/commonjs/NativeLiveTracking.js +21 -0
- package/lib/commonjs/NativeLiveTracking.js.map +1 -0
- package/lib/commonjs/filters/distanceTimeFilter.js +63 -0
- package/lib/commonjs/filters/distanceTimeFilter.js.map +1 -0
- package/lib/commonjs/index.js +103 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/serialization/locationSerializer.js +51 -0
- package/lib/commonjs/serialization/locationSerializer.js.map +1 -0
- package/lib/commonjs/types.js +77 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/distance.js +63 -0
- package/lib/commonjs/utils/distance.js.map +1 -0
- package/lib/commonjs/utils/retry.js +80 -0
- package/lib/commonjs/utils/retry.js.map +1 -0
- package/lib/commonjs/validation.js +463 -0
- package/lib/commonjs/validation.js.map +1 -0
- package/lib/module/EventEmitter.js +105 -0
- package/lib/module/EventEmitter.js.map +1 -0
- package/lib/module/LiveTracking.js +127 -0
- package/lib/module/LiveTracking.js.map +1 -0
- package/lib/module/NativeLiveTracking.js +16 -0
- package/lib/module/NativeLiveTracking.js.map +1 -0
- package/lib/module/filters/distanceTimeFilter.js +58 -0
- package/lib/module/filters/distanceTimeFilter.js.map +1 -0
- package/lib/module/index.js +32 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/serialization/locationSerializer.js +45 -0
- package/lib/module/serialization/locationSerializer.js.map +1 -0
- package/lib/module/types.js +94 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/distance.js +56 -0
- package/lib/module/utils/distance.js.map +1 -0
- package/lib/module/utils/retry.js +72 -0
- package/lib/module/utils/retry.js.map +1 -0
- package/lib/module/validation.js +456 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/EventEmitter.d.ts +65 -0
- package/lib/typescript/EventEmitter.d.ts.map +1 -0
- package/lib/typescript/LiveTracking.d.ts +23 -0
- package/lib/typescript/LiveTracking.d.ts.map +1 -0
- package/lib/typescript/NativeLiveTracking.d.ts +25 -0
- package/lib/typescript/NativeLiveTracking.d.ts.map +1 -0
- package/lib/typescript/filters/distanceTimeFilter.d.ts +44 -0
- package/lib/typescript/filters/distanceTimeFilter.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +21 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/serialization/locationSerializer.d.ts +39 -0
- package/lib/typescript/serialization/locationSerializer.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +217 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/utils/distance.d.ts +38 -0
- package/lib/typescript/utils/distance.d.ts.map +1 -0
- package/lib/typescript/utils/retry.d.ts +60 -0
- package/lib/typescript/utils/retry.d.ts.map +1 -0
- package/lib/typescript/validation.d.ts +26 -0
- package/lib/typescript/validation.d.ts.map +1 -0
- package/package.json +126 -0
- package/react-native-live-tracking.podspec +47 -0
- package/src/EventEmitter.ts +118 -0
- package/src/LiveTracking.ts +159 -0
- package/src/NativeLiveTracking.ts +29 -0
- package/src/filters/distanceTimeFilter.ts +75 -0
- package/src/index.ts +51 -0
- package/src/serialization/locationSerializer.ts +57 -0
- package/src/types.ts +252 -0
- package/src/utils/distance.ts +68 -0
- package/src/utils/retry.ts +75 -0
- package/src/validation.ts +552 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreLocation
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Main native module implementation for iOS.
|
|
6
|
+
* Extends RCTEventEmitter to support sending events to JavaScript.
|
|
7
|
+
* Supports both TurboModules (new architecture) and Bridge (legacy architecture).
|
|
8
|
+
*
|
|
9
|
+
* Wires together all engines:
|
|
10
|
+
* - LocationEngine: CLLocationManager wrapper for GPS updates
|
|
11
|
+
* - SyncEngineController: Multi-target Firebase sync with per-target batching, retry, and offline queue
|
|
12
|
+
* - OfflineQueueManager: CoreData offline queue for per-target persistence
|
|
13
|
+
* - NetworkListener: NWPathMonitor connectivity detection
|
|
14
|
+
* - ActivityRecognitionHandler: CMMotionActivityManager activity detection
|
|
15
|
+
* - MotionSleepManager: Sleep mode for battery optimization
|
|
16
|
+
* - PermissionHandler: Permission checks
|
|
17
|
+
* - BackgroundModeHelper: Significant location monitoring
|
|
18
|
+
* - TrackingCleanup: Stop/cleanup utility
|
|
19
|
+
*
|
|
20
|
+
* Requirements: 3.1, 4.4, 9.3, 9.4, 10.1, 10.2, 10.3, 10.4
|
|
21
|
+
*/
|
|
22
|
+
@objc(LiveTracking)
|
|
23
|
+
class LiveTracking: RCTEventEmitter {
|
|
24
|
+
|
|
25
|
+
// MARK: - Event Names
|
|
26
|
+
|
|
27
|
+
private static let EVENT_LOCATION_UPDATE = "onLocationUpdate"
|
|
28
|
+
private static let EVENT_TRACKING_ERROR = "onTrackingError"
|
|
29
|
+
private static let EVENT_QUEUE_OVERFLOW = "onQueueOverflow"
|
|
30
|
+
|
|
31
|
+
// MARK: - State
|
|
32
|
+
|
|
33
|
+
private enum TrackingState: String {
|
|
34
|
+
case idle
|
|
35
|
+
case configured
|
|
36
|
+
case tracking
|
|
37
|
+
case motionSleep = "motion_sleep"
|
|
38
|
+
case pausedGps = "paused_gps"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private var state: TrackingState = .idle
|
|
42
|
+
private var hasListeners: Bool = false
|
|
43
|
+
private var wasTrackingBeforeGpsDisabled: Bool = false
|
|
44
|
+
|
|
45
|
+
// MARK: - Engines
|
|
46
|
+
|
|
47
|
+
private var locationEngine: LocationEngine?
|
|
48
|
+
private var syncEngineController: SyncEngineController?
|
|
49
|
+
private var offlineQueueManager: OfflineQueueManager?
|
|
50
|
+
private var networkListener: NetworkListener?
|
|
51
|
+
private var activityRecognitionHandler: ActivityRecognitionHandler?
|
|
52
|
+
private var motionSleepManager: MotionSleepManager?
|
|
53
|
+
private var permissionHandler: PermissionHandler?
|
|
54
|
+
private var trackingCleanup: TrackingCleanup?
|
|
55
|
+
|
|
56
|
+
// MARK: - Configuration
|
|
57
|
+
|
|
58
|
+
private var intervalMs: Int = 10000
|
|
59
|
+
private var distanceFilterMeters: Double = 10.0
|
|
60
|
+
private var stopWhenStill: Bool = true
|
|
61
|
+
private var optimizationMode: String = "both"
|
|
62
|
+
private var iosNotificationEnabled: Bool = true
|
|
63
|
+
private var iosNotificationTitle: String?
|
|
64
|
+
private var iosNotificationText: String?
|
|
65
|
+
|
|
66
|
+
// MARK: - Error Codes
|
|
67
|
+
|
|
68
|
+
private static let ERROR_GPS_DISABLED = "GPS_DISABLED"
|
|
69
|
+
private static let ERROR_GPS_ENABLED = "GPS_ENABLED"
|
|
70
|
+
private static let ERROR_PERMISSION_REVOKED = "PERMISSION_REVOKED"
|
|
71
|
+
|
|
72
|
+
// MARK: - Tracking State
|
|
73
|
+
|
|
74
|
+
private var lastLocation: CLLocation?
|
|
75
|
+
private var lastUpdateTime: Date?
|
|
76
|
+
|
|
77
|
+
// MARK: - RCTEventEmitter Overrides
|
|
78
|
+
|
|
79
|
+
@objc
|
|
80
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override func supportedEvents() -> [String]! {
|
|
85
|
+
return [
|
|
86
|
+
LiveTracking.EVENT_LOCATION_UPDATE,
|
|
87
|
+
LiveTracking.EVENT_TRACKING_ERROR,
|
|
88
|
+
LiveTracking.EVENT_QUEUE_OVERFLOW
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override func startObserving() {
|
|
93
|
+
hasListeners = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override func stopObserving() {
|
|
97
|
+
// Do not disable event emission while tracking is active
|
|
98
|
+
if state == .tracking || state == .motionSleep {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
hasListeners = false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// MARK: - Public Methods (Exposed to JS)
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Configure the tracking module with a JSON config string.
|
|
108
|
+
* Parses the config and initializes all engines including SyncEngineController.
|
|
109
|
+
*
|
|
110
|
+
* Expected JSON structure:
|
|
111
|
+
* {
|
|
112
|
+
* "optimization": { "intervalMs": 10000, "distanceFilterMeters": 10, "stopWhenStill": true },
|
|
113
|
+
* "firebase": { "service": "RTDB"|"Firestore", "targets": [...] }
|
|
114
|
+
* }
|
|
115
|
+
*
|
|
116
|
+
* Requirements: 9.3, 9.4
|
|
117
|
+
*/
|
|
118
|
+
@objc
|
|
119
|
+
func configure(_ config: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
120
|
+
guard let data = config.data(using: .utf8) else {
|
|
121
|
+
reject("INVALID_CONFIG", "Configuration string is not valid UTF-8", nil)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
|
126
|
+
reject("INVALID_CONFIG", "Configuration string is not valid JSON", nil)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse firebase config (required)
|
|
131
|
+
guard let firebaseConfig = json["firebase"] as? [String: Any] else {
|
|
132
|
+
reject("INVALID_CONFIG", "Missing required field: firebase", nil)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
guard let service = firebaseConfig["service"] as? String,
|
|
137
|
+
(service == "RTDB" || service == "Firestore") else {
|
|
138
|
+
reject("INVALID_CONFIG", "Invalid or missing firebase.service. Must be 'RTDB' or 'Firestore'", nil)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate targets array exists
|
|
143
|
+
guard firebaseConfig["targets"] is [[String: Any]] else {
|
|
144
|
+
reject("INVALID_CONFIG", "Missing or invalid 'targets' array in firebase configuration", nil)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse optimization config (optional with defaults)
|
|
149
|
+
let optimizationConfig = json["optimization"] as? [String: Any]
|
|
150
|
+
|
|
151
|
+
let parsedIntervalMs = optimizationConfig?["intervalMs"] as? Int ?? 10000
|
|
152
|
+
let parsedDistanceFilter = optimizationConfig?["distanceFilterMeters"] as? Double
|
|
153
|
+
?? (optimizationConfig?["distanceFilterMeters"] as? Int).map { Double($0) }
|
|
154
|
+
?? 10.0
|
|
155
|
+
let parsedStopWhenStill = optimizationConfig?["stopWhenStill"] as? Bool ?? true
|
|
156
|
+
let parsedOptimizationMode = optimizationConfig?["mode"] as? String ?? "both"
|
|
157
|
+
|
|
158
|
+
// Validate values
|
|
159
|
+
if parsedIntervalMs <= 0 {
|
|
160
|
+
reject("INVALID_CONFIG", "optimization.intervalMs must be greater than 0", nil)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if parsedDistanceFilter <= 0 {
|
|
164
|
+
reject("INVALID_CONFIG", "optimization.distanceFilterMeters must be greater than 0", nil)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
let validModes = ["interval", "distance", "both"]
|
|
168
|
+
if !validModes.contains(parsedOptimizationMode) {
|
|
169
|
+
reject("INVALID_CONFIG", "optimization.mode must be 'interval', 'distance', or 'both'", nil)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Parse iosNotification config (optional)
|
|
174
|
+
if let iosNotificationConfig = json["iosNotification"] as? [String: Any] {
|
|
175
|
+
self.iosNotificationEnabled = iosNotificationConfig["enabled"] as? Bool ?? true
|
|
176
|
+
self.iosNotificationTitle = iosNotificationConfig["title"] as? String
|
|
177
|
+
self.iosNotificationText = iosNotificationConfig["text"] as? String
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Store configuration
|
|
181
|
+
self.intervalMs = parsedIntervalMs
|
|
182
|
+
self.distanceFilterMeters = parsedDistanceFilter
|
|
183
|
+
self.stopWhenStill = parsedStopWhenStill
|
|
184
|
+
self.optimizationMode = parsedOptimizationMode
|
|
185
|
+
|
|
186
|
+
// Initialize network listener first (needed by SyncEngineController)
|
|
187
|
+
self.networkListener = NetworkListener()
|
|
188
|
+
self.networkListener?.delegate = self
|
|
189
|
+
|
|
190
|
+
// Initialize offline queue manager
|
|
191
|
+
self.offlineQueueManager = OfflineQueueManager()
|
|
192
|
+
|
|
193
|
+
// Serialize the firebase config back to JSON for SyncEngineController
|
|
194
|
+
guard let firebaseJsonData = try? JSONSerialization.data(withJSONObject: firebaseConfig, options: []),
|
|
195
|
+
let firebaseJsonString = String(data: firebaseJsonData, encoding: .utf8) else {
|
|
196
|
+
reject("INVALID_CONFIG", "Failed to serialize firebase configuration", nil)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Initialize SyncEngineController with targets JSON
|
|
201
|
+
// Requirement 9.3: Deserialize JSON targets array and instantiate one write handler per SyncTarget
|
|
202
|
+
// Requirement 9.4: Reject with INVALID_CONFIG if targets JSON fails to parse
|
|
203
|
+
do {
|
|
204
|
+
self.syncEngineController = try SyncEngineController(
|
|
205
|
+
jsonString: firebaseJsonString,
|
|
206
|
+
networkStatus: self.networkListener!,
|
|
207
|
+
queueManager: self.offlineQueueManager!
|
|
208
|
+
)
|
|
209
|
+
self.syncEngineController?.delegate = self
|
|
210
|
+
} catch let error as SyncEngineError {
|
|
211
|
+
reject(error.errorCode, error.errorDescription ?? "Failed to initialize sync engine", nil)
|
|
212
|
+
return
|
|
213
|
+
} catch {
|
|
214
|
+
reject("INVALID_CONFIG", "Failed to initialize sync engine: \(error.localizedDescription)", nil)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Initialize remaining engines
|
|
219
|
+
self.locationEngine = LocationEngine()
|
|
220
|
+
self.locationEngine?.delegate = self
|
|
221
|
+
|
|
222
|
+
self.activityRecognitionHandler = ActivityRecognitionHandler()
|
|
223
|
+
self.activityRecognitionHandler?.stationaryThresholdMs = MotionSleepManager.STILL_THRESHOLD_MS
|
|
224
|
+
self.activityRecognitionHandler?.delegate = self
|
|
225
|
+
|
|
226
|
+
self.motionSleepManager = MotionSleepManager(
|
|
227
|
+
locationEngine: self.locationEngine!,
|
|
228
|
+
stopWhenStill: parsedStopWhenStill,
|
|
229
|
+
intervalMs: parsedIntervalMs,
|
|
230
|
+
distanceFilter: parsedDistanceFilter
|
|
231
|
+
)
|
|
232
|
+
self.motionSleepManager?.delegate = self
|
|
233
|
+
|
|
234
|
+
self.permissionHandler = PermissionHandler()
|
|
235
|
+
self.trackingCleanup = TrackingCleanup()
|
|
236
|
+
|
|
237
|
+
// Update state
|
|
238
|
+
self.state = .configured
|
|
239
|
+
|
|
240
|
+
resolve(nil)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Start location tracking.
|
|
245
|
+
* Checks permissions, starts location engine, activity recognition,
|
|
246
|
+
* network listener, and significant location monitoring.
|
|
247
|
+
*/
|
|
248
|
+
@objc
|
|
249
|
+
func start(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
250
|
+
guard state == .configured || state == .pausedGps else {
|
|
251
|
+
if state == .tracking || state == .motionSleep {
|
|
252
|
+
resolve(nil)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
reject("NOT_CONFIGURED", "Library must be configured before starting. Call configure() first.", nil)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
guard let permissionHandler = self.permissionHandler else {
|
|
260
|
+
reject("NOT_CONFIGURED", "Permission handler not initialized. Call configure() first.", nil)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check permissions
|
|
265
|
+
let permissionResult = permissionHandler.checkAllRequirements()
|
|
266
|
+
switch permissionResult {
|
|
267
|
+
case .granted:
|
|
268
|
+
// Log actual authorization level and upgrade to Always if needed
|
|
269
|
+
let authStatus = locationEngine?.getAuthorizationStatus() ?? .notDetermined
|
|
270
|
+
print("[LiveTracking] Permission granted — authorizationStatus: \(authStatus.rawValue) (3=whenInUse, 4=always)")
|
|
271
|
+
if authStatus == .authorizedWhenInUse {
|
|
272
|
+
print("[LiveTracking] Requesting Always authorization for background tracking...")
|
|
273
|
+
locationEngine?.requestAlwaysAuthorization()
|
|
274
|
+
}
|
|
275
|
+
break
|
|
276
|
+
case .denied(let errorCode, let message):
|
|
277
|
+
reject(errorCode, message, nil)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Show persistent notification if enabled and configured
|
|
282
|
+
if iosNotificationEnabled,
|
|
283
|
+
let title = iosNotificationTitle,
|
|
284
|
+
let text = iosNotificationText {
|
|
285
|
+
TrackingNotificationManager.shared.configure(title: title, body: text)
|
|
286
|
+
TrackingNotificationManager.shared.showTrackingNotification()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Ensure events are emitted to JS regardless of listener timing
|
|
290
|
+
hasListeners = true
|
|
291
|
+
|
|
292
|
+
// Start tracking engines
|
|
293
|
+
startTrackingEngines()
|
|
294
|
+
|
|
295
|
+
// Monitor app becoming active to detect GPS status changes
|
|
296
|
+
NotificationCenter.default.addObserver(
|
|
297
|
+
self,
|
|
298
|
+
selector: #selector(handleAppBecameActive),
|
|
299
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
300
|
+
object: nil
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
// Reset last update tracking
|
|
304
|
+
lastUpdateTime = nil
|
|
305
|
+
lastLocation = nil
|
|
306
|
+
|
|
307
|
+
// Update state
|
|
308
|
+
state = .tracking
|
|
309
|
+
|
|
310
|
+
// Flush any pending offline queues
|
|
311
|
+
syncEngineController?.flushOfflineQueues()
|
|
312
|
+
|
|
313
|
+
resolve(nil)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Stop location tracking.
|
|
318
|
+
* Flushes all partial batches via SyncEngineController, then stops all engines.
|
|
319
|
+
*
|
|
320
|
+
* Requirement 4.4: Flush all partially-filled batches before stop completes.
|
|
321
|
+
*/
|
|
322
|
+
@objc
|
|
323
|
+
func stop(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
324
|
+
guard state == .tracking || state == .motionSleep || state == .pausedGps else {
|
|
325
|
+
// Already stopped, resolve silently
|
|
326
|
+
resolve(nil)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Remove persistent notification
|
|
331
|
+
TrackingNotificationManager.shared.removeTrackingNotification()
|
|
332
|
+
|
|
333
|
+
// Flush all partial batches before stopping (Requirement 4.4)
|
|
334
|
+
syncEngineController?.flushAll { [weak self] in
|
|
335
|
+
guard let self = self else {
|
|
336
|
+
resolve(nil)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Stop tracking engines
|
|
341
|
+
self.stopTrackingEngines()
|
|
342
|
+
|
|
343
|
+
// Cleanup
|
|
344
|
+
self.trackingCleanup?.cleanup()
|
|
345
|
+
|
|
346
|
+
// Remove app lifecycle observer
|
|
347
|
+
NotificationCenter.default.removeObserver(
|
|
348
|
+
self,
|
|
349
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
350
|
+
object: nil
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// Reset state
|
|
354
|
+
self.lastLocation = nil
|
|
355
|
+
self.lastUpdateTime = nil
|
|
356
|
+
self.wasTrackingBeforeGpsDisabled = false
|
|
357
|
+
self.state = .configured
|
|
358
|
+
self.hasListeners = false
|
|
359
|
+
|
|
360
|
+
resolve(nil)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Start location engine, activity recognition, network listener, and significant location monitoring.
|
|
366
|
+
*/
|
|
367
|
+
private func startTrackingEngines() {
|
|
368
|
+
guard let locationEngine = self.locationEngine else { return }
|
|
369
|
+
|
|
370
|
+
// Start location engine
|
|
371
|
+
// When mode is 'interval', use kCLDistanceFilterNone so iOS OS delivers every GPS update.
|
|
372
|
+
// Distance filtering for 'distance'/'both' modes is handled at app level in shouldProcessLocation.
|
|
373
|
+
let clDistanceFilter: Double = (optimizationMode == "interval") ? kCLDistanceFilterNone : distanceFilterMeters
|
|
374
|
+
locationEngine.startLocationUpdates(intervalMs: intervalMs, distanceFilter: clDistanceFilter)
|
|
375
|
+
|
|
376
|
+
// Start activity recognition for motion sleep mode
|
|
377
|
+
activityRecognitionHandler?.startActivityRecognition()
|
|
378
|
+
|
|
379
|
+
// Start network listener for queue flush
|
|
380
|
+
networkListener?.startListening()
|
|
381
|
+
|
|
382
|
+
// Start significant location monitoring as fallback
|
|
383
|
+
let clManager = CLLocationManager()
|
|
384
|
+
if CLLocationManager.significantLocationChangeMonitoringAvailable() {
|
|
385
|
+
BackgroundModeHelper.shared.startSignificantLocationMonitoring(locationManager: clManager)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Mark tracking active
|
|
389
|
+
trackingCleanup?.markTrackingActive(locationManager: clManager)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Stop location engine, activity recognition, network listener, and significant location monitoring.
|
|
394
|
+
*/
|
|
395
|
+
private func stopTrackingEngines() {
|
|
396
|
+
// Stop location engine and significant location monitoring
|
|
397
|
+
if let locationEngine = self.locationEngine {
|
|
398
|
+
let clManager = CLLocationManager()
|
|
399
|
+
self.trackingCleanup?.stopAllTracking(
|
|
400
|
+
locationEngine: locationEngine,
|
|
401
|
+
backgroundHelper: BackgroundModeHelper.shared,
|
|
402
|
+
locationManager: clManager
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Stop activity recognition
|
|
407
|
+
self.activityRecognitionHandler?.stopActivityRecognition()
|
|
408
|
+
|
|
409
|
+
// Stop network listener
|
|
410
|
+
self.networkListener?.stopListening()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Called when the app becomes active. Used to detect GPS/location services status changes.
|
|
415
|
+
*/
|
|
416
|
+
@objc
|
|
417
|
+
private func handleAppBecameActive() {
|
|
418
|
+
checkGpsStatus()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Check current GPS/location services status and pause/resume tracking accordingly.
|
|
423
|
+
*/
|
|
424
|
+
private func checkGpsStatus() {
|
|
425
|
+
guard let permissionHandler = self.permissionHandler else { return }
|
|
426
|
+
|
|
427
|
+
let gpsEnabled: Bool
|
|
428
|
+
switch permissionHandler.checkLocationServicesEnabled() {
|
|
429
|
+
case .granted:
|
|
430
|
+
gpsEnabled = true
|
|
431
|
+
case .denied:
|
|
432
|
+
gpsEnabled = false
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if !gpsEnabled && (state == .tracking || state == .motionSleep) {
|
|
436
|
+
pauseTrackingDueToGps()
|
|
437
|
+
} else if gpsEnabled && state == .pausedGps && wasTrackingBeforeGpsDisabled {
|
|
438
|
+
resumeTrackingAfterGps()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Pause tracking when GPS/location services are disabled while tracking is active.
|
|
444
|
+
*/
|
|
445
|
+
private func pauseTrackingDueToGps() {
|
|
446
|
+
wasTrackingBeforeGpsDisabled = true
|
|
447
|
+
stopTrackingEngines()
|
|
448
|
+
state = .pausedGps
|
|
449
|
+
emitErrorEvent(
|
|
450
|
+
errorCode: LiveTracking.ERROR_GPS_DISABLED,
|
|
451
|
+
message: "GPS/Location services were disabled. Tracking paused and will resume automatically when GPS is enabled."
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resume tracking after GPS/location services are re-enabled.
|
|
457
|
+
*/
|
|
458
|
+
private func resumeTrackingAfterGps() {
|
|
459
|
+
guard let permissionHandler = self.permissionHandler else { return }
|
|
460
|
+
|
|
461
|
+
let permissionResult = permissionHandler.checkAllRequirements()
|
|
462
|
+
switch permissionResult {
|
|
463
|
+
case .granted:
|
|
464
|
+
startTrackingEngines()
|
|
465
|
+
state = .tracking
|
|
466
|
+
emitErrorEvent(
|
|
467
|
+
errorCode: LiveTracking.ERROR_GPS_ENABLED,
|
|
468
|
+
message: "GPS/Location services are enabled. Tracking resumed."
|
|
469
|
+
)
|
|
470
|
+
case .denied(let errorCode, let message):
|
|
471
|
+
state = .configured
|
|
472
|
+
wasTrackingBeforeGpsDisabled = false
|
|
473
|
+
emitErrorEvent(
|
|
474
|
+
errorCode: errorCode == PermissionHandler.ERROR_PERMISSION_DENIED
|
|
475
|
+
? LiveTracking.ERROR_PERMISSION_REVOKED
|
|
476
|
+
: errorCode,
|
|
477
|
+
message: errorCode == PermissionHandler.ERROR_PERMISSION_DENIED
|
|
478
|
+
? "Location permission was revoked while tracking was paused. Please grant permission to resume."
|
|
479
|
+
: message
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get the current tracking status as a JSON string.
|
|
486
|
+
* Returns state, isOnline, queuedLocations, lastLocation, batteryOptimization.
|
|
487
|
+
*/
|
|
488
|
+
@objc
|
|
489
|
+
func getStatus(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
490
|
+
let totalQueued = syncEngineController?.getTotalQueuedCount() ?? 0
|
|
491
|
+
|
|
492
|
+
var status: [String: Any] = [
|
|
493
|
+
"state": state.rawValue,
|
|
494
|
+
"isOnline": networkListener?.isOnline() ?? false,
|
|
495
|
+
"queuedLocations": totalQueued
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
// Battery optimization mode
|
|
499
|
+
let batteryOptimization: String
|
|
500
|
+
if !stopWhenStill {
|
|
501
|
+
batteryOptimization = "disabled"
|
|
502
|
+
} else if motionSleepManager?.isInSleepMode() == true {
|
|
503
|
+
batteryOptimization = "low_power"
|
|
504
|
+
} else {
|
|
505
|
+
batteryOptimization = "full_accuracy"
|
|
506
|
+
}
|
|
507
|
+
status["batteryOptimization"] = batteryOptimization
|
|
508
|
+
|
|
509
|
+
// Last location
|
|
510
|
+
if let location = lastLocation {
|
|
511
|
+
status["lastLocation"] = [
|
|
512
|
+
"latitude": location.coordinate.latitude,
|
|
513
|
+
"longitude": location.coordinate.longitude,
|
|
514
|
+
"timestamp": Int64(location.timestamp.timeIntervalSince1970 * 1000),
|
|
515
|
+
"accuracy": location.horizontalAccuracy,
|
|
516
|
+
"speed": location.speed >= 0 ? location.speed : NSNull(),
|
|
517
|
+
"altitude": location.altitude,
|
|
518
|
+
"bearing": location.course >= 0 ? location.course : NSNull()
|
|
519
|
+
] as [String: Any]
|
|
520
|
+
} else {
|
|
521
|
+
status["lastLocation"] = NSNull()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Serialize to JSON string
|
|
525
|
+
if let jsonData = try? JSONSerialization.data(withJSONObject: status, options: []),
|
|
526
|
+
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
527
|
+
resolve(jsonString)
|
|
528
|
+
} else {
|
|
529
|
+
resolve("{}")
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get the total number of queued locations waiting to be synced across all targets.
|
|
535
|
+
*
|
|
536
|
+
* Requirement 10.1: Total count across all targets.
|
|
537
|
+
* Requirement 10.4: Reject with NOT_CONFIGURED if called before configure().
|
|
538
|
+
*/
|
|
539
|
+
@objc
|
|
540
|
+
func getQueuedLocations(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
541
|
+
guard state != .idle, syncEngineController != nil else {
|
|
542
|
+
reject("NOT_CONFIGURED", "Library must be configured before querying queue status. Call configure() first.", nil)
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let count = syncEngineController?.getTotalQueuedCount() ?? 0
|
|
547
|
+
resolve(count)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get the queued location counts per target path as a JSON string.
|
|
552
|
+
* Returns a record mapping each configured target path to its queued location count.
|
|
553
|
+
* Targets with offlineQueue disabled report 0.
|
|
554
|
+
*
|
|
555
|
+
* Requirement 10.2: Per-target queue counts.
|
|
556
|
+
* Requirement 10.3: Non-queuing targets report 0.
|
|
557
|
+
* Requirement 10.4: Reject with NOT_CONFIGURED if called before configure().
|
|
558
|
+
*/
|
|
559
|
+
@objc
|
|
560
|
+
func getQueuedLocationsByTarget(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
561
|
+
guard state != .idle, let controller = syncEngineController else {
|
|
562
|
+
reject("NOT_CONFIGURED", "Library must be configured before querying queue status. Call configure() first.", nil)
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let counts = controller.getQueuedCounts()
|
|
567
|
+
|
|
568
|
+
// Serialize to JSON string for bridge transport
|
|
569
|
+
if let jsonData = try? JSONSerialization.data(withJSONObject: counts, options: []),
|
|
570
|
+
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
571
|
+
resolve(jsonString)
|
|
572
|
+
} else {
|
|
573
|
+
resolve("{}")
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// MARK: - Private Methods
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Apply distance/time filter to determine if a location update should be processed.
|
|
581
|
+
* The filtering strategy depends on optimizationMode:
|
|
582
|
+
* - 'interval': time elapsed >= intervalMs
|
|
583
|
+
* - 'distance': distance >= distanceFilterMeters
|
|
584
|
+
* - 'both': both conditions must be met (default)
|
|
585
|
+
*
|
|
586
|
+
* Invalid locations (coordinate 0,0 or negative accuracy) are rejected.
|
|
587
|
+
*/
|
|
588
|
+
private func shouldProcessLocation(_ location: CLLocation) -> Bool {
|
|
589
|
+
print("[LiveTracking] 📥 Raw location received: lat=\(location.coordinate.latitude) lon=\(location.coordinate.longitude) acc=\(location.horizontalAccuracy) ts=\(Int64(location.timestamp.timeIntervalSince1970 * 1000))")
|
|
590
|
+
|
|
591
|
+
// Reject invalid coordinates
|
|
592
|
+
if location.coordinate.latitude == 0.0 && location.coordinate.longitude == 0.0 {
|
|
593
|
+
print("[LiveTracking] ❌ Rejected: invalid coordinates (0,0)")
|
|
594
|
+
return false
|
|
595
|
+
}
|
|
596
|
+
if location.horizontalAccuracy < 0 {
|
|
597
|
+
print("[LiveTracking] ❌ Rejected: negative accuracy")
|
|
598
|
+
return false
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
guard let lastLoc = lastLocation, let lastTime = lastUpdateTime else {
|
|
602
|
+
print("[LiveTracking] ✅ Accepted: first location")
|
|
603
|
+
return true
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Check time filter using GPS timestamp — iOS batches deliveries so wall-clock time is unreliable
|
|
607
|
+
let timeDiffMs = Int(location.timestamp.timeIntervalSince(lastTime) * 1000)
|
|
608
|
+
let timeMet = timeDiffMs >= intervalMs
|
|
609
|
+
|
|
610
|
+
// Check distance filter
|
|
611
|
+
let distance = location.distance(from: lastLoc)
|
|
612
|
+
let distanceMet = distance >= distanceFilterMeters
|
|
613
|
+
|
|
614
|
+
print("[LiveTracking] 🔍 Filter check — mode=\(optimizationMode) timeDiffMs=\(timeDiffMs)/\(intervalMs) distance=\(String(format: "%.1f", distance))m timeMet=\(timeMet) distanceMet=\(distanceMet)")
|
|
615
|
+
|
|
616
|
+
switch optimizationMode {
|
|
617
|
+
case "interval":
|
|
618
|
+
if timeMet { print("[LiveTracking] ✅ Accepted: interval") } else { print("[LiveTracking] ⏳ Skipped: interval not met") }
|
|
619
|
+
return timeMet
|
|
620
|
+
case "distance":
|
|
621
|
+
if distanceMet { print("[LiveTracking] ✅ Accepted: distance") } else { print("[LiveTracking] ⏳ Skipped: distance not met") }
|
|
622
|
+
return distanceMet
|
|
623
|
+
default:
|
|
624
|
+
let result = timeMet && distanceMet
|
|
625
|
+
if result { print("[LiveTracking] ✅ Accepted: both") } else { print("[LiveTracking] ⏳ Skipped: both not met") }
|
|
626
|
+
return result
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Process a valid location update:
|
|
632
|
+
* 1. Emit event to JS
|
|
633
|
+
* 2. Dispatch to all sync targets via SyncEngineController
|
|
634
|
+
*
|
|
635
|
+
* Requirement 3.1: Dispatch to all configured targets in parallel.
|
|
636
|
+
*/
|
|
637
|
+
private func processLocationUpdate(_ location: CLLocation) {
|
|
638
|
+
// Update last known location and time
|
|
639
|
+
lastLocation = location
|
|
640
|
+
lastUpdateTime = location.timestamp
|
|
641
|
+
|
|
642
|
+
let locationData: [String: Any] = [
|
|
643
|
+
"latitude": location.coordinate.latitude,
|
|
644
|
+
"longitude": location.coordinate.longitude,
|
|
645
|
+
"timestamp": Int64(location.timestamp.timeIntervalSince1970 * 1000),
|
|
646
|
+
"accuracy": location.horizontalAccuracy,
|
|
647
|
+
"speed": location.speed >= 0 ? location.speed : NSNull(),
|
|
648
|
+
"altitude": location.altitude,
|
|
649
|
+
"bearing": location.course >= 0 ? location.course : NSNull()
|
|
650
|
+
]
|
|
651
|
+
|
|
652
|
+
// 1. Emit event to JavaScript
|
|
653
|
+
emitLocationEvent(locationData)
|
|
654
|
+
|
|
655
|
+
// 2. Dispatch to all sync targets via SyncEngineController
|
|
656
|
+
let dataPoint = LocationDataPoint(
|
|
657
|
+
latitude: location.coordinate.latitude,
|
|
658
|
+
longitude: location.coordinate.longitude,
|
|
659
|
+
timestamp: Int64(location.timestamp.timeIntervalSince1970 * 1000),
|
|
660
|
+
accuracy: location.horizontalAccuracy,
|
|
661
|
+
speed: location.speed >= 0 ? location.speed : nil,
|
|
662
|
+
altitude: location.altitude,
|
|
663
|
+
bearing: location.course >= 0 ? location.course : nil
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
print("[LiveTracking] 🚀 Dispatching to Firebase — lat=\(location.coordinate.latitude) lon=\(location.coordinate.longitude) ts=\(Int64(location.timestamp.timeIntervalSince1970 * 1000))")
|
|
667
|
+
syncEngineController?.dispatchLocation(dataPoint)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Emit a location update event to JavaScript.
|
|
672
|
+
*/
|
|
673
|
+
private func emitLocationEvent(_ locationData: [String: Any]) {
|
|
674
|
+
guard hasListeners else { return }
|
|
675
|
+
sendEvent(withName: LiveTracking.EVENT_LOCATION_UPDATE, body: locationData)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Emit a tracking error event to JavaScript.
|
|
680
|
+
*/
|
|
681
|
+
private func emitErrorEvent(errorCode: String, message: String, targetPath: String? = nil, method: String? = nil) {
|
|
682
|
+
guard hasListeners else { return }
|
|
683
|
+
var body: [String: Any] = [
|
|
684
|
+
"code": errorCode,
|
|
685
|
+
"message": message
|
|
686
|
+
]
|
|
687
|
+
if let targetPath = targetPath {
|
|
688
|
+
body["targetPath"] = targetPath
|
|
689
|
+
}
|
|
690
|
+
if let method = method {
|
|
691
|
+
body["method"] = method
|
|
692
|
+
}
|
|
693
|
+
sendEvent(withName: LiveTracking.EVENT_TRACKING_ERROR, body: body)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Emit a queue overflow warning event to JavaScript.
|
|
698
|
+
*/
|
|
699
|
+
private func emitQueueOverflowEvent(targetPath: String) {
|
|
700
|
+
guard hasListeners else { return }
|
|
701
|
+
sendEvent(withName: LiveTracking.EVENT_QUEUE_OVERFLOW, body: [
|
|
702
|
+
"code": "QUEUE_OVERFLOW",
|
|
703
|
+
"message": "Offline queue overflow for target: \(targetPath)",
|
|
704
|
+
"targetPath": targetPath
|
|
705
|
+
])
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// MARK: - LocationUpdateDelegate
|
|
710
|
+
|
|
711
|
+
extension LiveTracking: LocationUpdateDelegate {
|
|
712
|
+
func onLocationReceived(location: CLLocation) {
|
|
713
|
+
// Apply distance/time filter
|
|
714
|
+
if shouldProcessLocation(location) {
|
|
715
|
+
processLocationUpdate(location)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
func onLocationError(errorCode: String, message: String) {
|
|
720
|
+
emitErrorEvent(errorCode: errorCode, message: message)
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// MARK: - NetworkStateDelegate
|
|
725
|
+
|
|
726
|
+
extension LiveTracking: NetworkStateDelegate {
|
|
727
|
+
func onNetworkAvailable() {
|
|
728
|
+
// Flush offline queues when network is restored
|
|
729
|
+
syncEngineController?.flushOfflineQueues()
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
func onNetworkLost() {
|
|
733
|
+
// No action needed - locations continue to be queued locally
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// MARK: - ActivityStateDelegate
|
|
738
|
+
|
|
739
|
+
extension LiveTracking: ActivityStateDelegate {
|
|
740
|
+
func onActivityChanged(activity: ActivityType) {
|
|
741
|
+
// Forward to MotionSleepManager
|
|
742
|
+
motionSleepManager?.onActivityDetected(activity: activity)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
func onStationaryDurationExceeded(durationMs: Int64) {
|
|
746
|
+
// MotionSleepManager handles this via onActivityDetected
|
|
747
|
+
// This is an additional notification that can be used for logging
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// MARK: - MotionSleepDelegate
|
|
752
|
+
|
|
753
|
+
extension LiveTracking: MotionSleepDelegate {
|
|
754
|
+
func onSleepModeActivated() {
|
|
755
|
+
state = .motionSleep
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
func onSleepModeDeactivated() {
|
|
759
|
+
state = .tracking
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// MARK: - SyncEngineControllerDelegate
|
|
764
|
+
|
|
765
|
+
extension LiveTracking: SyncEngineControllerDelegate {
|
|
766
|
+
func onSyncError(targetPath: String, method: String, errorCode: String, message: String) {
|
|
767
|
+
emitErrorEvent(errorCode: errorCode, message: message, targetPath: targetPath, method: method)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
func onSyncQueueOverflow(targetPath: String) {
|
|
771
|
+
emitQueueOverflowEvent(targetPath: targetPath)
|
|
772
|
+
}
|
|
773
|
+
}
|