@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,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
|
+
}
|