react-native-nitro-location-tracking 0.1.12 → 0.1.14
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 +52 -0
- package/android/CMakeLists.txt +4 -1
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/AirplaneModeMonitor.kt +72 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/LocationEngine.kt +30 -9
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt +173 -1
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/PermissionStatusMonitor.kt +26 -0
- package/cpp/HybridNitroLocationComplexLogicsCalculation.cpp +204 -0
- package/cpp/HybridNitroLocationComplexLogicsCalculation.hpp +29 -0
- package/ios/LocationEngine.swift +11 -0
- package/ios/NitroLocationTracking.swift +68 -0
- package/lib/module/NitroLocationComplexLogicsCalculation.nitro.js +4 -0
- package/lib/module/NitroLocationComplexLogicsCalculation.nitro.js.map +1 -0
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NitroLocationComplexLogicsCalculation.nitro.d.ts +25 -0
- package/lib/typescript/src/NitroLocationComplexLogicsCalculation.nitro.d.ts.map +1 -0
- package/lib/typescript/src/NitroLocationTracking.nitro.d.ts +17 -0
- package/lib/typescript/src/NitroLocationTracking.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.cpp +25 -0
- package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/HybridNitroLocationTrackingSpec.kt +17 -0
- package/nitrogen/generated/android/nitrolocationtracking+autolinking.cmake +1 -0
- package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.hpp +22 -0
- package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec_cxx.swift +47 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationComplexLogicsCalculationSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationComplexLogicsCalculationSpec.hpp +72 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.cpp +3 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.hpp +3 -0
- package/nitrogen/generated/shared/c++/LocationPoint.hpp +99 -0
- package/nitrogen/generated/shared/c++/TripMathStats.hpp +95 -0
- package/package.json +1 -1
- package/src/NitroLocationComplexLogicsCalculation.nitro.ts +37 -0
- package/src/NitroLocationTracking.nitro.ts +19 -0
- package/src/index.tsx +10 -1
package/README.md
CHANGED
|
@@ -453,6 +453,36 @@ NitroLocationModule.onProviderStatusChange((gps, network) => {
|
|
|
453
453
|
});
|
|
454
454
|
```
|
|
455
455
|
|
|
456
|
+
### Prompt user to enable GPS
|
|
457
|
+
|
|
458
|
+
Ask the user to turn on device location. On Android this shows the native
|
|
459
|
+
Google Play Services in-app dialog ("For better experience, turn on device
|
|
460
|
+
location…") without leaving the app. On iOS there is no equivalent system
|
|
461
|
+
dialog, so this opens your app's Settings page and resolves after the user
|
|
462
|
+
returns to the app.
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
import NitroLocationModule from 'react-native-nitro-location-tracking';
|
|
466
|
+
|
|
467
|
+
async function ensureGpsOn() {
|
|
468
|
+
if (NitroLocationModule.isLocationServicesEnabled()) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
const enabled = await NitroLocationModule.openLocationSettings();
|
|
472
|
+
if (enabled) {
|
|
473
|
+
NitroLocationModule.startTracking();
|
|
474
|
+
} else {
|
|
475
|
+
// User declined the dialog (Android) or did not enable GPS (iOS)
|
|
476
|
+
}
|
|
477
|
+
return enabled;
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Platform behavior:**
|
|
482
|
+
|
|
483
|
+
- **Android** — Uses `SettingsClient.checkLocationSettings()` + `startResolutionForResult`. Resolves `true` if GPS is already on or if the user accepts the dialog, `false` if the user declines or the dialog cannot be shown.
|
|
484
|
+
- **iOS** — Opens the app's Settings page via `UIApplication.openSettingsURLString` and listens for `UIApplication.didBecomeActiveNotification` to detect the return to foreground. Resolves `true` if `CLLocationManager.locationServicesEnabled()` is on after the user returns, `false` otherwise.
|
|
485
|
+
|
|
456
486
|
### Permission Status
|
|
457
487
|
|
|
458
488
|
Check the current location permission status without prompting the user:
|
|
@@ -704,7 +734,10 @@ type PermissionStatus =
|
|
|
704
734
|
| `getTripStats()` | `TripStats` | Get current trip stats without stopping |
|
|
705
735
|
| `resetTripCalculation()` | `void` | Reset trip calculator |
|
|
706
736
|
| `isLocationServicesEnabled()` | `boolean` | Check if GPS/location is enabled on device |
|
|
737
|
+
| `openLocationSettings()` | `Promise<boolean>` | Prompt user to enable GPS. Resolves `true` if enabled, `false` if not |
|
|
707
738
|
| `onProviderStatusChange(callback)` | `void` | Register GPS/network provider status callback |
|
|
739
|
+
| `isAirplaneModeEnabled()` | `boolean` | Check if Airplane mode is active on Android |
|
|
740
|
+
| `onAirplaneModeChange(callback)` | `void` | Register Airplane mode state-transition callback |
|
|
708
741
|
| `getLocationPermissionStatus()` | `PermissionStatus` | Check current location permission without prompting |
|
|
709
742
|
| `requestLocationPermission()` | `Promise<PermissionStatus>` | Request location permission and return the resulting status |
|
|
710
743
|
| `onPermissionStatusChange(callback)` | `void` | Register a callback that fires when location permission status changes |
|
|
@@ -721,6 +754,25 @@ type PermissionStatus =
|
|
|
721
754
|
| `shortestRotation(from, to)` | Calculate shortest rotation path to avoid spinning |
|
|
722
755
|
| `requestLocationPermission()` | Request location + notification permissions (Android) |
|
|
723
756
|
|
|
757
|
+
### Pure C++ Math Engine
|
|
758
|
+
|
|
759
|
+
For computationally heavy tasks like array slicing and trip mapping, use the pure C++ Math Engine to bypass the JS thread entirely.
|
|
760
|
+
|
|
761
|
+
```typescript
|
|
762
|
+
import { NitroLocationCalculations } from 'react-native-nitro-location-tracking';
|
|
763
|
+
|
|
764
|
+
// Instantly compute heavy math directly in C++
|
|
765
|
+
const stats = NitroLocationCalculations.calculateTotalTripStats(points);
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
| Method | Returns | Description |
|
|
769
|
+
| :--- | :--- | :--- |
|
|
770
|
+
| `calculateTotalTripStats(points)` | `TripMathStats` | Instantly computes exact Haversine distance, time, and max/average speed over an array of thousands of points. |
|
|
771
|
+
| `filterAnomalousPoints(points, maxSpeedMps)` | `LocationPoint[]` | Cleans an array of points by mathematically stripping out teleportation jumps that exceed the given speed limit. |
|
|
772
|
+
| `smoothPath(points, toleranceMeters)` | `LocationPoint[]` | Simplifies a highly dense GPS path into perfect drawing lines using the Ramer-Douglas-Peucker algorithm. |
|
|
773
|
+
| `calculateBearing(lat1, lon1, lat2, lon2)` | `number` | Lightning fast C++ trigonometric bearing computation for raw coordinates. |
|
|
774
|
+
| `encodeGeohash(lat, lon, precision)` | `string` | Converts coordinates into a Geohash string instantly representing geological boundaries. |
|
|
775
|
+
|
|
724
776
|
## Publishing to npm
|
|
725
777
|
|
|
726
778
|
### Prerequisites
|
package/android/CMakeLists.txt
CHANGED
|
@@ -6,7 +6,10 @@ set(CMAKE_VERBOSE_MAKEFILE ON)
|
|
|
6
6
|
set(CMAKE_CXX_STANDARD 20)
|
|
7
7
|
|
|
8
8
|
# Define C++ library and add all sources
|
|
9
|
-
add_library(${PACKAGE_NAME} SHARED
|
|
9
|
+
add_library(${PACKAGE_NAME} SHARED
|
|
10
|
+
src/main/cpp/cpp-adapter.cpp
|
|
11
|
+
../cpp/HybridNitroLocationComplexLogicsCalculation.cpp
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
# Add Nitrogen specs :)
|
|
12
15
|
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/nitrolocationtracking+autolinking.cmake)
|
|
@@ -0,0 +1,72 @@
|
|
|
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.os.Build
|
|
8
|
+
import android.provider.Settings
|
|
9
|
+
import android.util.Log
|
|
10
|
+
|
|
11
|
+
class AirplaneModeMonitor(private val context: Context) {
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
private const val TAG = "AirplaneModeMonitor"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private var callback: ((Boolean) -> Unit)? = null
|
|
18
|
+
private var lastState: Boolean? = null
|
|
19
|
+
private var receiver: BroadcastReceiver? = null
|
|
20
|
+
|
|
21
|
+
fun setCallback(callback: (Boolean) -> Unit) {
|
|
22
|
+
this.callback = callback
|
|
23
|
+
|
|
24
|
+
// Emit current state immediately
|
|
25
|
+
val current = isAirplaneModeEnabled()
|
|
26
|
+
lastState = current
|
|
27
|
+
callback.invoke(current)
|
|
28
|
+
|
|
29
|
+
registerReceiver()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun isAirplaneModeEnabled(): Boolean {
|
|
33
|
+
return Settings.Global.getInt(
|
|
34
|
+
context.contentResolver,
|
|
35
|
+
Settings.Global.AIRPLANE_MODE_ON, 0
|
|
36
|
+
) != 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private fun registerReceiver() {
|
|
40
|
+
if (receiver != null) return
|
|
41
|
+
|
|
42
|
+
receiver = object : BroadcastReceiver() {
|
|
43
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
44
|
+
if (intent?.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
|
|
45
|
+
val isAirplaneModeOn = intent.getBooleanExtra("state", false)
|
|
46
|
+
if (isAirplaneModeOn != lastState) {
|
|
47
|
+
lastState = isAirplaneModeOn
|
|
48
|
+
Log.d(TAG, "Airplane mode changed: $isAirplaneModeOn")
|
|
49
|
+
callback?.invoke(isAirplaneModeOn)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
|
|
56
|
+
context.registerReceiver(receiver, filter)
|
|
57
|
+
Log.d(TAG, "Airplane mode receiver registered")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun destroy() {
|
|
61
|
+
receiver?.let {
|
|
62
|
+
try {
|
|
63
|
+
context.unregisterReceiver(it)
|
|
64
|
+
} catch (e: Exception) {
|
|
65
|
+
Log.w(TAG, "Error unregistering receiver: ${e.message}")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
receiver = null
|
|
69
|
+
callback = null
|
|
70
|
+
lastState = null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -34,7 +34,7 @@ class LocationEngine(private val context: Context) {
|
|
|
34
34
|
val isTracking: Boolean get() = tracking
|
|
35
35
|
|
|
36
36
|
@SuppressLint("MissingPermission")
|
|
37
|
-
fun start(config: LocationConfig) {
|
|
37
|
+
fun start(config: LocationConfig): Boolean {
|
|
38
38
|
if (tracking) {
|
|
39
39
|
locationCallback?.let { fusedClient.removeLocationUpdates(it) }
|
|
40
40
|
}
|
|
@@ -59,9 +59,25 @@ class LocationEngine(private val context: Context) {
|
|
|
59
59
|
Log.d(TAG, "onLocationAvailability — isLocationAvailable=${availability.isLocationAvailable}")
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
return try {
|
|
63
|
+
fusedClient.requestLocationUpdates(
|
|
64
|
+
request, locationCallback!!, Looper.getMainLooper())
|
|
65
|
+
tracking = true
|
|
66
|
+
true
|
|
67
|
+
} catch (e: SecurityException) {
|
|
68
|
+
// Permission was revoked between our caller's check and this call, or
|
|
69
|
+
// the FusedLocationProvider is otherwise refusing access. Fail closed
|
|
70
|
+
// instead of crashing the process.
|
|
71
|
+
Log.w(TAG, "requestLocationUpdates refused — permission missing: ${e.message}")
|
|
72
|
+
locationCallback = null
|
|
73
|
+
tracking = false
|
|
74
|
+
false
|
|
75
|
+
} catch (e: Exception) {
|
|
76
|
+
Log.e(TAG, "requestLocationUpdates failed: ${e.message}")
|
|
77
|
+
locationCallback = null
|
|
78
|
+
tracking = false
|
|
79
|
+
false
|
|
80
|
+
}
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
fun stop() {
|
|
@@ -72,13 +88,18 @@ class LocationEngine(private val context: Context) {
|
|
|
72
88
|
|
|
73
89
|
@SuppressLint("MissingPermission")
|
|
74
90
|
fun getCurrentLocation(callback: (LocationData?) -> Unit) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
try {
|
|
92
|
+
fusedClient.lastLocation.addOnSuccessListener { location ->
|
|
93
|
+
if (location != null) {
|
|
94
|
+
callback(locationToData(location))
|
|
95
|
+
} else {
|
|
96
|
+
callback(null)
|
|
97
|
+
}
|
|
98
|
+
}.addOnFailureListener {
|
|
79
99
|
callback(null)
|
|
80
100
|
}
|
|
81
|
-
}
|
|
101
|
+
} catch (e: SecurityException) {
|
|
102
|
+
Log.w(TAG, "getCurrentLocation refused — permission missing: ${e.message}")
|
|
82
103
|
callback(null)
|
|
83
104
|
}
|
|
84
105
|
}
|
package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
package com.margelo.nitro.nitrolocationtracking
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
3
5
|
import android.content.pm.PackageManager
|
|
4
6
|
import android.util.Log
|
|
5
7
|
import androidx.core.app.ActivityCompat
|
|
6
8
|
import androidx.core.content.ContextCompat
|
|
7
9
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
10
|
+
import com.facebook.react.bridge.BaseActivityEventListener
|
|
8
11
|
import com.facebook.react.bridge.ReactContext
|
|
9
12
|
import com.margelo.nitro.NitroModules
|
|
10
13
|
import com.margelo.nitro.core.Promise
|
|
14
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
15
|
import kotlin.coroutines.resume
|
|
12
16
|
import kotlin.coroutines.resumeWithException
|
|
13
17
|
import kotlin.coroutines.suspendCoroutine
|
|
@@ -18,6 +22,7 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
18
22
|
companion object {
|
|
19
23
|
private const val TAG = "NitroLocationTracking"
|
|
20
24
|
private const val PERMISSION_REQUEST_CODE = 9001
|
|
25
|
+
private const val GPS_RESOLUTION_REQUEST_CODE = 9002
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
private var locationEngine: LocationEngine? = null
|
|
@@ -28,6 +33,7 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
28
33
|
private var providerStatusMonitor: ProviderStatusMonitor? = null
|
|
29
34
|
private var permissionStatusMonitor: PermissionStatusMonitor? = null
|
|
30
35
|
private var mockLocationMonitor: MockLocationMonitor? = null
|
|
36
|
+
private var airplaneModeMonitor: AirplaneModeMonitor? = null
|
|
31
37
|
|
|
32
38
|
private var locationCallback: ((LocationData) -> Unit)? = null
|
|
33
39
|
private var motionCallback: ((Boolean) -> Unit)? = null
|
|
@@ -38,6 +44,7 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
38
44
|
private var providerStatusCallback: ((LocationProviderStatus, LocationProviderStatus) -> Unit)? = null
|
|
39
45
|
private var permissionStatusCallback: ((PermissionStatus) -> Unit)? = null
|
|
40
46
|
private var mockLocationCallback: ((Boolean) -> Unit)? = null
|
|
47
|
+
private var airplaneModeCallback: ((Boolean) -> Unit)? = null
|
|
41
48
|
|
|
42
49
|
private var locationConfig: LocationConfig? = null
|
|
43
50
|
|
|
@@ -56,8 +63,29 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
56
63
|
providerStatusMonitor = ProviderStatusMonitor(context)
|
|
57
64
|
permissionStatusMonitor = PermissionStatusMonitor(context)
|
|
58
65
|
mockLocationMonitor = MockLocationMonitor(context)
|
|
66
|
+
airplaneModeMonitor = AirplaneModeMonitor(context)
|
|
59
67
|
locationEngine?.dbWriter = dbWriter
|
|
60
68
|
connectionManager.dbWriter = dbWriter
|
|
69
|
+
|
|
70
|
+
// Always watch for permission revocation so we can proactively tear
|
|
71
|
+
// down tracking + the foreground service before the OS kills us for
|
|
72
|
+
// holding a location-type FGS without the matching permission.
|
|
73
|
+
permissionStatusMonitor?.setInternalCallback { status ->
|
|
74
|
+
if (status == PermissionStatus.DENIED || status == PermissionStatus.RESTRICTED) {
|
|
75
|
+
Log.w(TAG, "Location permission revoked — stopping tracking and foreground service")
|
|
76
|
+
try {
|
|
77
|
+
locationEngine?.stop()
|
|
78
|
+
} catch (e: Exception) {
|
|
79
|
+
Log.w(TAG, "Error stopping location engine: ${e.message}")
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
notificationService?.stopForegroundService()
|
|
83
|
+
} catch (e: Exception) {
|
|
84
|
+
Log.w(TAG, "Error stopping foreground service: ${e.message}")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
Log.d(TAG, "Components initialized successfully")
|
|
62
90
|
return true
|
|
63
91
|
}
|
|
@@ -78,6 +106,20 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
78
106
|
Log.e(TAG, "startTracking failed — could not initialize components")
|
|
79
107
|
return
|
|
80
108
|
}
|
|
109
|
+
|
|
110
|
+
// Permission guard. Starting a foreground service of type `location`
|
|
111
|
+
// without holding ACCESS_FINE_LOCATION throws SecurityException on
|
|
112
|
+
// Android 14+. And even on older versions, requestLocationUpdates
|
|
113
|
+
// will throw SecurityException. Fail closed and notify JS instead
|
|
114
|
+
// of crashing the process.
|
|
115
|
+
val permissionStatus = getLocationPermissionStatus()
|
|
116
|
+
if (permissionStatus == PermissionStatus.DENIED ||
|
|
117
|
+
permissionStatus == PermissionStatus.RESTRICTED) {
|
|
118
|
+
Log.w(TAG, "startTracking aborted — location permission is $permissionStatus")
|
|
119
|
+
permissionStatusCallback?.invoke(permissionStatus)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
81
123
|
val engine = locationEngine ?: return
|
|
82
124
|
engine.onLocation = { data ->
|
|
83
125
|
locationCallback?.invoke(data)
|
|
@@ -85,7 +127,14 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
85
127
|
engine.onMotionChange = { isMoving ->
|
|
86
128
|
motionCallback?.invoke(isMoving)
|
|
87
129
|
}
|
|
88
|
-
engine.start(config)
|
|
130
|
+
val started = engine.start(config)
|
|
131
|
+
if (!started) {
|
|
132
|
+
// engine.start() already logged the reason. Don't start the FGS
|
|
133
|
+
// if tracking itself could not be started — the FGS would just
|
|
134
|
+
// be killed immediately by the OS.
|
|
135
|
+
Log.w(TAG, "startTracking aborted — location engine refused to start")
|
|
136
|
+
return
|
|
137
|
+
}
|
|
89
138
|
|
|
90
139
|
try {
|
|
91
140
|
notificationService?.startForegroundService(
|
|
@@ -94,6 +143,12 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
94
143
|
)
|
|
95
144
|
} catch (e: SecurityException) {
|
|
96
145
|
Log.w(TAG, "Could not start foreground service — missing runtime permissions: ${e.message}")
|
|
146
|
+
// Roll back the tracking session so we don't leak a location
|
|
147
|
+
// request with no owning foreground service.
|
|
148
|
+
try { engine.stop() } catch (_: Exception) {}
|
|
149
|
+
} catch (e: Exception) {
|
|
150
|
+
Log.w(TAG, "Could not start foreground service: ${e.message}")
|
|
151
|
+
try { engine.stop() } catch (_: Exception) {}
|
|
97
152
|
}
|
|
98
153
|
}
|
|
99
154
|
|
|
@@ -364,6 +419,122 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
364
419
|
}
|
|
365
420
|
}
|
|
366
421
|
|
|
422
|
+
override fun openLocationSettings(): Promise<Boolean> {
|
|
423
|
+
return Promise.async {
|
|
424
|
+
suspendCoroutine { cont ->
|
|
425
|
+
val context = NitroModules.applicationContext
|
|
426
|
+
if (context == null) {
|
|
427
|
+
cont.resume(false)
|
|
428
|
+
return@suspendCoroutine
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
val reactContext = context as? ReactContext
|
|
432
|
+
val activity = reactContext?.currentActivity
|
|
433
|
+
if (reactContext == null || activity == null) {
|
|
434
|
+
Log.w(TAG, "openLocationSettings — no current Activity, using fallback")
|
|
435
|
+
openLocationSettingsFallback(context)
|
|
436
|
+
cont.resume(isLocationServicesEnabled())
|
|
437
|
+
return@suspendCoroutine
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Use a high-accuracy location request for the settings check.
|
|
441
|
+
// These values are internal to the SettingsClient call — they do
|
|
442
|
+
// not affect the tracking configuration.
|
|
443
|
+
val locationRequest = com.google.android.gms.location.LocationRequest.Builder(
|
|
444
|
+
com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY,
|
|
445
|
+
10_000L
|
|
446
|
+
).build()
|
|
447
|
+
|
|
448
|
+
val settingsRequest = com.google.android.gms.location.LocationSettingsRequest.Builder()
|
|
449
|
+
.addLocationRequest(locationRequest)
|
|
450
|
+
.setAlwaysShow(true)
|
|
451
|
+
.build()
|
|
452
|
+
|
|
453
|
+
val client = com.google.android.gms.location.LocationServices.getSettingsClient(context)
|
|
454
|
+
val task = client.checkLocationSettings(settingsRequest)
|
|
455
|
+
|
|
456
|
+
// Guard against double-resume — the continuation may otherwise
|
|
457
|
+
// be resumed by both the success listener and the activity
|
|
458
|
+
// result listener in rare race conditions.
|
|
459
|
+
val resumed = AtomicBoolean(false)
|
|
460
|
+
fun safeResume(value: Boolean) {
|
|
461
|
+
if (resumed.compareAndSet(false, true)) {
|
|
462
|
+
cont.resume(value)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
task.addOnSuccessListener {
|
|
467
|
+
// Location settings are already satisfied — GPS is on.
|
|
468
|
+
safeResume(true)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
task.addOnFailureListener { exception ->
|
|
472
|
+
if (exception is com.google.android.gms.common.api.ResolvableApiException) {
|
|
473
|
+
// Register the activity result listener BEFORE showing
|
|
474
|
+
// the dialog so we cannot miss the result.
|
|
475
|
+
val listener = object : BaseActivityEventListener() {
|
|
476
|
+
override fun onActivityResult(
|
|
477
|
+
activity: Activity?,
|
|
478
|
+
requestCode: Int,
|
|
479
|
+
resultCode: Int,
|
|
480
|
+
data: Intent?
|
|
481
|
+
) {
|
|
482
|
+
if (requestCode != GPS_RESOLUTION_REQUEST_CODE) return
|
|
483
|
+
reactContext.removeActivityEventListener(this)
|
|
484
|
+
// RESULT_OK = user tapped "OK" in the dialog and GPS is now on.
|
|
485
|
+
// RESULT_CANCELED = user tapped "No thanks" or dismissed.
|
|
486
|
+
val enabled = resultCode == Activity.RESULT_OK
|
|
487
|
+
safeResume(enabled)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
reactContext.addActivityEventListener(listener)
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
exception.startResolutionForResult(
|
|
494
|
+
activity, GPS_RESOLUTION_REQUEST_CODE
|
|
495
|
+
)
|
|
496
|
+
} catch (e: Exception) {
|
|
497
|
+
Log.w(TAG, "Failed to show resolution dialog: ${e.message}")
|
|
498
|
+
reactContext.removeActivityEventListener(listener)
|
|
499
|
+
openLocationSettingsFallback(context)
|
|
500
|
+
safeResume(isLocationServicesEnabled())
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
// Not resolvable (e.g. SETTINGS_CHANGE_UNAVAILABLE on an
|
|
504
|
+
// airplane-mode-locked device) — fall back to the system
|
|
505
|
+
// settings screen.
|
|
506
|
+
Log.w(TAG, "Location settings not resolvable: ${exception.message}")
|
|
507
|
+
openLocationSettingsFallback(context)
|
|
508
|
+
safeResume(isLocationServicesEnabled())
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private fun openLocationSettingsFallback(context: android.content.Context) {
|
|
516
|
+
val intent = android.content.Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)
|
|
517
|
+
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
518
|
+
try {
|
|
519
|
+
context.startActivity(intent)
|
|
520
|
+
} catch (e: Exception) {
|
|
521
|
+
Log.e(TAG, "Failed to open location settings: ${e.message}")
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// === Device State Monitoring ===
|
|
526
|
+
|
|
527
|
+
override fun isAirplaneModeEnabled(): Boolean {
|
|
528
|
+
ensureInitialized()
|
|
529
|
+
return airplaneModeMonitor?.isAirplaneModeEnabled() ?: false
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
override fun onAirplaneModeChange(callback: (isEnabled: Boolean) -> Unit) {
|
|
533
|
+
airplaneModeCallback = callback
|
|
534
|
+
ensureInitialized()
|
|
535
|
+
airplaneModeMonitor?.setCallback(callback)
|
|
536
|
+
}
|
|
537
|
+
|
|
367
538
|
// === Distance Utilities ===
|
|
368
539
|
|
|
369
540
|
override fun getDistanceBetween(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
|
|
@@ -399,5 +570,6 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
|
399
570
|
providerStatusMonitor?.destroy()
|
|
400
571
|
permissionStatusMonitor?.destroy()
|
|
401
572
|
mockLocationMonitor?.destroy()
|
|
573
|
+
airplaneModeMonitor?.destroy()
|
|
402
574
|
}
|
|
403
575
|
}
|
package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/PermissionStatusMonitor.kt
CHANGED
|
@@ -25,7 +25,9 @@ class PermissionStatusMonitor(private val context: Context) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
private var callback: ((PermissionStatus) -> Unit)? = null
|
|
28
|
+
private var internalCallback: ((PermissionStatus) -> Unit)? = null
|
|
28
29
|
private var lastStatus: PermissionStatus? = null
|
|
30
|
+
private var observerRegistered = false
|
|
29
31
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
30
32
|
|
|
31
33
|
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
|
@@ -36,8 +38,25 @@ class PermissionStatusMonitor(private val context: Context) {
|
|
|
36
38
|
|
|
37
39
|
fun setCallback(callback: (PermissionStatus) -> Unit) {
|
|
38
40
|
this.callback = callback
|
|
41
|
+
ensureObserverRegistered()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Internal callback used by the native module to react to permission loss
|
|
46
|
+
* (e.g. auto-stop tracking) independently of any JS listener. Registering
|
|
47
|
+
* this also starts the lifecycle observer, so native cleanup works even
|
|
48
|
+
* when the user has not called onPermissionStatusChange() from JS.
|
|
49
|
+
*/
|
|
50
|
+
fun setInternalCallback(callback: (PermissionStatus) -> Unit) {
|
|
51
|
+
this.internalCallback = callback
|
|
52
|
+
ensureObserverRegistered()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun ensureObserverRegistered() {
|
|
56
|
+
if (observerRegistered) return
|
|
39
57
|
// Capture the current status so we only fire on actual changes
|
|
40
58
|
lastStatus = getCurrentPermissionStatus()
|
|
59
|
+
observerRegistered = true
|
|
41
60
|
|
|
42
61
|
// addObserver MUST be called on the main thread
|
|
43
62
|
mainHandler.post {
|
|
@@ -46,6 +65,7 @@ class PermissionStatusMonitor(private val context: Context) {
|
|
|
46
65
|
Log.d(TAG, "Registered lifecycle observer for permission changes")
|
|
47
66
|
} catch (e: Exception) {
|
|
48
67
|
Log.e(TAG, "Failed to register lifecycle observer: ${e.message}")
|
|
68
|
+
observerRegistered = false
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
71
|
}
|
|
@@ -55,6 +75,10 @@ class PermissionStatusMonitor(private val context: Context) {
|
|
|
55
75
|
if (current != lastStatus) {
|
|
56
76
|
Log.d(TAG, "Permission status changed: $lastStatus -> $current")
|
|
57
77
|
lastStatus = current
|
|
78
|
+
// Native cleanup first, JS notification second — so by the time JS
|
|
79
|
+
// hears about the denial, the native side has already released
|
|
80
|
+
// resources (tracking session, foreground service).
|
|
81
|
+
internalCallback?.invoke(current)
|
|
58
82
|
callback?.invoke(current)
|
|
59
83
|
}
|
|
60
84
|
}
|
|
@@ -86,7 +110,9 @@ class PermissionStatusMonitor(private val context: Context) {
|
|
|
86
110
|
ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
|
|
87
111
|
} catch (_: Exception) {}
|
|
88
112
|
}
|
|
113
|
+
observerRegistered = false
|
|
89
114
|
callback = null
|
|
115
|
+
internalCallback = null
|
|
90
116
|
lastStatus = null
|
|
91
117
|
}
|
|
92
118
|
}
|