react-native-nitro-location-tracking 0.1.10 → 0.1.11

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.
@@ -3,6 +3,8 @@ package com.margelo.nitro.nitrolocationtracking
3
3
  import android.content.Context
4
4
  import android.content.pm.PackageManager
5
5
  import android.os.Build
6
+ import android.os.Handler
7
+ import android.os.Looper
6
8
  import android.util.Log
7
9
  import androidx.core.content.ContextCompat
8
10
  import androidx.lifecycle.DefaultLifecycleObserver
@@ -24,6 +26,7 @@ class PermissionStatusMonitor(private val context: Context) {
24
26
 
25
27
  private var callback: ((PermissionStatus) -> Unit)? = null
26
28
  private var lastStatus: PermissionStatus? = null
29
+ private val mainHandler = Handler(Looper.getMainLooper())
27
30
 
28
31
  private val lifecycleObserver = object : DefaultLifecycleObserver {
29
32
  override fun onStart(owner: LifecycleOwner) {
@@ -36,12 +39,14 @@ class PermissionStatusMonitor(private val context: Context) {
36
39
  // Capture the current status so we only fire on actual changes
37
40
  lastStatus = getCurrentPermissionStatus()
38
41
 
39
- // Observe the process lifecycle (app foreground / background)
40
- try {
41
- ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
42
- Log.d(TAG, "Registered lifecycle observer for permission changes")
43
- } catch (e: Exception) {
44
- Log.e(TAG, "Failed to register lifecycle observer: ${e.message}")
42
+ // addObserver MUST be called on the main thread
43
+ mainHandler.post {
44
+ try {
45
+ ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
46
+ Log.d(TAG, "Registered lifecycle observer for permission changes")
47
+ } catch (e: Exception) {
48
+ Log.e(TAG, "Failed to register lifecycle observer: ${e.message}")
49
+ }
45
50
  }
46
51
  }
47
52
 
@@ -76,9 +81,11 @@ class PermissionStatusMonitor(private val context: Context) {
76
81
  }
77
82
 
78
83
  fun destroy() {
79
- try {
80
- ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
81
- } catch (_: Exception) {}
84
+ mainHandler.post {
85
+ try {
86
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
87
+ } catch (_: Exception) {}
88
+ }
82
89
  callback = null
83
90
  lastStatus = null
84
91
  }
@@ -1,27 +1,54 @@
1
1
  package com.margelo.nitro.nitrolocationtracking
2
2
 
3
- import android.content.BroadcastReceiver
4
3
  import android.content.Context
5
- import android.content.Intent
6
- import android.content.IntentFilter
4
+ import android.database.ContentObserver
7
5
  import android.location.LocationManager
8
6
  import android.os.Build
7
+ import android.os.Handler
8
+ import android.os.Looper
9
+ import android.provider.Settings
9
10
  import android.util.Log
10
11
 
12
+ /**
13
+ * Monitors GPS / network location provider status changes using a ContentObserver
14
+ * on Settings.Secure.LOCATION_MODE.
15
+ *
16
+ * Why ContentObserver instead of BroadcastReceiver?
17
+ * Several OEMs (notably Samsung) suppress PROVIDERS_CHANGED_ACTION broadcasts,
18
+ * making BroadcastReceiver unreliable. ContentObserver watches the Settings DB
19
+ * directly and fires on all devices.
20
+ *
21
+ * Implementation notes:
22
+ * - ContentObserver.onChange fires BEFORE the LocationManager reflects the new
23
+ * state, so we post a short delayed read (150 ms) to get the correct values.
24
+ * - onChange can fire multiple times per toggle; we debounce with a pending
25
+ * Runnable and deduplicate by comparing with lastGps / lastNetwork.
26
+ */
11
27
  class ProviderStatusMonitor(private val context: Context) {
12
28
 
13
29
  companion object {
14
30
  private const val TAG = "ProviderStatusMonitor"
31
+ /** Delay (ms) to let the system apply the setting before we read it. */
32
+ private const val READ_DELAY_MS = 150L
15
33
  }
16
34
 
17
35
  private val locationManager =
18
36
  context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
37
+ private val mainHandler = Handler(Looper.getMainLooper())
38
+
19
39
  private var callback: ((LocationProviderStatus, LocationProviderStatus) -> Unit)? = null
20
- private var receiver: BroadcastReceiver? = null
40
+ private var contentObserver: ContentObserver? = null
41
+ private var pendingNotify: Runnable? = null
42
+
43
+ // Track last-notified values to avoid duplicate callbacks
44
+ private var lastGps: LocationProviderStatus? = null
45
+ private var lastNetwork: LocationProviderStatus? = null
21
46
 
22
47
  fun setCallback(callback: (LocationProviderStatus, LocationProviderStatus) -> Unit) {
23
48
  this.callback = callback
24
- registerReceiver()
49
+ registerObserver()
50
+ // Emit current status immediately (no delay needed — values are already settled)
51
+ emitCurrentStatus()
25
52
  }
26
53
 
27
54
  fun isLocationServicesEnabled(): Boolean {
@@ -33,41 +60,78 @@ class ProviderStatusMonitor(private val context: Context) {
33
60
  }
34
61
  }
35
62
 
36
- private fun registerReceiver() {
37
- if (receiver != null) return
63
+ // ── Observer registration ────────────────────────────────────────────
38
64
 
39
- receiver = object : BroadcastReceiver() {
40
- override fun onReceive(context: Context?, intent: Intent?) {
41
- if (intent?.action == LocationManager.PROVIDERS_CHANGED_ACTION) {
42
- notifyStatus()
43
- }
65
+ private fun registerObserver() {
66
+ if (contentObserver != null) return
67
+
68
+ contentObserver = object : ContentObserver(mainHandler) {
69
+ override fun onChange(selfChange: Boolean) {
70
+ super.onChange(selfChange)
71
+ scheduleNotify()
44
72
  }
45
73
  }
46
74
 
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")
75
+ // LOCATION_MODE is the single authoritative setting for the global
76
+ // location toggle (GPS + Network). Watching only this avoids the
77
+ // double-fire that occurs when observing LOCATION_PROVIDERS_ALLOWED too.
78
+ context.contentResolver.registerContentObserver(
79
+ Settings.Secure.getUriFor(Settings.Secure.LOCATION_MODE),
80
+ false,
81
+ contentObserver!!
82
+ )
83
+
84
+ Log.d(TAG, "ContentObserver registered")
85
+ }
86
+
87
+ // ── Debounced + delayed notification ─────────────────────────────────
88
+
89
+ /**
90
+ * Schedule a delayed read of the provider status.
91
+ * If onChange fires multiple times in quick succession, the previous
92
+ * pending Runnable is cancelled so we only read once after things settle.
93
+ */
94
+ private fun scheduleNotify() {
95
+ pendingNotify?.let { mainHandler.removeCallbacks(it) }
96
+
97
+ val runnable = Runnable { emitCurrentStatus() }
98
+ pendingNotify = runnable
99
+ mainHandler.postDelayed(runnable, READ_DELAY_MS)
54
100
  }
55
101
 
56
- private fun notifyStatus() {
102
+ /**
103
+ * Read the current GPS + Network provider state and invoke the callback
104
+ * only if the values actually changed since the last notification.
105
+ */
106
+ private fun emitCurrentStatus() {
57
107
  val gps = if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER))
58
108
  LocationProviderStatus.ENABLED else LocationProviderStatus.DISABLED
59
109
  val network = if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER))
60
110
  LocationProviderStatus.ENABLED else LocationProviderStatus.DISABLED
111
+
112
+ // Deduplicate — only fire if something actually changed
113
+ if (gps == lastGps && network == lastNetwork) return
114
+
115
+ lastGps = gps
116
+ lastNetwork = network
117
+ Log.d(TAG, "Provider status changed: GPS=$gps, Network=$network")
61
118
  callback?.invoke(gps, network)
62
119
  }
63
120
 
121
+ // ── Cleanup ──────────────────────────────────────────────────────────
122
+
64
123
  fun destroy() {
65
- receiver?.let {
124
+ pendingNotify?.let { mainHandler.removeCallbacks(it) }
125
+ pendingNotify = null
126
+
127
+ contentObserver?.let {
66
128
  try {
67
- context.unregisterReceiver(it)
129
+ context.contentResolver.unregisterContentObserver(it)
68
130
  } catch (_: Exception) {}
69
131
  }
70
- receiver = null
132
+ contentObserver = null
71
133
  callback = null
134
+ lastGps = null
135
+ lastNetwork = null
72
136
  }
73
137
  }
@@ -21,8 +21,31 @@ class LocationEngine: NSObject, CLLocationManagerDelegate {
21
21
  var rejectMockLocations: Bool = false
22
22
  let speedMonitor = SpeedMonitor()
23
23
  let tripCalculator = TripCalculator()
24
- var providerStatusCallback: ((LocationProviderStatus, LocationProviderStatus) -> Void)?
25
- var permissionStatusCallback: ((PermissionStatus) -> Void)?
24
+ var providerStatusCallback: ((LocationProviderStatus, LocationProviderStatus) -> Void)? {
25
+ didSet {
26
+ // Emit current status immediately when the callback is first set
27
+ if providerStatusCallback != nil {
28
+ let enabled = CLLocationManager.locationServicesEnabled()
29
+ let status: LocationProviderStatus = enabled ? .enabled : .disabled
30
+ lastProviderEnabled = enabled
31
+ providerStatusCallback?(status, status)
32
+ }
33
+ }
34
+ }
35
+ var permissionStatusCallback: ((PermissionStatus) -> Void)? {
36
+ didSet {
37
+ // Emit current status immediately when the callback is first set
38
+ if permissionStatusCallback != nil {
39
+ let current = Self.mapAuthStatus(CLLocationManager.authorizationStatus())
40
+ lastPermissionStatus = current
41
+ permissionStatusCallback?(current)
42
+ }
43
+ }
44
+ }
45
+
46
+ // Deduplication: track last-notified values
47
+ private var lastPermissionStatus: PermissionStatus?
48
+ private var lastProviderEnabled: Bool?
26
49
 
27
50
  /// The most recently received location from Core Location (for distance calculations)
28
51
  var lastCLLocation: CLLocation? {
@@ -193,6 +216,8 @@ class LocationEngine: NSObject, CLLocationManagerDelegate {
193
216
  return CLLocationManager.locationServicesEnabled()
194
217
  }
195
218
 
219
+ // MARK: - Authorization / Provider change delegate
220
+
196
221
  func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
197
222
  guard manager === locationManager else { return }
198
223
 
@@ -213,27 +238,39 @@ class LocationEngine: NSObject, CLLocationManagerDelegate {
213
238
  }
214
239
  }
215
240
 
216
- // Notify JS about permission status change
217
- let permStatus: PermissionStatus
218
- switch authStatus {
241
+ // Notify JS about permission status change (deduplicated)
242
+ let permStatus = Self.mapAuthStatus(authStatus)
243
+ if permStatus != lastPermissionStatus {
244
+ lastPermissionStatus = permStatus
245
+ permissionStatusCallback?(permStatus)
246
+ }
247
+
248
+ // Notify JS about provider status change (deduplicated)
249
+ let enabled = CLLocationManager.locationServicesEnabled()
250
+ if enabled != lastProviderEnabled {
251
+ lastProviderEnabled = enabled
252
+ let status: LocationProviderStatus = enabled ? .enabled : .disabled
253
+ providerStatusCallback?(status, status)
254
+ }
255
+ }
256
+
257
+ // MARK: - Helpers
258
+
259
+ private static func mapAuthStatus(_ status: CLAuthorizationStatus) -> PermissionStatus {
260
+ switch status {
219
261
  case .notDetermined:
220
- permStatus = .notdetermined
262
+ return .notdetermined
221
263
  case .restricted:
222
- permStatus = .restricted
264
+ return .restricted
223
265
  case .denied:
224
- permStatus = .denied
266
+ return .denied
225
267
  case .authorizedWhenInUse:
226
- permStatus = .wheninuse
268
+ return .wheninuse
227
269
  case .authorizedAlways:
228
- permStatus = .always
270
+ return .always
229
271
  @unknown default:
230
- permStatus = .notdetermined
272
+ return .notdetermined
231
273
  }
232
- permissionStatusCallback?(permStatus)
233
-
234
- let enabled = CLLocationManager.locationServicesEnabled()
235
- let status: LocationProviderStatus = enabled ? .enabled : .disabled
236
- providerStatusCallback?(status, status)
237
274
  }
238
275
  }
239
276
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-location-tracking",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "A React Native Nitro module for location tracking",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",