@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,16 @@
1
+ package com.livetracking
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class LiveTrackingPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(LiveTrackingModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return emptyList()
15
+ }
16
+ }
@@ -0,0 +1,93 @@
1
+ package com.livetracking.location
2
+
3
+ import android.content.Context
4
+ import android.location.Location
5
+ import android.os.Looper
6
+ import com.google.android.gms.location.FusedLocationProviderClient
7
+ import com.google.android.gms.location.LocationCallback
8
+ import com.google.android.gms.location.LocationRequest
9
+ import com.google.android.gms.location.LocationResult
10
+ import com.google.android.gms.location.LocationServices
11
+ import com.google.android.gms.location.Priority
12
+
13
+ /**
14
+ * Android Location Engine that wraps FusedLocationProviderClient.
15
+ * Provides high-accuracy location updates for live tracking.
16
+ *
17
+ * Requirements: 2.2
18
+ */
19
+ class LocationEngine(context: Context) {
20
+
21
+ /**
22
+ * Listener interface for receiving location updates from the engine.
23
+ */
24
+ interface LocationUpdateListener {
25
+ fun onLocationReceived(location: Location)
26
+ }
27
+
28
+ private val fusedLocationClient: FusedLocationProviderClient =
29
+ LocationServices.getFusedLocationProviderClient(context)
30
+
31
+ private var locationListener: LocationUpdateListener? = null
32
+
33
+ private val locationCallback = object : LocationCallback() {
34
+ override fun onLocationResult(locationResult: LocationResult) {
35
+ val location = locationResult.lastLocation ?: return
36
+ locationListener?.onLocationReceived(location)
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Set the listener that will receive location updates.
42
+ *
43
+ * @param listener The listener to receive location callbacks
44
+ */
45
+ fun setLocationListener(listener: LocationUpdateListener) {
46
+ this.locationListener = listener
47
+ }
48
+
49
+ /**
50
+ * Start receiving location updates with the specified interval and distance filter.
51
+ * Uses PRIORITY_HIGH_ACCURACY for best possible location accuracy.
52
+ *
53
+ * @param intervalMs The desired interval for location updates in milliseconds
54
+ * @param distanceFilter The minimum distance between updates in meters
55
+ * @throws SecurityException if location permissions are not granted
56
+ */
57
+ @Throws(SecurityException::class)
58
+ fun startLocationUpdates(intervalMs: Long, distanceFilter: Float) {
59
+ startLocationUpdates(intervalMs, distanceFilter, Priority.PRIORITY_HIGH_ACCURACY)
60
+ }
61
+
62
+ /**
63
+ * Start receiving location updates with the specified interval, distance filter, and priority.
64
+ *
65
+ * @param intervalMs The desired interval for location updates in milliseconds
66
+ * @param distanceFilter The minimum distance between updates in meters
67
+ * @param priority The location request priority (e.g., Priority.PRIORITY_HIGH_ACCURACY or Priority.PRIORITY_LOW_POWER)
68
+ * @throws SecurityException if location permissions are not granted
69
+ */
70
+ @Throws(SecurityException::class)
71
+ fun startLocationUpdates(intervalMs: Long, distanceFilter: Float, priority: Int) {
72
+ // When distanceFilter is 0 or negative, disable distance filtering entirely
73
+ // so FusedLocationProvider delivers updates purely on interval
74
+ val effectiveDistanceFilter = if (distanceFilter <= 0f) 0f else distanceFilter
75
+ val locationRequest = LocationRequest.Builder(priority, intervalMs)
76
+ .setMinUpdateDistanceMeters(effectiveDistanceFilter)
77
+ .setWaitForAccurateLocation(false)
78
+ .build()
79
+
80
+ fusedLocationClient.requestLocationUpdates(
81
+ locationRequest,
82
+ locationCallback,
83
+ Looper.getMainLooper()
84
+ )
85
+ }
86
+
87
+ /**
88
+ * Stop receiving location updates and remove the location callback.
89
+ */
90
+ fun stopLocationUpdates() {
91
+ fusedLocationClient.removeLocationUpdates(locationCallback)
92
+ }
93
+ }
@@ -0,0 +1,127 @@
1
+ package com.livetracking.network
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.Network
6
+ import android.net.NetworkCapabilities
7
+ import android.os.Build
8
+ import androidx.annotation.RequiresApi
9
+
10
+ /**
11
+ * Listener interface for network state changes.
12
+ * Implementations receive callbacks when connectivity is gained or lost.
13
+ */
14
+ interface NetworkStateListener {
15
+ /**
16
+ * Called when network connectivity is restored.
17
+ * Use this to trigger queue flush for pending location data.
18
+ */
19
+ fun onNetworkAvailable()
20
+
21
+ /**
22
+ * Called when network connectivity is lost.
23
+ */
24
+ fun onNetworkLost()
25
+ }
26
+
27
+ /**
28
+ * NetworkListener monitors device network connectivity using ConnectivityManager.
29
+ * When connectivity is restored after being offline, it notifies the registered listener
30
+ * so that pending queued locations can be flushed to Firebase.
31
+ *
32
+ * Requires API 24+ for registerDefaultNetworkCallback.
33
+ *
34
+ * Usage:
35
+ * ```
36
+ * val listener = NetworkListener(context)
37
+ * listener.setNetworkStateListener(object : NetworkStateListener {
38
+ * override fun onNetworkAvailable() { /* flush queue */ }
39
+ * override fun onNetworkLost() { /* mark offline */ }
40
+ * })
41
+ * listener.startListening()
42
+ * // ...
43
+ * listener.stopListening()
44
+ * ```
45
+ */
46
+ class NetworkListener(private val context: Context) {
47
+
48
+ private val connectivityManager: ConnectivityManager =
49
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
50
+
51
+ private var networkStateListener: NetworkStateListener? = null
52
+ private var isListening = false
53
+ private var currentlyOnline = false
54
+
55
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
56
+ override fun onAvailable(network: Network) {
57
+ currentlyOnline = true
58
+ networkStateListener?.onNetworkAvailable()
59
+ }
60
+
61
+ override fun onLost(network: Network) {
62
+ currentlyOnline = false
63
+ networkStateListener?.onNetworkLost()
64
+ }
65
+ }
66
+
67
+ init {
68
+ // Initialize current connectivity state
69
+ currentlyOnline = checkCurrentConnectivity()
70
+ }
71
+
72
+ /**
73
+ * Set the listener that will receive network state change callbacks.
74
+ *
75
+ * @param listener The NetworkStateListener implementation to notify
76
+ */
77
+ fun setNetworkStateListener(listener: NetworkStateListener) {
78
+ this.networkStateListener = listener
79
+ }
80
+
81
+ /**
82
+ * Start listening for network connectivity changes.
83
+ * Registers a default network callback with ConnectivityManager (API 24+).
84
+ * If already listening, this is a no-op.
85
+ */
86
+ @RequiresApi(Build.VERSION_CODES.N)
87
+ fun startListening() {
88
+ if (isListening) return
89
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
90
+ isListening = true
91
+ }
92
+
93
+ /**
94
+ * Stop listening for network connectivity changes.
95
+ * Unregisters the network callback from ConnectivityManager.
96
+ * If not currently listening, this is a no-op.
97
+ */
98
+ fun stopListening() {
99
+ if (!isListening) return
100
+ try {
101
+ connectivityManager.unregisterNetworkCallback(networkCallback)
102
+ } catch (e: IllegalArgumentException) {
103
+ // Callback was not registered, ignore
104
+ }
105
+ isListening = false
106
+ }
107
+
108
+ /**
109
+ * Returns the current network connectivity state.
110
+ *
111
+ * @return true if the device currently has network connectivity, false otherwise
112
+ */
113
+ fun isOnline(): Boolean {
114
+ return currentlyOnline
115
+ }
116
+
117
+ /**
118
+ * Check current connectivity state using ConnectivityManager.
119
+ * Uses NetworkCapabilities for API 23+ for accurate detection.
120
+ */
121
+ private fun checkCurrentConnectivity(): Boolean {
122
+ val activeNetwork = connectivityManager.activeNetwork ?: return false
123
+ val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
124
+ return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
125
+ capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
126
+ }
127
+ }
@@ -0,0 +1,248 @@
1
+ package com.livetracking.optimizer
2
+
3
+ import android.app.PendingIntent
4
+ import android.content.BroadcastReceiver
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.IntentFilter
8
+ import android.os.Build
9
+ import android.os.SystemClock
10
+ import com.google.android.gms.location.ActivityRecognition
11
+ import com.google.android.gms.location.ActivityRecognitionClient
12
+ import com.google.android.gms.location.ActivityRecognitionResult
13
+ import com.google.android.gms.location.DetectedActivity
14
+
15
+ /**
16
+ * Android Activity Recognition Handler that wraps ActivityRecognitionClient.
17
+ * Detects user activity (STILL, ON_FOOT, IN_VEHICLE) and tracks STILL duration
18
+ * to support Motion Sleep Mode for battery optimization.
19
+ *
20
+ * Requirements: 8.1, 8.3
21
+ */
22
+ class ActivityRecognitionHandler(private val context: Context) {
23
+
24
+ companion object {
25
+ private const val ACTION_ACTIVITY_RECOGNIZED =
26
+ "com.livetracking.ACTION_ACTIVITY_RECOGNIZED"
27
+
28
+ /** Default detection interval in milliseconds */
29
+ private const val DETECTION_INTERVAL_MS = 10_000L
30
+
31
+ /** Minimum confidence level to accept an activity detection */
32
+ private const val MIN_CONFIDENCE = 50
33
+ }
34
+
35
+ /**
36
+ * Activity types recognized by the handler.
37
+ */
38
+ enum class ActivityType {
39
+ STILL,
40
+ ON_FOOT,
41
+ IN_VEHICLE,
42
+ UNKNOWN
43
+ }
44
+
45
+ /**
46
+ * Listener interface for receiving activity state changes.
47
+ */
48
+ interface ActivityStateListener {
49
+ /**
50
+ * Called when the detected activity type changes.
51
+ *
52
+ * @param activity The new detected activity type
53
+ */
54
+ fun onActivityChanged(activity: ActivityType)
55
+
56
+ /**
57
+ * Called when the STILL duration exceeds the configured threshold.
58
+ *
59
+ * @param durationMs The duration in milliseconds that the device has been still
60
+ */
61
+ fun onStillDurationExceeded(durationMs: Long)
62
+ }
63
+
64
+ private val activityRecognitionClient: ActivityRecognitionClient =
65
+ ActivityRecognition.getClient(context)
66
+
67
+ private var listener: ActivityStateListener? = null
68
+ private var currentActivity: ActivityType = ActivityType.UNKNOWN
69
+ private var stillStartTimeMs: Long = 0L
70
+ private var isStill: Boolean = false
71
+ private var isRunning: Boolean = false
72
+
73
+ /** Threshold in milliseconds for STILL duration notification (default: 3 minutes) */
74
+ var stillThresholdMs: Long = 180_000L
75
+
76
+ private var stillDurationExceededNotified: Boolean = false
77
+
78
+ private val activityReceiver = object : BroadcastReceiver() {
79
+ override fun onReceive(context: Context?, intent: Intent?) {
80
+ if (intent == null) return
81
+ if (ActivityRecognitionResult.hasResult(intent)) {
82
+ val result = ActivityRecognitionResult.extractResult(intent) ?: return
83
+ handleActivityResult(result)
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Set the listener that will receive activity state changes.
90
+ *
91
+ * @param listener The listener to receive activity callbacks
92
+ */
93
+ fun setActivityStateListener(listener: ActivityStateListener) {
94
+ this.listener = listener
95
+ }
96
+
97
+ /**
98
+ * Start activity recognition updates.
99
+ * Registers a BroadcastReceiver and requests periodic activity detection
100
+ * from the ActivityRecognitionClient.
101
+ *
102
+ * @throws SecurityException if ACTIVITY_RECOGNITION permission is not granted
103
+ */
104
+ @Throws(SecurityException::class)
105
+ fun startActivityRecognition() {
106
+ if (isRunning) return
107
+
108
+ registerReceiver()
109
+
110
+ val pendingIntent = createPendingIntent()
111
+ activityRecognitionClient.requestActivityUpdates(
112
+ DETECTION_INTERVAL_MS,
113
+ pendingIntent
114
+ )
115
+
116
+ isRunning = true
117
+ }
118
+
119
+ /**
120
+ * Stop activity recognition updates.
121
+ * Unregisters the BroadcastReceiver and removes activity detection requests.
122
+ */
123
+ fun stopActivityRecognition() {
124
+ if (!isRunning) return
125
+
126
+ val pendingIntent = createPendingIntent()
127
+ activityRecognitionClient.removeActivityUpdates(pendingIntent)
128
+
129
+ unregisterReceiver()
130
+
131
+ // Reset state
132
+ isRunning = false
133
+ isStill = false
134
+ stillStartTimeMs = 0L
135
+ stillDurationExceededNotified = false
136
+ currentActivity = ActivityType.UNKNOWN
137
+ }
138
+
139
+ /**
140
+ * Get the current detected activity type.
141
+ *
142
+ * @return The current activity type
143
+ */
144
+ fun getCurrentActivity(): ActivityType = currentActivity
145
+
146
+ /**
147
+ * Get the current STILL duration in milliseconds.
148
+ * Returns 0 if the device is not currently still.
149
+ *
150
+ * @return Duration in milliseconds that the device has been still, or 0
151
+ */
152
+ fun getStillDurationMs(): Long {
153
+ if (!isStill || stillStartTimeMs == 0L) return 0L
154
+ return SystemClock.elapsedRealtime() - stillStartTimeMs
155
+ }
156
+
157
+ /**
158
+ * Check if the device is currently in STILL state.
159
+ *
160
+ * @return true if the device is detected as still
161
+ */
162
+ fun isDeviceStill(): Boolean = isStill
163
+
164
+ private fun handleActivityResult(result: ActivityRecognitionResult) {
165
+ val mostProbableActivity = result.mostProbableActivity
166
+ if (mostProbableActivity.confidence < MIN_CONFIDENCE) return
167
+
168
+ val newActivity = mapToActivityType(mostProbableActivity.type)
169
+ val previousActivity = currentActivity
170
+ currentActivity = newActivity
171
+
172
+ if (newActivity != previousActivity) {
173
+ listener?.onActivityChanged(newActivity)
174
+ }
175
+
176
+ when (newActivity) {
177
+ ActivityType.STILL -> handleStillState()
178
+ else -> handleMovingState()
179
+ }
180
+ }
181
+
182
+ private fun handleStillState() {
183
+ if (!isStill) {
184
+ // Transition to STILL - start tracking duration
185
+ isStill = true
186
+ stillStartTimeMs = SystemClock.elapsedRealtime()
187
+ stillDurationExceededNotified = false
188
+ } else {
189
+ // Already still - check if threshold exceeded
190
+ val duration = SystemClock.elapsedRealtime() - stillStartTimeMs
191
+ if (duration >= stillThresholdMs && !stillDurationExceededNotified) {
192
+ stillDurationExceededNotified = true
193
+ listener?.onStillDurationExceeded(duration)
194
+ }
195
+ }
196
+ }
197
+
198
+ private fun handleMovingState() {
199
+ if (isStill) {
200
+ // Transition from STILL to moving
201
+ isStill = false
202
+ stillStartTimeMs = 0L
203
+ stillDurationExceededNotified = false
204
+ }
205
+ }
206
+
207
+ private fun mapToActivityType(detectedActivityType: Int): ActivityType {
208
+ return when (detectedActivityType) {
209
+ DetectedActivity.STILL -> ActivityType.STILL
210
+ DetectedActivity.ON_FOOT,
211
+ DetectedActivity.WALKING,
212
+ DetectedActivity.RUNNING -> ActivityType.ON_FOOT
213
+ DetectedActivity.IN_VEHICLE -> ActivityType.IN_VEHICLE
214
+ DetectedActivity.ON_BICYCLE -> ActivityType.IN_VEHICLE
215
+ else -> ActivityType.UNKNOWN
216
+ }
217
+ }
218
+
219
+ private fun createPendingIntent(): PendingIntent {
220
+ val intent = Intent(ACTION_ACTIVITY_RECOGNIZED)
221
+ intent.setPackage(context.packageName)
222
+
223
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
224
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
225
+ } else {
226
+ PendingIntent.FLAG_UPDATE_CURRENT
227
+ }
228
+
229
+ return PendingIntent.getBroadcast(context, 0, intent, flags)
230
+ }
231
+
232
+ private fun registerReceiver() {
233
+ val intentFilter = IntentFilter(ACTION_ACTIVITY_RECOGNIZED)
234
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
235
+ context.registerReceiver(activityReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
236
+ } else {
237
+ context.registerReceiver(activityReceiver, intentFilter)
238
+ }
239
+ }
240
+
241
+ private fun unregisterReceiver() {
242
+ try {
243
+ context.unregisterReceiver(activityReceiver)
244
+ } catch (e: IllegalArgumentException) {
245
+ // Receiver was not registered, ignore
246
+ }
247
+ }
248
+ }
@@ -0,0 +1,130 @@
1
+ package com.livetracking.optimizer
2
+
3
+ import android.os.SystemClock
4
+ import com.google.android.gms.location.DetectedActivity
5
+ import com.google.android.gms.location.Priority
6
+ import com.livetracking.location.LocationEngine
7
+
8
+ /**
9
+ * Manages Motion Sleep Mode for battery optimization.
10
+ *
11
+ * When the device is detected as STILL for more than 3 minutes and `stopWhenStill` is enabled,
12
+ * this manager switches location updates to low-power mode. When movement is detected again,
13
+ * it restores high-accuracy location updates.
14
+ *
15
+ * Requirements: 8.1, 8.2, 8.4
16
+ */
17
+ class MotionSleepManager(
18
+ private val locationEngine: LocationEngine,
19
+ private val stopWhenStill: Boolean,
20
+ private val intervalMs: Long,
21
+ private val distanceFilter: Float
22
+ ) {
23
+
24
+ /**
25
+ * Listener interface for motion sleep mode state changes.
26
+ */
27
+ interface MotionSleepListener {
28
+ fun onSleepModeActivated()
29
+ fun onSleepModeDeactivated()
30
+ }
31
+
32
+ companion object {
33
+ /**
34
+ * Duration threshold in milliseconds before entering sleep mode.
35
+ * Device must be STILL for more than 3 minutes (180,000 ms).
36
+ */
37
+ const val STILL_THRESHOLD_MS = 180_000L
38
+ }
39
+
40
+ private var listener: MotionSleepListener? = null
41
+ private var inSleepMode: Boolean = false
42
+ private var stillStartTime: Long = 0L
43
+ private var isStill: Boolean = false
44
+
45
+ /**
46
+ * Set the listener that will receive sleep mode state change callbacks.
47
+ *
48
+ * @param listener The listener to receive callbacks, or null to remove
49
+ */
50
+ fun setListener(listener: MotionSleepListener?) {
51
+ this.listener = listener
52
+ }
53
+
54
+ /**
55
+ * Called when an activity detection update is received.
56
+ *
57
+ * If `stopWhenStill` is false, this method is a no-op.
58
+ *
59
+ * Behavior:
60
+ * - STILL detected: starts tracking still duration. If still > 3 minutes, enters sleep mode.
61
+ * - ON_FOOT or IN_VEHICLE detected: exits sleep mode if active, resets still tracking.
62
+ *
63
+ * @param activityType The detected activity type from ActivityRecognitionClient
64
+ * (e.g., DetectedActivity.STILL, DetectedActivity.ON_FOOT)
65
+ */
66
+ fun onActivityDetected(activityType: Int) {
67
+ if (!stopWhenStill) {
68
+ return
69
+ }
70
+
71
+ when (activityType) {
72
+ DetectedActivity.STILL -> {
73
+ if (!isStill) {
74
+ // Start tracking still duration
75
+ isStill = true
76
+ stillStartTime = SystemClock.elapsedRealtime()
77
+ } else {
78
+ // Already still, check if threshold exceeded
79
+ val stillDuration = SystemClock.elapsedRealtime() - stillStartTime
80
+ if (stillDuration > STILL_THRESHOLD_MS && !inSleepMode) {
81
+ enterSleepMode()
82
+ }
83
+ }
84
+ }
85
+
86
+ DetectedActivity.ON_FOOT,
87
+ DetectedActivity.WALKING,
88
+ DetectedActivity.RUNNING,
89
+ DetectedActivity.IN_VEHICLE,
90
+ DetectedActivity.ON_BICYCLE -> {
91
+ isStill = false
92
+ stillStartTime = 0L
93
+ if (inSleepMode) {
94
+ exitSleepMode()
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Returns whether the manager is currently in sleep mode (low-power location).
102
+ *
103
+ * @return true if sleep mode is active, false otherwise
104
+ */
105
+ fun isInSleepMode(): Boolean {
106
+ return inSleepMode
107
+ }
108
+
109
+ /**
110
+ * Enter sleep mode: stop current location updates and restart with low-power priority.
111
+ * This reduces GPS usage when the device is stationary.
112
+ */
113
+ private fun enterSleepMode() {
114
+ inSleepMode = true
115
+ locationEngine.stopLocationUpdates()
116
+ locationEngine.startLocationUpdates(intervalMs * 5, distanceFilter, Priority.PRIORITY_LOW_POWER)
117
+ listener?.onSleepModeActivated()
118
+ }
119
+
120
+ /**
121
+ * Exit sleep mode: stop current location updates and restart with high-accuracy priority.
122
+ * This restores full GPS accuracy when movement is detected.
123
+ */
124
+ private fun exitSleepMode() {
125
+ inSleepMode = false
126
+ locationEngine.stopLocationUpdates()
127
+ locationEngine.startLocationUpdates(intervalMs, distanceFilter, Priority.PRIORITY_HIGH_ACCURACY)
128
+ listener?.onSleepModeDeactivated()
129
+ }
130
+ }