@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,728 @@
|
|
|
1
|
+
package com.livetracking
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.IntentFilter
|
|
7
|
+
import android.location.Location
|
|
8
|
+
import android.location.LocationManager
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import com.facebook.react.bridge.Promise
|
|
13
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
14
|
+
import com.livetracking.location.LocationEngine
|
|
15
|
+
import com.livetracking.network.NetworkListener
|
|
16
|
+
import com.livetracking.network.NetworkStateListener
|
|
17
|
+
import com.livetracking.optimizer.ActivityRecognitionHandler
|
|
18
|
+
import com.livetracking.optimizer.MotionSleepManager
|
|
19
|
+
import com.livetracking.permissions.PermissionHandler
|
|
20
|
+
import com.livetracking.permissions.PermissionResult
|
|
21
|
+
import com.livetracking.queue.QueueEngine
|
|
22
|
+
import com.livetracking.receiver.TrackingStateStore
|
|
23
|
+
import com.livetracking.service.TrackingForegroundService
|
|
24
|
+
import com.livetracking.sync.LocationDataPoint
|
|
25
|
+
import com.livetracking.sync.SyncEngineController
|
|
26
|
+
import com.livetracking.sync.TargetEventListener
|
|
27
|
+
import org.json.JSONObject
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shared implementation for both old and new architecture.
|
|
31
|
+
* Contains the actual business logic that is shared between Bridge and TurboModule.
|
|
32
|
+
*
|
|
33
|
+
* Wires all engines together:
|
|
34
|
+
* - LocationEngine: GPS location updates via FusedLocationProvider
|
|
35
|
+
* - QueueEngine: Offline queue (Room database) with per-target support
|
|
36
|
+
* - SyncEngineController: Multi-target Firebase writes (RTDB or Firestore)
|
|
37
|
+
* - NetworkListener: Connectivity monitoring
|
|
38
|
+
* - ActivityRecognitionHandler: Activity detection
|
|
39
|
+
* - MotionSleepManager: Battery optimization via motion sleep
|
|
40
|
+
* - PermissionHandler: Permission checks
|
|
41
|
+
* - TrackingForegroundService: Foreground service for background tracking
|
|
42
|
+
*
|
|
43
|
+
* Requirements: 3.1, 3.5, 4.4, 9.2, 9.4, 10.1, 10.2, 10.3, 10.4
|
|
44
|
+
*/
|
|
45
|
+
class LiveTrackingModuleImpl(private val reactContext: ReactApplicationContext) {
|
|
46
|
+
|
|
47
|
+
companion object {
|
|
48
|
+
const val NAME = "LiveTracking"
|
|
49
|
+
|
|
50
|
+
// Event names emitted to JavaScript
|
|
51
|
+
const val EVENT_LOCATION_UPDATE = "onLocationUpdate"
|
|
52
|
+
const val EVENT_TRACKING_ERROR = "onTrackingError"
|
|
53
|
+
|
|
54
|
+
// Default configuration values
|
|
55
|
+
private const val DEFAULT_INTERVAL_MS = 10000L
|
|
56
|
+
private const val DEFAULT_DISTANCE_FILTER_METERS = 10f
|
|
57
|
+
private const val DEFAULT_STOP_WHEN_STILL = true
|
|
58
|
+
private const val DEFAULT_OPTIMIZATION_MODE = "both"
|
|
59
|
+
|
|
60
|
+
// Tracking states
|
|
61
|
+
private const val STATE_IDLE = "idle"
|
|
62
|
+
private const val STATE_CONFIGURED = "configured"
|
|
63
|
+
private const val STATE_TRACKING = "tracking"
|
|
64
|
+
private const val STATE_MOTION_SLEEP = "motion_sleep"
|
|
65
|
+
private const val STATE_PAUSED_GPS = "paused_gps"
|
|
66
|
+
|
|
67
|
+
// Error codes
|
|
68
|
+
const val ERROR_GPS_DISABLED = "GPS_DISABLED"
|
|
69
|
+
const val ERROR_GPS_ENABLED = "GPS_ENABLED"
|
|
70
|
+
const val ERROR_PERMISSION_REVOKED = "PERMISSION_REVOKED"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Engines
|
|
74
|
+
private var locationEngine: LocationEngine? = null
|
|
75
|
+
private var queueEngine: QueueEngine? = null
|
|
76
|
+
private var syncEngineController: SyncEngineController? = null
|
|
77
|
+
private var networkListener: NetworkListener? = null
|
|
78
|
+
private var activityRecognitionHandler: ActivityRecognitionHandler? = null
|
|
79
|
+
private var motionSleepManager: MotionSleepManager? = null
|
|
80
|
+
private val permissionHandler = PermissionHandler()
|
|
81
|
+
|
|
82
|
+
// Configuration
|
|
83
|
+
private var intervalMs: Long = DEFAULT_INTERVAL_MS
|
|
84
|
+
private var distanceFilterMeters: Float = DEFAULT_DISTANCE_FILTER_METERS
|
|
85
|
+
private var stopWhenStill: Boolean = DEFAULT_STOP_WHEN_STILL
|
|
86
|
+
private var optimizationMode: String = DEFAULT_OPTIMIZATION_MODE
|
|
87
|
+
private var firebaseService: String? = null
|
|
88
|
+
private var notificationEnabled: Boolean = true
|
|
89
|
+
private var notificationTitle: String = "Live Tracking"
|
|
90
|
+
private var notificationText: String = "Tracking your location"
|
|
91
|
+
private var notificationIcon: String? = null
|
|
92
|
+
private var notificationChannelId: String = TrackingForegroundService.DEFAULT_CHANNEL_ID
|
|
93
|
+
private var notificationChannelName: String = TrackingForegroundService.DEFAULT_CHANNEL_NAME
|
|
94
|
+
private var iosNotificationEnabled: Boolean = true
|
|
95
|
+
|
|
96
|
+
// State
|
|
97
|
+
private var trackingState: String = STATE_IDLE
|
|
98
|
+
private var lastLocation: Location? = null
|
|
99
|
+
private var lastUpdateTimestamp: Long = 0L
|
|
100
|
+
private var wasTrackingBeforeGpsDisabled: Boolean = false
|
|
101
|
+
private var gpsStatusReceiver: android.content.BroadcastReceiver? = null
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse JSON config string and create all engines with config values.
|
|
105
|
+
* Validates required fields and applies defaults for optional ones.
|
|
106
|
+
*
|
|
107
|
+
* Parses the `firebase.targets` array and instantiates a SyncEngineController
|
|
108
|
+
* with one TargetHandler per configured sync target.
|
|
109
|
+
*
|
|
110
|
+
* @param config JSON string containing TrackingConfig
|
|
111
|
+
* @param promise Promise to resolve/reject
|
|
112
|
+
*/
|
|
113
|
+
fun configure(config: String, promise: Promise) {
|
|
114
|
+
try {
|
|
115
|
+
val json = JSONObject(config)
|
|
116
|
+
|
|
117
|
+
// Parse optimization config
|
|
118
|
+
val optimization = json.optJSONObject("optimization")
|
|
119
|
+
if (optimization != null) {
|
|
120
|
+
intervalMs = optimization.optLong("intervalMs", DEFAULT_INTERVAL_MS)
|
|
121
|
+
distanceFilterMeters = optimization.optDouble("distanceFilterMeters", DEFAULT_DISTANCE_FILTER_METERS.toDouble()).toFloat()
|
|
122
|
+
stopWhenStill = optimization.optBoolean("stopWhenStill", DEFAULT_STOP_WHEN_STILL)
|
|
123
|
+
optimizationMode = optimization.optString("mode", DEFAULT_OPTIMIZATION_MODE)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate optimization values
|
|
127
|
+
if (intervalMs <= 0) {
|
|
128
|
+
promise.reject("INVALID_CONFIG", "optimization.intervalMs must be greater than 0")
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
if (distanceFilterMeters <= 0) {
|
|
132
|
+
promise.reject("INVALID_CONFIG", "optimization.distanceFilterMeters must be greater than 0")
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
if (optimizationMode != "interval" && optimizationMode != "distance" && optimizationMode != "both") {
|
|
136
|
+
promise.reject("INVALID_CONFIG", "optimization.mode must be 'interval', 'distance', or 'both'")
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Parse firebase config
|
|
141
|
+
val firebase = json.optJSONObject("firebase")
|
|
142
|
+
if (firebase == null) {
|
|
143
|
+
promise.reject("INVALID_CONFIG", "firebase configuration is required")
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
firebaseService = firebase.optString("service", "")
|
|
148
|
+
if (firebaseService != "RTDB" && firebaseService != "Firestore") {
|
|
149
|
+
promise.reject("INVALID_CONFIG", "firebase.service must be 'RTDB' or 'Firestore'")
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parse targets array from firebase config
|
|
154
|
+
val targetsArray = firebase.optJSONArray("targets")
|
|
155
|
+
if (targetsArray == null || targetsArray.length() == 0) {
|
|
156
|
+
promise.reject("INVALID_CONFIG", "firebase.targets array is required and must contain at least one sync target")
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
val targetsJson = targetsArray.toString()
|
|
161
|
+
|
|
162
|
+
// Parse Android notification config
|
|
163
|
+
val androidNotification = json.optJSONObject("androidNotification")
|
|
164
|
+
if (androidNotification != null) {
|
|
165
|
+
notificationEnabled = androidNotification.optBoolean("enabled", true)
|
|
166
|
+
notificationTitle = androidNotification.optString("title", "Live Tracking")
|
|
167
|
+
notificationText = androidNotification.optString("text", "Tracking your location")
|
|
168
|
+
notificationIcon = androidNotification.optString("icon", "").ifEmpty { null }
|
|
169
|
+
notificationChannelId = androidNotification.optString("channelId", TrackingForegroundService.DEFAULT_CHANNEL_ID)
|
|
170
|
+
notificationChannelName = androidNotification.optString("channelName", TrackingForegroundService.DEFAULT_CHANNEL_NAME)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Parse iOS notification config
|
|
174
|
+
val iosNotification = json.optJSONObject("iosNotification")
|
|
175
|
+
if (iosNotification != null) {
|
|
176
|
+
iosNotificationEnabled = iosNotification.optBoolean("enabled", true)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Initialize engines (including SyncEngineController with targets)
|
|
180
|
+
initializeEngines(targetsJson)
|
|
181
|
+
|
|
182
|
+
trackingState = STATE_CONFIGURED
|
|
183
|
+
promise.resolve(null)
|
|
184
|
+
} catch (e: IllegalArgumentException) {
|
|
185
|
+
// SyncEngineController throws IllegalArgumentException for invalid targets JSON
|
|
186
|
+
promise.reject("INVALID_CONFIG", "Failed to parse targets configuration: ${e.message}")
|
|
187
|
+
} catch (e: Exception) {
|
|
188
|
+
promise.reject("INVALID_CONFIG", "Failed to parse configuration: ${e.message}")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Start tracking: check permissions, start foreground service, start location engine,
|
|
194
|
+
* start activity recognition, start network listener, save tracking state.
|
|
195
|
+
*
|
|
196
|
+
* @param promise Promise to resolve/reject
|
|
197
|
+
*/
|
|
198
|
+
fun start(promise: Promise) {
|
|
199
|
+
if (trackingState == STATE_IDLE) {
|
|
200
|
+
promise.reject("NOT_CONFIGURED", "Call configure() before start()")
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (trackingState == STATE_TRACKING || trackingState == STATE_MOTION_SLEEP) {
|
|
205
|
+
promise.resolve(null) // Already tracking
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check permissions
|
|
210
|
+
val permissionResult = permissionHandler.checkAllTrackingRequirements(reactContext)
|
|
211
|
+
if (permissionResult is PermissionResult.Denied) {
|
|
212
|
+
promise.reject(permissionResult.errorCode, permissionResult.message)
|
|
213
|
+
emitError(permissionResult.errorCode, permissionResult.message)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Start foreground service (always required for background tracking on Android)
|
|
219
|
+
val foregroundTitle = if (notificationEnabled) notificationTitle else "Live Tracking"
|
|
220
|
+
val foregroundText = if (notificationEnabled) notificationText else "Tracking your location"
|
|
221
|
+
TrackingForegroundService.start(
|
|
222
|
+
context = reactContext,
|
|
223
|
+
title = foregroundTitle,
|
|
224
|
+
text = foregroundText,
|
|
225
|
+
icon = if (notificationEnabled) notificationIcon else null,
|
|
226
|
+
channelId = notificationChannelId,
|
|
227
|
+
channelName = notificationChannelName
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
// Start tracking engines
|
|
231
|
+
startLocationTracking()
|
|
232
|
+
|
|
233
|
+
// Monitor GPS status changes so we can pause/resume automatically
|
|
234
|
+
registerGpsStatusListener()
|
|
235
|
+
|
|
236
|
+
// Save tracking state for boot receiver
|
|
237
|
+
TrackingStateStore.saveTrackingActive(reactContext, true)
|
|
238
|
+
|
|
239
|
+
trackingState = STATE_TRACKING
|
|
240
|
+
promise.resolve(null)
|
|
241
|
+
} catch (e: SecurityException) {
|
|
242
|
+
promise.reject("PERMISSION_DENIED", "Location permission denied: ${e.message}")
|
|
243
|
+
emitError("PERMISSION_DENIED", "Location permission denied: ${e.message}")
|
|
244
|
+
} catch (e: Exception) {
|
|
245
|
+
promise.reject("SERVICE_UNAVAILABLE", "Failed to start tracking: ${e.message}")
|
|
246
|
+
emitError("SERVICE_UNAVAILABLE", "Failed to start tracking: ${e.message}")
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Stop all engines, stop foreground service, save tracking state as inactive.
|
|
252
|
+
* Flushes all partially-filled batches via SyncEngineController before stopping.
|
|
253
|
+
*
|
|
254
|
+
* @param promise Promise to resolve/reject
|
|
255
|
+
*/
|
|
256
|
+
fun stop(promise: Promise) {
|
|
257
|
+
try {
|
|
258
|
+
// Flush all partially-filled batches before stopping (Requirement 4.4)
|
|
259
|
+
syncEngineController?.flushAll()
|
|
260
|
+
|
|
261
|
+
// Stop tracking engines
|
|
262
|
+
stopLocationTracking()
|
|
263
|
+
|
|
264
|
+
// Stop foreground service
|
|
265
|
+
TrackingForegroundService.stop(reactContext)
|
|
266
|
+
|
|
267
|
+
// Unregister GPS status listener
|
|
268
|
+
unregisterGpsStatusListener()
|
|
269
|
+
|
|
270
|
+
// Save tracking state as inactive
|
|
271
|
+
TrackingStateStore.saveTrackingActive(reactContext, false)
|
|
272
|
+
|
|
273
|
+
// Reset state
|
|
274
|
+
trackingState = STATE_CONFIGURED
|
|
275
|
+
lastLocation = null
|
|
276
|
+
lastUpdateTimestamp = 0L
|
|
277
|
+
wasTrackingBeforeGpsDisabled = false
|
|
278
|
+
|
|
279
|
+
promise.resolve(null)
|
|
280
|
+
} catch (e: Exception) {
|
|
281
|
+
promise.reject("STOP_FAILED", "Failed to stop tracking: ${e.message}")
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Start location engine, activity recognition, and network listener.
|
|
287
|
+
*/
|
|
288
|
+
private fun startLocationTracking() {
|
|
289
|
+
// Start location engine
|
|
290
|
+
// When mode is 'interval', pass 0f distance filter so FusedLocationProvider
|
|
291
|
+
// delivers updates purely on interval without distance gating
|
|
292
|
+
val effectiveDistanceFilter = if (optimizationMode == "interval") 0f else distanceFilterMeters
|
|
293
|
+
locationEngine?.startLocationUpdates(intervalMs, effectiveDistanceFilter)
|
|
294
|
+
|
|
295
|
+
// Start activity recognition (for motion sleep mode)
|
|
296
|
+
if (stopWhenStill) {
|
|
297
|
+
try {
|
|
298
|
+
activityRecognitionHandler?.startActivityRecognition()
|
|
299
|
+
} catch (e: SecurityException) {
|
|
300
|
+
// Activity recognition permission not granted, continue without it
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Start network listener
|
|
305
|
+
networkListener?.startListening()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Stop location engine, activity recognition, and network listener.
|
|
310
|
+
*/
|
|
311
|
+
private fun stopLocationTracking() {
|
|
312
|
+
// Stop location engine
|
|
313
|
+
locationEngine?.stopLocationUpdates()
|
|
314
|
+
|
|
315
|
+
// Stop activity recognition
|
|
316
|
+
activityRecognitionHandler?.stopActivityRecognition()
|
|
317
|
+
|
|
318
|
+
// Stop network listener
|
|
319
|
+
networkListener?.stopListening()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Register a BroadcastReceiver that listens for GPS/location mode changes.
|
|
324
|
+
* When GPS is disabled during an active tracking session, tracking is paused.
|
|
325
|
+
* When GPS is re-enabled, tracking resumes automatically.
|
|
326
|
+
*/
|
|
327
|
+
private fun registerGpsStatusListener() {
|
|
328
|
+
if (gpsStatusReceiver != null) return
|
|
329
|
+
|
|
330
|
+
gpsStatusReceiver = object : BroadcastReceiver() {
|
|
331
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
332
|
+
checkGpsStatus()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
val filter = IntentFilter().apply {
|
|
337
|
+
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
|
|
338
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
339
|
+
addAction(LocationManager.MODE_CHANGED_ACTION)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
344
|
+
reactContext.registerReceiver(
|
|
345
|
+
gpsStatusReceiver,
|
|
346
|
+
filter,
|
|
347
|
+
Context.RECEIVER_NOT_EXPORTED
|
|
348
|
+
)
|
|
349
|
+
} else {
|
|
350
|
+
reactContext.registerReceiver(gpsStatusReceiver, filter)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Unregister the GPS status BroadcastReceiver.
|
|
356
|
+
*/
|
|
357
|
+
private fun unregisterGpsStatusListener() {
|
|
358
|
+
gpsStatusReceiver?.let { receiver ->
|
|
359
|
+
try {
|
|
360
|
+
reactContext.unregisterReceiver(receiver)
|
|
361
|
+
} catch (e: IllegalArgumentException) {
|
|
362
|
+
// Receiver was not registered, ignore
|
|
363
|
+
}
|
|
364
|
+
gpsStatusReceiver = null
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Check current GPS status and pause/resume tracking accordingly.
|
|
370
|
+
*/
|
|
371
|
+
private fun checkGpsStatus() {
|
|
372
|
+
val gpsEnabled = permissionHandler.isGpsEnabled(reactContext)
|
|
373
|
+
|
|
374
|
+
if (!gpsEnabled && (trackingState == STATE_TRACKING || trackingState == STATE_MOTION_SLEEP)) {
|
|
375
|
+
pauseTrackingDueToGps()
|
|
376
|
+
} else if (gpsEnabled && trackingState == STATE_PAUSED_GPS && wasTrackingBeforeGpsDisabled) {
|
|
377
|
+
resumeTrackingAfterGps()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Pause tracking when GPS is disabled while tracking is active.
|
|
383
|
+
* Keeps the foreground service running so tracking can resume automatically.
|
|
384
|
+
*/
|
|
385
|
+
private fun pauseTrackingDueToGps() {
|
|
386
|
+
wasTrackingBeforeGpsDisabled = true
|
|
387
|
+
stopLocationTracking()
|
|
388
|
+
trackingState = STATE_PAUSED_GPS
|
|
389
|
+
emitError(
|
|
390
|
+
ERROR_GPS_DISABLED,
|
|
391
|
+
"GPS/Location services were disabled. Tracking paused and will resume automatically when GPS is enabled."
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Resume tracking after GPS has been re-enabled.
|
|
397
|
+
*/
|
|
398
|
+
private fun resumeTrackingAfterGps() {
|
|
399
|
+
try {
|
|
400
|
+
startLocationTracking()
|
|
401
|
+
trackingState = STATE_TRACKING
|
|
402
|
+
emitError(
|
|
403
|
+
ERROR_GPS_ENABLED,
|
|
404
|
+
"GPS/Location services are enabled. Tracking resumed."
|
|
405
|
+
)
|
|
406
|
+
} catch (e: SecurityException) {
|
|
407
|
+
trackingState = STATE_CONFIGURED
|
|
408
|
+
wasTrackingBeforeGpsDisabled = false
|
|
409
|
+
emitError(
|
|
410
|
+
ERROR_PERMISSION_REVOKED,
|
|
411
|
+
"Location permission was revoked while tracking was paused. Please grant permission to resume."
|
|
412
|
+
)
|
|
413
|
+
} catch (e: Exception) {
|
|
414
|
+
emitError("GPS_RESUME_FAILED", "Failed to resume tracking after GPS enabled: ${e.message}")
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Return JSON with state, isOnline, queuedLocations, lastLocation, batteryOptimization.
|
|
420
|
+
*
|
|
421
|
+
* @param promise Promise to resolve with JSON string
|
|
422
|
+
*/
|
|
423
|
+
fun getStatus(promise: Promise) {
|
|
424
|
+
try {
|
|
425
|
+
val status = JSONObject()
|
|
426
|
+
status.put("state", trackingState)
|
|
427
|
+
status.put("isOnline", networkListener?.isOnline() ?: true)
|
|
428
|
+
|
|
429
|
+
// Get queued locations count from SyncEngineController (sum of all targets)
|
|
430
|
+
val queuedCount = syncEngineController?.getQueuedCounts()?.values?.sum() ?: 0
|
|
431
|
+
status.put("queuedLocations", queuedCount)
|
|
432
|
+
|
|
433
|
+
// Last location
|
|
434
|
+
val last = lastLocation
|
|
435
|
+
if (last != null) {
|
|
436
|
+
val locationJson = JSONObject()
|
|
437
|
+
locationJson.put("latitude", last.latitude)
|
|
438
|
+
locationJson.put("longitude", last.longitude)
|
|
439
|
+
locationJson.put("timestamp", last.time)
|
|
440
|
+
locationJson.put("accuracy", last.accuracy.toDouble())
|
|
441
|
+
locationJson.put("speed", if (last.hasSpeed()) last.speed.toDouble() else JSONObject.NULL)
|
|
442
|
+
locationJson.put("altitude", if (last.hasAltitude()) last.altitude else JSONObject.NULL)
|
|
443
|
+
locationJson.put("bearing", if (last.hasBearing()) last.bearing.toDouble() else JSONObject.NULL)
|
|
444
|
+
status.put("lastLocation", locationJson)
|
|
445
|
+
} else {
|
|
446
|
+
status.put("lastLocation", JSONObject.NULL)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Battery optimization state
|
|
450
|
+
val batteryOptimization = when {
|
|
451
|
+
!stopWhenStill -> "disabled"
|
|
452
|
+
motionSleepManager?.isInSleepMode() == true -> "low_power"
|
|
453
|
+
else -> "full_accuracy"
|
|
454
|
+
}
|
|
455
|
+
status.put("batteryOptimization", batteryOptimization)
|
|
456
|
+
|
|
457
|
+
promise.resolve(status.toString())
|
|
458
|
+
} catch (e: Exception) {
|
|
459
|
+
promise.reject("STATUS_ERROR", "Failed to get status: ${e.message}")
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Return the total count of queued locations across all targets.
|
|
465
|
+
* Rejects with NOT_CONFIGURED if called before configure().
|
|
466
|
+
*
|
|
467
|
+
* @param promise Promise to resolve with queue count
|
|
468
|
+
*/
|
|
469
|
+
fun getQueuedLocations(promise: Promise) {
|
|
470
|
+
if (trackingState == STATE_IDLE) {
|
|
471
|
+
promise.reject("NOT_CONFIGURED", "Call configure() before getQueuedLocations()")
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
val count = syncEngineController?.getQueuedCounts()?.values?.sum() ?: 0
|
|
476
|
+
promise.resolve(count)
|
|
477
|
+
} catch (e: Exception) {
|
|
478
|
+
promise.resolve(0)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Return a JSON string mapping each configured target path to its queued location count.
|
|
484
|
+
* Targets with offlineQueue disabled report 0.
|
|
485
|
+
* Rejects with NOT_CONFIGURED if called before configure().
|
|
486
|
+
*
|
|
487
|
+
* @param promise Promise to resolve with JSON string (e.g., {"path1": 5, "path2": 0})
|
|
488
|
+
* Requirements: 10.2, 10.3, 10.4
|
|
489
|
+
*/
|
|
490
|
+
fun getQueuedLocationsByTarget(promise: Promise) {
|
|
491
|
+
if (trackingState == STATE_IDLE) {
|
|
492
|
+
promise.reject("NOT_CONFIGURED", "Call configure() before getQueuedLocationsByTarget()")
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
val counts = syncEngineController?.getQueuedCounts() ?: emptyMap()
|
|
497
|
+
val result = JSONObject()
|
|
498
|
+
for ((path, count) in counts) {
|
|
499
|
+
result.put(path, count)
|
|
500
|
+
}
|
|
501
|
+
promise.resolve(result.toString())
|
|
502
|
+
} catch (e: Exception) {
|
|
503
|
+
promise.reject("STATUS_ERROR", "Failed to get queued locations by target: ${e.message}")
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// --- Private Implementation ---
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Initialize all engines with the current configuration.
|
|
511
|
+
*
|
|
512
|
+
* @param targetsJson JSON array string of sync target configurations
|
|
513
|
+
* @throws IllegalArgumentException if targetsJson fails to parse or contains invalid targets
|
|
514
|
+
*/
|
|
515
|
+
private fun initializeEngines(targetsJson: String) {
|
|
516
|
+
// Location Engine
|
|
517
|
+
locationEngine = LocationEngine(reactContext).apply {
|
|
518
|
+
setLocationListener(object : LocationEngine.LocationUpdateListener {
|
|
519
|
+
override fun onLocationReceived(location: Location) {
|
|
520
|
+
handleLocationUpdate(location)
|
|
521
|
+
}
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Queue Engine (provides per-target offline queue operations)
|
|
526
|
+
queueEngine = QueueEngine(reactContext)
|
|
527
|
+
|
|
528
|
+
// Network Listener
|
|
529
|
+
networkListener = NetworkListener(reactContext).apply {
|
|
530
|
+
setNetworkStateListener(object : NetworkStateListener {
|
|
531
|
+
override fun onNetworkAvailable() {
|
|
532
|
+
handleNetworkRestored()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
override fun onNetworkLost() {
|
|
536
|
+
// No action needed - offline queue will hold data for targets with offlineQueue enabled
|
|
537
|
+
}
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// SyncEngineController — replaces FirebaseSyncEngine
|
|
542
|
+
// Parses targets JSON and instantiates one TargetHandler per target.
|
|
543
|
+
// Throws IllegalArgumentException if targets JSON is invalid (Requirement 9.4)
|
|
544
|
+
syncEngineController = SyncEngineController(
|
|
545
|
+
targetsJson = targetsJson,
|
|
546
|
+
firebaseService = firebaseService!!,
|
|
547
|
+
networkChecker = { networkListener?.isOnline() ?: true },
|
|
548
|
+
offlineQueueProvider = queueEngine,
|
|
549
|
+
eventListener = object : TargetEventListener {
|
|
550
|
+
override fun onWriteError(targetPath: String, method: String, errorCode: String, message: String) {
|
|
551
|
+
emitError(errorCode, "Target '$targetPath' ($method): $message")
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
override fun onQueueOverflow(targetPath: String) {
|
|
555
|
+
emitError("QUEUE_OVERFLOW", "Offline queue overflow for target '$targetPath' — oldest data point evicted")
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
// Activity Recognition Handler
|
|
561
|
+
activityRecognitionHandler = ActivityRecognitionHandler(reactContext).apply {
|
|
562
|
+
stillThresholdMs = MotionSleepManager.STILL_THRESHOLD_MS
|
|
563
|
+
setActivityStateListener(object : ActivityRecognitionHandler.ActivityStateListener {
|
|
564
|
+
override fun onActivityChanged(activity: ActivityRecognitionHandler.ActivityType) {
|
|
565
|
+
handleActivityChanged(activity)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
override fun onStillDurationExceeded(durationMs: Long) {
|
|
569
|
+
// MotionSleepManager handles this via onActivityDetected
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Motion Sleep Manager
|
|
575
|
+
motionSleepManager = MotionSleepManager(
|
|
576
|
+
locationEngine = locationEngine!!,
|
|
577
|
+
stopWhenStill = stopWhenStill,
|
|
578
|
+
intervalMs = intervalMs,
|
|
579
|
+
distanceFilter = distanceFilterMeters
|
|
580
|
+
).apply {
|
|
581
|
+
setListener(object : MotionSleepManager.MotionSleepListener {
|
|
582
|
+
override fun onSleepModeActivated() {
|
|
583
|
+
trackingState = STATE_MOTION_SLEEP
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
override fun onSleepModeDeactivated() {
|
|
587
|
+
trackingState = STATE_TRACKING
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Handle a new location update from LocationEngine.
|
|
595
|
+
* Applies distance/time filter, emits event to JS, dispatches to all sync targets.
|
|
596
|
+
*
|
|
597
|
+
* Requirement 3.1: Dispatches to all configured sync targets in parallel via SyncEngineController.
|
|
598
|
+
*/
|
|
599
|
+
private fun handleLocationUpdate(location: Location) {
|
|
600
|
+
// Apply distance/time filter
|
|
601
|
+
if (!shouldAcceptLocation(location)) {
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Update last location and timestamp
|
|
606
|
+
lastLocation = location
|
|
607
|
+
lastUpdateTimestamp = System.currentTimeMillis()
|
|
608
|
+
|
|
609
|
+
// Emit location event to JavaScript
|
|
610
|
+
emitLocationUpdate(location)
|
|
611
|
+
|
|
612
|
+
// Dispatch to all configured sync targets via SyncEngineController
|
|
613
|
+
val dataPoint = LocationDataPoint(
|
|
614
|
+
latitude = location.latitude,
|
|
615
|
+
longitude = location.longitude,
|
|
616
|
+
timestamp = location.time,
|
|
617
|
+
accuracy = location.accuracy,
|
|
618
|
+
speed = if (location.hasSpeed()) location.speed else null,
|
|
619
|
+
altitude = if (location.hasAltitude()) location.altitude else null,
|
|
620
|
+
bearing = if (location.hasBearing()) location.bearing else null
|
|
621
|
+
)
|
|
622
|
+
syncEngineController?.dispatchLocation(dataPoint)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Distance/Time Matrix filter.
|
|
627
|
+
* Accepts location based on the configured optimization mode:
|
|
628
|
+
* - 'interval': time since last update >= intervalMs
|
|
629
|
+
* - 'distance': distance from last location >= distanceFilterMeters
|
|
630
|
+
* - 'both': both conditions must be met (default)
|
|
631
|
+
*
|
|
632
|
+
* First location is always accepted.
|
|
633
|
+
* Invalid locations (accuracy < 0 or coordinate 0,0) are rejected.
|
|
634
|
+
*/
|
|
635
|
+
private fun shouldAcceptLocation(newLocation: Location): Boolean {
|
|
636
|
+
// Reject invalid coordinates
|
|
637
|
+
if (newLocation.latitude == 0.0 && newLocation.longitude == 0.0) {
|
|
638
|
+
return false
|
|
639
|
+
}
|
|
640
|
+
if (newLocation.accuracy < 0) {
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
val last = lastLocation ?: return true // First location always accepted
|
|
645
|
+
|
|
646
|
+
// Check time condition
|
|
647
|
+
val timeDiff = System.currentTimeMillis() - lastUpdateTimestamp
|
|
648
|
+
val timeMet = timeDiff >= intervalMs
|
|
649
|
+
|
|
650
|
+
// Check distance condition
|
|
651
|
+
val distance = last.distanceTo(newLocation)
|
|
652
|
+
val distanceMet = distance >= distanceFilterMeters
|
|
653
|
+
|
|
654
|
+
return when (optimizationMode) {
|
|
655
|
+
"interval" -> timeMet
|
|
656
|
+
"distance" -> distanceMet
|
|
657
|
+
else -> timeMet && distanceMet
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Emit location update event to JavaScript layer.
|
|
663
|
+
*/
|
|
664
|
+
private fun emitLocationUpdate(location: Location) {
|
|
665
|
+
try {
|
|
666
|
+
val params = Arguments.createMap().apply {
|
|
667
|
+
putDouble("latitude", location.latitude)
|
|
668
|
+
putDouble("longitude", location.longitude)
|
|
669
|
+
putDouble("timestamp", location.time.toDouble())
|
|
670
|
+
putDouble("accuracy", location.accuracy.toDouble())
|
|
671
|
+
putDouble("speed", if (location.hasSpeed()) location.speed.toDouble() else 0.0)
|
|
672
|
+
putDouble("altitude", if (location.hasAltitude()) location.altitude else 0.0)
|
|
673
|
+
putDouble("bearing", if (location.hasBearing()) location.bearing.toDouble() else 0.0)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
reactContext
|
|
677
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
678
|
+
.emit(EVENT_LOCATION_UPDATE, params)
|
|
679
|
+
} catch (e: Exception) {
|
|
680
|
+
// JS module may not be available yet, ignore
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Emit error event to JavaScript layer.
|
|
686
|
+
*/
|
|
687
|
+
private fun emitError(code: String, message: String) {
|
|
688
|
+
try {
|
|
689
|
+
val params = Arguments.createMap().apply {
|
|
690
|
+
putString("code", code)
|
|
691
|
+
putString("message", message)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
reactContext
|
|
695
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
696
|
+
.emit(EVENT_TRACKING_ERROR, params)
|
|
697
|
+
} catch (e: Exception) {
|
|
698
|
+
// JS module may not be available yet, ignore
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Handle network restored: flush offline queues for all targets with offlineQueue enabled.
|
|
704
|
+
*/
|
|
705
|
+
private fun handleNetworkRestored() {
|
|
706
|
+
if (trackingState == STATE_TRACKING || trackingState == STATE_MOTION_SLEEP) {
|
|
707
|
+
syncEngineController?.flushOfflineQueues()
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Handle activity state change from ActivityRecognitionHandler.
|
|
713
|
+
* Delegates to MotionSleepManager for sleep mode management.
|
|
714
|
+
*/
|
|
715
|
+
private fun handleActivityChanged(activity: ActivityRecognitionHandler.ActivityType) {
|
|
716
|
+
val detectedActivityType = when (activity) {
|
|
717
|
+
ActivityRecognitionHandler.ActivityType.STILL ->
|
|
718
|
+
com.google.android.gms.location.DetectedActivity.STILL
|
|
719
|
+
ActivityRecognitionHandler.ActivityType.ON_FOOT ->
|
|
720
|
+
com.google.android.gms.location.DetectedActivity.ON_FOOT
|
|
721
|
+
ActivityRecognitionHandler.ActivityType.IN_VEHICLE ->
|
|
722
|
+
com.google.android.gms.location.DetectedActivity.IN_VEHICLE
|
|
723
|
+
ActivityRecognitionHandler.ActivityType.UNKNOWN ->
|
|
724
|
+
com.google.android.gms.location.DetectedActivity.UNKNOWN
|
|
725
|
+
}
|
|
726
|
+
motionSleepManager?.onActivityDetected(detectedActivityType)
|
|
727
|
+
}
|
|
728
|
+
}
|