@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,145 @@
1
+ package com.livetracking.service
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.Service
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.os.Build
10
+ import android.os.IBinder
11
+ import androidx.core.app.NotificationCompat
12
+
13
+ /**
14
+ * Android Foreground Service for persistent location tracking.
15
+ * Keeps the app alive in the background with a persistent notification,
16
+ * preventing the OS from killing the process.
17
+ *
18
+ * Requirements: 2.1, 2.4, 2.5
19
+ */
20
+ class TrackingForegroundService : Service() {
21
+
22
+ companion object {
23
+ const val DEFAULT_CHANNEL_ID = "live_tracking_channel"
24
+ const val DEFAULT_CHANNEL_NAME = "Live Tracking"
25
+ const val NOTIFICATION_ID = 1001
26
+
27
+ private const val EXTRA_NOTIFICATION_TITLE = "extra_notification_title"
28
+ private const val EXTRA_NOTIFICATION_TEXT = "extra_notification_text"
29
+ private const val EXTRA_NOTIFICATION_ICON = "extra_notification_icon"
30
+ private const val EXTRA_CHANNEL_ID = "extra_channel_id"
31
+ private const val EXTRA_CHANNEL_NAME = "extra_channel_name"
32
+
33
+ /**
34
+ * Start the foreground service with notification configuration.
35
+ */
36
+ fun start(
37
+ context: Context,
38
+ title: String = "Live Tracking",
39
+ text: String = "Tracking your location",
40
+ icon: String? = null,
41
+ channelId: String = DEFAULT_CHANNEL_ID,
42
+ channelName: String = DEFAULT_CHANNEL_NAME
43
+ ) {
44
+ val intent = Intent(context, TrackingForegroundService::class.java).apply {
45
+ putExtra(EXTRA_NOTIFICATION_TITLE, title)
46
+ putExtra(EXTRA_NOTIFICATION_TEXT, text)
47
+ putExtra(EXTRA_CHANNEL_ID, channelId)
48
+ putExtra(EXTRA_CHANNEL_NAME, channelName)
49
+ if (icon != null) {
50
+ putExtra(EXTRA_NOTIFICATION_ICON, icon)
51
+ }
52
+ }
53
+
54
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
55
+ context.startForegroundService(intent)
56
+ } else {
57
+ context.startService(intent)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Stop the foreground service.
63
+ */
64
+ fun stop(context: Context) {
65
+ val intent = Intent(context, TrackingForegroundService::class.java)
66
+ context.stopService(intent)
67
+ }
68
+ }
69
+
70
+ override fun onBind(intent: Intent?): IBinder? {
71
+ // Not a bound service
72
+ return null
73
+ }
74
+
75
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
76
+ val title = intent?.getStringExtra(EXTRA_NOTIFICATION_TITLE) ?: "Live Tracking"
77
+ val text = intent?.getStringExtra(EXTRA_NOTIFICATION_TEXT) ?: "Tracking your location"
78
+ val iconName = intent?.getStringExtra(EXTRA_NOTIFICATION_ICON)
79
+ val channelId = intent?.getStringExtra(EXTRA_CHANNEL_ID) ?: DEFAULT_CHANNEL_ID
80
+ val channelName = intent?.getStringExtra(EXTRA_CHANNEL_NAME) ?: DEFAULT_CHANNEL_NAME
81
+
82
+ createNotificationChannel(channelId, channelName)
83
+
84
+ val notification = buildNotification(title, text, iconName, channelId)
85
+ startForeground(NOTIFICATION_ID, notification)
86
+
87
+ return START_STICKY
88
+ }
89
+
90
+ override fun onDestroy() {
91
+ super.onDestroy()
92
+ stopForeground(STOP_FOREGROUND_REMOVE)
93
+ }
94
+
95
+ /**
96
+ * Creates the notification channel for Android O and above.
97
+ */
98
+ private fun createNotificationChannel(channelId: String, channelName: String) {
99
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
100
+ val channel = NotificationChannel(
101
+ channelId,
102
+ channelName,
103
+ NotificationManager.IMPORTANCE_LOW
104
+ ).apply {
105
+ description = "Notification channel for live location tracking"
106
+ setShowBadge(false)
107
+ }
108
+
109
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
110
+ notificationManager.createNotificationChannel(channel)
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Builds the persistent notification displayed while tracking is active.
116
+ */
117
+ private fun buildNotification(
118
+ title: String,
119
+ text: String,
120
+ iconName: String?,
121
+ channelId: String
122
+ ): Notification {
123
+ val iconResId = if (iconName != null) {
124
+ resources.getIdentifier(iconName, "drawable", packageName)
125
+ } else {
126
+ 0
127
+ }
128
+
129
+ // Fallback to app icon if custom icon not found
130
+ val finalIconResId = if (iconResId != 0) {
131
+ iconResId
132
+ } else {
133
+ applicationInfo.icon
134
+ }
135
+
136
+ return NotificationCompat.Builder(this, channelId)
137
+ .setContentTitle(title)
138
+ .setContentText(text)
139
+ .setSmallIcon(finalIconResId)
140
+ .setOngoing(true)
141
+ .setPriority(NotificationCompat.PRIORITY_LOW)
142
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
143
+ .build()
144
+ }
145
+ }
@@ -0,0 +1,277 @@
1
+ package com.livetracking.sync
2
+
3
+ import com.google.firebase.database.FirebaseDatabase
4
+ import com.google.firebase.firestore.FirebaseFirestore
5
+ import com.livetracking.queue.QueuedLocation
6
+ import java.util.concurrent.ExecutorService
7
+ import java.util.concurrent.Executors
8
+ import kotlin.math.pow
9
+ import kotlin.random.Random
10
+
11
+ /**
12
+ * Callback interface for Firebase sync operations.
13
+ */
14
+ interface SyncCallback {
15
+ fun onSuccess()
16
+ fun onError(errorCode: String, message: String)
17
+ }
18
+
19
+ /**
20
+ * FirebaseSyncEngine handles writing location data to Firebase.
21
+ *
22
+ * Supports both Firebase Realtime Database (RTDB) and Cloud Firestore.
23
+ * Implements retry with exponential backoff for resilient writes.
24
+ *
25
+ * - updateCurrentLocation: overwrites the current location at currentLocationPath
26
+ * - pushHistoryBatch: appends a batch of locations to historyPath
27
+ *
28
+ * @param service Either "RTDB" or "Firestore"
29
+ * @param currentLocationPath Firebase path for current location (overwrite)
30
+ * @param historyPath Firebase path for history locations (append)
31
+ */
32
+ class FirebaseSyncEngine(
33
+ private val service: String,
34
+ private val currentLocationPath: String?,
35
+ private val historyPath: String?
36
+ ) {
37
+
38
+ companion object {
39
+ private const val BASE_DELAY_MS = 1000L
40
+ private const val MULTIPLIER = 2.0
41
+ private const val JITTER_MS = 200L
42
+ private const val MAX_RETRIES_CURRENT = 3
43
+ private const val MAX_RETRIES_HISTORY = 5
44
+ }
45
+
46
+ private val executor: ExecutorService = Executors.newSingleThreadExecutor()
47
+
48
+ /**
49
+ * Update the current location at the configured currentLocationPath.
50
+ * Uses set/update (overwrite) semantics.
51
+ * Retries up to 3 times with exponential backoff on failure.
52
+ *
53
+ * @param latitude Location latitude
54
+ * @param longitude Location longitude
55
+ * @param timestamp Unix timestamp in milliseconds
56
+ * @param accuracy Location accuracy in meters
57
+ * @param speed Speed in m/s, nullable
58
+ * @param callback SyncCallback for success/error notification
59
+ */
60
+ fun updateCurrentLocation(
61
+ latitude: Double,
62
+ longitude: Double,
63
+ timestamp: Long,
64
+ accuracy: Float,
65
+ speed: Float?,
66
+ callback: SyncCallback
67
+ ) {
68
+ if (currentLocationPath == null) {
69
+ callback.onError("NO_PATH", "currentLocationPath is not configured")
70
+ return
71
+ }
72
+
73
+ val data = hashMapOf<String, Any>(
74
+ "latitude" to latitude,
75
+ "longitude" to longitude,
76
+ "timestamp" to timestamp,
77
+ "accuracy" to accuracy.toDouble(),
78
+ "updatedAt" to timestamp
79
+ )
80
+ speed?.let { data["speed"] = it.toDouble() }
81
+
82
+ executor.execute {
83
+ executeWithRetry(
84
+ maxRetries = MAX_RETRIES_CURRENT,
85
+ operation = { attemptCallback ->
86
+ writeCurrentLocation(data, attemptCallback)
87
+ },
88
+ callback = callback
89
+ )
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Push a batch of locations to the configured historyPath.
95
+ * Uses push/append semantics (each location gets a unique key).
96
+ * Retries up to 5 times with exponential backoff on failure.
97
+ *
98
+ * @param locations List of QueuedLocation to send
99
+ * @param callback SyncCallback for success/error notification
100
+ */
101
+ fun pushHistoryBatch(locations: List<QueuedLocation>, callback: SyncCallback) {
102
+ if (historyPath == null) {
103
+ callback.onError("NO_PATH", "historyPath is not configured")
104
+ return
105
+ }
106
+
107
+ if (locations.isEmpty()) {
108
+ callback.onSuccess()
109
+ return
110
+ }
111
+
112
+ executor.execute {
113
+ executeWithRetry(
114
+ maxRetries = MAX_RETRIES_HISTORY,
115
+ operation = { attemptCallback ->
116
+ writeHistoryBatch(locations, attemptCallback)
117
+ },
118
+ callback = callback
119
+ )
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Shutdown the executor service. Call when the engine is no longer needed.
125
+ */
126
+ fun shutdown() {
127
+ executor.shutdown()
128
+ }
129
+
130
+ // --- Private implementation ---
131
+
132
+ private fun writeCurrentLocation(data: Map<String, Any>, callback: SyncCallback) {
133
+ when (service) {
134
+ "RTDB" -> writeCurrentLocationRTDB(data, callback)
135
+ "Firestore" -> writeCurrentLocationFirestore(data, callback)
136
+ else -> callback.onError("INVALID_SERVICE", "Unknown service: $service. Use 'RTDB' or 'Firestore'.")
137
+ }
138
+ }
139
+
140
+ private fun writeCurrentLocationRTDB(data: Map<String, Any>, callback: SyncCallback) {
141
+ val reference = FirebaseDatabase.getInstance().getReference(currentLocationPath!!)
142
+ reference.setValue(data)
143
+ .addOnSuccessListener { callback.onSuccess() }
144
+ .addOnFailureListener { e ->
145
+ callback.onError("FIREBASE_WRITE_FAILED", e.message ?: "RTDB write failed")
146
+ }
147
+ }
148
+
149
+ private fun writeCurrentLocationFirestore(data: Map<String, Any>, callback: SyncCallback) {
150
+ val document = FirebaseFirestore.getInstance().document(currentLocationPath!!)
151
+ document.set(data)
152
+ .addOnSuccessListener { callback.onSuccess() }
153
+ .addOnFailureListener { e ->
154
+ callback.onError("FIREBASE_WRITE_FAILED", e.message ?: "Firestore write failed")
155
+ }
156
+ }
157
+
158
+ private fun writeHistoryBatch(locations: List<QueuedLocation>, callback: SyncCallback) {
159
+ when (service) {
160
+ "RTDB" -> writeHistoryBatchRTDB(locations, callback)
161
+ "Firestore" -> writeHistoryBatchFirestore(locations, callback)
162
+ else -> callback.onError("INVALID_SERVICE", "Unknown service: $service. Use 'RTDB' or 'Firestore'.")
163
+ }
164
+ }
165
+
166
+ private fun writeHistoryBatchRTDB(locations: List<QueuedLocation>, callback: SyncCallback) {
167
+ val reference = FirebaseDatabase.getInstance().getReference(historyPath!!)
168
+ val updates = hashMapOf<String, Any>()
169
+
170
+ for (location in locations) {
171
+ val key = reference.push().key ?: continue
172
+ updates[key] = locationToMap(location)
173
+ }
174
+
175
+ if (updates.isEmpty()) {
176
+ callback.onSuccess()
177
+ return
178
+ }
179
+
180
+ reference.updateChildren(updates)
181
+ .addOnSuccessListener { callback.onSuccess() }
182
+ .addOnFailureListener { e ->
183
+ callback.onError("FIREBASE_WRITE_FAILED", e.message ?: "RTDB batch write failed")
184
+ }
185
+ }
186
+
187
+ private fun writeHistoryBatchFirestore(locations: List<QueuedLocation>, callback: SyncCallback) {
188
+ val firestore = FirebaseFirestore.getInstance()
189
+ val collection = firestore.collection(historyPath!!)
190
+ val batch = firestore.batch()
191
+
192
+ for (location in locations) {
193
+ val docRef = collection.document()
194
+ batch.set(docRef, locationToMap(location))
195
+ }
196
+
197
+ batch.commit()
198
+ .addOnSuccessListener { callback.onSuccess() }
199
+ .addOnFailureListener { e ->
200
+ callback.onError("FIREBASE_WRITE_FAILED", e.message ?: "Firestore batch write failed")
201
+ }
202
+ }
203
+
204
+ private fun locationToMap(location: QueuedLocation): Map<String, Any?> {
205
+ val map = hashMapOf<String, Any?>(
206
+ "latitude" to location.latitude,
207
+ "longitude" to location.longitude,
208
+ "timestamp" to location.timestamp,
209
+ "accuracy" to location.accuracy.toDouble(),
210
+ "speed" to location.speed?.toDouble()
211
+ )
212
+ location.altitude?.let { map["altitude"] = it }
213
+ location.bearing?.let { map["bearing"] = it.toDouble() }
214
+ return map
215
+ }
216
+
217
+ /**
218
+ * Execute an operation with exponential backoff retry.
219
+ *
220
+ * Retry delay formula: baseDelay * 2^(attempt-1) ± jitter
221
+ * - Base delay: 1000ms
222
+ * - Multiplier: 2x
223
+ * - Jitter: ±200ms (random)
224
+ *
225
+ * @param maxRetries Maximum number of retry attempts
226
+ * @param operation The Firebase write operation to execute
227
+ * @param callback Final callback after all retries exhausted or success
228
+ */
229
+ private fun executeWithRetry(
230
+ maxRetries: Int,
231
+ operation: (SyncCallback) -> Unit,
232
+ callback: SyncCallback
233
+ ) {
234
+ var attempt = 0
235
+
236
+ fun tryOperation() {
237
+ attempt++
238
+ operation(object : SyncCallback {
239
+ override fun onSuccess() {
240
+ callback.onSuccess()
241
+ }
242
+
243
+ override fun onError(errorCode: String, message: String) {
244
+ if (attempt >= maxRetries) {
245
+ callback.onError(errorCode, "Failed after $attempt attempts: $message")
246
+ } else {
247
+ val delay = calculateBackoffDelay(attempt)
248
+ try {
249
+ Thread.sleep(delay)
250
+ } catch (e: InterruptedException) {
251
+ Thread.currentThread().interrupt()
252
+ callback.onError("INTERRUPTED", "Retry interrupted")
253
+ return
254
+ }
255
+ tryOperation()
256
+ }
257
+ }
258
+ })
259
+ }
260
+
261
+ tryOperation()
262
+ }
263
+
264
+ /**
265
+ * Calculate exponential backoff delay with jitter.
266
+ *
267
+ * Formula: baseDelay * 2^(attempt-1) ± random jitter
268
+ *
269
+ * @param attempt Current attempt number (1-indexed)
270
+ * @return Delay in milliseconds
271
+ */
272
+ internal fun calculateBackoffDelay(attempt: Int): Long {
273
+ val exponentialDelay = (BASE_DELAY_MS * MULTIPLIER.pow((attempt - 1).toDouble())).toLong()
274
+ val jitter = Random.nextLong(-JITTER_MS, JITTER_MS + 1)
275
+ return maxOf(0L, exponentialDelay + jitter)
276
+ }
277
+ }
@@ -0,0 +1,31 @@
1
+ package com.livetracking.sync
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 null, they should be omitted or written as null
13
+ * in the Firebase payload — never as placeholder values (0, -1, etc.).
14
+ *
15
+ * @param latitude Location latitude in degrees
16
+ * @param longitude Location longitude in degrees
17
+ * @param timestamp Unix timestamp in milliseconds
18
+ * @param accuracy Location accuracy in meters
19
+ * @param speed Speed in m/s, or null if unavailable
20
+ * @param altitude Altitude in meters, or null if unavailable
21
+ * @param bearing Bearing in degrees, or null if unavailable
22
+ */
23
+ data class LocationDataPoint(
24
+ val latitude: Double,
25
+ val longitude: Double,
26
+ val timestamp: Long,
27
+ val accuracy: Float,
28
+ val speed: Float? = null,
29
+ val altitude: Double? = null,
30
+ val bearing: Float? = null
31
+ )
@@ -0,0 +1,220 @@
1
+ package com.livetracking.sync
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONException
5
+ import org.json.JSONObject
6
+ import java.util.concurrent.ExecutorService
7
+ import java.util.concurrent.Executors
8
+
9
+ /**
10
+ * SyncEngineController replaces the monolithic FirebaseSyncEngine.
11
+ *
12
+ * It manages a list of TargetHandler instances — one per configured sync target.
13
+ * When a location arrives, it fans out to all handlers in parallel.
14
+ * Each handler independently manages batching, retry, and offline queue logic.
15
+ *
16
+ * Responsibilities:
17
+ * - Parse targets JSON and instantiate one TargetHandler per target
18
+ * - Dispatch location data to all handlers concurrently
19
+ * - Flush all partial batches on stop
20
+ * - Report per-target queued location counts
21
+ * - Clean up all handlers on shutdown
22
+ *
23
+ * @param targetsJson JSON array string of sync target configurations
24
+ * @param firebaseService The Firebase service type ("RTDB" or "Firestore")
25
+ * @param networkChecker Function that returns current online status
26
+ * @param offlineQueueProvider Optional provider for offline persistence
27
+ * @param eventListener Listener for error/warning events
28
+ * @throws IllegalArgumentException if targetsJson fails to parse or contains invalid targets
29
+ */
30
+ class SyncEngineController(
31
+ targetsJson: String,
32
+ private val firebaseService: String,
33
+ private val networkChecker: () -> Boolean,
34
+ private val offlineQueueProvider: OfflineQueueProvider? = null,
35
+ private val eventListener: TargetEventListener? = null
36
+ ) {
37
+
38
+ private val handlers: List<TargetHandler>
39
+ private val dispatchExecutor: ExecutorService = Executors.newCachedThreadPool()
40
+
41
+ init {
42
+ handlers = parseAndCreateHandlers(targetsJson)
43
+ }
44
+
45
+ /**
46
+ * Dispatch a location data point to all configured sync targets in parallel.
47
+ *
48
+ * Each target handler receives the location independently and decides
49
+ * whether to accumulate (batch), write immediately, or queue offline.
50
+ * A failure in one target does not block others.
51
+ *
52
+ * @param location The location data point to dispatch
53
+ */
54
+ fun dispatchLocation(location: LocationDataPoint) {
55
+ for (handler in handlers) {
56
+ dispatchExecutor.execute {
57
+ handler.dispatch(location)
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Flush all partially-filled batches for all targets.
64
+ *
65
+ * Called during stop() to ensure no accumulated data is lost.
66
+ * Blocks until all handlers have completed their flush.
67
+ */
68
+ fun flushAll() {
69
+ for (handler in handlers) {
70
+ handler.flush()
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get the number of queued locations for each configured target.
76
+ *
77
+ * Returns a map of target path → queued count.
78
+ * Targets with offlineQueue disabled report 0.
79
+ *
80
+ * @return Map of target path to queued location count
81
+ */
82
+ fun getQueuedCounts(): Map<String, Int> {
83
+ val counts = mutableMapOf<String, Int>()
84
+ for (handler in handlers) {
85
+ counts[handler.targetPath] = handler.getQueuedCount()
86
+ }
87
+ return counts
88
+ }
89
+
90
+ /**
91
+ * Shutdown all handlers and release resources.
92
+ *
93
+ * Cancels any in-progress retries and batch timers.
94
+ * After shutdown, no further dispatches will be processed.
95
+ */
96
+ fun shutdown() {
97
+ for (handler in handlers) {
98
+ handler.shutdown()
99
+ }
100
+ dispatchExecutor.shutdown()
101
+ }
102
+
103
+ /**
104
+ * Flush offline queues for all targets that have offlineQueue enabled.
105
+ * Called when network connectivity is restored.
106
+ */
107
+ fun flushOfflineQueues() {
108
+ for (handler in handlers) {
109
+ handler.flushOfflineQueue()
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get the list of configured target paths.
115
+ *
116
+ * @return List of target path strings
117
+ */
118
+ fun getTargetPaths(): List<String> {
119
+ return handlers.map { it.targetPath }
120
+ }
121
+
122
+ // --- Private implementation ---
123
+
124
+ /**
125
+ * Parse the targets JSON array and create one TargetHandler per target.
126
+ *
127
+ * @param targetsJson JSON array string of sync target configurations
128
+ * @return List of TargetHandler instances
129
+ * @throws IllegalArgumentException if JSON is invalid or targets are missing required fields
130
+ */
131
+ private fun parseAndCreateHandlers(targetsJson: String): List<TargetHandler> {
132
+ val configs = parseTargetsJson(targetsJson)
133
+ return configs.map { config ->
134
+ val queueProvider = if (config.offlineQueue) offlineQueueProvider else null
135
+ TargetHandler(
136
+ config = config,
137
+ firebaseService = firebaseService,
138
+ networkChecker = networkChecker,
139
+ offlineQueueProvider = queueProvider,
140
+ eventListener = eventListener
141
+ )
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Parse a JSON array string into a list of SyncTargetConfig objects.
147
+ *
148
+ * Each target must have:
149
+ * - "path" (string, required)
150
+ * - "method" (string, required: "set", "push", or "update")
151
+ * - "batchSize" (int, optional, defaults to 1)
152
+ * - "offlineQueue" (boolean, optional, defaults to false)
153
+ *
154
+ * @param json JSON array string
155
+ * @return List of SyncTargetConfig
156
+ * @throws IllegalArgumentException if parsing fails or required fields are missing
157
+ */
158
+ private fun parseTargetsJson(json: String): List<SyncTargetConfig> {
159
+ val jsonArray: JSONArray
160
+ try {
161
+ jsonArray = JSONArray(json)
162
+ } catch (e: JSONException) {
163
+ throw IllegalArgumentException(
164
+ "Failed to parse targets JSON: ${e.message}"
165
+ )
166
+ }
167
+
168
+ if (jsonArray.length() == 0) {
169
+ throw IllegalArgumentException(
170
+ "Targets array is empty — at least one sync target is required"
171
+ )
172
+ }
173
+
174
+ val configs = mutableListOf<SyncTargetConfig>()
175
+
176
+ for (i in 0 until jsonArray.length()) {
177
+ val obj: JSONObject
178
+ try {
179
+ obj = jsonArray.getJSONObject(i)
180
+ } catch (e: JSONException) {
181
+ throw IllegalArgumentException(
182
+ "Target at index $i is not a valid JSON object: ${e.message}"
183
+ )
184
+ }
185
+
186
+ val path = obj.optString("path", "")
187
+ if (path.isEmpty()) {
188
+ throw IllegalArgumentException(
189
+ "Target at index $i is missing required field 'path'"
190
+ )
191
+ }
192
+
193
+ val method = obj.optString("method", "")
194
+ if (method.isEmpty()) {
195
+ throw IllegalArgumentException(
196
+ "Target at index $i is missing required field 'method'"
197
+ )
198
+ }
199
+ if (method !in listOf("set", "push", "update")) {
200
+ throw IllegalArgumentException(
201
+ "Target at index $i has invalid method '$method'. Must be 'set', 'push', or 'update'"
202
+ )
203
+ }
204
+
205
+ val batchSize = obj.optInt("batchSize", 1)
206
+ val offlineQueue = obj.optBoolean("offlineQueue", false)
207
+
208
+ configs.add(
209
+ SyncTargetConfig(
210
+ path = path,
211
+ method = method,
212
+ batchSize = batchSize,
213
+ offlineQueue = offlineQueue
214
+ )
215
+ )
216
+ }
217
+
218
+ return configs
219
+ }
220
+ }
@@ -0,0 +1,20 @@
1
+ package com.livetracking.sync
2
+
3
+ /**
4
+ * Configuration for a single sync target.
5
+ *
6
+ * Each sync target defines a Firebase path, write method, optional batch size,
7
+ * and optional offline queue preference. The native SyncEngineController
8
+ * instantiates one TargetHandler per SyncTargetConfig.
9
+ *
10
+ * @param path Firebase path to write location data to
11
+ * @param method Write method: "set" (overwrite), "push" (append), or "update" (merge)
12
+ * @param batchSize Number of location points to accumulate before writing. Default: 1 (immediate write)
13
+ * @param offlineQueue Whether to persist data locally when offline. Default: false
14
+ */
15
+ data class SyncTargetConfig(
16
+ val path: String,
17
+ val method: String,
18
+ val batchSize: Int = 1,
19
+ val offlineQueue: Boolean = false
20
+ )