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