@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.permissions
2
+
3
+ import android.Manifest
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.location.LocationManager
7
+ import android.os.Build
8
+ import androidx.core.content.ContextCompat
9
+
10
+ /**
11
+ * Represents the result of a permission check.
12
+ */
13
+ sealed class PermissionResult {
14
+ /** All required permissions are granted. */
15
+ object Granted : PermissionResult()
16
+
17
+ /** One or more permissions are denied or a required service is disabled. */
18
+ data class Denied(
19
+ val errorCode: String,
20
+ val message: String
21
+ ) : PermissionResult()
22
+ }
23
+
24
+ /**
25
+ * Handles location permission checks for the LiveTracking library.
26
+ * Provides clear error codes when permissions are denied or GPS is disabled.
27
+ *
28
+ * Validates: Requirements 10.1, 10.2
29
+ */
30
+ class PermissionHandler {
31
+
32
+ companion object {
33
+ const val ERROR_PERMISSION_DENIED = "PERMISSION_DENIED"
34
+ const val ERROR_GPS_DISABLED = "GPS_DISABLED"
35
+ }
36
+
37
+ /**
38
+ * Checks if fine and coarse location permissions are granted.
39
+ *
40
+ * @param context Android context used for permission checks
41
+ * @return [PermissionResult.Granted] if both ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION
42
+ * are granted, [PermissionResult.Denied] otherwise with error code PERMISSION_DENIED
43
+ */
44
+ fun checkLocationPermissions(context: Context): PermissionResult {
45
+ val fineLocationGranted = ContextCompat.checkSelfPermission(
46
+ context,
47
+ Manifest.permission.ACCESS_FINE_LOCATION
48
+ ) == PackageManager.PERMISSION_GRANTED
49
+
50
+ val coarseLocationGranted = ContextCompat.checkSelfPermission(
51
+ context,
52
+ Manifest.permission.ACCESS_COARSE_LOCATION
53
+ ) == PackageManager.PERMISSION_GRANTED
54
+
55
+ return if (fineLocationGranted && coarseLocationGranted) {
56
+ PermissionResult.Granted
57
+ } else {
58
+ val deniedPermissions = mutableListOf<String>()
59
+ if (!fineLocationGranted) deniedPermissions.add("ACCESS_FINE_LOCATION")
60
+ if (!coarseLocationGranted) deniedPermissions.add("ACCESS_COARSE_LOCATION")
61
+
62
+ PermissionResult.Denied(
63
+ errorCode = ERROR_PERMISSION_DENIED,
64
+ message = "Location permissions denied: ${deniedPermissions.joinToString(", ")}. " +
65
+ "Please grant location permissions to enable tracking."
66
+ )
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Checks if background location permission is granted (Android 10+ / API 29+).
72
+ * On devices below Android 10, background location is implicitly granted
73
+ * when foreground location is granted.
74
+ *
75
+ * @param context Android context used for permission checks
76
+ * @return [PermissionResult.Granted] if background location is available,
77
+ * [PermissionResult.Denied] otherwise with error code PERMISSION_DENIED
78
+ */
79
+ fun checkBackgroundLocationPermission(context: Context): PermissionResult {
80
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
81
+ // Below Android 10, background location is granted with foreground location
82
+ return PermissionResult.Granted
83
+ }
84
+
85
+ val backgroundLocationGranted = ContextCompat.checkSelfPermission(
86
+ context,
87
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
88
+ ) == PackageManager.PERMISSION_GRANTED
89
+
90
+ return if (backgroundLocationGranted) {
91
+ PermissionResult.Granted
92
+ } else {
93
+ PermissionResult.Denied(
94
+ errorCode = ERROR_PERMISSION_DENIED,
95
+ message = "Background location permission denied: ACCESS_BACKGROUND_LOCATION. " +
96
+ "Please grant 'Allow all the time' location permission for background tracking."
97
+ )
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Checks if GPS/location services are enabled on the device.
103
+ *
104
+ * @param context Android context used to access LocationManager
105
+ * @return true if GPS provider is enabled, false otherwise
106
+ */
107
+ fun isGpsEnabled(context: Context): Boolean {
108
+ val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
109
+ ?: return false
110
+ return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
111
+ }
112
+
113
+ /**
114
+ * Performs a comprehensive check of all permissions and services required for tracking.
115
+ * Checks location permissions, background location, and GPS status in order.
116
+ *
117
+ * @param context Android context
118
+ * @return [PermissionResult.Granted] if all checks pass, or the first
119
+ * [PermissionResult.Denied] encountered
120
+ */
121
+ fun checkAllTrackingRequirements(context: Context): PermissionResult {
122
+ // Check foreground location permissions first
123
+ val locationResult = checkLocationPermissions(context)
124
+ if (locationResult is PermissionResult.Denied) {
125
+ return locationResult
126
+ }
127
+
128
+ // Check background location permission
129
+ val backgroundResult = checkBackgroundLocationPermission(context)
130
+ if (backgroundResult is PermissionResult.Denied) {
131
+ return backgroundResult
132
+ }
133
+
134
+ // Check GPS is enabled
135
+ if (!isGpsEnabled(context)) {
136
+ return PermissionResult.Denied(
137
+ errorCode = ERROR_GPS_DISABLED,
138
+ message = "GPS/Location services are disabled. " +
139
+ "Please enable location services in device settings."
140
+ )
141
+ }
142
+
143
+ return PermissionResult.Granted
144
+ }
145
+ }
@@ -0,0 +1,167 @@
1
+ package com.livetracking.queue
2
+
3
+ import android.content.Context
4
+ import com.livetracking.sync.OfflineQueueProvider
5
+ import java.util.UUID
6
+ import java.util.concurrent.Callable
7
+ import java.util.concurrent.ExecutorService
8
+ import java.util.concurrent.Executors
9
+ import java.util.concurrent.Future
10
+
11
+ /**
12
+ * QueueEngine manages the offline location queue using SQLite.
13
+ * All database operations run on a dedicated background thread.
14
+ *
15
+ * Implements [OfflineQueueProvider] to provide per-target offline queue
16
+ * operations used by [TargetHandler] for target-scoped persistence.
17
+ */
18
+ class QueueEngine(context: Context) : OfflineQueueProvider {
19
+
20
+ private val db: TrackingDatabase = TrackingDatabase.getInstance(context)
21
+ private val executor: ExecutorService = Executors.newSingleThreadExecutor()
22
+
23
+ // --- Legacy methods (backward-compatible, operate without target_path filter) ---
24
+
25
+ fun enqueue(
26
+ latitude: Double,
27
+ longitude: Double,
28
+ timestamp: Long,
29
+ accuracy: Float,
30
+ speed: Float?,
31
+ altitude: Double?,
32
+ bearing: Float?
33
+ ): Future<Unit> {
34
+ return executor.submit(Callable {
35
+ val location = QueuedLocation(
36
+ id = UUID.randomUUID().toString(),
37
+ latitude = latitude,
38
+ longitude = longitude,
39
+ timestamp = timestamp,
40
+ accuracy = accuracy,
41
+ speed = speed,
42
+ altitude = altitude,
43
+ bearing = bearing,
44
+ createdAt = System.currentTimeMillis()
45
+ )
46
+ db.insert(location)
47
+ })
48
+ }
49
+
50
+ fun dequeueBatch(size: Int): Future<List<QueuedLocation>> {
51
+ return executor.submit(Callable {
52
+ db.getOldestBatch(size)
53
+ })
54
+ }
55
+
56
+ fun removeBatch(ids: List<String>): Future<Unit> {
57
+ return executor.submit(Callable {
58
+ db.deleteByIds(ids)
59
+ })
60
+ }
61
+
62
+ fun count(): Future<Int> {
63
+ return executor.submit(Callable {
64
+ db.getCount()
65
+ })
66
+ }
67
+
68
+ // --- Per-target methods (OfflineQueueProvider implementation) ---
69
+
70
+ /**
71
+ * Enqueue a location data point for a specific target path.
72
+ * Delegates to [TrackingDatabase.insertForTarget] which enforces the
73
+ * 10,000 data point cap per target with oldest-eviction.
74
+ */
75
+ override fun enqueueForTarget(
76
+ targetPath: String,
77
+ latitude: Double,
78
+ longitude: Double,
79
+ timestamp: Long,
80
+ accuracy: Float,
81
+ speed: Float?,
82
+ altitude: Double?,
83
+ bearing: Float?
84
+ ): Future<Unit> {
85
+ return executor.submit(Callable {
86
+ val location = QueuedLocation(
87
+ id = UUID.randomUUID().toString(),
88
+ latitude = latitude,
89
+ longitude = longitude,
90
+ timestamp = timestamp,
91
+ accuracy = accuracy,
92
+ speed = speed,
93
+ altitude = altitude,
94
+ bearing = bearing,
95
+ createdAt = System.currentTimeMillis()
96
+ )
97
+ db.insertForTarget(location, targetPath)
98
+ })
99
+ }
100
+
101
+ /**
102
+ * Dequeue the oldest batch of queued locations for a specific target path.
103
+ * Results are ordered chronologically (oldest first).
104
+ *
105
+ * @param targetPath The target path to dequeue from
106
+ * @param size Maximum number of locations to dequeue
107
+ * @return Future resolving to the list of queued locations
108
+ */
109
+ override fun dequeueBatchForTarget(targetPath: String, size: Int): Future<List<QueuedLocation>> {
110
+ return executor.submit(Callable {
111
+ db.getOldestBatchForTarget(targetPath, size)
112
+ })
113
+ }
114
+
115
+ /**
116
+ * Get the count of queued locations for a specific target path.
117
+ *
118
+ * @param targetPath The target path to count
119
+ * @return Future resolving to the count of queued locations
120
+ */
121
+ override fun countForTarget(targetPath: String): Future<Int> {
122
+ return executor.submit(Callable {
123
+ db.getCountForTarget(targetPath)
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Get the count of queued locations grouped by target path.
129
+ *
130
+ * @return Future resolving to a map of target_path → count
131
+ */
132
+ fun countsByTarget(): Future<Map<String, Int>> {
133
+ return executor.submit(Callable {
134
+ db.getCountsByTarget()
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Evict the oldest queued location for a specific target path.
140
+ * Used to enforce the 10,000 data point cap per target.
141
+ *
142
+ * @param targetPath The target path to evict from
143
+ * @return Future resolving when eviction is complete
144
+ */
145
+ override fun evictOldestForTarget(targetPath: String): Future<Unit> {
146
+ return executor.submit(Callable {
147
+ db.evictOldestForTarget(targetPath)
148
+ })
149
+ }
150
+
151
+ /**
152
+ * Remove a batch of queued locations by their IDs.
153
+ * Used after successful write confirmation during offline queue flush.
154
+ *
155
+ * @param ids List of location IDs to remove
156
+ * @return Future resolving when removal is complete
157
+ */
158
+ override fun removeBatchForTarget(ids: List<String>): Future<Unit> {
159
+ return executor.submit(Callable {
160
+ db.deleteByIds(ids)
161
+ })
162
+ }
163
+
164
+ fun shutdown() {
165
+ executor.shutdown()
166
+ }
167
+ }
@@ -0,0 +1,16 @@
1
+ package com.livetracking.queue
2
+
3
+ /**
4
+ * Data class representing a queued location entry.
5
+ */
6
+ data class QueuedLocation(
7
+ val id: String,
8
+ val latitude: Double,
9
+ val longitude: Double,
10
+ val timestamp: Long,
11
+ val accuracy: Float,
12
+ val speed: Float?,
13
+ val altitude: Double?,
14
+ val bearing: Float?,
15
+ val createdAt: Long
16
+ )
@@ -0,0 +1,239 @@
1
+ package com.livetracking.queue
2
+
3
+ import android.content.ContentValues
4
+ import android.content.Context
5
+ import android.database.sqlite.SQLiteDatabase
6
+ import android.database.sqlite.SQLiteOpenHelper
7
+
8
+ /**
9
+ * SQLite database helper for the location queue.
10
+ * Supports per-target offline queues with a target_path column.
11
+ * Replaces Room to avoid kapt/ksp annotation processor dependency issues.
12
+ */
13
+ class TrackingDatabase(context: Context) : SQLiteOpenHelper(
14
+ context, DATABASE_NAME, null, DATABASE_VERSION
15
+ ) {
16
+ companion object {
17
+ private const val DATABASE_NAME = "live_tracking_db"
18
+ private const val DATABASE_VERSION = 2
19
+ private const val TABLE_NAME = "queued_locations"
20
+ private const val INDEX_NAME = "idx_queued_target_path"
21
+
22
+ /**
23
+ * Maximum number of queued data points per target.
24
+ * When this limit is reached, the oldest data point is evicted.
25
+ */
26
+ const val MAX_QUEUE_SIZE_PER_TARGET = 10_000
27
+
28
+ @Volatile
29
+ private var INSTANCE: TrackingDatabase? = null
30
+
31
+ fun getInstance(context: Context): TrackingDatabase {
32
+ return INSTANCE ?: synchronized(this) {
33
+ INSTANCE ?: TrackingDatabase(context.applicationContext).also { INSTANCE = it }
34
+ }
35
+ }
36
+ }
37
+
38
+ override fun onCreate(db: SQLiteDatabase) {
39
+ db.execSQL("""
40
+ CREATE TABLE $TABLE_NAME (
41
+ id TEXT PRIMARY KEY,
42
+ latitude REAL NOT NULL,
43
+ longitude REAL NOT NULL,
44
+ timestamp INTEGER NOT NULL,
45
+ accuracy REAL NOT NULL,
46
+ speed REAL,
47
+ altitude REAL,
48
+ bearing REAL,
49
+ createdAt INTEGER NOT NULL,
50
+ target_path TEXT NOT NULL DEFAULT ''
51
+ )
52
+ """.trimIndent())
53
+ db.execSQL(
54
+ "CREATE INDEX $INDEX_NAME ON $TABLE_NAME(target_path, createdAt ASC)"
55
+ )
56
+ }
57
+
58
+ override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
59
+ if (oldVersion < 2) {
60
+ // Migration v1 → v2: add target_path column with default empty string
61
+ db.execSQL(
62
+ "ALTER TABLE $TABLE_NAME ADD COLUMN target_path TEXT NOT NULL DEFAULT ''"
63
+ )
64
+ db.execSQL(
65
+ "CREATE INDEX $INDEX_NAME ON $TABLE_NAME(target_path, createdAt ASC)"
66
+ )
67
+ }
68
+ }
69
+
70
+ // --- Legacy methods (backward-compatible, operate without target_path filter) ---
71
+
72
+ fun insert(location: QueuedLocation) {
73
+ val values = ContentValues().apply {
74
+ put("id", location.id)
75
+ put("latitude", location.latitude)
76
+ put("longitude", location.longitude)
77
+ put("timestamp", location.timestamp)
78
+ put("accuracy", location.accuracy)
79
+ put("speed", location.speed)
80
+ put("altitude", location.altitude)
81
+ put("bearing", location.bearing)
82
+ put("createdAt", location.createdAt)
83
+ }
84
+ writableDatabase.insert(TABLE_NAME, null, values)
85
+ }
86
+
87
+ fun getOldestBatch(limit: Int): List<QueuedLocation> {
88
+ val locations = mutableListOf<QueuedLocation>()
89
+ val cursor = readableDatabase.query(
90
+ TABLE_NAME, null, null, null, null, null,
91
+ "createdAt ASC", limit.toString()
92
+ )
93
+ cursor.use {
94
+ while (it.moveToNext()) {
95
+ locations.add(cursorToQueuedLocation(it))
96
+ }
97
+ }
98
+ return locations
99
+ }
100
+
101
+ fun deleteByIds(ids: List<String>) {
102
+ if (ids.isEmpty()) return
103
+ val placeholders = ids.joinToString(",") { "?" }
104
+ writableDatabase.delete(TABLE_NAME, "id IN ($placeholders)", ids.toTypedArray())
105
+ }
106
+
107
+ fun getCount(): Int {
108
+ val cursor = readableDatabase.rawQuery("SELECT COUNT(*) FROM $TABLE_NAME", null)
109
+ cursor.use {
110
+ it.moveToFirst()
111
+ return it.getInt(0)
112
+ }
113
+ }
114
+
115
+ // --- Per-target methods ---
116
+
117
+ /**
118
+ * Insert a location for a specific target path.
119
+ * Enforces the 10,000 data point cap per target by evicting the oldest
120
+ * entry when the limit is reached.
121
+ */
122
+ fun insertForTarget(location: QueuedLocation, targetPath: String) {
123
+ val db = writableDatabase
124
+ // Enforce cap: evict oldest if at limit
125
+ val currentCount = getCountForTarget(targetPath)
126
+ if (currentCount >= MAX_QUEUE_SIZE_PER_TARGET) {
127
+ evictOldestForTarget(targetPath)
128
+ }
129
+
130
+ val values = ContentValues().apply {
131
+ put("id", location.id)
132
+ put("latitude", location.latitude)
133
+ put("longitude", location.longitude)
134
+ put("timestamp", location.timestamp)
135
+ put("accuracy", location.accuracy)
136
+ put("speed", location.speed)
137
+ put("altitude", location.altitude)
138
+ put("bearing", location.bearing)
139
+ put("createdAt", location.createdAt)
140
+ put("target_path", targetPath)
141
+ }
142
+ db.insert(TABLE_NAME, null, values)
143
+ }
144
+
145
+ /**
146
+ * Get the oldest batch of queued locations for a specific target path.
147
+ * Results are ordered by createdAt ASC (oldest first).
148
+ */
149
+ fun getOldestBatchForTarget(targetPath: String, limit: Int): List<QueuedLocation> {
150
+ val locations = mutableListOf<QueuedLocation>()
151
+ val cursor = readableDatabase.query(
152
+ TABLE_NAME,
153
+ null,
154
+ "target_path = ?",
155
+ arrayOf(targetPath),
156
+ null,
157
+ null,
158
+ "createdAt ASC",
159
+ limit.toString()
160
+ )
161
+ cursor.use {
162
+ while (it.moveToNext()) {
163
+ locations.add(cursorToQueuedLocation(it))
164
+ }
165
+ }
166
+ return locations
167
+ }
168
+
169
+ /**
170
+ * Get the count of queued locations for a specific target path.
171
+ */
172
+ fun getCountForTarget(targetPath: String): Int {
173
+ val cursor = readableDatabase.rawQuery(
174
+ "SELECT COUNT(*) FROM $TABLE_NAME WHERE target_path = ?",
175
+ arrayOf(targetPath)
176
+ )
177
+ cursor.use {
178
+ it.moveToFirst()
179
+ return it.getInt(0)
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get the count of queued locations grouped by target path.
185
+ * Returns a map of target_path → count.
186
+ */
187
+ fun getCountsByTarget(): Map<String, Int> {
188
+ val counts = mutableMapOf<String, Int>()
189
+ val cursor = readableDatabase.rawQuery(
190
+ "SELECT target_path, COUNT(*) FROM $TABLE_NAME GROUP BY target_path",
191
+ null
192
+ )
193
+ cursor.use {
194
+ while (it.moveToNext()) {
195
+ val path = it.getString(0)
196
+ val count = it.getInt(1)
197
+ counts[path] = count
198
+ }
199
+ }
200
+ return counts
201
+ }
202
+
203
+ /**
204
+ * Evict the oldest queued location for a specific target path.
205
+ * Used to enforce the 10,000 data point cap per target.
206
+ */
207
+ fun evictOldestForTarget(targetPath: String) {
208
+ writableDatabase.execSQL(
209
+ """
210
+ DELETE FROM $TABLE_NAME WHERE id = (
211
+ SELECT id FROM $TABLE_NAME
212
+ WHERE target_path = ?
213
+ ORDER BY createdAt ASC
214
+ LIMIT 1
215
+ )
216
+ """.trimIndent(),
217
+ arrayOf(targetPath)
218
+ )
219
+ }
220
+
221
+ // --- Private helpers ---
222
+
223
+ private fun cursorToQueuedLocation(cursor: android.database.Cursor): QueuedLocation {
224
+ return QueuedLocation(
225
+ id = cursor.getString(cursor.getColumnIndexOrThrow("id")),
226
+ latitude = cursor.getDouble(cursor.getColumnIndexOrThrow("latitude")),
227
+ longitude = cursor.getDouble(cursor.getColumnIndexOrThrow("longitude")),
228
+ timestamp = cursor.getLong(cursor.getColumnIndexOrThrow("timestamp")),
229
+ accuracy = cursor.getFloat(cursor.getColumnIndexOrThrow("accuracy")),
230
+ speed = if (cursor.isNull(cursor.getColumnIndexOrThrow("speed"))) null
231
+ else cursor.getFloat(cursor.getColumnIndexOrThrow("speed")),
232
+ altitude = if (cursor.isNull(cursor.getColumnIndexOrThrow("altitude"))) null
233
+ else cursor.getDouble(cursor.getColumnIndexOrThrow("altitude")),
234
+ bearing = if (cursor.isNull(cursor.getColumnIndexOrThrow("bearing"))) null
235
+ else cursor.getFloat(cursor.getColumnIndexOrThrow("bearing")),
236
+ createdAt = cursor.getLong(cursor.getColumnIndexOrThrow("createdAt"))
237
+ )
238
+ }
239
+ }
@@ -0,0 +1,53 @@
1
+ package com.livetracking.receiver
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.SharedPreferences
7
+ import com.livetracking.service.TrackingForegroundService
8
+
9
+ /**
10
+ * BroadcastReceiver that listens for BOOT_COMPLETED to auto-restart
11
+ * location tracking after device reboot.
12
+ *
13
+ * Requirements: 2.3
14
+ */
15
+ class BootReceiver : BroadcastReceiver() {
16
+
17
+ override fun onReceive(context: Context, intent: Intent) {
18
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
19
+ if (TrackingStateStore.isTrackingActive(context)) {
20
+ TrackingForegroundService.start(context)
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Helper object for persisting tracking state across device reboots.
28
+ * Uses SharedPreferences to store whether tracking was active before reboot.
29
+ */
30
+ object TrackingStateStore {
31
+
32
+ private const val PREFS_NAME = "live_tracking_prefs"
33
+ private const val KEY_IS_TRACKING_ACTIVE = "is_tracking_active"
34
+
35
+ private fun getPrefs(context: Context): SharedPreferences {
36
+ return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
37
+ }
38
+
39
+ /**
40
+ * Save the current tracking active state.
41
+ * Call with `true` when tracking starts, `false` when tracking stops.
42
+ */
43
+ fun saveTrackingActive(context: Context, isActive: Boolean) {
44
+ getPrefs(context).edit().putBoolean(KEY_IS_TRACKING_ACTIVE, isActive).apply()
45
+ }
46
+
47
+ /**
48
+ * Check if tracking was active before the last reboot/shutdown.
49
+ */
50
+ fun isTrackingActive(context: Context): Boolean {
51
+ return getPrefs(context).getBoolean(KEY_IS_TRACKING_ACTIVE, false)
52
+ }
53
+ }