react-native-nitro-location-tracking 0.1.5 → 0.1.6
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/README.md +677 -7
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/GeofenceManager.kt +148 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/LocationEngine.kt +55 -1
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt +127 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/ProviderStatusMonitor.kt +73 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/SpeedMonitor.kt +38 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/TripCalculator.kt +85 -0
- package/ios/GeofenceManager.swift +69 -0
- package/ios/LocationEngine.swift +56 -2
- package/ios/NitroLocationTracking.swift +104 -0
- package/ios/SpeedMonitor.swift +48 -0
- package/ios/TripCalculator.swift +93 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NitroLocationTracking.nitro.d.ts +44 -0
- package/lib/typescript/src/NitroLocationTracking.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/test.d.ts +1 -0
- package/lib/typescript/test.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JFunc_void_GeofenceEvent_std__string.hpp +78 -0
- package/nitrogen/generated/android/c++/JFunc_void_LocationData.hpp +1 -0
- package/nitrogen/generated/android/c++/JFunc_void_LocationProviderStatus_LocationProviderStatus.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_SpeedAlertType_double.hpp +77 -0
- package/nitrogen/generated/android/c++/JGeofenceEvent.hpp +58 -0
- package/nitrogen/generated/android/c++/JGeofenceRegion.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.cpp +102 -0
- package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.hpp +16 -0
- package/nitrogen/generated/android/c++/JLocationData.hpp +8 -4
- package/nitrogen/generated/android/c++/JLocationProviderStatus.hpp +58 -0
- package/nitrogen/generated/android/c++/JPermissionStatus.hpp +67 -0
- package/nitrogen/generated/android/c++/JSpeedAlertType.hpp +61 -0
- package/nitrogen/generated/android/c++/JSpeedConfig.hpp +65 -0
- package/nitrogen/generated/android/c++/JTripStats.hpp +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_GeofenceEvent_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_LocationProviderStatus_LocationProviderStatus.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_SpeedAlertType_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/GeofenceEvent.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/GeofenceRegion.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/HybridNitroLocationTrackingSpec.kt +79 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationData.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationProviderStatus.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/PermissionStatus.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/SpeedAlertType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/SpeedConfig.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/TripStats.kt +50 -0
- package/nitrogen/generated/android/nitrolocationtrackingOnLoad.cpp +6 -0
- package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.cpp +24 -0
- package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.hpp +124 -0
- package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Umbrella.hpp +22 -0
- package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.hpp +130 -0
- package/nitrogen/generated/ios/swift/Func_void_GeofenceEvent_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_LocationProviderStatus_LocationProviderStatus.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_SpeedAlertType_double.swift +46 -0
- package/nitrogen/generated/ios/swift/GeofenceEvent.swift +40 -0
- package/nitrogen/generated/ios/swift/GeofenceRegion.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec.swift +16 -0
- package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec_cxx.swift +197 -0
- package/nitrogen/generated/ios/swift/LocationData.swift +20 -2
- package/nitrogen/generated/ios/swift/LocationProviderStatus.swift +40 -0
- package/nitrogen/generated/ios/swift/PermissionStatus.swift +52 -0
- package/nitrogen/generated/ios/swift/SpeedAlertType.swift +44 -0
- package/nitrogen/generated/ios/swift/SpeedConfig.swift +39 -0
- package/nitrogen/generated/ios/swift/TripStats.swift +49 -0
- package/nitrogen/generated/shared/c++/GeofenceEvent.hpp +76 -0
- package/nitrogen/generated/shared/c++/GeofenceRegion.hpp +103 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.cpp +16 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.hpp +37 -0
- package/nitrogen/generated/shared/c++/LocationData.hpp +7 -3
- package/nitrogen/generated/shared/c++/LocationProviderStatus.hpp +76 -0
- package/nitrogen/generated/shared/c++/PermissionStatus.hpp +88 -0
- package/nitrogen/generated/shared/c++/SpeedAlertType.hpp +80 -0
- package/nitrogen/generated/shared/c++/SpeedConfig.hpp +91 -0
- package/nitrogen/generated/shared/c++/TripStats.hpp +99 -0
- package/package.json +2 -2
- package/src/NitroLocationTracking.nitro.ts +71 -0
- package/src/index.tsx +10 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrolocationtracking
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.PendingIntent
|
|
5
|
+
import android.content.BroadcastReceiver
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.content.IntentFilter
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import androidx.core.content.ContextCompat
|
|
12
|
+
import com.google.android.gms.location.Geofence
|
|
13
|
+
import com.google.android.gms.location.GeofencingClient
|
|
14
|
+
import com.google.android.gms.location.GeofencingRequest
|
|
15
|
+
import com.google.android.gms.location.LocationServices
|
|
16
|
+
|
|
17
|
+
class GeofenceManager(private val context: Context) {
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
private const val TAG = "GeofenceManager"
|
|
21
|
+
private const val ACTION_GEOFENCE = "com.margelo.nitro.nitrolocationtracking.GEOFENCE_EVENT"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private val geofencingClient: GeofencingClient =
|
|
25
|
+
LocationServices.getGeofencingClient(context)
|
|
26
|
+
private val activeRegions = mutableMapOf<String, GeofenceRegion>()
|
|
27
|
+
private var callback: ((GeofenceEvent, String) -> Unit)? = null
|
|
28
|
+
private var receiver: BroadcastReceiver? = null
|
|
29
|
+
|
|
30
|
+
fun setCallback(callback: (GeofenceEvent, String) -> Unit) {
|
|
31
|
+
this.callback = callback
|
|
32
|
+
registerReceiver()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@SuppressLint("MissingPermission")
|
|
36
|
+
fun addGeofence(region: GeofenceRegion) {
|
|
37
|
+
val geofence = Geofence.Builder()
|
|
38
|
+
.setRequestId(region.id)
|
|
39
|
+
.setCircularRegion(region.latitude, region.longitude, region.radius.toFloat())
|
|
40
|
+
.setExpirationDuration(Geofence.NEVER_EXPIRE)
|
|
41
|
+
.setTransitionTypes(buildTransitionTypes(region))
|
|
42
|
+
.build()
|
|
43
|
+
|
|
44
|
+
val request = GeofencingRequest.Builder()
|
|
45
|
+
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
|
|
46
|
+
.addGeofence(geofence)
|
|
47
|
+
.build()
|
|
48
|
+
|
|
49
|
+
geofencingClient.addGeofences(request, getGeofencePendingIntent())
|
|
50
|
+
.addOnSuccessListener {
|
|
51
|
+
activeRegions[region.id] = region
|
|
52
|
+
Log.d(TAG, "Geofence added: ${region.id}")
|
|
53
|
+
}
|
|
54
|
+
.addOnFailureListener { e ->
|
|
55
|
+
Log.e(TAG, "Failed to add geofence ${region.id}: ${e.message}")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun removeGeofence(regionId: String) {
|
|
60
|
+
geofencingClient.removeGeofences(listOf(regionId))
|
|
61
|
+
.addOnSuccessListener {
|
|
62
|
+
activeRegions.remove(regionId)
|
|
63
|
+
Log.d(TAG, "Geofence removed: $regionId")
|
|
64
|
+
}
|
|
65
|
+
.addOnFailureListener { e ->
|
|
66
|
+
Log.e(TAG, "Failed to remove geofence $regionId: ${e.message}")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun removeAllGeofences() {
|
|
71
|
+
geofencingClient.removeGeofences(getGeofencePendingIntent())
|
|
72
|
+
.addOnSuccessListener {
|
|
73
|
+
activeRegions.clear()
|
|
74
|
+
Log.d(TAG, "All geofences removed")
|
|
75
|
+
}
|
|
76
|
+
.addOnFailureListener { e ->
|
|
77
|
+
Log.e(TAG, "Failed to remove all geofences: ${e.message}")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun buildTransitionTypes(region: GeofenceRegion): Int {
|
|
82
|
+
var types = 0
|
|
83
|
+
if (region.notifyOnEntry) types = types or Geofence.GEOFENCE_TRANSITION_ENTER
|
|
84
|
+
if (region.notifyOnExit) types = types or Geofence.GEOFENCE_TRANSITION_EXIT
|
|
85
|
+
return types
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun getGeofencePendingIntent(): PendingIntent {
|
|
89
|
+
val intent = Intent(ACTION_GEOFENCE).apply {
|
|
90
|
+
setPackage(context.packageName)
|
|
91
|
+
}
|
|
92
|
+
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
93
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
94
|
+
} else {
|
|
95
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
96
|
+
}
|
|
97
|
+
return PendingIntent.getBroadcast(context, 0, intent, flags)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private fun registerReceiver() {
|
|
101
|
+
if (receiver != null) return
|
|
102
|
+
|
|
103
|
+
receiver = object : BroadcastReceiver() {
|
|
104
|
+
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
105
|
+
if (intent?.action != ACTION_GEOFENCE) return
|
|
106
|
+
val geofencingEvent = com.google.android.gms.location.GeofencingEvent.fromIntent(intent)
|
|
107
|
+
if (geofencingEvent == null || geofencingEvent.hasError()) {
|
|
108
|
+
Log.e(TAG, "Geofencing event error: ${geofencingEvent?.errorCode}")
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
val transition = geofencingEvent.geofenceTransition
|
|
113
|
+
val event = when (transition) {
|
|
114
|
+
Geofence.GEOFENCE_TRANSITION_ENTER -> GeofenceEvent.ENTER
|
|
115
|
+
Geofence.GEOFENCE_TRANSITION_EXIT -> GeofenceEvent.EXIT
|
|
116
|
+
else -> return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
geofencingEvent.triggeringGeofences?.forEach { geofence ->
|
|
120
|
+
callback?.invoke(event, geofence.requestId)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
val filter = IntentFilter(ACTION_GEOFENCE)
|
|
126
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
127
|
+
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
|
128
|
+
} else {
|
|
129
|
+
ContextCompat.registerReceiver(
|
|
130
|
+
context,
|
|
131
|
+
receiver,
|
|
132
|
+
filter,
|
|
133
|
+
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun destroy() {
|
|
139
|
+
removeAllGeofences()
|
|
140
|
+
receiver?.let {
|
|
141
|
+
try {
|
|
142
|
+
context.unregisterReceiver(it)
|
|
143
|
+
} catch (_: Exception) {}
|
|
144
|
+
}
|
|
145
|
+
receiver = null
|
|
146
|
+
callback = null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
package com.margelo.nitro.nitrolocationtracking
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.AppOpsManager
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.location.Location
|
|
7
|
+
import android.os.Build
|
|
6
8
|
import android.os.Looper
|
|
9
|
+
import android.provider.Settings
|
|
7
10
|
import android.util.Log
|
|
8
11
|
import com.google.android.gms.location.*
|
|
9
12
|
import kotlin.coroutines.resume
|
|
@@ -21,6 +24,9 @@ class LocationEngine(private val context: Context) {
|
|
|
21
24
|
var onMotionChange: ((Boolean) -> Unit)? = null
|
|
22
25
|
var dbWriter: NativeDBWriter? = null
|
|
23
26
|
var currentRideId: String? = null
|
|
27
|
+
var rejectMockLocations: Boolean = false
|
|
28
|
+
val speedMonitor = SpeedMonitor()
|
|
29
|
+
val tripCalculator = TripCalculator()
|
|
24
30
|
private var lastSpeed = 0f
|
|
25
31
|
private var tracking = false
|
|
26
32
|
|
|
@@ -86,14 +92,31 @@ class LocationEngine(private val context: Context) {
|
|
|
86
92
|
|
|
87
93
|
private fun processLocation(location: Location) {
|
|
88
94
|
val data = locationToData(location)
|
|
95
|
+
|
|
96
|
+
// Skip mock locations if rejection is enabled
|
|
97
|
+
if (rejectMockLocations && data.isMockLocation == true) {
|
|
98
|
+
Log.d(TAG, "Rejecting mock location")
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
onLocation?.invoke(data)
|
|
90
103
|
|
|
104
|
+
// Feed to speed monitor and trip calculator
|
|
105
|
+
speedMonitor.feedLocation(data)
|
|
106
|
+
tripCalculator.feedLocation(data)
|
|
107
|
+
|
|
91
108
|
val isMoving = location.speed > 0.5f
|
|
92
109
|
if (isMoving != (lastSpeed > 0.5f)) onMotionChange?.invoke(isMoving)
|
|
93
110
|
lastSpeed = location.speed
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
private fun locationToData(location: Location): LocationData {
|
|
114
|
+
val isMock = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
115
|
+
location.isMock
|
|
116
|
+
} else {
|
|
117
|
+
@Suppress("DEPRECATION")
|
|
118
|
+
location.isFromMockProvider
|
|
119
|
+
}
|
|
97
120
|
return LocationData(
|
|
98
121
|
latitude = location.latitude,
|
|
99
122
|
longitude = location.longitude,
|
|
@@ -101,7 +124,38 @@ class LocationEngine(private val context: Context) {
|
|
|
101
124
|
speed = location.speed.toDouble(),
|
|
102
125
|
bearing = location.bearing.toDouble(),
|
|
103
126
|
accuracy = location.accuracy.toDouble(),
|
|
104
|
-
timestamp = location.time.toDouble()
|
|
127
|
+
timestamp = location.time.toDouble(),
|
|
128
|
+
isMockLocation = isMock
|
|
105
129
|
)
|
|
106
130
|
}
|
|
131
|
+
|
|
132
|
+
@SuppressLint("DiscouragedPrivateApi")
|
|
133
|
+
fun isFakeGpsEnabled(): Boolean {
|
|
134
|
+
// Pre-API 23: check the global mock location setting
|
|
135
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
136
|
+
@Suppress("DEPRECATION")
|
|
137
|
+
val mockSetting = Settings.Secure.getString(
|
|
138
|
+
context.contentResolver,
|
|
139
|
+
Settings.Secure.ALLOW_MOCK_LOCATION
|
|
140
|
+
)
|
|
141
|
+
return mockSetting == "1"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// API 23+: check if any app holds MOCK_LOCATION permission via AppOpsManager
|
|
145
|
+
try {
|
|
146
|
+
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
|
147
|
+
// Use reflection to access undocumented OP_MOCK_LOCATION (op code 58)
|
|
148
|
+
val opMockLocation = 58
|
|
149
|
+
val method = AppOpsManager::class.java.getMethod(
|
|
150
|
+
"checkOp", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java
|
|
151
|
+
)
|
|
152
|
+
val result = method.invoke(
|
|
153
|
+
appOps, opMockLocation, android.os.Process.myUid(), context.packageName
|
|
154
|
+
) as Int
|
|
155
|
+
return result == AppOpsManager.MODE_ALLOWED
|
|
156
|
+
} catch (e: Exception) {
|
|
157
|
+
Log.w(TAG, "Could not check mock location app ops: ${e.message}")
|
|
158
|
+
}
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
107
161
|
}
|
package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt
CHANGED
|
@@ -19,11 +19,16 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
19
19
|
private var connectionManager = ConnectionManager()
|
|
20
20
|
private var dbWriter: NativeDBWriter? = null
|
|
21
21
|
private var notificationService: NotificationService? = null
|
|
22
|
+
private var geofenceManager: GeofenceManager? = null
|
|
23
|
+
private var providerStatusMonitor: ProviderStatusMonitor? = null
|
|
22
24
|
|
|
23
25
|
private var locationCallback: ((LocationData) -> Unit)? = null
|
|
24
26
|
private var motionCallback: ((Boolean) -> Unit)? = null
|
|
25
27
|
private var connectionStateCallback: ((ConnectionState) -> Unit)? = null
|
|
26
28
|
private var messageCallback: ((String) -> Unit)? = null
|
|
29
|
+
private var geofenceCallback: ((GeofenceEvent, String) -> Unit)? = null
|
|
30
|
+
private var speedAlertCallback: ((SpeedAlertType, Double) -> Unit)? = null
|
|
31
|
+
private var providerStatusCallback: ((LocationProviderStatus, LocationProviderStatus) -> Unit)? = null
|
|
27
32
|
|
|
28
33
|
private var locationConfig: LocationConfig? = null
|
|
29
34
|
|
|
@@ -37,6 +42,8 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
37
42
|
locationEngine = LocationEngine(context)
|
|
38
43
|
dbWriter = NativeDBWriter(context)
|
|
39
44
|
notificationService = NotificationService(context)
|
|
45
|
+
geofenceManager = GeofenceManager(context)
|
|
46
|
+
providerStatusMonitor = ProviderStatusMonitor(context)
|
|
40
47
|
locationEngine?.dbWriter = dbWriter
|
|
41
48
|
connectionManager.dbWriter = dbWriter
|
|
42
49
|
Log.d(TAG, "Components initialized successfully")
|
|
@@ -158,6 +165,124 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
// === Fake GPS Detection ===
|
|
169
|
+
|
|
170
|
+
override fun isFakeGpsEnabled(): Boolean {
|
|
171
|
+
ensureInitialized()
|
|
172
|
+
return locationEngine?.isFakeGpsEnabled() ?: false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
override fun setRejectMockLocations(reject: Boolean) {
|
|
176
|
+
ensureInitialized()
|
|
177
|
+
locationEngine?.rejectMockLocations = reject
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// === Geofencing ===
|
|
181
|
+
|
|
182
|
+
override fun addGeofence(region: GeofenceRegion) {
|
|
183
|
+
ensureInitialized()
|
|
184
|
+
geofenceManager?.addGeofence(region)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
override fun removeGeofence(regionId: String) {
|
|
188
|
+
ensureInitialized()
|
|
189
|
+
geofenceManager?.removeGeofence(regionId)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
override fun removeAllGeofences() {
|
|
193
|
+
ensureInitialized()
|
|
194
|
+
geofenceManager?.removeAllGeofences()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
override fun onGeofenceEvent(callback: (event: GeofenceEvent, regionId: String) -> Unit) {
|
|
198
|
+
geofenceCallback = callback
|
|
199
|
+
ensureInitialized()
|
|
200
|
+
geofenceManager?.setCallback(callback)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// === Speed Monitoring ===
|
|
204
|
+
|
|
205
|
+
override fun configureSpeedMonitor(config: SpeedConfig) {
|
|
206
|
+
ensureInitialized()
|
|
207
|
+
locationEngine?.speedMonitor?.configure(config)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
override fun onSpeedAlert(callback: (alert: SpeedAlertType, currentSpeedKmh: Double) -> Unit) {
|
|
211
|
+
speedAlertCallback = callback
|
|
212
|
+
ensureInitialized()
|
|
213
|
+
locationEngine?.speedMonitor?.setCallback(callback)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
override fun getCurrentSpeed(): Double {
|
|
217
|
+
return locationEngine?.speedMonitor?.getCurrentSpeed() ?: 0.0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// === Distance Calculator ===
|
|
221
|
+
|
|
222
|
+
override fun startTripCalculation() {
|
|
223
|
+
ensureInitialized()
|
|
224
|
+
locationEngine?.tripCalculator?.start()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
override fun stopTripCalculation(): TripStats {
|
|
228
|
+
return locationEngine?.tripCalculator?.stop() ?: TripStats(
|
|
229
|
+
distanceMeters = 0.0, durationMs = 0.0, averageSpeedKmh = 0.0,
|
|
230
|
+
maxSpeedKmh = 0.0, pointCount = 0.0
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override fun getTripStats(): TripStats {
|
|
235
|
+
return locationEngine?.tripCalculator?.getStats() ?: TripStats(
|
|
236
|
+
distanceMeters = 0.0, durationMs = 0.0, averageSpeedKmh = 0.0,
|
|
237
|
+
maxSpeedKmh = 0.0, pointCount = 0.0
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
override fun resetTripCalculation() {
|
|
242
|
+
locationEngine?.tripCalculator?.reset()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// === Location Provider Status ===
|
|
246
|
+
|
|
247
|
+
override fun isLocationServicesEnabled(): Boolean {
|
|
248
|
+
ensureInitialized()
|
|
249
|
+
return providerStatusMonitor?.isLocationServicesEnabled() ?: false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override fun onProviderStatusChange(callback: (gps: LocationProviderStatus, network: LocationProviderStatus) -> Unit) {
|
|
253
|
+
providerStatusCallback = callback
|
|
254
|
+
ensureInitialized()
|
|
255
|
+
providerStatusMonitor?.setCallback(callback)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// === Permission Status ===
|
|
259
|
+
|
|
260
|
+
override fun getLocationPermissionStatus(): PermissionStatus {
|
|
261
|
+
val context = NitroModules.applicationContext ?: return PermissionStatus.NOTDETERMINED
|
|
262
|
+
|
|
263
|
+
val fineGranted = androidx.core.content.ContextCompat.checkSelfPermission(
|
|
264
|
+
context, android.Manifest.permission.ACCESS_FINE_LOCATION
|
|
265
|
+
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
266
|
+
|
|
267
|
+
if (!fineGranted) {
|
|
268
|
+
// Check if we've ever asked — if the app just installed, it's "notDetermined"
|
|
269
|
+
// Android doesn't have a direct "notDetermined" state, so we treat
|
|
270
|
+
// not-granted as DENIED (the JS side can use requestPermission to prompt)
|
|
271
|
+
return PermissionStatus.DENIED
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Fine location granted — check background
|
|
275
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
|
276
|
+
val bgGranted = androidx.core.content.ContextCompat.checkSelfPermission(
|
|
277
|
+
context, android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
|
278
|
+
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
279
|
+
return if (bgGranted) PermissionStatus.ALWAYS else PermissionStatus.WHENINUSE
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Pre-Android 10: fine location = always (no separate background permission)
|
|
283
|
+
return PermissionStatus.ALWAYS
|
|
284
|
+
}
|
|
285
|
+
|
|
161
286
|
// === Notifications ===
|
|
162
287
|
|
|
163
288
|
override fun showLocalNotification(title: String, body: String) {
|
|
@@ -176,5 +301,7 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
176
301
|
locationEngine?.stop()
|
|
177
302
|
connectionManager.disconnect()
|
|
178
303
|
notificationService?.stopForegroundService()
|
|
304
|
+
geofenceManager?.destroy()
|
|
305
|
+
providerStatusMonitor?.destroy()
|
|
179
306
|
}
|
|
180
307
|
}
|
package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/ProviderStatusMonitor.kt
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrolocationtracking
|
|
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.LocationManager
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.util.Log
|
|
10
|
+
|
|
11
|
+
class ProviderStatusMonitor(private val context: Context) {
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
private const val TAG = "ProviderStatusMonitor"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private val locationManager =
|
|
18
|
+
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
19
|
+
private var callback: ((LocationProviderStatus, LocationProviderStatus) -> Unit)? = null
|
|
20
|
+
private var receiver: BroadcastReceiver? = null
|
|
21
|
+
|
|
22
|
+
fun setCallback(callback: (LocationProviderStatus, LocationProviderStatus) -> Unit) {
|
|
23
|
+
this.callback = callback
|
|
24
|
+
registerReceiver()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun isLocationServicesEnabled(): Boolean {
|
|
28
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
29
|
+
locationManager.isLocationEnabled
|
|
30
|
+
} else {
|
|
31
|
+
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
|
|
32
|
+
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private fun registerReceiver() {
|
|
37
|
+
if (receiver != null) return
|
|
38
|
+
|
|
39
|
+
receiver = object : BroadcastReceiver() {
|
|
40
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
41
|
+
if (intent?.action == LocationManager.PROVIDERS_CHANGED_ACTION) {
|
|
42
|
+
notifyStatus()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
|
|
48
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
49
|
+
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
|
50
|
+
} else {
|
|
51
|
+
context.registerReceiver(receiver, filter)
|
|
52
|
+
}
|
|
53
|
+
Log.d(TAG, "Registered provider status receiver")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun notifyStatus() {
|
|
57
|
+
val gps = if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER))
|
|
58
|
+
LocationProviderStatus.ENABLED else LocationProviderStatus.DISABLED
|
|
59
|
+
val network = if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER))
|
|
60
|
+
LocationProviderStatus.ENABLED else LocationProviderStatus.DISABLED
|
|
61
|
+
callback?.invoke(gps, network)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun destroy() {
|
|
65
|
+
receiver?.let {
|
|
66
|
+
try {
|
|
67
|
+
context.unregisterReceiver(it)
|
|
68
|
+
} catch (_: Exception) {}
|
|
69
|
+
}
|
|
70
|
+
receiver = null
|
|
71
|
+
callback = null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrolocationtracking
|
|
2
|
+
|
|
3
|
+
class SpeedMonitor {
|
|
4
|
+
|
|
5
|
+
private var config: SpeedConfig? = null
|
|
6
|
+
private var callback: ((SpeedAlertType, Double) -> Unit)? = null
|
|
7
|
+
private var currentState: SpeedAlertType = SpeedAlertType.NORMALIZED
|
|
8
|
+
private var lastSpeedKmh: Double = 0.0
|
|
9
|
+
|
|
10
|
+
fun configure(config: SpeedConfig) {
|
|
11
|
+
this.config = config
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fun setCallback(callback: (SpeedAlertType, Double) -> Unit) {
|
|
15
|
+
this.callback = callback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fun getCurrentSpeed(): Double = lastSpeedKmh
|
|
19
|
+
|
|
20
|
+
fun feedLocation(data: LocationData) {
|
|
21
|
+
val cfg = config ?: return
|
|
22
|
+
|
|
23
|
+
val speedKmh = data.speed * 3.6 // m/s → km/h
|
|
24
|
+
lastSpeedKmh = speedKmh
|
|
25
|
+
|
|
26
|
+
val newState = when {
|
|
27
|
+
speedKmh > cfg.maxSpeedKmh -> SpeedAlertType.EXCEEDED
|
|
28
|
+
speedKmh < cfg.minSpeedKmh -> SpeedAlertType.BELOW_MINIMUM
|
|
29
|
+
else -> SpeedAlertType.NORMALIZED
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Only fire callback on state transitions
|
|
33
|
+
if (newState != currentState) {
|
|
34
|
+
currentState = newState
|
|
35
|
+
callback?.invoke(newState, speedKmh)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrolocationtracking
|
|
2
|
+
|
|
3
|
+
import kotlin.math.*
|
|
4
|
+
|
|
5
|
+
class TripCalculator {
|
|
6
|
+
|
|
7
|
+
private var active = false
|
|
8
|
+
private var startTimeMs: Long = 0L
|
|
9
|
+
private var totalDistanceMeters: Double = 0.0
|
|
10
|
+
private var maxSpeedKmh: Double = 0.0
|
|
11
|
+
private var speedSumKmh: Double = 0.0
|
|
12
|
+
private var pointCount: Int = 0
|
|
13
|
+
private var lastLat: Double? = null
|
|
14
|
+
private var lastLon: Double? = null
|
|
15
|
+
|
|
16
|
+
val isActive: Boolean get() = active
|
|
17
|
+
|
|
18
|
+
fun start() {
|
|
19
|
+
reset()
|
|
20
|
+
active = true
|
|
21
|
+
startTimeMs = System.currentTimeMillis()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fun stop(): TripStats {
|
|
25
|
+
active = false
|
|
26
|
+
return getStats()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fun getStats(): TripStats {
|
|
30
|
+
val durationMs = if (startTimeMs > 0) {
|
|
31
|
+
(System.currentTimeMillis() - startTimeMs).toDouble()
|
|
32
|
+
} else 0.0
|
|
33
|
+
val avgSpeed = if (pointCount > 0) speedSumKmh / pointCount else 0.0
|
|
34
|
+
return TripStats(
|
|
35
|
+
distanceMeters = totalDistanceMeters,
|
|
36
|
+
durationMs = durationMs,
|
|
37
|
+
averageSpeedKmh = avgSpeed,
|
|
38
|
+
maxSpeedKmh = maxSpeedKmh,
|
|
39
|
+
pointCount = pointCount.toDouble()
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun reset() {
|
|
44
|
+
active = false
|
|
45
|
+
startTimeMs = 0L
|
|
46
|
+
totalDistanceMeters = 0.0
|
|
47
|
+
maxSpeedKmh = 0.0
|
|
48
|
+
speedSumKmh = 0.0
|
|
49
|
+
pointCount = 0
|
|
50
|
+
lastLat = null
|
|
51
|
+
lastLon = null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun feedLocation(data: LocationData) {
|
|
55
|
+
if (!active) return
|
|
56
|
+
|
|
57
|
+
val speedKmh = data.speed * 3.6 // m/s → km/h
|
|
58
|
+
if (speedKmh > maxSpeedKmh) maxSpeedKmh = speedKmh
|
|
59
|
+
speedSumKmh += speedKmh
|
|
60
|
+
pointCount++
|
|
61
|
+
|
|
62
|
+
val prevLat = lastLat
|
|
63
|
+
val prevLon = lastLon
|
|
64
|
+
lastLat = data.latitude
|
|
65
|
+
lastLon = data.longitude
|
|
66
|
+
|
|
67
|
+
if (prevLat != null && prevLon != null) {
|
|
68
|
+
totalDistanceMeters += haversine(prevLat, prevLon, data.latitude, data.longitude)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
companion object {
|
|
73
|
+
private const val EARTH_RADIUS_M = 6371000.0
|
|
74
|
+
|
|
75
|
+
fun haversine(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
|
|
76
|
+
val dLat = Math.toRadians(lat2 - lat1)
|
|
77
|
+
val dLon = Math.toRadians(lon2 - lon1)
|
|
78
|
+
val a = sin(dLat / 2).pow(2) +
|
|
79
|
+
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
|
|
80
|
+
sin(dLon / 2).pow(2)
|
|
81
|
+
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
|
82
|
+
return EARTH_RADIUS_M * c
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
|
|
3
|
+
class GeofenceManager: NSObject, CLLocationManagerDelegate {
|
|
4
|
+
|
|
5
|
+
private let locationManager = CLLocationManager()
|
|
6
|
+
private var activeRegions = [String: GeofenceRegion]()
|
|
7
|
+
private var callback: ((GeofenceEvent, String) -> Void)?
|
|
8
|
+
|
|
9
|
+
override init() {
|
|
10
|
+
super.init()
|
|
11
|
+
locationManager.delegate = self
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func setCallback(_ callback: @escaping (GeofenceEvent, String) -> Void) {
|
|
15
|
+
self.callback = callback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func addGeofence(_ region: GeofenceRegion) {
|
|
19
|
+
let clRegion = CLCircularRegion(
|
|
20
|
+
center: CLLocationCoordinate2D(latitude: region.latitude, longitude: region.longitude),
|
|
21
|
+
radius: region.radius,
|
|
22
|
+
identifier: region.id
|
|
23
|
+
)
|
|
24
|
+
clRegion.notifyOnEntry = region.notifyOnEntry
|
|
25
|
+
clRegion.notifyOnExit = region.notifyOnExit
|
|
26
|
+
|
|
27
|
+
locationManager.startMonitoring(for: clRegion)
|
|
28
|
+
activeRegions[region.id] = region
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func removeGeofence(_ regionId: String) {
|
|
32
|
+
for monitored in locationManager.monitoredRegions {
|
|
33
|
+
if monitored.identifier == regionId {
|
|
34
|
+
locationManager.stopMonitoring(for: monitored)
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
activeRegions.removeValue(forKey: regionId)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func removeAllGeofences() {
|
|
42
|
+
for monitored in locationManager.monitoredRegions {
|
|
43
|
+
locationManager.stopMonitoring(for: monitored)
|
|
44
|
+
}
|
|
45
|
+
activeRegions.removeAll()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MARK: - CLLocationManagerDelegate
|
|
49
|
+
|
|
50
|
+
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
|
|
51
|
+
callback?(.enter, region.identifier)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
|
|
55
|
+
callback?(.exit, region.identifier)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?,
|
|
59
|
+
withError error: Error) {
|
|
60
|
+
if let id = region?.identifier {
|
|
61
|
+
print("[GeofenceManager] Monitoring failed for region \(id): \(error.localizedDescription)")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func destroy() {
|
|
66
|
+
removeAllGeofences()
|
|
67
|
+
callback = nil
|
|
68
|
+
}
|
|
69
|
+
}
|