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