@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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/android/build.gradle +71 -0
  4. package/android/gradle.properties +7 -0
  5. package/android/src/main/AndroidManifest.xml +40 -0
  6. package/android/src/main/java/com/livetracking/LiveTrackingModuleImpl.kt +728 -0
  7. package/android/src/main/java/com/livetracking/LiveTrackingPackage.kt +16 -0
  8. package/android/src/main/java/com/livetracking/location/LocationEngine.kt +93 -0
  9. package/android/src/main/java/com/livetracking/network/NetworkListener.kt +127 -0
  10. package/android/src/main/java/com/livetracking/optimizer/ActivityRecognitionHandler.kt +248 -0
  11. package/android/src/main/java/com/livetracking/optimizer/MotionSleepManager.kt +130 -0
  12. package/android/src/main/java/com/livetracking/permissions/PermissionHandler.kt +145 -0
  13. package/android/src/main/java/com/livetracking/queue/QueueEngine.kt +167 -0
  14. package/android/src/main/java/com/livetracking/queue/QueuedLocation.kt +16 -0
  15. package/android/src/main/java/com/livetracking/queue/TrackingDatabase.kt +239 -0
  16. package/android/src/main/java/com/livetracking/receiver/BootReceiver.kt +53 -0
  17. package/android/src/main/java/com/livetracking/service/TrackingForegroundService.kt +145 -0
  18. package/android/src/main/java/com/livetracking/sync/FirebaseSyncEngine.kt +277 -0
  19. package/android/src/main/java/com/livetracking/sync/LocationDataPoint.kt +31 -0
  20. package/android/src/main/java/com/livetracking/sync/SyncEngineController.kt +220 -0
  21. package/android/src/main/java/com/livetracking/sync/SyncTargetConfig.kt +20 -0
  22. package/android/src/main/java/com/livetracking/sync/TargetHandler.kt +601 -0
  23. package/android/src/newarch/java/com/livetracking/LiveTrackingModule.kt +64 -0
  24. package/android/src/oldarch/java/com/livetracking/LiveTrackingModule.kt +70 -0
  25. package/android/src/test/java/com/livetracking/BackoffCalculationTest.kt +216 -0
  26. package/android/src/test/java/com/livetracking/BatchAccumulatorTest.kt +391 -0
  27. package/android/src/test/java/com/livetracking/BootReceiverTest.kt +247 -0
  28. package/android/src/test/java/com/livetracking/FirebaseSyncEngineTest.kt +337 -0
  29. package/android/src/test/java/com/livetracking/LocationEngineTest.kt +202 -0
  30. package/android/src/test/java/com/livetracking/MotionSleepManagerTest.kt +420 -0
  31. package/android/src/test/java/com/livetracking/OfflineQueueTest.kt +462 -0
  32. package/android/src/test/java/com/livetracking/PermissionHandlerTest.kt +200 -0
  33. package/android/src/test/java/com/livetracking/QueueEngineTest.kt +335 -0
  34. package/android/src/test/java/com/livetracking/SyncEngineControllerTest.kt +855 -0
  35. package/ios/ActivityRecognitionHandler.swift +196 -0
  36. package/ios/BackgroundModeHelper.swift +132 -0
  37. package/ios/FirebaseSyncEngine.swift +276 -0
  38. package/ios/LiveTracking-Bridging-Header.h +2 -0
  39. package/ios/LiveTracking.m +37 -0
  40. package/ios/LiveTracking.swift +773 -0
  41. package/ios/LocationDataPoint.swift +56 -0
  42. package/ios/LocationEngine.swift +160 -0
  43. package/ios/MotionSleepManager.swift +151 -0
  44. package/ios/NetworkListener.swift +105 -0
  45. package/ios/OfflineQueueManager.swift +503 -0
  46. package/ios/PermissionHandler.swift +148 -0
  47. package/ios/QueueEngine.swift +249 -0
  48. package/ios/SyncEngineController.swift +396 -0
  49. package/ios/SyncTargetConfig.swift +36 -0
  50. package/ios/TargetHandler.swift +715 -0
  51. package/ios/Tests/ActivityRecognitionHandlerTests.swift +259 -0
  52. package/ios/Tests/FirebaseSyncEngineTests.swift +303 -0
  53. package/ios/Tests/LocationEngineTests.swift +244 -0
  54. package/ios/Tests/MotionSleepManagerTests.swift +355 -0
  55. package/ios/Tests/NetworkListenerTests.swift +188 -0
  56. package/ios/Tests/OfflineQueueFlushTests.swift +375 -0
  57. package/ios/Tests/PermissionHandlerTests.swift +238 -0
  58. package/ios/Tests/QueueEngineTests.swift +346 -0
  59. package/ios/TrackingCleanup.swift +93 -0
  60. package/ios/TrackingNotificationManager.swift +187 -0
  61. package/lib/commonjs/EventEmitter.js +113 -0
  62. package/lib/commonjs/EventEmitter.js.map +1 -0
  63. package/lib/commonjs/LiveTracking.js +134 -0
  64. package/lib/commonjs/LiveTracking.js.map +1 -0
  65. package/lib/commonjs/NativeLiveTracking.js +21 -0
  66. package/lib/commonjs/NativeLiveTracking.js.map +1 -0
  67. package/lib/commonjs/filters/distanceTimeFilter.js +63 -0
  68. package/lib/commonjs/filters/distanceTimeFilter.js.map +1 -0
  69. package/lib/commonjs/index.js +103 -0
  70. package/lib/commonjs/index.js.map +1 -0
  71. package/lib/commonjs/serialization/locationSerializer.js +51 -0
  72. package/lib/commonjs/serialization/locationSerializer.js.map +1 -0
  73. package/lib/commonjs/types.js +77 -0
  74. package/lib/commonjs/types.js.map +1 -0
  75. package/lib/commonjs/utils/distance.js +63 -0
  76. package/lib/commonjs/utils/distance.js.map +1 -0
  77. package/lib/commonjs/utils/retry.js +80 -0
  78. package/lib/commonjs/utils/retry.js.map +1 -0
  79. package/lib/commonjs/validation.js +463 -0
  80. package/lib/commonjs/validation.js.map +1 -0
  81. package/lib/module/EventEmitter.js +105 -0
  82. package/lib/module/EventEmitter.js.map +1 -0
  83. package/lib/module/LiveTracking.js +127 -0
  84. package/lib/module/LiveTracking.js.map +1 -0
  85. package/lib/module/NativeLiveTracking.js +16 -0
  86. package/lib/module/NativeLiveTracking.js.map +1 -0
  87. package/lib/module/filters/distanceTimeFilter.js +58 -0
  88. package/lib/module/filters/distanceTimeFilter.js.map +1 -0
  89. package/lib/module/index.js +32 -0
  90. package/lib/module/index.js.map +1 -0
  91. package/lib/module/serialization/locationSerializer.js +45 -0
  92. package/lib/module/serialization/locationSerializer.js.map +1 -0
  93. package/lib/module/types.js +94 -0
  94. package/lib/module/types.js.map +1 -0
  95. package/lib/module/utils/distance.js +56 -0
  96. package/lib/module/utils/distance.js.map +1 -0
  97. package/lib/module/utils/retry.js +72 -0
  98. package/lib/module/utils/retry.js.map +1 -0
  99. package/lib/module/validation.js +456 -0
  100. package/lib/module/validation.js.map +1 -0
  101. package/lib/typescript/EventEmitter.d.ts +65 -0
  102. package/lib/typescript/EventEmitter.d.ts.map +1 -0
  103. package/lib/typescript/LiveTracking.d.ts +23 -0
  104. package/lib/typescript/LiveTracking.d.ts.map +1 -0
  105. package/lib/typescript/NativeLiveTracking.d.ts +25 -0
  106. package/lib/typescript/NativeLiveTracking.d.ts.map +1 -0
  107. package/lib/typescript/filters/distanceTimeFilter.d.ts +44 -0
  108. package/lib/typescript/filters/distanceTimeFilter.d.ts.map +1 -0
  109. package/lib/typescript/index.d.ts +21 -0
  110. package/lib/typescript/index.d.ts.map +1 -0
  111. package/lib/typescript/serialization/locationSerializer.d.ts +39 -0
  112. package/lib/typescript/serialization/locationSerializer.d.ts.map +1 -0
  113. package/lib/typescript/types.d.ts +217 -0
  114. package/lib/typescript/types.d.ts.map +1 -0
  115. package/lib/typescript/utils/distance.d.ts +38 -0
  116. package/lib/typescript/utils/distance.d.ts.map +1 -0
  117. package/lib/typescript/utils/retry.d.ts +60 -0
  118. package/lib/typescript/utils/retry.d.ts.map +1 -0
  119. package/lib/typescript/validation.d.ts +26 -0
  120. package/lib/typescript/validation.d.ts.map +1 -0
  121. package/package.json +126 -0
  122. package/react-native-live-tracking.podspec +47 -0
  123. package/src/EventEmitter.ts +118 -0
  124. package/src/LiveTracking.ts +159 -0
  125. package/src/NativeLiveTracking.ts +29 -0
  126. package/src/filters/distanceTimeFilter.ts +75 -0
  127. package/src/index.ts +51 -0
  128. package/src/serialization/locationSerializer.ts +57 -0
  129. package/src/types.ts +252 -0
  130. package/src/utils/distance.ts +68 -0
  131. package/src/utils/retry.ts +75 -0
  132. 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
+ }