@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,56 @@
1
+ import Foundation
2
+
3
+ /**
4
+ * Represents a single location data point dispatched to sync targets.
5
+ *
6
+ * Required fields (always available from device sensors):
7
+ * - latitude, longitude, timestamp, accuracy
8
+ *
9
+ * Optional fields (may be unavailable depending on device/sensor state):
10
+ * - speed, altitude, bearing
11
+ *
12
+ * When optional fields are nil, they should be omitted or written as null
13
+ * in the Firebase payload — never as placeholder values (0, -1, etc.).
14
+ *
15
+ * Requirements: 9.3
16
+ */
17
+ struct LocationDataPoint {
18
+ /// Location latitude in degrees
19
+ let latitude: Double
20
+
21
+ /// Location longitude in degrees
22
+ let longitude: Double
23
+
24
+ /// Unix timestamp in milliseconds
25
+ let timestamp: Int64
26
+
27
+ /// Location accuracy in meters
28
+ let accuracy: Double
29
+
30
+ /// Speed in m/s, or nil if unavailable
31
+ let speed: Double?
32
+
33
+ /// Altitude in meters, or nil if unavailable
34
+ let altitude: Double?
35
+
36
+ /// Bearing in degrees, or nil if unavailable
37
+ let bearing: Double?
38
+
39
+ init(
40
+ latitude: Double,
41
+ longitude: Double,
42
+ timestamp: Int64,
43
+ accuracy: Double,
44
+ speed: Double? = nil,
45
+ altitude: Double? = nil,
46
+ bearing: Double? = nil
47
+ ) {
48
+ self.latitude = latitude
49
+ self.longitude = longitude
50
+ self.timestamp = timestamp
51
+ self.accuracy = accuracy
52
+ self.speed = speed
53
+ self.altitude = altitude
54
+ self.bearing = bearing
55
+ }
56
+ }
@@ -0,0 +1,160 @@
1
+ import Foundation
2
+ import CoreLocation
3
+
4
+ /**
5
+ * Protocol for receiving location updates and errors from the LocationEngine.
6
+ */
7
+ protocol LocationUpdateDelegate: AnyObject {
8
+ func onLocationReceived(location: CLLocation)
9
+ func onLocationError(errorCode: String, message: String)
10
+ }
11
+
12
+ /**
13
+ * Default implementation for optional delegate method so existing code keeps compiling.
14
+ */
15
+ extension LocationUpdateDelegate {
16
+ func onLocationError(errorCode: String, message: String) {
17
+ // No-op by default
18
+ }
19
+ }
20
+
21
+ /**
22
+ * iOS Location Engine that wraps CLLocationManager.
23
+ * Provides high-accuracy location updates for live tracking with background support.
24
+ *
25
+ * Requirements: 3.1, 3.2
26
+ */
27
+ class LocationEngine: NSObject, CLLocationManagerDelegate {
28
+
29
+ private var locationManager: CLLocationManager!
30
+
31
+ weak var delegate: LocationUpdateDelegate?
32
+
33
+ override init() {
34
+ super.init()
35
+ let initOnMain = { [self] in
36
+ self.locationManager = CLLocationManager()
37
+ self.locationManager.delegate = self
38
+ }
39
+ if Thread.isMainThread {
40
+ initOnMain()
41
+ } else {
42
+ DispatchQueue.main.sync(execute: initOnMain)
43
+ }
44
+ }
45
+
46
+ // MARK: - Public Methods
47
+
48
+ func requestAlwaysAuthorization() {
49
+ if Thread.isMainThread {
50
+ self.locationManager.requestAlwaysAuthorization()
51
+ } else {
52
+ DispatchQueue.main.async { [weak self] in
53
+ self?.locationManager.requestAlwaysAuthorization()
54
+ }
55
+ }
56
+ }
57
+
58
+ func getAuthorizationStatus() -> CLAuthorizationStatus {
59
+ if #available(iOS 14.0, *) {
60
+ return locationManager.authorizationStatus
61
+ } else {
62
+ return CLLocationManager.authorizationStatus()
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Start receiving location updates with the specified interval and distance filter.
68
+ * Uses kCLLocationAccuracyBest for best possible location accuracy.
69
+ *
70
+ * - Parameter intervalMs: The desired interval for location updates in milliseconds (used for time-based filtering at a higher level)
71
+ * - Parameter distanceFilter: The minimum distance between updates in meters
72
+ */
73
+ func startLocationUpdates(intervalMs: Int, distanceFilter: Double) {
74
+ startLocationUpdates(intervalMs: intervalMs, distanceFilter: distanceFilter, accuracy: kCLLocationAccuracyBest)
75
+ }
76
+
77
+ /**
78
+ * Start receiving location updates with the specified interval, distance filter, and accuracy.
79
+ * Allows configurable accuracy for scenarios like sleep mode where lower accuracy saves battery.
80
+ *
81
+ * - Parameter intervalMs: The desired interval for location updates in milliseconds (used for time-based filtering at a higher level)
82
+ * - Parameter distanceFilter: The minimum distance between updates in meters
83
+ * - Parameter accuracy: The desired location accuracy (e.g., kCLLocationAccuracyBest or kCLLocationAccuracyKilometer)
84
+ */
85
+ func startLocationUpdates(intervalMs: Int, distanceFilter: Double, accuracy: CLLocationAccuracy) {
86
+ let setup = { [weak self] in
87
+ guard let self = self else { return }
88
+ self.locationManager.desiredAccuracy = accuracy
89
+ self.locationManager.distanceFilter = distanceFilter
90
+ self.locationManager.allowsBackgroundLocationUpdates = true
91
+ self.locationManager.pausesLocationUpdatesAutomatically = false
92
+ // Use otherNavigation so iOS does not throttle/suspend updates when device is stationary
93
+ self.locationManager.activityType = .otherNavigation
94
+ self.locationManager.showsBackgroundLocationIndicator = true
95
+ self.locationManager.startUpdatingLocation()
96
+ }
97
+ if Thread.isMainThread {
98
+ setup()
99
+ } else {
100
+ DispatchQueue.main.async(execute: setup)
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Stop receiving location updates.
106
+ */
107
+ func stopLocationUpdates() {
108
+ if Thread.isMainThread {
109
+ locationManager.stopUpdatingLocation()
110
+ } else {
111
+ DispatchQueue.main.async { [weak self] in
112
+ self?.locationManager.stopUpdatingLocation()
113
+ }
114
+ }
115
+ }
116
+
117
+ // MARK: - CLLocationManagerDelegate
118
+
119
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
120
+ guard let location = locations.last else { return }
121
+ print("[LocationEngine] 📡 didUpdateLocations called — delegate alive=\(delegate != nil) lat=\(location.coordinate.latitude) ts=\(Int64(location.timestamp.timeIntervalSince1970 * 1000))")
122
+ delegate?.onLocationReceived(location: location)
123
+ }
124
+
125
+ func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
126
+ print("[LocationEngine] ⚠️ CLLocationManager PAUSED location updates!")
127
+ }
128
+
129
+ func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) {
130
+ print("[LocationEngine] ✅ CLLocationManager RESUMED location updates")
131
+ }
132
+
133
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
134
+ let errorCode: String
135
+ let message: String
136
+
137
+ if let clError = error as? CLError {
138
+ switch clError.code {
139
+ case .denied:
140
+ errorCode = "PERMISSION_DENIED"
141
+ message = "Location updates failed: permission denied."
142
+ case .locationUnknown:
143
+ errorCode = "LOCATION_UNKNOWN"
144
+ message = "Location temporarily unavailable: \(error.localizedDescription)"
145
+ case .network:
146
+ errorCode = "NETWORK_ERROR"
147
+ message = "Location network error: \(error.localizedDescription)"
148
+ default:
149
+ errorCode = "LOCATION_ERROR"
150
+ message = "Location update failed: \(error.localizedDescription)"
151
+ }
152
+ } else {
153
+ errorCode = "LOCATION_ERROR"
154
+ message = "Location update failed: \(error.localizedDescription)"
155
+ }
156
+
157
+ delegate?.onLocationError(errorCode: errorCode, message: message)
158
+ print("[LocationEngine] \(errorCode): \(message)")
159
+ }
160
+ }
@@ -0,0 +1,151 @@
1
+ import Foundation
2
+ import CoreLocation
3
+
4
+ /**
5
+ * Protocol for receiving motion sleep mode state changes.
6
+ */
7
+ protocol MotionSleepDelegate: AnyObject {
8
+ /**
9
+ * Called when sleep mode is activated (device stationary > 3 minutes).
10
+ */
11
+ func onSleepModeActivated()
12
+
13
+ /**
14
+ * Called when sleep mode is deactivated (movement detected).
15
+ */
16
+ func onSleepModeDeactivated()
17
+ }
18
+
19
+ /**
20
+ * Manages Motion Sleep Mode for battery optimization on iOS.
21
+ *
22
+ * When the device is detected as stationary for more than 3 minutes and `stopWhenStill` is enabled,
23
+ * this manager switches location updates to kCLDesiredAccuracyKilometer (low-power mode).
24
+ * When movement is detected again, it restores kCLDesiredAccuracyBest location updates.
25
+ *
26
+ * Requirements: 8.1, 8.2, 8.4
27
+ */
28
+ class MotionSleepManager {
29
+
30
+ // MARK: - Constants
31
+
32
+ /// Duration threshold in milliseconds before entering sleep mode.
33
+ /// Device must be stationary for more than 3 minutes (180,000 ms).
34
+ static let STILL_THRESHOLD_MS: Int64 = 180_000
35
+
36
+ // MARK: - Properties
37
+
38
+ weak var delegate: MotionSleepDelegate?
39
+
40
+ private let locationEngine: LocationEngine
41
+ private let stopWhenStill: Bool
42
+ private let intervalMs: Int
43
+ private let distanceFilter: Double
44
+
45
+ private var inSleepMode: Bool = false
46
+ private var stationaryStartTime: Date?
47
+ private var isStationary: Bool = false
48
+
49
+ // MARK: - Initialization
50
+
51
+ /**
52
+ * Initialize the MotionSleepManager.
53
+ *
54
+ * - Parameter locationEngine: The location engine to control GPS accuracy
55
+ * - Parameter stopWhenStill: Whether motion sleep mode is enabled
56
+ * - Parameter intervalMs: The configured location update interval in milliseconds
57
+ * - Parameter distanceFilter: The configured distance filter in meters
58
+ */
59
+ init(locationEngine: LocationEngine, stopWhenStill: Bool, intervalMs: Int, distanceFilter: Double) {
60
+ self.locationEngine = locationEngine
61
+ self.stopWhenStill = stopWhenStill
62
+ self.intervalMs = intervalMs
63
+ self.distanceFilter = distanceFilter
64
+ }
65
+
66
+ // MARK: - Public Methods
67
+
68
+ /**
69
+ * Called when an activity detection update is received.
70
+ *
71
+ * If `stopWhenStill` is false, this method is a no-op.
72
+ *
73
+ * Behavior:
74
+ * - stationary detected: starts tracking duration. If stationary > 3 minutes, enters sleep mode.
75
+ * - walking or automotive detected: exits sleep mode if active, resets stationary tracking.
76
+ *
77
+ * - Parameter activity: The detected activity type from ActivityRecognitionHandler
78
+ */
79
+ func onActivityDetected(activity: ActivityType) {
80
+ if !stopWhenStill {
81
+ return
82
+ }
83
+
84
+ switch activity {
85
+ case .stationary:
86
+ if !isStationary {
87
+ // Start tracking stationary duration
88
+ isStationary = true
89
+ stationaryStartTime = Date()
90
+ } else {
91
+ // Already stationary, check if threshold exceeded
92
+ guard let startTime = stationaryStartTime else { return }
93
+ let stationaryDurationMs = Int64(Date().timeIntervalSince(startTime) * 1000)
94
+ if stationaryDurationMs > MotionSleepManager.STILL_THRESHOLD_MS && !inSleepMode {
95
+ enterSleepMode()
96
+ }
97
+ }
98
+
99
+ case .walking, .automotive:
100
+ isStationary = false
101
+ stationaryStartTime = nil
102
+ if inSleepMode {
103
+ exitSleepMode()
104
+ }
105
+
106
+ case .unknown:
107
+ break
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Returns whether the manager is currently in sleep mode (low-power location).
113
+ *
114
+ * - Returns: true if sleep mode is active, false otherwise
115
+ */
116
+ func isInSleepMode() -> Bool {
117
+ return inSleepMode
118
+ }
119
+
120
+ // MARK: - Private Methods
121
+
122
+ /**
123
+ * Enter sleep mode: stop current location updates and restart with kCLDesiredAccuracyKilometer.
124
+ * This reduces GPS usage when the device is stationary.
125
+ */
126
+ private func enterSleepMode() {
127
+ inSleepMode = true
128
+ locationEngine.stopLocationUpdates()
129
+ locationEngine.startLocationUpdates(
130
+ intervalMs: intervalMs,
131
+ distanceFilter: distanceFilter,
132
+ accuracy: kCLLocationAccuracyKilometer
133
+ )
134
+ delegate?.onSleepModeActivated()
135
+ }
136
+
137
+ /**
138
+ * Exit sleep mode: stop current location updates and restart with kCLDesiredAccuracyBest.
139
+ * This restores full GPS accuracy when movement is detected.
140
+ */
141
+ private func exitSleepMode() {
142
+ inSleepMode = false
143
+ locationEngine.stopLocationUpdates()
144
+ locationEngine.startLocationUpdates(
145
+ intervalMs: intervalMs,
146
+ distanceFilter: distanceFilter,
147
+ accuracy: kCLLocationAccuracyBest
148
+ )
149
+ delegate?.onSleepModeDeactivated()
150
+ }
151
+ }
@@ -0,0 +1,105 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ /**
5
+ * Protocol for receiving network state change notifications.
6
+ * Implementations receive callbacks when connectivity is gained or lost.
7
+ */
8
+ protocol NetworkStateDelegate: AnyObject {
9
+ /**
10
+ * Called when network connectivity is restored.
11
+ * Use this to trigger queue flush for pending location data.
12
+ */
13
+ func onNetworkAvailable()
14
+
15
+ /**
16
+ * Called when network connectivity is lost.
17
+ */
18
+ func onNetworkLost()
19
+ }
20
+
21
+ /**
22
+ * NetworkListener monitors device network connectivity using NWPathMonitor.
23
+ * When connectivity is restored after being offline, it notifies the registered delegate
24
+ * so that pending queued locations can be flushed to Firebase.
25
+ *
26
+ * Conforms to NetworkStatusProvider for use with SyncEngineController.
27
+ *
28
+ * Requirements: 6.2, 6.4
29
+ *
30
+ * Usage:
31
+ * ```
32
+ * let listener = NetworkListener()
33
+ * listener.delegate = self
34
+ * listener.startListening()
35
+ * // ...
36
+ * listener.stopListening()
37
+ * ```
38
+ */
39
+ class NetworkListener: NetworkStatusProvider {
40
+
41
+ // MARK: - Properties
42
+
43
+ private let monitor: NWPathMonitor
44
+ private let monitorQueue: DispatchQueue
45
+ private var isListening: Bool = false
46
+ private var currentlyOnline: Bool = false
47
+
48
+ weak var delegate: NetworkStateDelegate?
49
+
50
+ // MARK: - Initialization
51
+
52
+ init() {
53
+ monitor = NWPathMonitor()
54
+ monitorQueue = DispatchQueue(label: "com.livetracking.network.monitor", qos: .utility)
55
+ }
56
+
57
+ // MARK: - Public Methods
58
+
59
+ /**
60
+ * Start listening for network connectivity changes.
61
+ * Starts NWPathMonitor on a background queue.
62
+ * If already listening, this is a no-op.
63
+ */
64
+ func startListening() {
65
+ guard !isListening else { return }
66
+
67
+ monitor.pathUpdateHandler = { [weak self] path in
68
+ guard let self = self else { return }
69
+
70
+ let wasOnline = self.currentlyOnline
71
+ let isNowOnline = path.status == .satisfied
72
+
73
+ self.currentlyOnline = isNowOnline
74
+
75
+ if isNowOnline && !wasOnline {
76
+ self.delegate?.onNetworkAvailable()
77
+ } else if !isNowOnline && wasOnline {
78
+ self.delegate?.onNetworkLost()
79
+ }
80
+ }
81
+
82
+ monitor.start(queue: monitorQueue)
83
+ isListening = true
84
+ }
85
+
86
+ /**
87
+ * Stop listening for network connectivity changes.
88
+ * Cancels the NWPathMonitor.
89
+ * If not currently listening, this is a no-op.
90
+ */
91
+ func stopListening() {
92
+ guard isListening else { return }
93
+ monitor.cancel()
94
+ isListening = false
95
+ }
96
+
97
+ /**
98
+ * Returns the current network connectivity state.
99
+ *
100
+ * - Returns: true if the device currently has network connectivity, false otherwise
101
+ */
102
+ func isOnline() -> Bool {
103
+ return currentlyOnline
104
+ }
105
+ }