@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,196 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreMotion
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Activity types recognized by the handler.
|
|
6
|
+
*/
|
|
7
|
+
enum ActivityType {
|
|
8
|
+
case stationary
|
|
9
|
+
case walking
|
|
10
|
+
case automotive
|
|
11
|
+
case unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Protocol for receiving activity state changes from ActivityRecognitionHandler.
|
|
16
|
+
*/
|
|
17
|
+
protocol ActivityStateDelegate: AnyObject {
|
|
18
|
+
/**
|
|
19
|
+
* Called when the detected activity type changes.
|
|
20
|
+
*
|
|
21
|
+
* - Parameter activity: The new detected activity type
|
|
22
|
+
*/
|
|
23
|
+
func onActivityChanged(activity: ActivityType)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called when the stationary duration exceeds the configured threshold.
|
|
27
|
+
*
|
|
28
|
+
* - Parameter durationMs: The duration in milliseconds that the device has been stationary
|
|
29
|
+
*/
|
|
30
|
+
func onStationaryDurationExceeded(durationMs: Int64)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* iOS Activity Recognition Handler that wraps CMMotionActivityManager.
|
|
35
|
+
* Detects user activity (stationary, walking, automotive) and tracks stationary duration
|
|
36
|
+
* to support Motion Sleep Mode for battery optimization.
|
|
37
|
+
*
|
|
38
|
+
* Requirements: 8.1, 8.3
|
|
39
|
+
*/
|
|
40
|
+
class ActivityRecognitionHandler {
|
|
41
|
+
|
|
42
|
+
// MARK: - Properties
|
|
43
|
+
|
|
44
|
+
weak var delegate: ActivityStateDelegate?
|
|
45
|
+
|
|
46
|
+
/// Threshold in milliseconds for stationary duration notification (default: 3 minutes)
|
|
47
|
+
var stationaryThresholdMs: Int64 = 180_000
|
|
48
|
+
|
|
49
|
+
private let motionActivityManager: CMMotionActivityManager
|
|
50
|
+
private let operationQueue: OperationQueue
|
|
51
|
+
|
|
52
|
+
private var currentActivity: ActivityType = .unknown
|
|
53
|
+
private var stationaryStartTime: Date?
|
|
54
|
+
private var isStationary: Bool = false
|
|
55
|
+
private var isRunning: Bool = false
|
|
56
|
+
private var stationaryDurationExceededNotified: Bool = false
|
|
57
|
+
|
|
58
|
+
// MARK: - Initialization
|
|
59
|
+
|
|
60
|
+
init() {
|
|
61
|
+
motionActivityManager = CMMotionActivityManager()
|
|
62
|
+
operationQueue = OperationQueue()
|
|
63
|
+
operationQueue.name = "com.livetracking.activityRecognition"
|
|
64
|
+
operationQueue.maxConcurrentOperationCount = 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Public Methods
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the current detected activity type.
|
|
71
|
+
*
|
|
72
|
+
* - Returns: The current activity type
|
|
73
|
+
*/
|
|
74
|
+
func getCurrentActivity() -> ActivityType {
|
|
75
|
+
return currentActivity
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the current stationary duration in milliseconds.
|
|
80
|
+
* Returns 0 if the device is not currently stationary.
|
|
81
|
+
*
|
|
82
|
+
* - Returns: Duration in milliseconds that the device has been stationary, or 0
|
|
83
|
+
*/
|
|
84
|
+
func getStationaryDurationMs() -> Int64 {
|
|
85
|
+
guard isStationary, let startTime = stationaryStartTime else {
|
|
86
|
+
return 0
|
|
87
|
+
}
|
|
88
|
+
return Int64(Date().timeIntervalSince(startTime) * 1000)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if the device is currently in stationary state.
|
|
93
|
+
*
|
|
94
|
+
* - Returns: true if the device is detected as stationary
|
|
95
|
+
*/
|
|
96
|
+
func isDeviceStationary() -> Bool {
|
|
97
|
+
return isStationary
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Start activity recognition updates.
|
|
102
|
+
* Uses CMMotionActivityManager to receive periodic activity detection updates.
|
|
103
|
+
*
|
|
104
|
+
* Note: Requires Motion & Fitness permission (NSMotionUsageDescription in Info.plist).
|
|
105
|
+
*/
|
|
106
|
+
func startActivityRecognition() {
|
|
107
|
+
guard CMMotionActivityManager.isActivityAvailable() else {
|
|
108
|
+
print("[ActivityRecognitionHandler] Activity recognition is not available on this device")
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if isRunning { return }
|
|
113
|
+
|
|
114
|
+
motionActivityManager.startActivityUpdates(to: operationQueue) { [weak self] activity in
|
|
115
|
+
guard let self = self, let activity = activity else { return }
|
|
116
|
+
self.handleActivityUpdate(activity)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isRunning = true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Stop activity recognition updates.
|
|
124
|
+
* Stops CMMotionActivityManager updates and resets internal state.
|
|
125
|
+
*/
|
|
126
|
+
func stopActivityRecognition() {
|
|
127
|
+
if !isRunning { return }
|
|
128
|
+
|
|
129
|
+
motionActivityManager.stopActivityUpdates()
|
|
130
|
+
|
|
131
|
+
// Reset state
|
|
132
|
+
isRunning = false
|
|
133
|
+
isStationary = false
|
|
134
|
+
stationaryStartTime = nil
|
|
135
|
+
stationaryDurationExceededNotified = false
|
|
136
|
+
currentActivity = .unknown
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MARK: - Private Methods
|
|
140
|
+
|
|
141
|
+
private func handleActivityUpdate(_ activity: CMMotionActivity) {
|
|
142
|
+
let newActivity = mapToActivityType(activity)
|
|
143
|
+
let previousActivity = currentActivity
|
|
144
|
+
currentActivity = newActivity
|
|
145
|
+
|
|
146
|
+
if newActivity != previousActivity {
|
|
147
|
+
delegate?.onActivityChanged(activity: newActivity)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
switch newActivity {
|
|
151
|
+
case .stationary:
|
|
152
|
+
handleStationaryState()
|
|
153
|
+
case .walking, .automotive:
|
|
154
|
+
handleMovingState()
|
|
155
|
+
case .unknown:
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func handleStationaryState() {
|
|
161
|
+
if !isStationary {
|
|
162
|
+
// Transition to stationary - start tracking duration
|
|
163
|
+
isStationary = true
|
|
164
|
+
stationaryStartTime = Date()
|
|
165
|
+
stationaryDurationExceededNotified = false
|
|
166
|
+
} else {
|
|
167
|
+
// Already stationary - check if threshold exceeded
|
|
168
|
+
let durationMs = getStationaryDurationMs()
|
|
169
|
+
if durationMs >= stationaryThresholdMs && !stationaryDurationExceededNotified {
|
|
170
|
+
stationaryDurationExceededNotified = true
|
|
171
|
+
delegate?.onStationaryDurationExceeded(durationMs: durationMs)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private func handleMovingState() {
|
|
177
|
+
if isStationary {
|
|
178
|
+
// Transition from stationary to moving
|
|
179
|
+
isStationary = false
|
|
180
|
+
stationaryStartTime = nil
|
|
181
|
+
stationaryDurationExceededNotified = false
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func mapToActivityType(_ activity: CMMotionActivity) -> ActivityType {
|
|
186
|
+
if activity.stationary {
|
|
187
|
+
return .stationary
|
|
188
|
+
} else if activity.walking || activity.running {
|
|
189
|
+
return .walking
|
|
190
|
+
} else if activity.automotive {
|
|
191
|
+
return .automotive
|
|
192
|
+
} else {
|
|
193
|
+
return .unknown
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreLocation
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BackgroundModeHelper
|
|
6
|
+
*
|
|
7
|
+
* Utility class that provides significant location change monitoring as a fallback
|
|
8
|
+
* mechanism when the app is terminated by the system. Significant location changes
|
|
9
|
+
* will relaunch the app, allowing tracking to resume.
|
|
10
|
+
*
|
|
11
|
+
* ## Important: Consuming App Configuration Required
|
|
12
|
+
*
|
|
13
|
+
* This library does NOT modify the consuming app's Info.plist. The consuming app
|
|
14
|
+
* MUST add the following entries to their Info.plist:
|
|
15
|
+
*
|
|
16
|
+
* ### 1. UIBackgroundModes
|
|
17
|
+
* Add `location` to the `UIBackgroundModes` array:
|
|
18
|
+
* ```xml
|
|
19
|
+
* <key>UIBackgroundModes</key>
|
|
20
|
+
* <array>
|
|
21
|
+
* <string>location</string>
|
|
22
|
+
* </array>
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* ### 2. Location Usage Descriptions
|
|
26
|
+
* Add both usage description keys:
|
|
27
|
+
* ```xml
|
|
28
|
+
* <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
29
|
+
* <string>This app needs location access to track your position in the background.</string>
|
|
30
|
+
* <key>NSLocationWhenInUseUsageDescription</key>
|
|
31
|
+
* <string>This app needs location access to track your position.</string>
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* ### How Significant Location Change Monitoring Works
|
|
35
|
+
*
|
|
36
|
+
* When the app is terminated by the system (e.g., due to memory pressure), iOS can
|
|
37
|
+
* still deliver significant location change events. These events will relaunch the app
|
|
38
|
+
* in the background with `UIApplication.LaunchOptionsKey.location` set in the launch
|
|
39
|
+
* options dictionary.
|
|
40
|
+
*
|
|
41
|
+
* The consuming app should check for this key in `application(_:didFinishLaunchingWithOptions:)`
|
|
42
|
+
* and restart location tracking if present:
|
|
43
|
+
*
|
|
44
|
+
* ```swift
|
|
45
|
+
* func application(_ application: UIApplication,
|
|
46
|
+
* didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
47
|
+
* if launchOptions?[.location] != nil {
|
|
48
|
+
* // App was relaunched due to a significant location change.
|
|
49
|
+
* // Restart tracking here.
|
|
50
|
+
* }
|
|
51
|
+
* return true
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* Significant location changes are triggered by cell tower transitions and typically
|
|
56
|
+
* fire every 500 meters or more. This is less precise than continuous GPS but ensures
|
|
57
|
+
* the app can resume full-accuracy tracking after termination.
|
|
58
|
+
*/
|
|
59
|
+
@objc
|
|
60
|
+
class BackgroundModeHelper: NSObject, SignificantLocationMonitoring {
|
|
61
|
+
|
|
62
|
+
// MARK: - Singleton
|
|
63
|
+
|
|
64
|
+
@objc static let shared = BackgroundModeHelper()
|
|
65
|
+
|
|
66
|
+
private override init() {
|
|
67
|
+
super.init()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// MARK: - Significant Location Monitoring
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Starts significant location change monitoring as a fallback mechanism.
|
|
74
|
+
*
|
|
75
|
+
* This should be called when tracking begins so that if the app is terminated
|
|
76
|
+
* by the system, iOS will relaunch the app upon detecting a significant location
|
|
77
|
+
* change (typically cell tower transitions, ~500m or more).
|
|
78
|
+
*
|
|
79
|
+
* - Parameter locationManager: The CLLocationManager instance to use for monitoring.
|
|
80
|
+
*
|
|
81
|
+
* - Note: The consuming app must have `UIBackgroundModes` with `location` in Info.plist
|
|
82
|
+
* and the user must have granted "Always" location permission for this to work
|
|
83
|
+
* after app termination.
|
|
84
|
+
*/
|
|
85
|
+
@objc
|
|
86
|
+
func startSignificantLocationMonitoring(locationManager: CLLocationManager) {
|
|
87
|
+
guard CLLocationManager.significantLocationChangeMonitoringAvailable() else {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
locationManager.startMonitoringSignificantLocationChanges()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Stops significant location change monitoring.
|
|
95
|
+
*
|
|
96
|
+
* Call this when tracking is stopped to prevent unnecessary app relaunches
|
|
97
|
+
* after termination.
|
|
98
|
+
*
|
|
99
|
+
* - Parameter locationManager: The CLLocationManager instance to stop monitoring on.
|
|
100
|
+
*/
|
|
101
|
+
@objc
|
|
102
|
+
func stopSignificantLocationMonitoring(locationManager: CLLocationManager) {
|
|
103
|
+
locationManager.stopMonitoringSignificantLocationChanges()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks whether significant location change monitoring is available on this device.
|
|
108
|
+
*
|
|
109
|
+
* Significant location monitoring may not be available on all devices (e.g., devices
|
|
110
|
+
* without cellular hardware). Always check availability before starting monitoring.
|
|
111
|
+
*
|
|
112
|
+
* - Returns: `true` if significant location change monitoring is available, `false` otherwise.
|
|
113
|
+
*/
|
|
114
|
+
@objc
|
|
115
|
+
func isSignificantLocationMonitoringAvailable() -> Bool {
|
|
116
|
+
return CLLocationManager.significantLocationChangeMonitoringAvailable()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Checks if the app was launched due to a significant location change event.
|
|
121
|
+
*
|
|
122
|
+
* Call this from `application(_:didFinishLaunchingWithOptions:)` to determine
|
|
123
|
+
* if the app should restart tracking.
|
|
124
|
+
*
|
|
125
|
+
* - Parameter launchOptions: The launch options dictionary from AppDelegate.
|
|
126
|
+
* - Returns: `true` if the app was relaunched due to a location event.
|
|
127
|
+
*/
|
|
128
|
+
@objc
|
|
129
|
+
func wasLaunchedDueToLocationEvent(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
130
|
+
return launchOptions?[.location] != nil
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import FirebaseDatabase
|
|
3
|
+
import FirebaseFirestore
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Protocol for Firebase sync operation callbacks.
|
|
7
|
+
*/
|
|
8
|
+
protocol SyncCallback: AnyObject {
|
|
9
|
+
func onSuccess()
|
|
10
|
+
func onError(errorCode: String, message: String)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* FirebaseSyncEngine handles writing location data to Firebase.
|
|
15
|
+
*
|
|
16
|
+
* Supports both Firebase Realtime Database (RTDB) and Cloud Firestore.
|
|
17
|
+
* Implements retry with exponential backoff for resilient writes.
|
|
18
|
+
*
|
|
19
|
+
* - updateCurrentLocation: overwrites the current location at currentLocationPath
|
|
20
|
+
* - pushHistoryBatch: appends a batch of locations to historyPath
|
|
21
|
+
*
|
|
22
|
+
* Requirements: 4.1, 4.3, 5.2, 10.4
|
|
23
|
+
*/
|
|
24
|
+
class FirebaseSyncEngine {
|
|
25
|
+
|
|
26
|
+
// MARK: - Constants
|
|
27
|
+
|
|
28
|
+
private static let baseDelayMs: Int = 1000
|
|
29
|
+
private static let multiplier: Double = 2.0
|
|
30
|
+
private static let jitterMs: Int = 200
|
|
31
|
+
private static let maxRetriesCurrent: Int = 3
|
|
32
|
+
private static let maxRetriesHistory: Int = 5
|
|
33
|
+
|
|
34
|
+
// MARK: - Properties
|
|
35
|
+
|
|
36
|
+
private let service: String
|
|
37
|
+
private let currentLocationPath: String?
|
|
38
|
+
private let historyPath: String?
|
|
39
|
+
private let syncQueue = DispatchQueue(label: "com.livetracking.firebase.sync", qos: .utility)
|
|
40
|
+
|
|
41
|
+
// MARK: - Initialization
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the FirebaseSyncEngine.
|
|
45
|
+
*
|
|
46
|
+
* - Parameter service: Either "RTDB" or "Firestore"
|
|
47
|
+
* - Parameter currentLocationPath: Firebase path for current location (overwrite)
|
|
48
|
+
* - Parameter historyPath: Firebase path for history locations (append)
|
|
49
|
+
*/
|
|
50
|
+
init(service: String, currentLocationPath: String?, historyPath: String?) {
|
|
51
|
+
self.service = service
|
|
52
|
+
self.currentLocationPath = currentLocationPath
|
|
53
|
+
self.historyPath = historyPath
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - Public Methods
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update the current location at the configured currentLocationPath.
|
|
60
|
+
* Uses set/update (overwrite) semantics.
|
|
61
|
+
* Retries up to 3 times with exponential backoff on failure.
|
|
62
|
+
*
|
|
63
|
+
* - Parameter latitude: Location latitude
|
|
64
|
+
* - Parameter longitude: Location longitude
|
|
65
|
+
* - Parameter timestamp: Unix timestamp in milliseconds
|
|
66
|
+
* - Parameter accuracy: Location accuracy in meters
|
|
67
|
+
* - Parameter speed: Speed in m/s, nullable
|
|
68
|
+
* - Parameter callback: SyncCallback for success/error notification
|
|
69
|
+
*/
|
|
70
|
+
func updateCurrentLocation(
|
|
71
|
+
latitude: Double,
|
|
72
|
+
longitude: Double,
|
|
73
|
+
timestamp: Int64,
|
|
74
|
+
accuracy: Double,
|
|
75
|
+
speed: Double?,
|
|
76
|
+
callback: SyncCallback
|
|
77
|
+
) {
|
|
78
|
+
guard let path = currentLocationPath else {
|
|
79
|
+
callback.onError(errorCode: "NO_PATH", message: "currentLocationPath is not configured")
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var data: [String: Any] = [
|
|
84
|
+
"latitude": latitude,
|
|
85
|
+
"longitude": longitude,
|
|
86
|
+
"timestamp": timestamp,
|
|
87
|
+
"accuracy": accuracy,
|
|
88
|
+
"updatedAt": timestamp
|
|
89
|
+
]
|
|
90
|
+
if let speed = speed {
|
|
91
|
+
data["speed"] = speed
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
syncQueue.async { [weak self] in
|
|
95
|
+
guard let self = self else { return }
|
|
96
|
+
self.executeWithRetry(
|
|
97
|
+
maxRetries: FirebaseSyncEngine.maxRetriesCurrent,
|
|
98
|
+
operation: { completion in
|
|
99
|
+
self.performCurrentLocationWrite(path: path, data: data, completion: completion)
|
|
100
|
+
},
|
|
101
|
+
callback: callback
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Push a batch of locations to the configured historyPath.
|
|
108
|
+
* Uses push/append semantics (each location gets a unique key).
|
|
109
|
+
* Retries up to 5 times with exponential backoff on failure.
|
|
110
|
+
*
|
|
111
|
+
* - Parameter locations: Array of location dictionaries to send
|
|
112
|
+
* - Parameter callback: SyncCallback for success/error notification
|
|
113
|
+
*/
|
|
114
|
+
func pushHistoryBatch(locations: [[String: Any]], callback: SyncCallback) {
|
|
115
|
+
guard let path = historyPath else {
|
|
116
|
+
callback.onError(errorCode: "NO_PATH", message: "historyPath is not configured")
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if locations.isEmpty {
|
|
121
|
+
callback.onSuccess()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
syncQueue.async { [weak self] in
|
|
126
|
+
guard let self = self else { return }
|
|
127
|
+
self.executeWithRetry(
|
|
128
|
+
maxRetries: FirebaseSyncEngine.maxRetriesHistory,
|
|
129
|
+
operation: { completion in
|
|
130
|
+
self.performHistoryBatchWrite(path: path, locations: locations, completion: completion)
|
|
131
|
+
},
|
|
132
|
+
callback: callback
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// MARK: - Private Write Methods
|
|
138
|
+
|
|
139
|
+
private func performCurrentLocationWrite(
|
|
140
|
+
path: String,
|
|
141
|
+
data: [String: Any],
|
|
142
|
+
completion: @escaping (Bool, String?, String?) -> Void
|
|
143
|
+
) {
|
|
144
|
+
switch service {
|
|
145
|
+
case "RTDB":
|
|
146
|
+
let reference = Database.database().reference(withPath: path)
|
|
147
|
+
reference.setValue(data) { error, _ in
|
|
148
|
+
if let error = error {
|
|
149
|
+
completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
|
|
150
|
+
} else {
|
|
151
|
+
completion(true, nil, nil)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
case "Firestore":
|
|
155
|
+
let document = Firestore.firestore().document(path)
|
|
156
|
+
document.setData(data) { error in
|
|
157
|
+
if let error = error {
|
|
158
|
+
completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
|
|
159
|
+
} else {
|
|
160
|
+
completion(true, nil, nil)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
default:
|
|
164
|
+
completion(false, "INVALID_SERVICE", "Unknown service: \(service). Use 'RTDB' or 'Firestore'.")
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private func performHistoryBatchWrite(
|
|
169
|
+
path: String,
|
|
170
|
+
locations: [[String: Any]],
|
|
171
|
+
completion: @escaping (Bool, String?, String?) -> Void
|
|
172
|
+
) {
|
|
173
|
+
switch service {
|
|
174
|
+
case "RTDB":
|
|
175
|
+
let reference = Database.database().reference(withPath: path)
|
|
176
|
+
var updates: [String: Any] = [:]
|
|
177
|
+
|
|
178
|
+
for location in locations {
|
|
179
|
+
let key = reference.childByAutoId().key ?? UUID().uuidString
|
|
180
|
+
updates[key] = location
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if updates.isEmpty {
|
|
184
|
+
completion(true, nil, nil)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
reference.updateChildValues(updates) { error, _ in
|
|
189
|
+
if let error = error {
|
|
190
|
+
completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
|
|
191
|
+
} else {
|
|
192
|
+
completion(true, nil, nil)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case "Firestore":
|
|
197
|
+
let firestore = Firestore.firestore()
|
|
198
|
+
let collection = firestore.collection(path)
|
|
199
|
+
let batch = firestore.batch()
|
|
200
|
+
|
|
201
|
+
for location in locations {
|
|
202
|
+
let docRef = collection.document()
|
|
203
|
+
batch.setData(location, forDocument: docRef)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
batch.commit { error in
|
|
207
|
+
if let error = error {
|
|
208
|
+
completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
|
|
209
|
+
} else {
|
|
210
|
+
completion(true, nil, nil)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
default:
|
|
215
|
+
completion(false, "INVALID_SERVICE", "Unknown service: \(service). Use 'RTDB' or 'Firestore'.")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// MARK: - Retry Logic
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Execute an operation with exponential backoff retry.
|
|
223
|
+
*
|
|
224
|
+
* Retry delay formula: baseDelay × 2^(attempt-1) ± jitter
|
|
225
|
+
* - Base delay: 1000ms
|
|
226
|
+
* - Multiplier: 2x
|
|
227
|
+
* - Jitter: ±200ms (random)
|
|
228
|
+
*
|
|
229
|
+
* - Parameter maxRetries: Maximum number of retry attempts
|
|
230
|
+
* - Parameter operation: The Firebase write operation to execute, calls completion(success, errorCode, message)
|
|
231
|
+
* - Parameter callback: Final callback after all retries exhausted or success
|
|
232
|
+
*/
|
|
233
|
+
private func executeWithRetry(
|
|
234
|
+
maxRetries: Int,
|
|
235
|
+
operation: @escaping (@escaping (Bool, String?, String?) -> Void) -> Void,
|
|
236
|
+
callback: SyncCallback
|
|
237
|
+
) {
|
|
238
|
+
var attempt = 0
|
|
239
|
+
|
|
240
|
+
func tryOperation() {
|
|
241
|
+
attempt += 1
|
|
242
|
+
operation { success, errorCode, message in
|
|
243
|
+
if success {
|
|
244
|
+
callback.onSuccess()
|
|
245
|
+
} else {
|
|
246
|
+
if attempt >= maxRetries {
|
|
247
|
+
callback.onError(
|
|
248
|
+
errorCode: errorCode ?? "FIREBASE_WRITE_FAILED",
|
|
249
|
+
message: "Failed after \(attempt) attempts: \(message ?? "Unknown error")"
|
|
250
|
+
)
|
|
251
|
+
} else {
|
|
252
|
+
let delay = self.calculateBackoffDelay(attempt: attempt)
|
|
253
|
+
Thread.sleep(forTimeInterval: Double(delay) / 1000.0)
|
|
254
|
+
tryOperation()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
tryOperation()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Calculate exponential backoff delay with jitter.
|
|
265
|
+
*
|
|
266
|
+
* Formula: baseDelay × 2^(attempt-1) ± random jitter
|
|
267
|
+
*
|
|
268
|
+
* - Parameter attempt: Current attempt number (1-indexed)
|
|
269
|
+
* - Returns: Delay in milliseconds
|
|
270
|
+
*/
|
|
271
|
+
func calculateBackoffDelay(attempt: Int) -> Int {
|
|
272
|
+
let exponentialDelay = Double(FirebaseSyncEngine.baseDelayMs) * pow(FirebaseSyncEngine.multiplier, Double(attempt - 1))
|
|
273
|
+
let jitter = Int.random(in: -FirebaseSyncEngine.jitterMs...FirebaseSyncEngine.jitterMs)
|
|
274
|
+
return max(0, Int(exponentialDelay) + jitter)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Objective-C bridge file for the LiveTracking Swift module.
|
|
6
|
+
*
|
|
7
|
+
* Uses RCT_EXTERN_MODULE to expose Swift methods to React Native.
|
|
8
|
+
* Also conforms to RCTTurboModule so the module is discoverable via
|
|
9
|
+
* TurboModuleRegistry in New Architecture / Bridgeless mode (RN 0.76+).
|
|
10
|
+
*/
|
|
11
|
+
@interface RCT_EXTERN_MODULE(LiveTracking, RCTEventEmitter)
|
|
12
|
+
|
|
13
|
+
RCT_EXTERN_METHOD(configure:(NSString *)config
|
|
14
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
15
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
16
|
+
|
|
17
|
+
RCT_EXTERN_METHOD(start:(RCTPromiseResolveBlock)resolve
|
|
18
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
19
|
+
|
|
20
|
+
RCT_EXTERN_METHOD(stop:(RCTPromiseResolveBlock)resolve
|
|
21
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
22
|
+
|
|
23
|
+
RCT_EXTERN_METHOD(getStatus:(RCTPromiseResolveBlock)resolve
|
|
24
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
25
|
+
|
|
26
|
+
RCT_EXTERN_METHOD(getQueuedLocations:(RCTPromiseResolveBlock)resolve
|
|
27
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
28
|
+
|
|
29
|
+
RCT_EXTERN_METHOD(getQueuedLocationsByTarget:(RCTPromiseResolveBlock)resolve
|
|
30
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
31
|
+
|
|
32
|
+
+ (BOOL)requiresMainQueueSetup
|
|
33
|
+
{
|
|
34
|
+
return NO;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@end
|