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.
Files changed (76) hide show
  1. package/README.md +677 -7
  2. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/GeofenceManager.kt +148 -0
  3. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/LocationEngine.kt +55 -1
  4. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt +127 -0
  5. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/ProviderStatusMonitor.kt +73 -0
  6. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/SpeedMonitor.kt +38 -0
  7. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/TripCalculator.kt +85 -0
  8. package/ios/GeofenceManager.swift +69 -0
  9. package/ios/LocationEngine.swift +56 -2
  10. package/ios/NitroLocationTracking.swift +104 -0
  11. package/ios/SpeedMonitor.swift +48 -0
  12. package/ios/TripCalculator.swift +93 -0
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/typescript/src/NitroLocationTracking.nitro.d.ts +44 -0
  15. package/lib/typescript/src/NitroLocationTracking.nitro.d.ts.map +1 -1
  16. package/lib/typescript/src/index.d.ts +1 -1
  17. package/lib/typescript/src/index.d.ts.map +1 -1
  18. package/lib/typescript/test.d.ts +1 -0
  19. package/lib/typescript/test.d.ts.map +1 -0
  20. package/nitrogen/generated/android/c++/JFunc_void_GeofenceEvent_std__string.hpp +78 -0
  21. package/nitrogen/generated/android/c++/JFunc_void_LocationData.hpp +1 -0
  22. package/nitrogen/generated/android/c++/JFunc_void_LocationProviderStatus_LocationProviderStatus.hpp +77 -0
  23. package/nitrogen/generated/android/c++/JFunc_void_SpeedAlertType_double.hpp +77 -0
  24. package/nitrogen/generated/android/c++/JGeofenceEvent.hpp +58 -0
  25. package/nitrogen/generated/android/c++/JGeofenceRegion.hpp +77 -0
  26. package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.cpp +102 -0
  27. package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.hpp +16 -0
  28. package/nitrogen/generated/android/c++/JLocationData.hpp +8 -4
  29. package/nitrogen/generated/android/c++/JLocationProviderStatus.hpp +58 -0
  30. package/nitrogen/generated/android/c++/JPermissionStatus.hpp +67 -0
  31. package/nitrogen/generated/android/c++/JSpeedAlertType.hpp +61 -0
  32. package/nitrogen/generated/android/c++/JSpeedConfig.hpp +65 -0
  33. package/nitrogen/generated/android/c++/JTripStats.hpp +73 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_GeofenceEvent_std__string.kt +80 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_LocationProviderStatus_LocationProviderStatus.kt +80 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_SpeedAlertType_double.kt +80 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/GeofenceEvent.kt +23 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/GeofenceRegion.kt +53 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/HybridNitroLocationTrackingSpec.kt +79 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationData.kt +6 -3
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationProviderStatus.kt +23 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/PermissionStatus.kt +26 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/SpeedAlertType.kt +24 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/SpeedConfig.kt +44 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/TripStats.kt +50 -0
  46. package/nitrogen/generated/android/nitrolocationtrackingOnLoad.cpp +6 -0
  47. package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.cpp +24 -0
  48. package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.hpp +124 -0
  49. package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Umbrella.hpp +22 -0
  50. package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.hpp +130 -0
  51. package/nitrogen/generated/ios/swift/Func_void_GeofenceEvent_std__string.swift +46 -0
  52. package/nitrogen/generated/ios/swift/Func_void_LocationProviderStatus_LocationProviderStatus.swift +46 -0
  53. package/nitrogen/generated/ios/swift/Func_void_SpeedAlertType_double.swift +46 -0
  54. package/nitrogen/generated/ios/swift/GeofenceEvent.swift +40 -0
  55. package/nitrogen/generated/ios/swift/GeofenceRegion.swift +54 -0
  56. package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec.swift +16 -0
  57. package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec_cxx.swift +197 -0
  58. package/nitrogen/generated/ios/swift/LocationData.swift +20 -2
  59. package/nitrogen/generated/ios/swift/LocationProviderStatus.swift +40 -0
  60. package/nitrogen/generated/ios/swift/PermissionStatus.swift +52 -0
  61. package/nitrogen/generated/ios/swift/SpeedAlertType.swift +44 -0
  62. package/nitrogen/generated/ios/swift/SpeedConfig.swift +39 -0
  63. package/nitrogen/generated/ios/swift/TripStats.swift +49 -0
  64. package/nitrogen/generated/shared/c++/GeofenceEvent.hpp +76 -0
  65. package/nitrogen/generated/shared/c++/GeofenceRegion.hpp +103 -0
  66. package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.cpp +16 -0
  67. package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.hpp +37 -0
  68. package/nitrogen/generated/shared/c++/LocationData.hpp +7 -3
  69. package/nitrogen/generated/shared/c++/LocationProviderStatus.hpp +76 -0
  70. package/nitrogen/generated/shared/c++/PermissionStatus.hpp +88 -0
  71. package/nitrogen/generated/shared/c++/SpeedAlertType.hpp +80 -0
  72. package/nitrogen/generated/shared/c++/SpeedConfig.hpp +91 -0
  73. package/nitrogen/generated/shared/c++/TripStats.hpp +99 -0
  74. package/package.json +2 -2
  75. package/src/NitroLocationTracking.nitro.ts +71 -0
  76. 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
  }
@@ -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
  }
@@ -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
+ }