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