@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,202 @@
1
+ package com.livetracking
2
+
3
+ import android.location.Location
4
+ import com.google.android.gms.location.FusedLocationProviderClient
5
+ import com.google.android.gms.location.LocationCallback
6
+ import com.google.android.gms.location.LocationResult
7
+ import com.google.android.gms.location.Priority
8
+ import com.livetracking.location.LocationEngine
9
+ import io.mockk.*
10
+ import org.junit.Assert.*
11
+ import org.junit.Before
12
+ import org.junit.Test
13
+
14
+ /**
15
+ * Unit tests for LocationEngine.
16
+ *
17
+ * NOTE: LocationEngine wraps FusedLocationProviderClient which requires Google Play Services.
18
+ * These tests verify the logic layer that CAN be tested without Android instrumentation:
19
+ * - LocationCallback forwarding behavior
20
+ * - Listener registration
21
+ * - Priority parameter mapping
22
+ *
23
+ * Full integration tests with actual FusedLocationProviderClient require instrumented tests
24
+ * (androidTest) running on a device or emulator.
25
+ */
26
+ class LocationEngineTest {
27
+
28
+ private lateinit var mockFusedClient: FusedLocationProviderClient
29
+ private lateinit var mockListener: LocationEngine.LocationUpdateListener
30
+
31
+ @Before
32
+ fun setup() {
33
+ mockFusedClient = mockk(relaxed = true)
34
+ mockListener = mockk(relaxed = true)
35
+ }
36
+
37
+ // --- Listener Registration Tests ---
38
+
39
+ @Test
40
+ fun `LocationUpdateListener interface has onLocationReceived method`() {
41
+ // Verify the interface contract exists and is callable
42
+ val listener = object : LocationEngine.LocationUpdateListener {
43
+ var receivedLocation: Location? = null
44
+ override fun onLocationReceived(location: Location) {
45
+ receivedLocation = location
46
+ }
47
+ }
48
+
49
+ val mockLocation = mockk<Location>(relaxed = true)
50
+ listener.onLocationReceived(mockLocation)
51
+
52
+ assertEquals(mockLocation, listener.receivedLocation)
53
+ }
54
+
55
+ @Test
56
+ fun `LocationUpdateListener receives location from callback`() {
57
+ // Simulate what happens inside the LocationCallback when a location is received
58
+ val listener = object : LocationEngine.LocationUpdateListener {
59
+ var receivedLocation: Location? = null
60
+ override fun onLocationReceived(location: Location) {
61
+ receivedLocation = location
62
+ }
63
+ }
64
+
65
+ val mockLocation = mockk<Location>(relaxed = true) {
66
+ every { latitude } returns 37.7749
67
+ every { longitude } returns -122.4194
68
+ every { accuracy } returns 10.0f
69
+ every { time } returns 1700000000000L
70
+ }
71
+
72
+ listener.onLocationReceived(mockLocation)
73
+
74
+ assertNotNull(listener.receivedLocation)
75
+ assertEquals(37.7749, listener.receivedLocation!!.latitude, 0.0001)
76
+ assertEquals(-122.4194, listener.receivedLocation!!.longitude, 0.0001)
77
+ }
78
+
79
+ @Test
80
+ fun `LocationUpdateListener handles multiple sequential locations`() {
81
+ val locations = mutableListOf<Location>()
82
+ val listener = object : LocationEngine.LocationUpdateListener {
83
+ override fun onLocationReceived(location: Location) {
84
+ locations.add(location)
85
+ }
86
+ }
87
+
88
+ val loc1 = mockk<Location>(relaxed = true) { every { latitude } returns 1.0 }
89
+ val loc2 = mockk<Location>(relaxed = true) { every { latitude } returns 2.0 }
90
+ val loc3 = mockk<Location>(relaxed = true) { every { latitude } returns 3.0 }
91
+
92
+ listener.onLocationReceived(loc1)
93
+ listener.onLocationReceived(loc2)
94
+ listener.onLocationReceived(loc3)
95
+
96
+ assertEquals(3, locations.size)
97
+ assertEquals(1.0, locations[0].latitude, 0.0001)
98
+ assertEquals(2.0, locations[1].latitude, 0.0001)
99
+ assertEquals(3.0, locations[2].latitude, 0.0001)
100
+ }
101
+
102
+ // --- Priority Constants Tests ---
103
+
104
+ @Test
105
+ fun `PRIORITY_HIGH_ACCURACY constant is available`() {
106
+ // Verify the priority constant used by LocationEngine is accessible
107
+ assertEquals(100, Priority.PRIORITY_HIGH_ACCURACY)
108
+ }
109
+
110
+ @Test
111
+ fun `PRIORITY_LOW_POWER constant is available`() {
112
+ // Verify the low-power priority constant used in sleep mode
113
+ assertEquals(104, Priority.PRIORITY_LOW_POWER)
114
+ }
115
+
116
+ @Test
117
+ fun `PRIORITY_BALANCED_POWER_ACCURACY constant is available`() {
118
+ assertEquals(102, Priority.PRIORITY_BALANCED_POWER_ACCURACY)
119
+ }
120
+
121
+ // --- LocationCallback Behavior Tests ---
122
+
123
+ @Test
124
+ fun `LocationCallback does not crash when listener is null`() {
125
+ // Simulates the scenario where locationCallback fires but no listener is set.
126
+ // The actual LocationEngine handles this with null-safe call (locationListener?.onLocationReceived)
127
+ // This test verifies the pattern is safe.
128
+ var listenerRef: LocationEngine.LocationUpdateListener? = null
129
+
130
+ val mockLocation = mockk<Location>(relaxed = true)
131
+
132
+ // Should not throw - mirrors the null-safe call in LocationEngine
133
+ listenerRef?.onLocationReceived(mockLocation)
134
+ }
135
+
136
+ @Test
137
+ fun `LocationCallback ignores null lastLocation from LocationResult`() {
138
+ // LocationResult.lastLocation can be null; the engine should handle this gracefully
139
+ val mockLocationResult = mockk<LocationResult> {
140
+ every { lastLocation } returns null
141
+ }
142
+
143
+ // Simulating the guard: val location = locationResult.lastLocation ?: return
144
+ val lastLocation = mockLocationResult.lastLocation
145
+ assertNull(lastLocation)
146
+ }
147
+
148
+ @Test
149
+ fun `LocationCallback forwards non-null lastLocation to listener`() {
150
+ val receivedLocations = mutableListOf<Location>()
151
+ val listener = object : LocationEngine.LocationUpdateListener {
152
+ override fun onLocationReceived(location: Location) {
153
+ receivedLocations.add(location)
154
+ }
155
+ }
156
+
157
+ val mockLocation = mockk<Location>(relaxed = true) {
158
+ every { latitude } returns 40.7128
159
+ every { longitude } returns -74.0060
160
+ }
161
+
162
+ val mockLocationResult = mockk<LocationResult> {
163
+ every { lastLocation } returns mockLocation
164
+ }
165
+
166
+ // Simulate the callback logic: val location = locationResult.lastLocation ?: return
167
+ val location = mockLocationResult.lastLocation
168
+ if (location != null) {
169
+ listener.onLocationReceived(location)
170
+ }
171
+
172
+ assertEquals(1, receivedLocations.size)
173
+ assertEquals(40.7128, receivedLocations[0].latitude, 0.0001)
174
+ assertEquals(-74.0060, receivedLocations[0].longitude, 0.0001)
175
+ }
176
+
177
+ // --- Parameter Validation Tests ---
178
+
179
+ @Test
180
+ fun `startLocationUpdates accepts valid interval and distance parameters`() {
181
+ // Verify that typical parameter values are within expected ranges
182
+ val intervalMs = 5000L
183
+ val distanceFilter = 10.0f
184
+
185
+ assertTrue(intervalMs > 0)
186
+ assertTrue(distanceFilter >= 0)
187
+ }
188
+
189
+ @Test
190
+ fun `startLocationUpdates accepts zero distance filter`() {
191
+ // Zero distance filter means every location update is delivered
192
+ val distanceFilter = 0.0f
193
+ assertTrue(distanceFilter >= 0)
194
+ }
195
+
196
+ @Test
197
+ fun `LocationEngine class exists in correct package`() {
198
+ // Structural test: verify the class is accessible
199
+ val clazz = LocationEngine::class.java
200
+ assertEquals("com.livetracking.location.LocationEngine", clazz.name)
201
+ }
202
+ }
@@ -0,0 +1,420 @@
1
+ package com.livetracking
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
+ import com.livetracking.optimizer.MotionSleepManager
8
+ import io.mockk.*
9
+ import org.junit.Assert.*
10
+ import org.junit.Before
11
+ import org.junit.Test
12
+
13
+ /**
14
+ * Unit tests for MotionSleepManager.
15
+ *
16
+ * Tests the motion sleep threshold logic:
17
+ * - STILL detection and duration tracking
18
+ * - Sleep mode activation after 3-minute threshold
19
+ * - Sleep mode deactivation on movement
20
+ * - stopWhenStill flag behavior
21
+ *
22
+ * NOTE: Uses MockK to mock SystemClock.elapsedRealtime() for time-based tests.
23
+ * LocationEngine is mocked to verify startLocationUpdates/stopLocationUpdates calls.
24
+ */
25
+ class MotionSleepManagerTest {
26
+
27
+ private lateinit var mockLocationEngine: LocationEngine
28
+ private lateinit var mockListener: MotionSleepManager.MotionSleepListener
29
+ private lateinit var manager: MotionSleepManager
30
+ private lateinit var managerDisabled: MotionSleepManager
31
+
32
+ companion object {
33
+ private const val DEFAULT_INTERVAL_MS = 5000L
34
+ private const val DEFAULT_DISTANCE_FILTER = 10.0f
35
+ private const val THREE_MINUTES_MS = 180_000L
36
+ }
37
+
38
+ @Before
39
+ fun setup() {
40
+ mockLocationEngine = mockk(relaxed = true)
41
+ mockListener = mockk(relaxed = true)
42
+
43
+ manager = MotionSleepManager(
44
+ locationEngine = mockLocationEngine,
45
+ stopWhenStill = true,
46
+ intervalMs = DEFAULT_INTERVAL_MS,
47
+ distanceFilter = DEFAULT_DISTANCE_FILTER
48
+ )
49
+ manager.setListener(mockListener)
50
+
51
+ managerDisabled = MotionSleepManager(
52
+ locationEngine = mockLocationEngine,
53
+ stopWhenStill = false,
54
+ intervalMs = DEFAULT_INTERVAL_MS,
55
+ distanceFilter = DEFAULT_DISTANCE_FILTER
56
+ )
57
+ managerDisabled.setListener(mockListener)
58
+
59
+ // Mock SystemClock.elapsedRealtime() for time-based tests
60
+ mockkStatic(SystemClock::class)
61
+ }
62
+
63
+ // --- STILL Threshold Tests ---
64
+
65
+ @Test
66
+ fun `STILL for less than 3 minutes does NOT activate sleep mode`() {
67
+ val startTime = 1000000L
68
+
69
+ // First STILL detection - starts tracking
70
+ every { SystemClock.elapsedRealtime() } returns startTime
71
+ manager.onActivityDetected(DetectedActivity.STILL)
72
+
73
+ assertFalse("Should not be in sleep mode yet", manager.isInSleepMode())
74
+
75
+ // Second STILL detection at 2 minutes (120,000ms) - still under threshold
76
+ every { SystemClock.elapsedRealtime() } returns startTime + 120_000L
77
+ manager.onActivityDetected(DetectedActivity.STILL)
78
+
79
+ assertFalse("Should not be in sleep mode at 2 minutes", manager.isInSleepMode())
80
+ verify(exactly = 0) { mockListener.onSleepModeActivated() }
81
+ }
82
+
83
+ @Test
84
+ fun `STILL for exactly 3 minutes does NOT activate sleep mode`() {
85
+ val startTime = 1000000L
86
+
87
+ // First STILL detection
88
+ every { SystemClock.elapsedRealtime() } returns startTime
89
+ manager.onActivityDetected(DetectedActivity.STILL)
90
+
91
+ // At exactly 3 minutes (threshold is > 3 minutes, not >=)
92
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS
93
+ manager.onActivityDetected(DetectedActivity.STILL)
94
+
95
+ assertFalse("Should not be in sleep mode at exactly 3 minutes", manager.isInSleepMode())
96
+ verify(exactly = 0) { mockListener.onSleepModeActivated() }
97
+ }
98
+
99
+ @Test
100
+ fun `STILL for more than 3 minutes activates sleep mode`() {
101
+ val startTime = 1000000L
102
+
103
+ // First STILL detection - starts tracking
104
+ every { SystemClock.elapsedRealtime() } returns startTime
105
+ manager.onActivityDetected(DetectedActivity.STILL)
106
+
107
+ // Second STILL detection after 3+ minutes
108
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
109
+ manager.onActivityDetected(DetectedActivity.STILL)
110
+
111
+ assertTrue("Should be in sleep mode after 3+ minutes", manager.isInSleepMode())
112
+ verify(exactly = 1) { mockListener.onSleepModeActivated() }
113
+ }
114
+
115
+ @Test
116
+ fun `STILL for 5 minutes activates sleep mode`() {
117
+ val startTime = 1000000L
118
+
119
+ every { SystemClock.elapsedRealtime() } returns startTime
120
+ manager.onActivityDetected(DetectedActivity.STILL)
121
+
122
+ every { SystemClock.elapsedRealtime() } returns startTime + 300_000L // 5 minutes
123
+ manager.onActivityDetected(DetectedActivity.STILL)
124
+
125
+ assertTrue("Should be in sleep mode after 5 minutes", manager.isInSleepMode())
126
+ }
127
+
128
+ // --- Movement After Sleep Mode Tests ---
129
+
130
+ @Test
131
+ fun `ON_FOOT after sleep mode deactivates it`() {
132
+ val startTime = 1000000L
133
+
134
+ // Enter sleep mode
135
+ every { SystemClock.elapsedRealtime() } returns startTime
136
+ manager.onActivityDetected(DetectedActivity.STILL)
137
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
138
+ manager.onActivityDetected(DetectedActivity.STILL)
139
+
140
+ assertTrue("Should be in sleep mode", manager.isInSleepMode())
141
+
142
+ // Movement detected - ON_FOOT
143
+ manager.onActivityDetected(DetectedActivity.ON_FOOT)
144
+
145
+ assertFalse("Should exit sleep mode on ON_FOOT", manager.isInSleepMode())
146
+ verify(exactly = 1) { mockListener.onSleepModeDeactivated() }
147
+ }
148
+
149
+ @Test
150
+ fun `IN_VEHICLE after sleep mode deactivates it`() {
151
+ val startTime = 1000000L
152
+
153
+ // Enter sleep mode
154
+ every { SystemClock.elapsedRealtime() } returns startTime
155
+ manager.onActivityDetected(DetectedActivity.STILL)
156
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
157
+ manager.onActivityDetected(DetectedActivity.STILL)
158
+
159
+ assertTrue("Should be in sleep mode", manager.isInSleepMode())
160
+
161
+ // Movement detected - IN_VEHICLE
162
+ manager.onActivityDetected(DetectedActivity.IN_VEHICLE)
163
+
164
+ assertFalse("Should exit sleep mode on IN_VEHICLE", manager.isInSleepMode())
165
+ verify(exactly = 1) { mockListener.onSleepModeDeactivated() }
166
+ }
167
+
168
+ @Test
169
+ fun `WALKING after sleep mode deactivates it`() {
170
+ val startTime = 1000000L
171
+
172
+ // Enter sleep mode
173
+ every { SystemClock.elapsedRealtime() } returns startTime
174
+ manager.onActivityDetected(DetectedActivity.STILL)
175
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
176
+ manager.onActivityDetected(DetectedActivity.STILL)
177
+
178
+ assertTrue("Should be in sleep mode", manager.isInSleepMode())
179
+
180
+ // Movement detected - WALKING
181
+ manager.onActivityDetected(DetectedActivity.WALKING)
182
+
183
+ assertFalse("Should exit sleep mode on WALKING", manager.isInSleepMode())
184
+ verify(exactly = 1) { mockListener.onSleepModeDeactivated() }
185
+ }
186
+
187
+ @Test
188
+ fun `RUNNING after sleep mode deactivates it`() {
189
+ val startTime = 1000000L
190
+
191
+ // Enter sleep mode
192
+ every { SystemClock.elapsedRealtime() } returns startTime
193
+ manager.onActivityDetected(DetectedActivity.STILL)
194
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
195
+ manager.onActivityDetected(DetectedActivity.STILL)
196
+
197
+ assertTrue("Should be in sleep mode", manager.isInSleepMode())
198
+
199
+ // Movement detected - RUNNING
200
+ manager.onActivityDetected(DetectedActivity.RUNNING)
201
+
202
+ assertFalse("Should exit sleep mode on RUNNING", manager.isInSleepMode())
203
+ }
204
+
205
+ @Test
206
+ fun `ON_BICYCLE after sleep mode deactivates it`() {
207
+ val startTime = 1000000L
208
+
209
+ // Enter sleep mode
210
+ every { SystemClock.elapsedRealtime() } returns startTime
211
+ manager.onActivityDetected(DetectedActivity.STILL)
212
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
213
+ manager.onActivityDetected(DetectedActivity.STILL)
214
+
215
+ assertTrue("Should be in sleep mode", manager.isInSleepMode())
216
+
217
+ // Movement detected - ON_BICYCLE
218
+ manager.onActivityDetected(DetectedActivity.ON_BICYCLE)
219
+
220
+ assertFalse("Should exit sleep mode on ON_BICYCLE", manager.isInSleepMode())
221
+ }
222
+
223
+ // --- stopWhenStill=false Tests ---
224
+
225
+ @Test
226
+ fun `stopWhenStill false makes onActivityDetected a no-op for STILL`() {
227
+ val startTime = 1000000L
228
+
229
+ every { SystemClock.elapsedRealtime() } returns startTime
230
+ managerDisabled.onActivityDetected(DetectedActivity.STILL)
231
+
232
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
233
+ managerDisabled.onActivityDetected(DetectedActivity.STILL)
234
+
235
+ assertFalse("Should never enter sleep mode when disabled", managerDisabled.isInSleepMode())
236
+ verify(exactly = 0) { mockListener.onSleepModeActivated() }
237
+ }
238
+
239
+ @Test
240
+ fun `stopWhenStill false makes onActivityDetected a no-op for ON_FOOT`() {
241
+ managerDisabled.onActivityDetected(DetectedActivity.ON_FOOT)
242
+
243
+ assertFalse(managerDisabled.isInSleepMode())
244
+ verify(exactly = 0) { mockListener.onSleepModeDeactivated() }
245
+ }
246
+
247
+ @Test
248
+ fun `stopWhenStill false makes onActivityDetected a no-op for IN_VEHICLE`() {
249
+ managerDisabled.onActivityDetected(DetectedActivity.IN_VEHICLE)
250
+
251
+ assertFalse(managerDisabled.isInSleepMode())
252
+ verify(exactly = 0) { mockListener.onSleepModeDeactivated() }
253
+ }
254
+
255
+ // --- isInSleepMode State Tests ---
256
+
257
+ @Test
258
+ fun `isInSleepMode returns false initially`() {
259
+ assertFalse(manager.isInSleepMode())
260
+ }
261
+
262
+ @Test
263
+ fun `isInSleepMode returns true after entering sleep mode`() {
264
+ val startTime = 1000000L
265
+
266
+ every { SystemClock.elapsedRealtime() } returns startTime
267
+ manager.onActivityDetected(DetectedActivity.STILL)
268
+
269
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
270
+ manager.onActivityDetected(DetectedActivity.STILL)
271
+
272
+ assertTrue(manager.isInSleepMode())
273
+ }
274
+
275
+ @Test
276
+ fun `isInSleepMode returns false after exiting sleep mode`() {
277
+ val startTime = 1000000L
278
+
279
+ // Enter sleep mode
280
+ every { SystemClock.elapsedRealtime() } returns startTime
281
+ manager.onActivityDetected(DetectedActivity.STILL)
282
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
283
+ manager.onActivityDetected(DetectedActivity.STILL)
284
+
285
+ assertTrue(manager.isInSleepMode())
286
+
287
+ // Exit sleep mode
288
+ manager.onActivityDetected(DetectedActivity.ON_FOOT)
289
+
290
+ assertFalse(manager.isInSleepMode())
291
+ }
292
+
293
+ // --- LocationEngine Interaction Tests ---
294
+
295
+ @Test
296
+ fun `entering sleep mode switches to low-power location updates`() {
297
+ val startTime = 1000000L
298
+
299
+ every { SystemClock.elapsedRealtime() } returns startTime
300
+ manager.onActivityDetected(DetectedActivity.STILL)
301
+
302
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
303
+ manager.onActivityDetected(DetectedActivity.STILL)
304
+
305
+ // Verify location engine was stopped and restarted with low power
306
+ verify(exactly = 1) { mockLocationEngine.stopLocationUpdates() }
307
+ verify(exactly = 1) {
308
+ mockLocationEngine.startLocationUpdates(
309
+ DEFAULT_INTERVAL_MS * 5, // 5x interval in sleep mode
310
+ DEFAULT_DISTANCE_FILTER,
311
+ Priority.PRIORITY_LOW_POWER
312
+ )
313
+ }
314
+ }
315
+
316
+ @Test
317
+ fun `exiting sleep mode restores high-accuracy location updates`() {
318
+ val startTime = 1000000L
319
+
320
+ // Enter sleep mode
321
+ every { SystemClock.elapsedRealtime() } returns startTime
322
+ manager.onActivityDetected(DetectedActivity.STILL)
323
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
324
+ manager.onActivityDetected(DetectedActivity.STILL)
325
+
326
+ clearMocks(mockLocationEngine, answers = false)
327
+
328
+ // Exit sleep mode
329
+ manager.onActivityDetected(DetectedActivity.ON_FOOT)
330
+
331
+ // Verify location engine was stopped and restarted with high accuracy
332
+ verify(exactly = 1) { mockLocationEngine.stopLocationUpdates() }
333
+ verify(exactly = 1) {
334
+ mockLocationEngine.startLocationUpdates(
335
+ DEFAULT_INTERVAL_MS,
336
+ DEFAULT_DISTANCE_FILTER,
337
+ Priority.PRIORITY_HIGH_ACCURACY
338
+ )
339
+ }
340
+ }
341
+
342
+ // --- Edge Cases ---
343
+
344
+ @Test
345
+ fun `multiple STILL detections before threshold do not activate sleep mode`() {
346
+ val startTime = 1000000L
347
+
348
+ // Multiple STILL detections, each 1 minute apart (under threshold)
349
+ every { SystemClock.elapsedRealtime() } returns startTime
350
+ manager.onActivityDetected(DetectedActivity.STILL)
351
+
352
+ every { SystemClock.elapsedRealtime() } returns startTime + 60_000L
353
+ manager.onActivityDetected(DetectedActivity.STILL)
354
+
355
+ every { SystemClock.elapsedRealtime() } returns startTime + 120_000L
356
+ manager.onActivityDetected(DetectedActivity.STILL)
357
+
358
+ assertFalse("Should not be in sleep mode at 2 minutes", manager.isInSleepMode())
359
+ }
360
+
361
+ @Test
362
+ fun `movement resets still timer`() {
363
+ val startTime = 1000000L
364
+
365
+ // STILL for 2 minutes
366
+ every { SystemClock.elapsedRealtime() } returns startTime
367
+ manager.onActivityDetected(DetectedActivity.STILL)
368
+
369
+ every { SystemClock.elapsedRealtime() } returns startTime + 120_000L
370
+ manager.onActivityDetected(DetectedActivity.STILL)
371
+
372
+ // Brief movement
373
+ manager.onActivityDetected(DetectedActivity.ON_FOOT)
374
+
375
+ // STILL again for 2 minutes (should NOT trigger sleep because timer was reset)
376
+ every { SystemClock.elapsedRealtime() } returns startTime + 200_000L
377
+ manager.onActivityDetected(DetectedActivity.STILL)
378
+
379
+ every { SystemClock.elapsedRealtime() } returns startTime + 320_000L
380
+ manager.onActivityDetected(DetectedActivity.STILL)
381
+
382
+ assertFalse(
383
+ "Should not be in sleep mode - timer was reset by movement",
384
+ manager.isInSleepMode()
385
+ )
386
+ }
387
+
388
+ @Test
389
+ fun `movement when not in sleep mode does not call deactivated listener`() {
390
+ // No sleep mode entered, just movement
391
+ manager.onActivityDetected(DetectedActivity.ON_FOOT)
392
+
393
+ verify(exactly = 0) { mockListener.onSleepModeDeactivated() }
394
+ }
395
+
396
+ @Test
397
+ fun `STILL_THRESHOLD_MS constant is 180000`() {
398
+ assertEquals(180_000L, MotionSleepManager.STILL_THRESHOLD_MS)
399
+ }
400
+
401
+ @Test
402
+ fun `sleep mode interval is 5x normal interval`() {
403
+ val startTime = 1000000L
404
+
405
+ every { SystemClock.elapsedRealtime() } returns startTime
406
+ manager.onActivityDetected(DetectedActivity.STILL)
407
+
408
+ every { SystemClock.elapsedRealtime() } returns startTime + THREE_MINUTES_MS + 1000L
409
+ manager.onActivityDetected(DetectedActivity.STILL)
410
+
411
+ // Verify the sleep mode interval is 5x the configured interval
412
+ verify {
413
+ mockLocationEngine.startLocationUpdates(
414
+ eq(DEFAULT_INTERVAL_MS * 5),
415
+ any(),
416
+ any()
417
+ )
418
+ }
419
+ }
420
+ }