react-native-nitro-geolocation 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -1
- package/README.md +318 -0
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/GetCurrentPosition.kt +341 -0
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocation.kt +76 -5
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/RequestAuthorization.kt +164 -0
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/WatchPosition.kt +228 -0
- package/ios/LocationManager.swift +529 -0
- package/ios/NitroGeolocation.swift +96 -2
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/c++/JAuthorizationLevelInternal.hpp +62 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +74 -0
- package/nitrogen/generated/android/c++/JFunc_void_GeolocationError.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_GeolocationResponse.hpp +79 -0
- package/nitrogen/generated/android/c++/JGeolocationCoordinates.hpp +77 -0
- package/nitrogen/generated/android/c++/JGeolocationError.hpp +69 -0
- package/nitrogen/generated/android/c++/JGeolocationOptions.hpp +77 -0
- package/nitrogen/generated/android/c++/JGeolocationResponse.hpp +59 -0
- package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.cpp +98 -0
- package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JLocationProviderInternal.hpp +62 -0
- package/nitrogen/generated/android/c++/JRNConfigurationInternal.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevelInternal.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationError.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationResponse.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationCoordinates.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationError.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationOptions.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationResponse.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationSpec.kt +87 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderInternal.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/RNConfigurationInternal.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/nitrogeolocationOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrogeolocation+autolinking.cmake +81 -0
- package/nitrogen/generated/android/nitrogeolocation+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrogeolocationOnLoad.cpp +50 -0
- package/nitrogen/generated/android/nitrogeolocationOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/NitroGeolocation+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.cpp +56 -0
- package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.hpp +252 -0
- package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Umbrella.hpp +67 -0
- package/nitrogen/generated/ios/NitroGeolocationAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroGeolocationAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.hpp +125 -0
- package/nitrogen/generated/ios/swift/AuthorizationLevelInternal.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_GeolocationError.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_GeolocationResponse.swift +47 -0
- package/nitrogen/generated/ios/swift/GeolocationCoordinates.swift +149 -0
- package/nitrogen/generated/ios/swift/GeolocationError.swift +79 -0
- package/nitrogen/generated/ios/swift/GeolocationOptions.swift +185 -0
- package/nitrogen/generated/ios/swift/GeolocationResponse.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec_cxx.swift +236 -0
- package/nitrogen/generated/ios/swift/LocationProviderInternal.swift +44 -0
- package/nitrogen/generated/ios/swift/RNConfigurationInternal.swift +104 -0
- package/nitrogen/generated/shared/c++/AuthorizationLevelInternal.hpp +80 -0
- package/nitrogen/generated/shared/c++/GeolocationCoordinates.hpp +91 -0
- package/nitrogen/generated/shared/c++/GeolocationError.hpp +83 -0
- package/nitrogen/generated/shared/c++/GeolocationOptions.hpp +91 -0
- package/nitrogen/generated/shared/c++/GeolocationResponse.hpp +72 -0
- package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.hpp +79 -0
- package/nitrogen/generated/shared/c++/LocationProviderInternal.hpp +80 -0
- package/nitrogen/generated/shared/c++/RNConfigurationInternal.hpp +84 -0
- package/package.json +34 -10
- package/src/NitroGeolocation.nitro.ts +38 -3
- package/src/NitroGeolocationModule.ts +5 -0
- package/src/clearWatch.ts +13 -0
- package/src/getCurrentPosition.ts +14 -0
- package/src/index.tsx +32 -7
- package/src/requestAuthorization.ts +9 -0
- package/src/setRNConfiguration.ts +22 -0
- package/src/stopObserving.ts +12 -0
- package/src/types.ts +43 -0
- package/src/watchPosition.ts +26 -0
- package/nitro.json +0 -17
- package/turbo.json +0 -42
package/LICENSE
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This project is a Nitro Modules port of @react-native-community/geolocation
|
|
4
|
+
|
|
5
|
+
Original work Copyright (c) 2022-present, React Native Community
|
|
6
|
+
Modified work Copyright (c) 2025, jingjing2222
|
|
4
7
|
|
|
5
8
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
9
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# react-native-nitro-geolocation
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-nitro-geolocation)
|
|
4
|
+
|
|
5
|
+
[`@react-native-community/geolocation`](https://github.com/michalchudziak/react-native-geolocation)
|
|
6
|
+
for the **React Native New Architecture** — with 100% API compatibility.
|
|
7
|
+
|
|
8
|
+

|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🧭 Introduction
|
|
20
|
+
|
|
21
|
+
The `@react-native-community/geolocation` package has long been the standard way to access device location in React Native apps.
|
|
22
|
+
|
|
23
|
+
With React Native moving toward **Nitro Modules**, **Fabric**, and **JSI**,
|
|
24
|
+
this project — **React Native Nitro Geolocation** — brings the same familiar API to the new architecture.
|
|
25
|
+
|
|
26
|
+
It provides the **same API surface** with:
|
|
27
|
+
|
|
28
|
+
- 🚀 Faster performance via direct **JSI bindings**
|
|
29
|
+
- 📱 Improved native consistency (Android + iOS)
|
|
30
|
+
- 🔁 Seamless migration from `@react-native-community/geolocation`
|
|
31
|
+
- 🧩 TypeScript-first developer experience
|
|
32
|
+
- 🔄 100% API compatibility (drop-in replacement)
|
|
33
|
+
|
|
34
|
+
Whether upgrading an existing app or building a new one,
|
|
35
|
+
**React Native Nitro Geolocation** keeps the simplicity you know — with modern internals.
|
|
36
|
+
|
|
37
|
+
## 🏗 Architecture Comparison
|
|
38
|
+
|
|
39
|
+
### 🧩 Origin: Event-based Architecture (`@react-native-community/geolocation`)
|
|
40
|
+
|
|
41
|
+
~~~
|
|
42
|
+
JavaScript
|
|
43
|
+
↓ EventEmitter.addListener('geolocationDidChange', callback)
|
|
44
|
+
↓ (Callback stored in JS)
|
|
45
|
+
React Native Bridge (JSON serialization)
|
|
46
|
+
↓
|
|
47
|
+
Native Layer (Android/iOS)
|
|
48
|
+
↓ LocationListener receives updates
|
|
49
|
+
↓ emit('geolocationDidChange', data)
|
|
50
|
+
↓
|
|
51
|
+
EventEmitter dispatches to all listeners
|
|
52
|
+
↓
|
|
53
|
+
User callback executed
|
|
54
|
+
~~~
|
|
55
|
+
|
|
56
|
+
**Key traits:**
|
|
57
|
+
- Callbacks stored only in JS
|
|
58
|
+
- Bridge serialization on every update
|
|
59
|
+
- One shared event stream
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### ⚡ Modern: Direct Callback Architecture (`react-native-nitro-geolocation`)
|
|
64
|
+
|
|
65
|
+
~~~
|
|
66
|
+
JavaScript
|
|
67
|
+
↓ Geolocation.watchPosition(success, error)
|
|
68
|
+
↓ (Callbacks passed directly to native via JSI)
|
|
69
|
+
JSI Layer (No Bridge!)
|
|
70
|
+
↓
|
|
71
|
+
Native Layer (Kotlin/Swift)
|
|
72
|
+
↓ callback.success(position) → JSI direct call
|
|
73
|
+
↓
|
|
74
|
+
User callback executed immediately
|
|
75
|
+
~~~
|
|
76
|
+
|
|
77
|
+
**Key traits:**
|
|
78
|
+
- Callbacks passed as native JSI references
|
|
79
|
+
- No Bridge serialization
|
|
80
|
+
- Independent callback per watcher
|
|
81
|
+
- Native → JS communication in real time
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ⚡ Quick Start
|
|
86
|
+
|
|
87
|
+
### 1. Installation
|
|
88
|
+
|
|
89
|
+
~~~bash
|
|
90
|
+
# Install Nitro core and Geolocation module
|
|
91
|
+
yarn add react-native-nitro-modules react-native-nitro-geolocation
|
|
92
|
+
|
|
93
|
+
# or using npm
|
|
94
|
+
npm install react-native-nitro-modules react-native-nitro-geolocation
|
|
95
|
+
~~~
|
|
96
|
+
|
|
97
|
+
After installation, rebuild your native app:
|
|
98
|
+
|
|
99
|
+
~~~bash
|
|
100
|
+
cd ios && pod install
|
|
101
|
+
~~~
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 2. iOS Setup
|
|
106
|
+
|
|
107
|
+
Add the following permissions to your **Info.plist**:
|
|
108
|
+
|
|
109
|
+
~~~xml
|
|
110
|
+
<key>NSLocationWhenInUseUsageDescription</key>
|
|
111
|
+
<string>This app requires access to your location while it’s in use.</string>
|
|
112
|
+
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
113
|
+
<string>This app requires access to your location at all times.</string>
|
|
114
|
+
~~~
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### 3. Android Setup
|
|
119
|
+
|
|
120
|
+
Add these permissions to your **AndroidManifest.xml**:
|
|
121
|
+
|
|
122
|
+
~~~xml
|
|
123
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
124
|
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
125
|
+
~~~
|
|
126
|
+
|
|
127
|
+
Optional (for background access):
|
|
128
|
+
|
|
129
|
+
~~~xml
|
|
130
|
+
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
131
|
+
~~~
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### 4. Usage Example
|
|
136
|
+
|
|
137
|
+
~~~tsx
|
|
138
|
+
import Geolocation from 'react-native-nitro-geolocation';
|
|
139
|
+
|
|
140
|
+
Geolocation.getCurrentPosition(
|
|
141
|
+
(position) => {
|
|
142
|
+
console.log('Latitude:', position.coords.latitude);
|
|
143
|
+
console.log('Longitude:', position.coords.longitude);
|
|
144
|
+
},
|
|
145
|
+
(error) => {
|
|
146
|
+
console.error('Location error:', error);
|
|
147
|
+
},
|
|
148
|
+
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Subscribe to continuous updates
|
|
152
|
+
const watchId = Geolocation.watchPosition(
|
|
153
|
+
(position) => {
|
|
154
|
+
console.log('Updated position:', position);
|
|
155
|
+
},
|
|
156
|
+
(error) => console.error(error),
|
|
157
|
+
);
|
|
158
|
+
~~~
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### Migrating from `@react-native-community/geolocation`
|
|
163
|
+
|
|
164
|
+
Nitro Geolocation is **100% API-compatible** with the original package.
|
|
165
|
+
You can migrate simply by replacing imports:
|
|
166
|
+
|
|
167
|
+
~~~diff
|
|
168
|
+
- import Geolocation from '@react-native-community/geolocation';
|
|
169
|
+
+ import Geolocation from 'react-native-nitro-geolocation';
|
|
170
|
+
~~~
|
|
171
|
+
|
|
172
|
+
**Benefits:**
|
|
173
|
+
- Better performance via JSI
|
|
174
|
+
- Reduced bridge overhead
|
|
175
|
+
- Improved permission consistency
|
|
176
|
+
- Built-in TypeScript definitions
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 🧠 API Methods
|
|
181
|
+
|
|
182
|
+
### Summary
|
|
183
|
+
- `setRNConfiguration`
|
|
184
|
+
- `requestAuthorization`
|
|
185
|
+
- `getCurrentPosition`
|
|
186
|
+
- `watchPosition`
|
|
187
|
+
- `clearWatch`
|
|
188
|
+
- `stopObserving`
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### `setRNConfiguration()`
|
|
193
|
+
|
|
194
|
+
Sets configuration options used for all location requests.
|
|
195
|
+
|
|
196
|
+
~~~tsx
|
|
197
|
+
import Geolocation from 'react-native-nitro-geolocation';
|
|
198
|
+
|
|
199
|
+
Geolocation.setRNConfiguration({
|
|
200
|
+
skipPermissionRequests: false,
|
|
201
|
+
authorizationLevel: 'auto',
|
|
202
|
+
enableBackgroundLocationUpdates: true,
|
|
203
|
+
locationProvider: 'auto',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// recommended
|
|
207
|
+
import { setRNConfiguration } from 'react-native-nitro-geolocation';
|
|
208
|
+
|
|
209
|
+
setRNConfiguration({
|
|
210
|
+
skipPermissionRequests: false,
|
|
211
|
+
authorizationLevel: 'auto',
|
|
212
|
+
enableBackgroundLocationUpdates: true,
|
|
213
|
+
locationProvider: 'auto',
|
|
214
|
+
});
|
|
215
|
+
~~~
|
|
216
|
+
|
|
217
|
+
**Options:**
|
|
218
|
+
- `skipPermissionRequests` — default `false`
|
|
219
|
+
- `authorizationLevel` — `'always' | 'whenInUse' | 'auto'` *(iOS only)*
|
|
220
|
+
- `enableBackgroundLocationUpdates` — *(iOS only)*
|
|
221
|
+
- `locationProvider` — `'playServices' | 'android' | 'auto'` *(Android only)*
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### `requestAuthorization()`
|
|
226
|
+
|
|
227
|
+
Requests location permission from the system.
|
|
228
|
+
|
|
229
|
+
~~~tsx
|
|
230
|
+
import Geolocation from 'react-native-nitro-geolocation';
|
|
231
|
+
|
|
232
|
+
Geolocation.requestAuthorization(
|
|
233
|
+
() => console.log('Permission granted'),
|
|
234
|
+
(error) => console.error('Permission error:', error),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// recommended
|
|
238
|
+
import { requestAuthorization } from 'react-native-nitro-geolocation';
|
|
239
|
+
|
|
240
|
+
requestAuthorization(
|
|
241
|
+
() => console.log('Permission granted'),
|
|
242
|
+
(error) => console.error('Permission error:', error),
|
|
243
|
+
);
|
|
244
|
+
~~~
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### `getCurrentPosition()`
|
|
249
|
+
|
|
250
|
+
Retrieves the current device location once.
|
|
251
|
+
|
|
252
|
+
~~~tsx
|
|
253
|
+
import Geolocation from 'react-native-nitro-geolocation';
|
|
254
|
+
|
|
255
|
+
Geolocation.getCurrentPosition(
|
|
256
|
+
(position) => console.log(position),
|
|
257
|
+
(error) => console.error(error),
|
|
258
|
+
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 },
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// recommended
|
|
262
|
+
import { getCurrentPosition } from 'react-native-nitro-geolocation';
|
|
263
|
+
|
|
264
|
+
getCurrentPosition(
|
|
265
|
+
(position) => console.log(position),
|
|
266
|
+
(error) => console.error(error),
|
|
267
|
+
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 },
|
|
268
|
+
);
|
|
269
|
+
~~~
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### `watchPosition()`
|
|
274
|
+
|
|
275
|
+
Watches location changes and calls the success callback each time.
|
|
276
|
+
|
|
277
|
+
~~~tsx
|
|
278
|
+
import Geolocation from 'react-native-nitro-geolocation';
|
|
279
|
+
|
|
280
|
+
const id = Geolocation.watchPosition(
|
|
281
|
+
(position) => console.log('Position:', position),
|
|
282
|
+
(error) => console.error(error),
|
|
283
|
+
{ interval: 5000, distanceFilter: 10 },
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// recommended
|
|
287
|
+
import { watchPosition } from 'react-native-nitro-geolocation';
|
|
288
|
+
|
|
289
|
+
const id = watchPosition(
|
|
290
|
+
(position) => console.log('Position:', position),
|
|
291
|
+
(error) => console.error(error),
|
|
292
|
+
{ interval: 5000, distanceFilter: 10 },
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
~~~
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### `clearWatch()`
|
|
300
|
+
|
|
301
|
+
Stops watching location updates for a given watch ID.
|
|
302
|
+
|
|
303
|
+
~~~tsx
|
|
304
|
+
import Geolocation from 'react-native-nitro-geolocation';
|
|
305
|
+
Geolocation.clearWatch(id);
|
|
306
|
+
|
|
307
|
+
// recommended
|
|
308
|
+
import { clearWatch } from 'react-native-nitro-geolocation';
|
|
309
|
+
clearWatch(id);
|
|
310
|
+
~~~
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 🧪 Summary
|
|
315
|
+
|
|
316
|
+
**React Native Nitro Geolocation** transforms the geolocation API
|
|
317
|
+
from a bridge-based, event-driven system into a **JSI-powered direct-callback model** —
|
|
318
|
+
delivering native-level performance with **zero API changes** for developers.
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrogeolocation
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.location.Location
|
|
7
|
+
import android.location.LocationListener
|
|
8
|
+
import android.location.LocationManager
|
|
9
|
+
import android.os.Bundle
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import androidx.core.content.ContextCompat
|
|
14
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
15
|
+
|
|
16
|
+
class GetCurrentPosition(private val reactContext: ReactApplicationContext) {
|
|
17
|
+
|
|
18
|
+
fun execute(
|
|
19
|
+
success: (position: GeolocationResponse) -> Unit,
|
|
20
|
+
error: ((error: GeolocationError) -> Unit)?,
|
|
21
|
+
options: GeolocationOptions?
|
|
22
|
+
) {
|
|
23
|
+
val locationManager =
|
|
24
|
+
reactContext.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
|
|
25
|
+
if (locationManager == null) {
|
|
26
|
+
Log.e(TAG, "LocationManager is not available")
|
|
27
|
+
error?.invoke(createError(POSITION_UNAVAILABLE, "LocationManager is not available"))
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
val opts = parseOptions(options)
|
|
32
|
+
val provider = getValidProvider(locationManager, opts.enableHighAccuracy)
|
|
33
|
+
|
|
34
|
+
if (provider == null) {
|
|
35
|
+
Log.e(TAG, "No location provider available")
|
|
36
|
+
error?.invoke(createError(POSITION_UNAVAILABLE, "No location provider available"))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
val lastKnownLocation = locationManager.getLastKnownLocation(provider)
|
|
42
|
+
|
|
43
|
+
// Check if cached location is fresh enough
|
|
44
|
+
if (lastKnownLocation != null && isCachedLocationValid(lastKnownLocation, opts)) {
|
|
45
|
+
success(locationToPosition(lastKnownLocation))
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If maximumAge is Infinity and we have a last known location, use it
|
|
50
|
+
if (lastKnownLocation != null && opts.maximumAge == Double.POSITIVE_INFINITY) {
|
|
51
|
+
success(locationToPosition(lastKnownLocation))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Request fresh location
|
|
56
|
+
requestFreshLocation(locationManager, provider, opts, success, error, lastKnownLocation)
|
|
57
|
+
} catch (e: SecurityException) {
|
|
58
|
+
Log.e(TAG, "Security exception: ${e.message}")
|
|
59
|
+
error?.invoke(
|
|
60
|
+
createError(PERMISSION_DENIED, "Location permission denied: ${e.message}")
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ===== Helper Functions =====
|
|
66
|
+
|
|
67
|
+
private fun parseOptions(options: GeolocationOptions?): ParsedOptions {
|
|
68
|
+
return ParsedOptions(
|
|
69
|
+
timeout = options?.timeout ?: DEFAULT_TIMEOUT,
|
|
70
|
+
maximumAge = options?.maximumAge ?: DEFAULT_MAXIMUM_AGE,
|
|
71
|
+
enableHighAccuracy = options?.enableHighAccuracy ?: false
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun isCachedLocationValid(location: Location, options: ParsedOptions): Boolean {
|
|
76
|
+
val age = System.currentTimeMillis() - location.time
|
|
77
|
+
return age < options.maximumAge
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private fun getValidProvider(locationManager: LocationManager, highAccuracy: Boolean): String? {
|
|
81
|
+
val preferredProvider =
|
|
82
|
+
if (highAccuracy) LocationManager.GPS_PROVIDER else LocationManager.NETWORK_PROVIDER
|
|
83
|
+
val fallbackProvider =
|
|
84
|
+
if (highAccuracy) LocationManager.NETWORK_PROVIDER else LocationManager.GPS_PROVIDER
|
|
85
|
+
|
|
86
|
+
return when {
|
|
87
|
+
isProviderValid(locationManager, preferredProvider) -> preferredProvider
|
|
88
|
+
isProviderValid(locationManager, fallbackProvider) -> fallbackProvider
|
|
89
|
+
else -> null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private fun isProviderValid(locationManager: LocationManager, provider: String): Boolean {
|
|
94
|
+
if (!locationManager.isProviderEnabled(provider)) return false
|
|
95
|
+
|
|
96
|
+
val permission =
|
|
97
|
+
if (provider == LocationManager.GPS_PROVIDER)
|
|
98
|
+
Manifest.permission.ACCESS_FINE_LOCATION
|
|
99
|
+
else Manifest.permission.ACCESS_COARSE_LOCATION
|
|
100
|
+
|
|
101
|
+
return ContextCompat.checkSelfPermission(reactContext, permission) ==
|
|
102
|
+
PackageManager.PERMISSION_GRANTED
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun requestFreshLocation(
|
|
106
|
+
locationManager: LocationManager,
|
|
107
|
+
provider: String,
|
|
108
|
+
options: ParsedOptions,
|
|
109
|
+
success: (GeolocationResponse) -> Unit,
|
|
110
|
+
error: ((GeolocationError) -> Unit)?,
|
|
111
|
+
fallbackLocation: Location?
|
|
112
|
+
) {
|
|
113
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
114
|
+
// Android 11+ supports getCurrentLocation
|
|
115
|
+
requestCurrentLocationModern(
|
|
116
|
+
locationManager,
|
|
117
|
+
provider,
|
|
118
|
+
options,
|
|
119
|
+
success,
|
|
120
|
+
error,
|
|
121
|
+
fallbackLocation
|
|
122
|
+
)
|
|
123
|
+
} else {
|
|
124
|
+
// Fallback to requestLocationUpdates
|
|
125
|
+
requestCurrentLocationLegacy(
|
|
126
|
+
locationManager,
|
|
127
|
+
provider,
|
|
128
|
+
options,
|
|
129
|
+
success,
|
|
130
|
+
error,
|
|
131
|
+
fallbackLocation
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@androidx.annotation.RequiresApi(android.os.Build.VERSION_CODES.R)
|
|
137
|
+
private fun requestCurrentLocationModern(
|
|
138
|
+
locationManager: LocationManager,
|
|
139
|
+
provider: String,
|
|
140
|
+
options: ParsedOptions,
|
|
141
|
+
success: (GeolocationResponse) -> Unit,
|
|
142
|
+
error: ((GeolocationError) -> Unit)?,
|
|
143
|
+
fallbackLocation: Location?
|
|
144
|
+
) {
|
|
145
|
+
val handler = Handler(Looper.getMainLooper())
|
|
146
|
+
val cancellationSignal = android.os.CancellationSignal()
|
|
147
|
+
|
|
148
|
+
val timeoutRunnable = Runnable {
|
|
149
|
+
cancellationSignal.cancel()
|
|
150
|
+
error?.invoke(createError(TIMEOUT, "Location request timed out"))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
locationManager.getCurrentLocation(
|
|
155
|
+
provider,
|
|
156
|
+
cancellationSignal,
|
|
157
|
+
{ runnable -> handler.post(runnable) }
|
|
158
|
+
) { location ->
|
|
159
|
+
handler.removeCallbacks(timeoutRunnable)
|
|
160
|
+
|
|
161
|
+
val bestLocation = selectBestLocation(location, fallbackLocation)
|
|
162
|
+
|
|
163
|
+
if (bestLocation != null) {
|
|
164
|
+
success(locationToPosition(bestLocation))
|
|
165
|
+
} else {
|
|
166
|
+
Log.e(TAG, "No location available")
|
|
167
|
+
error?.invoke(createError(POSITION_UNAVAILABLE, "Unable to get location"))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
handler.postDelayed(timeoutRunnable, options.timeout.toLong())
|
|
171
|
+
} catch (e: SecurityException) {
|
|
172
|
+
Log.e(TAG, "SecurityException: ${e.message}")
|
|
173
|
+
error?.invoke(createError(PERMISSION_DENIED, "Permission denied: ${e.message}"))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun selectBestLocation(newLocation: Location?, fallbackLocation: Location?): Location? {
|
|
178
|
+
return when {
|
|
179
|
+
newLocation == null -> fallbackLocation
|
|
180
|
+
fallbackLocation == null -> newLocation
|
|
181
|
+
isBetterLocation(newLocation, fallbackLocation) -> newLocation
|
|
182
|
+
else -> fallbackLocation
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private fun requestCurrentLocationLegacy(
|
|
187
|
+
locationManager: LocationManager,
|
|
188
|
+
provider: String,
|
|
189
|
+
options: ParsedOptions,
|
|
190
|
+
success: (GeolocationResponse) -> Unit,
|
|
191
|
+
error: ((GeolocationError) -> Unit)?,
|
|
192
|
+
fallbackLocation: Location?
|
|
193
|
+
) {
|
|
194
|
+
val handler = Handler(Looper.getMainLooper())
|
|
195
|
+
var isResolved = false
|
|
196
|
+
var oldLocation = fallbackLocation
|
|
197
|
+
lateinit var listener: LocationListener
|
|
198
|
+
|
|
199
|
+
val timeoutRunnable = Runnable {
|
|
200
|
+
if (!isResolved) {
|
|
201
|
+
isResolved = true
|
|
202
|
+
locationManager.removeUpdates(listener)
|
|
203
|
+
error?.invoke(createError(TIMEOUT, "Location request timed out"))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
listener =
|
|
208
|
+
object : LocationListener {
|
|
209
|
+
override fun onLocationChanged(location: Location) {
|
|
210
|
+
synchronized(this) {
|
|
211
|
+
if (!isResolved) {
|
|
212
|
+
val bestLocation = selectBestLocation(location, oldLocation)
|
|
213
|
+
if (bestLocation == location) {
|
|
214
|
+
isResolved = true
|
|
215
|
+
handler.removeCallbacks(timeoutRunnable)
|
|
216
|
+
locationManager.removeUpdates(this)
|
|
217
|
+
success(locationToPosition(location))
|
|
218
|
+
}
|
|
219
|
+
oldLocation = location
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
|
|
225
|
+
override fun onProviderEnabled(provider: String) {}
|
|
226
|
+
override fun onProviderDisabled(provider: String) {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
locationManager.requestLocationUpdates(
|
|
231
|
+
provider,
|
|
232
|
+
100,
|
|
233
|
+
1f,
|
|
234
|
+
listener,
|
|
235
|
+
Looper.getMainLooper()
|
|
236
|
+
)
|
|
237
|
+
handler.postDelayed(timeoutRunnable, options.timeout.toLong())
|
|
238
|
+
} catch (e: SecurityException) {
|
|
239
|
+
Log.e(TAG, "SecurityException in requestLocationUpdates: ${e.message}")
|
|
240
|
+
error?.invoke(createError(PERMISSION_DENIED, "Permission denied: ${e.message}"))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Determines whether one Location reading is better than the current Location fix Taken from
|
|
246
|
+
* Android Examples: https://developer.android.com/guide/topics/location/strategies.html
|
|
247
|
+
*/
|
|
248
|
+
private fun isBetterLocation(location: Location, currentBestLocation: Location?): Boolean {
|
|
249
|
+
if (currentBestLocation == null) {
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
val timeDelta = location.time - currentBestLocation.time
|
|
254
|
+
val isSignificantlyNewer = timeDelta > TWO_MINUTES
|
|
255
|
+
val isSignificantlyOlder = timeDelta < -TWO_MINUTES
|
|
256
|
+
val isNewer = timeDelta > 0
|
|
257
|
+
|
|
258
|
+
if (isSignificantlyNewer) {
|
|
259
|
+
return true
|
|
260
|
+
} else if (isSignificantlyOlder) {
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
val accuracyDelta = (location.accuracy - currentBestLocation.accuracy).toInt()
|
|
265
|
+
val isLessAccurate = accuracyDelta > 0
|
|
266
|
+
val isMoreAccurate = accuracyDelta < 0
|
|
267
|
+
val isSignificantlyLessAccurate = accuracyDelta > 200
|
|
268
|
+
|
|
269
|
+
val isFromSameProvider = isSameProvider(location.provider, currentBestLocation.provider)
|
|
270
|
+
|
|
271
|
+
return when {
|
|
272
|
+
isMoreAccurate -> true
|
|
273
|
+
isNewer && !isLessAccurate -> true
|
|
274
|
+
isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true
|
|
275
|
+
else -> false
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private fun isSameProvider(provider1: String?, provider2: String?): Boolean {
|
|
280
|
+
if (provider1 == null) {
|
|
281
|
+
return provider2 == null
|
|
282
|
+
}
|
|
283
|
+
return provider1 == provider2
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ===== Data Conversion =====
|
|
287
|
+
|
|
288
|
+
private fun locationToPosition(location: Location): GeolocationResponse {
|
|
289
|
+
return GeolocationResponse(
|
|
290
|
+
coords =
|
|
291
|
+
GeolocationCoordinates(
|
|
292
|
+
latitude = location.latitude,
|
|
293
|
+
longitude = location.longitude,
|
|
294
|
+
altitude = if (location.hasAltitude()) location.altitude else null,
|
|
295
|
+
accuracy = location.accuracy.toDouble(),
|
|
296
|
+
altitudeAccuracy =
|
|
297
|
+
if (android.os.Build.VERSION.SDK_INT >=
|
|
298
|
+
android.os.Build.VERSION_CODES.O &&
|
|
299
|
+
location.hasVerticalAccuracy()
|
|
300
|
+
)
|
|
301
|
+
location.verticalAccuracyMeters.toDouble()
|
|
302
|
+
else null,
|
|
303
|
+
heading =
|
|
304
|
+
if (location.hasBearing()) location.bearing.toDouble()
|
|
305
|
+
else null,
|
|
306
|
+
speed = if (location.hasSpeed()) location.speed.toDouble() else null
|
|
307
|
+
),
|
|
308
|
+
timestamp = location.time.toDouble()
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private fun createError(code: Int, message: String): GeolocationError {
|
|
313
|
+
return GeolocationError(
|
|
314
|
+
code = code.toDouble(),
|
|
315
|
+
message = message,
|
|
316
|
+
PERMISSION_DENIED = PERMISSION_DENIED.toDouble(),
|
|
317
|
+
POSITION_UNAVAILABLE = POSITION_UNAVAILABLE.toDouble(),
|
|
318
|
+
TIMEOUT = TIMEOUT.toDouble()
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ===== Data Classes =====
|
|
323
|
+
|
|
324
|
+
private data class ParsedOptions(
|
|
325
|
+
val timeout: Double,
|
|
326
|
+
val maximumAge: Double,
|
|
327
|
+
val enableHighAccuracy: Boolean
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
companion object {
|
|
331
|
+
private const val TAG = "GetCurrentPosition"
|
|
332
|
+
const val PERMISSION_DENIED = 1
|
|
333
|
+
const val POSITION_UNAVAILABLE = 2
|
|
334
|
+
const val TIMEOUT = 3
|
|
335
|
+
|
|
336
|
+
const val DEFAULT_TIMEOUT = 10 * 60 * 1000.0 // 10 minutes
|
|
337
|
+
const val DEFAULT_MAXIMUM_AGE = Double.POSITIVE_INFINITY
|
|
338
|
+
|
|
339
|
+
private const val TWO_MINUTES = 1000 * 60 * 2L // 2 minutes in milliseconds
|
|
340
|
+
}
|
|
341
|
+
}
|