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
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
|
|
3
|
+
class LocationManager: NSObject, CLLocationManagerDelegate {
|
|
4
|
+
// MARK: - Types
|
|
5
|
+
|
|
6
|
+
enum AuthorizationType {
|
|
7
|
+
case always
|
|
8
|
+
case whenInUse
|
|
9
|
+
case none
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private struct LocationRequest {
|
|
13
|
+
let success: (GeolocationResponse) -> Void
|
|
14
|
+
let error: ((GeolocationError) -> Void)?
|
|
15
|
+
let options: ParsedOptions
|
|
16
|
+
var timer: Timer?
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private struct WatchSubscription {
|
|
20
|
+
let success: (GeolocationResponse) -> Void
|
|
21
|
+
let error: ((GeolocationError) -> Void)?
|
|
22
|
+
let options: ParsedOptions
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private struct ParsedOptions {
|
|
26
|
+
let timeout: Double
|
|
27
|
+
let maximumAge: Double
|
|
28
|
+
let accuracy: CLLocationAccuracy
|
|
29
|
+
let distanceFilter: CLLocationDistance
|
|
30
|
+
let useSignificantChanges: Bool
|
|
31
|
+
|
|
32
|
+
static func parse(from options: GeolocationOptions?) -> ParsedOptions {
|
|
33
|
+
let timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
|
34
|
+
let maximumAge = options?.maximumAge ?? DEFAULT_MAXIMUM_AGE
|
|
35
|
+
let enableHighAccuracy = options?.enableHighAccuracy ?? false
|
|
36
|
+
let accuracy =
|
|
37
|
+
enableHighAccuracy ? kCLLocationAccuracyBest : kCLLocationAccuracyHundredMeters
|
|
38
|
+
let distanceFilter = options?.distanceFilter ?? kCLDistanceFilterNone
|
|
39
|
+
let useSignificantChanges = options?.useSignificantChanges ?? false
|
|
40
|
+
|
|
41
|
+
return ParsedOptions(
|
|
42
|
+
timeout: timeout,
|
|
43
|
+
maximumAge: maximumAge,
|
|
44
|
+
accuracy: accuracy,
|
|
45
|
+
distanceFilter: distanceFilter,
|
|
46
|
+
useSignificantChanges: useSignificantChanges
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - Properties
|
|
52
|
+
|
|
53
|
+
private var locationManager: CLLocationManager?
|
|
54
|
+
private var lastLocation: CLLocation?
|
|
55
|
+
private var usingSignificantChanges: Bool = false
|
|
56
|
+
|
|
57
|
+
// Authorization
|
|
58
|
+
private var queuedAuthorizationCallbacks:
|
|
59
|
+
[(success: (() -> Void)?, error: ((GeolocationError) -> Void)?)] = []
|
|
60
|
+
|
|
61
|
+
// getCurrentPosition
|
|
62
|
+
private var pendingRequests: [LocationRequest] = []
|
|
63
|
+
|
|
64
|
+
// watchPosition
|
|
65
|
+
private var activeWatches: [Double: WatchSubscription] = [:]
|
|
66
|
+
private var nextWatchId: Double = 1
|
|
67
|
+
|
|
68
|
+
// Error codes
|
|
69
|
+
private let PERMISSION_DENIED = 1
|
|
70
|
+
private let POSITION_UNAVAILABLE = 2
|
|
71
|
+
private let TIMEOUT = 3
|
|
72
|
+
|
|
73
|
+
// MARK: - Constants
|
|
74
|
+
|
|
75
|
+
private static let DEFAULT_TIMEOUT: Double = 10 * 60 * 1000 // 10 minutes in ms
|
|
76
|
+
private static let DEFAULT_MAXIMUM_AGE: Double = Double.infinity
|
|
77
|
+
|
|
78
|
+
// MARK: - Authorization
|
|
79
|
+
|
|
80
|
+
func requestAuthorization(
|
|
81
|
+
authType: AuthorizationType,
|
|
82
|
+
skipPermissionRequests: Bool,
|
|
83
|
+
enableBackgroundLocationUpdates: Bool,
|
|
84
|
+
success: (() -> Void)?,
|
|
85
|
+
error: ((GeolocationError) -> Void)?
|
|
86
|
+
) {
|
|
87
|
+
DispatchQueue.main.async { [weak self] in
|
|
88
|
+
guard let self = self else { return }
|
|
89
|
+
|
|
90
|
+
self.initializeLocationManagerIfNeeded()
|
|
91
|
+
self.enqueueAuthorizationCallbacks(success: success, error: error)
|
|
92
|
+
|
|
93
|
+
// Skip permission requests if configured
|
|
94
|
+
if skipPermissionRequests {
|
|
95
|
+
if enableBackgroundLocationUpdates {
|
|
96
|
+
self.enableBackgroundLocationUpdatesIfNeeded()
|
|
97
|
+
}
|
|
98
|
+
self.handleAuthorizationSuccess()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if already authorized
|
|
103
|
+
let currentStatus = CLLocationManager.authorizationStatus()
|
|
104
|
+
if currentStatus == .authorizedAlways || currentStatus == .authorizedWhenInUse {
|
|
105
|
+
self.handleAuthorizationSuccess()
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if currentStatus == .denied || currentStatus == .restricted {
|
|
110
|
+
self.handleAuthorizationError(for: currentStatus)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Not determined yet, request permission
|
|
115
|
+
self.requestPermission(for: authType)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private func enqueueAuthorizationCallbacks(
|
|
120
|
+
success: (() -> Void)?, error: ((GeolocationError) -> Void)?
|
|
121
|
+
) {
|
|
122
|
+
guard success != nil || error != nil else { return }
|
|
123
|
+
queuedAuthorizationCallbacks.append((success: success, error: error))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func requestPermission(for type: AuthorizationType) {
|
|
127
|
+
switch type {
|
|
128
|
+
case .always:
|
|
129
|
+
locationManager?.requestAlwaysAuthorization()
|
|
130
|
+
enableBackgroundLocationUpdatesIfNeeded()
|
|
131
|
+
case .whenInUse:
|
|
132
|
+
locationManager?.requestWhenInUseAuthorization()
|
|
133
|
+
case .none:
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private func enableBackgroundLocationUpdatesIfNeeded() {
|
|
139
|
+
guard
|
|
140
|
+
let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes")
|
|
141
|
+
as? [String],
|
|
142
|
+
backgroundModes.contains("location")
|
|
143
|
+
else {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
locationManager?.allowsBackgroundLocationUpdates = true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - Get Current Position
|
|
150
|
+
|
|
151
|
+
func getCurrentPosition(
|
|
152
|
+
success: @escaping (GeolocationResponse) -> Void,
|
|
153
|
+
error: ((GeolocationError) -> Void)?,
|
|
154
|
+
options: GeolocationOptions?
|
|
155
|
+
) {
|
|
156
|
+
DispatchQueue.main.async { [weak self] in
|
|
157
|
+
guard let self = self else { return }
|
|
158
|
+
|
|
159
|
+
let parsedOptions = ParsedOptions.parse(from: options)
|
|
160
|
+
|
|
161
|
+
// Check authorization
|
|
162
|
+
let status = CLLocationManager.authorizationStatus()
|
|
163
|
+
if status == .denied || status == .restricted {
|
|
164
|
+
let message =
|
|
165
|
+
status == .restricted
|
|
166
|
+
? "This application is not authorized to use location services"
|
|
167
|
+
: "User denied access to location services."
|
|
168
|
+
error?(self.createError(code: self.PERMISSION_DENIED, message: message))
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if !CLLocationManager.locationServicesEnabled() {
|
|
173
|
+
error?(
|
|
174
|
+
self.createError(
|
|
175
|
+
code: self.POSITION_UNAVAILABLE, message: "Location services disabled."))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check cached location
|
|
180
|
+
if let cached = self.lastLocation,
|
|
181
|
+
self.isCachedLocationValid(cached, options: parsedOptions)
|
|
182
|
+
{
|
|
183
|
+
success(self.locationToPosition(cached))
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
self.initializeLocationManagerIfNeeded()
|
|
188
|
+
|
|
189
|
+
// Configure location manager (use best accuracy from all pending requests)
|
|
190
|
+
self.updateLocationManagerConfiguration()
|
|
191
|
+
|
|
192
|
+
// Create request
|
|
193
|
+
var request = LocationRequest(
|
|
194
|
+
success: success,
|
|
195
|
+
error: error,
|
|
196
|
+
options: parsedOptions,
|
|
197
|
+
timer: nil
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// Setup timeout
|
|
201
|
+
let timer = Timer.scheduledTimer(
|
|
202
|
+
withTimeInterval: parsedOptions.timeout / 1000.0, repeats: false
|
|
203
|
+
) { [weak self] timer in
|
|
204
|
+
self?.handleTimeout(for: timer)
|
|
205
|
+
}
|
|
206
|
+
request.timer = timer
|
|
207
|
+
|
|
208
|
+
self.pendingRequests.append(request)
|
|
209
|
+
|
|
210
|
+
// Start location updates
|
|
211
|
+
self.startMonitoring()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MARK: - Watch Position
|
|
216
|
+
|
|
217
|
+
func watchPosition(
|
|
218
|
+
success: @escaping (GeolocationResponse) -> Void,
|
|
219
|
+
error: ((GeolocationError) -> Void)?,
|
|
220
|
+
options: GeolocationOptions?
|
|
221
|
+
) -> Double {
|
|
222
|
+
var resultWatchId: Double = 0
|
|
223
|
+
|
|
224
|
+
DispatchQueue.main.sync { [weak self] in
|
|
225
|
+
guard let self = self else { return }
|
|
226
|
+
|
|
227
|
+
let parsedOptions = ParsedOptions.parse(from: options)
|
|
228
|
+
let watchId = self.nextWatchId
|
|
229
|
+
self.nextWatchId += 1
|
|
230
|
+
|
|
231
|
+
let subscription = WatchSubscription(
|
|
232
|
+
success: success,
|
|
233
|
+
error: error,
|
|
234
|
+
options: parsedOptions
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self.activeWatches[watchId] = subscription
|
|
238
|
+
|
|
239
|
+
self.initializeLocationManagerIfNeeded()
|
|
240
|
+
self.updateLocationManagerConfiguration()
|
|
241
|
+
self.startMonitoring()
|
|
242
|
+
|
|
243
|
+
resultWatchId = watchId
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return resultWatchId
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func clearWatch(watchId: Double) {
|
|
250
|
+
DispatchQueue.main.async { [weak self] in
|
|
251
|
+
guard let self = self else { return }
|
|
252
|
+
|
|
253
|
+
self.activeWatches.removeValue(forKey: watchId)
|
|
254
|
+
|
|
255
|
+
// Stop monitoring if no more watches or pending requests
|
|
256
|
+
if self.activeWatches.isEmpty && self.pendingRequests.isEmpty {
|
|
257
|
+
self.stopMonitoring()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
func stopObserving() {
|
|
263
|
+
DispatchQueue.main.async { [weak self] in
|
|
264
|
+
guard let self = self else { return }
|
|
265
|
+
|
|
266
|
+
self.activeWatches.removeAll()
|
|
267
|
+
|
|
268
|
+
// Stop monitoring if no pending requests
|
|
269
|
+
if self.pendingRequests.isEmpty {
|
|
270
|
+
self.stopMonitoring()
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// MARK: - CLLocationManagerDelegate
|
|
276
|
+
|
|
277
|
+
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
278
|
+
let status = getCurrentAuthorizationStatus(from: manager)
|
|
279
|
+
|
|
280
|
+
switch status {
|
|
281
|
+
case .authorizedAlways, .authorizedWhenInUse:
|
|
282
|
+
handleAuthorizationSuccess()
|
|
283
|
+
startMonitoring()
|
|
284
|
+
case .denied, .restricted:
|
|
285
|
+
handleAuthorizationError(for: status)
|
|
286
|
+
case .notDetermined:
|
|
287
|
+
break
|
|
288
|
+
@unknown default:
|
|
289
|
+
break
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
294
|
+
guard let location = locations.last else { return }
|
|
295
|
+
|
|
296
|
+
lastLocation = location
|
|
297
|
+
let position = locationToPosition(location)
|
|
298
|
+
|
|
299
|
+
// 1. Fire all pending getCurrentPosition requests
|
|
300
|
+
for request in pendingRequests {
|
|
301
|
+
request.timer?.invalidate()
|
|
302
|
+
request.success(position)
|
|
303
|
+
}
|
|
304
|
+
pendingRequests.removeAll()
|
|
305
|
+
|
|
306
|
+
// 2. Fire all active watchPosition subscriptions
|
|
307
|
+
for (_, watch) in activeWatches {
|
|
308
|
+
watch.success(position)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 3. Stop monitoring if no more watches or pending requests
|
|
312
|
+
if activeWatches.isEmpty && pendingRequests.isEmpty {
|
|
313
|
+
stopMonitoring()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
318
|
+
let geoError: GeolocationError
|
|
319
|
+
|
|
320
|
+
if let clError = error as? CLError {
|
|
321
|
+
switch clError.code {
|
|
322
|
+
case .denied:
|
|
323
|
+
geoError = createError(
|
|
324
|
+
code: PERMISSION_DENIED, message: "User denied access to location services.")
|
|
325
|
+
case .locationUnknown:
|
|
326
|
+
// Location is temporarily unavailable, keep trying
|
|
327
|
+
return
|
|
328
|
+
default:
|
|
329
|
+
geoError = createError(
|
|
330
|
+
code: POSITION_UNAVAILABLE,
|
|
331
|
+
message: "Unable to retrieve location: \(error.localizedDescription)")
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
geoError = createError(
|
|
335
|
+
code: POSITION_UNAVAILABLE,
|
|
336
|
+
message: "Unable to retrieve location: \(error.localizedDescription)")
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Fire all pending requests with error
|
|
340
|
+
for request in pendingRequests {
|
|
341
|
+
request.timer?.invalidate()
|
|
342
|
+
request.error?(geoError)
|
|
343
|
+
}
|
|
344
|
+
pendingRequests.removeAll()
|
|
345
|
+
|
|
346
|
+
// Fire all active watches with error
|
|
347
|
+
for (_, watch) in activeWatches {
|
|
348
|
+
watch.error?(geoError)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
stopMonitoring()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// MARK: - Helper Functions
|
|
355
|
+
|
|
356
|
+
private func initializeLocationManagerIfNeeded() {
|
|
357
|
+
guard locationManager == nil else { return }
|
|
358
|
+
locationManager = CLLocationManager()
|
|
359
|
+
locationManager?.delegate = self
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private func updateLocationManagerConfiguration() {
|
|
363
|
+
guard let manager = locationManager else { return }
|
|
364
|
+
|
|
365
|
+
// Find the best (most accurate) settings from all pending requests and active watches
|
|
366
|
+
var bestAccuracy = kCLLocationAccuracyHundredMeters
|
|
367
|
+
var smallestDistanceFilter = kCLDistanceFilterNone
|
|
368
|
+
var shouldUseSignificantChanges = false
|
|
369
|
+
|
|
370
|
+
for request in pendingRequests {
|
|
371
|
+
bestAccuracy = min(bestAccuracy, request.options.accuracy)
|
|
372
|
+
smallestDistanceFilter = min(smallestDistanceFilter, request.options.distanceFilter)
|
|
373
|
+
shouldUseSignificantChanges =
|
|
374
|
+
shouldUseSignificantChanges || request.options.useSignificantChanges
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for (_, watch) in activeWatches {
|
|
378
|
+
bestAccuracy = min(bestAccuracy, watch.options.accuracy)
|
|
379
|
+
smallestDistanceFilter = min(smallestDistanceFilter, watch.options.distanceFilter)
|
|
380
|
+
shouldUseSignificantChanges =
|
|
381
|
+
shouldUseSignificantChanges || watch.options.useSignificantChanges
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
manager.desiredAccuracy = bestAccuracy
|
|
385
|
+
manager.distanceFilter = smallestDistanceFilter
|
|
386
|
+
|
|
387
|
+
// Update significant changes mode if changed
|
|
388
|
+
if shouldUseSignificantChanges != usingSignificantChanges {
|
|
389
|
+
stopMonitoring()
|
|
390
|
+
usingSignificantChanges = shouldUseSignificantChanges
|
|
391
|
+
startMonitoring()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private func startMonitoring() {
|
|
396
|
+
if usingSignificantChanges {
|
|
397
|
+
locationManager?.startMonitoringSignificantLocationChanges()
|
|
398
|
+
} else {
|
|
399
|
+
locationManager?.startUpdatingLocation()
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private func stopMonitoring() {
|
|
404
|
+
if usingSignificantChanges {
|
|
405
|
+
locationManager?.stopMonitoringSignificantLocationChanges()
|
|
406
|
+
} else {
|
|
407
|
+
locationManager?.stopUpdatingLocation()
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private func isCachedLocationValid(_ location: CLLocation, options: ParsedOptions) -> Bool {
|
|
412
|
+
// Check if maximumAge is infinity
|
|
413
|
+
if options.maximumAge.isInfinite {
|
|
414
|
+
return true
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check age
|
|
418
|
+
let age = Date().timeIntervalSince(location.timestamp) * 1000 // convert to ms
|
|
419
|
+
guard age < options.maximumAge else {
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check accuracy
|
|
424
|
+
guard location.horizontalAccuracy <= options.accuracy else {
|
|
425
|
+
return false
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return true
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private func handleTimeout(for timer: Timer) {
|
|
432
|
+
// Find and remove the request with this timer
|
|
433
|
+
if let index = pendingRequests.firstIndex(where: { $0.timer === timer }) {
|
|
434
|
+
let request = pendingRequests[index]
|
|
435
|
+
pendingRequests.remove(at: index)
|
|
436
|
+
|
|
437
|
+
// Always return timeout error
|
|
438
|
+
let timeoutSeconds = request.options.timeout / 1000.0
|
|
439
|
+
let message = String(format: "Unable to fetch location within %.1fs.", timeoutSeconds)
|
|
440
|
+
request.error?(createError(code: TIMEOUT, message: message))
|
|
441
|
+
|
|
442
|
+
// Stop monitoring if no more watches or pending requests
|
|
443
|
+
if activeWatches.isEmpty && pendingRequests.isEmpty {
|
|
444
|
+
stopMonitoring()
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private func getCurrentAuthorizationStatus(from manager: CLLocationManager)
|
|
450
|
+
-> CLAuthorizationStatus
|
|
451
|
+
{
|
|
452
|
+
if #available(iOS 14.0, *) {
|
|
453
|
+
return manager.authorizationStatus
|
|
454
|
+
} else {
|
|
455
|
+
return CLLocationManager.authorizationStatus()
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private func handleAuthorizationSuccess() {
|
|
460
|
+
invokeQueuedAuthorizationCallbacks(success: true)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private func handleAuthorizationError(for status: CLAuthorizationStatus) {
|
|
464
|
+
let error = createGeolocationError(for: status)
|
|
465
|
+
invokeQueuedAuthorizationCallbacks(error: error)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private func createGeolocationError(for status: CLAuthorizationStatus) -> GeolocationError {
|
|
469
|
+
let message =
|
|
470
|
+
status == .restricted
|
|
471
|
+
? "This application is not authorized to use location services"
|
|
472
|
+
: "User denied access to location services."
|
|
473
|
+
|
|
474
|
+
return GeolocationError(
|
|
475
|
+
code: Double(PERMISSION_DENIED),
|
|
476
|
+
message: message,
|
|
477
|
+
PERMISSION_DENIED: Double(PERMISSION_DENIED),
|
|
478
|
+
POSITION_UNAVAILABLE: Double(POSITION_UNAVAILABLE),
|
|
479
|
+
TIMEOUT: Double(TIMEOUT)
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private func invokeQueuedAuthorizationCallbacks(
|
|
484
|
+
success: Bool = false, error: GeolocationError? = nil
|
|
485
|
+
) {
|
|
486
|
+
for callback in queuedAuthorizationCallbacks {
|
|
487
|
+
if success {
|
|
488
|
+
callback.success?()
|
|
489
|
+
} else if let error = error {
|
|
490
|
+
callback.error?(error)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
queuedAuthorizationCallbacks.removeAll()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private func locationToPosition(_ location: CLLocation) -> GeolocationResponse {
|
|
497
|
+
let altitude = location.verticalAccuracy < 0 ? 0.0 : location.altitude
|
|
498
|
+
let altitudeAccuracy = location.verticalAccuracy < 0 ? 0.0 : location.verticalAccuracy
|
|
499
|
+
let heading = location.course >= 0 ? location.course : -1.0
|
|
500
|
+
let speed = location.speed >= 0 ? location.speed : 0.0
|
|
501
|
+
|
|
502
|
+
let coordsObj = GeolocationCoordinates(
|
|
503
|
+
latitude: location.coordinate.latitude,
|
|
504
|
+
longitude: location.coordinate.longitude,
|
|
505
|
+
altitude: altitude,
|
|
506
|
+
accuracy: location.horizontalAccuracy,
|
|
507
|
+
altitudeAccuracy: altitudeAccuracy,
|
|
508
|
+
heading: heading,
|
|
509
|
+
speed: speed
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
let position = GeolocationResponse(
|
|
513
|
+
coords: coordsObj,
|
|
514
|
+
timestamp: location.timestamp.timeIntervalSince1970 * 1000
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return position
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private func createError(code: Int, message: String) -> GeolocationError {
|
|
521
|
+
return GeolocationError(
|
|
522
|
+
code: Double(code),
|
|
523
|
+
message: message,
|
|
524
|
+
PERMISSION_DENIED: Double(PERMISSION_DENIED),
|
|
525
|
+
POSITION_UNAVAILABLE: Double(POSITION_UNAVAILABLE),
|
|
526
|
+
TIMEOUT: Double(TIMEOUT)
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
@@ -1,5 +1,99 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
|
|
1
3
|
class NitroGeolocation: HybridNitroGeolocationSpec {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
// MARK: - Properties
|
|
5
|
+
|
|
6
|
+
private var configuration: RNConfigurationInternal = RNConfigurationInternal(
|
|
7
|
+
skipPermissionRequests: false,
|
|
8
|
+
authorizationLevel: nil,
|
|
9
|
+
enableBackgroundLocationUpdates: nil,
|
|
10
|
+
locationProvider: nil
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
private let locationManager = LocationManager()
|
|
14
|
+
|
|
15
|
+
// MARK: - Public API
|
|
16
|
+
|
|
17
|
+
public func setRNConfiguration(config: RNConfigurationInternal) throws {
|
|
18
|
+
configuration = config
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public func requestAuthorization(success: (() -> Void)?, error: ((GeolocationError) -> Void)?)
|
|
22
|
+
throws
|
|
23
|
+
{
|
|
24
|
+
let authType = determineAuthorizationType()
|
|
25
|
+
let skipPermissionRequests = configuration.skipPermissionRequests
|
|
26
|
+
let enableBackgroundLocationUpdates = configuration.enableBackgroundLocationUpdates ?? false
|
|
27
|
+
|
|
28
|
+
locationManager.requestAuthorization(
|
|
29
|
+
authType: authType,
|
|
30
|
+
skipPermissionRequests: skipPermissionRequests,
|
|
31
|
+
enableBackgroundLocationUpdates: enableBackgroundLocationUpdates,
|
|
32
|
+
success: success,
|
|
33
|
+
error: error
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public func getCurrentPosition(
|
|
38
|
+
success: @escaping (GeolocationResponse) -> Void, error: ((GeolocationError) -> Void)?,
|
|
39
|
+
options: GeolocationOptions?
|
|
40
|
+
) throws {
|
|
41
|
+
locationManager.getCurrentPosition(success: success, error: error, options: options)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public func watchPosition(
|
|
45
|
+
success: @escaping (GeolocationResponse) -> Void, error: ((GeolocationError) -> Void)?,
|
|
46
|
+
options: GeolocationOptions?
|
|
47
|
+
) throws -> Double {
|
|
48
|
+
return locationManager.watchPosition(success: success, error: error, options: options)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public func clearWatch(watchId: Double) throws {
|
|
52
|
+
locationManager.clearWatch(watchId: watchId)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public func stopObserving() throws {
|
|
56
|
+
locationManager.stopObserving()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// MARK: - Authorization Helpers
|
|
60
|
+
|
|
61
|
+
private func determineAuthorizationType() -> LocationManager.AuthorizationType {
|
|
62
|
+
guard let authLevel = configuration.authorizationLevel else {
|
|
63
|
+
return determineAuthorizationTypeFromInfoPlist()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
switch authLevel {
|
|
67
|
+
case .always:
|
|
68
|
+
return .always
|
|
69
|
+
case .wheninuse:
|
|
70
|
+
return .whenInUse
|
|
71
|
+
case .auto:
|
|
72
|
+
return determineAuthorizationTypeFromInfoPlist()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func determineAuthorizationTypeFromInfoPlist() -> LocationManager.AuthorizationType {
|
|
77
|
+
if hasInfoPlistKey(for: .always) {
|
|
78
|
+
return .always
|
|
79
|
+
} else if hasInfoPlistKey(for: .whenInUse) {
|
|
80
|
+
return .whenInUse
|
|
81
|
+
}
|
|
82
|
+
return .none
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private func hasInfoPlistKey(for type: LocationManager.AuthorizationType) -> Bool {
|
|
86
|
+
switch type {
|
|
87
|
+
case .always:
|
|
88
|
+
return Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription")
|
|
89
|
+
!= nil
|
|
90
|
+
|| Bundle.main.object(
|
|
91
|
+
forInfoDictionaryKey: "NSLocationAlwaysAndWhenInUseUsageDescription") != nil
|
|
92
|
+
case .whenInUse:
|
|
93
|
+
return Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription")
|
|
94
|
+
!= nil
|
|
95
|
+
case .none:
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
4
98
|
}
|
|
5
99
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
** linguist-generated=true
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JAuthorizationLevelInternal.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © 2025 Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include "AuthorizationLevelInternal.hpp"
|
|
12
|
+
|
|
13
|
+
namespace margelo::nitro::nitrogeolocation {
|
|
14
|
+
|
|
15
|
+
using namespace facebook;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The C++ JNI bridge between the C++ enum "AuthorizationLevelInternal" and the the Kotlin enum "AuthorizationLevelInternal".
|
|
19
|
+
*/
|
|
20
|
+
struct JAuthorizationLevelInternal final: public jni::JavaClass<JAuthorizationLevelInternal> {
|
|
21
|
+
public:
|
|
22
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitrogeolocation/AuthorizationLevelInternal;";
|
|
23
|
+
|
|
24
|
+
public:
|
|
25
|
+
/**
|
|
26
|
+
* Convert this Java/Kotlin-based enum to the C++ enum AuthorizationLevelInternal.
|
|
27
|
+
*/
|
|
28
|
+
[[maybe_unused]]
|
|
29
|
+
[[nodiscard]]
|
|
30
|
+
AuthorizationLevelInternal toCpp() const {
|
|
31
|
+
static const auto clazz = javaClassStatic();
|
|
32
|
+
static const auto fieldOrdinal = clazz->getField<int>("value");
|
|
33
|
+
int ordinal = this->getFieldValue(fieldOrdinal);
|
|
34
|
+
return static_cast<AuthorizationLevelInternal>(ordinal);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public:
|
|
38
|
+
/**
|
|
39
|
+
* Create a Java/Kotlin-based enum with the given C++ enum's value.
|
|
40
|
+
*/
|
|
41
|
+
[[maybe_unused]]
|
|
42
|
+
static jni::alias_ref<JAuthorizationLevelInternal> fromCpp(AuthorizationLevelInternal value) {
|
|
43
|
+
static const auto clazz = javaClassStatic();
|
|
44
|
+
static const auto fieldALWAYS = clazz->getStaticField<JAuthorizationLevelInternal>("ALWAYS");
|
|
45
|
+
static const auto fieldWHENINUSE = clazz->getStaticField<JAuthorizationLevelInternal>("WHENINUSE");
|
|
46
|
+
static const auto fieldAUTO = clazz->getStaticField<JAuthorizationLevelInternal>("AUTO");
|
|
47
|
+
|
|
48
|
+
switch (value) {
|
|
49
|
+
case AuthorizationLevelInternal::ALWAYS:
|
|
50
|
+
return clazz->getStaticFieldValue(fieldALWAYS);
|
|
51
|
+
case AuthorizationLevelInternal::WHENINUSE:
|
|
52
|
+
return clazz->getStaticFieldValue(fieldWHENINUSE);
|
|
53
|
+
case AuthorizationLevelInternal::AUTO:
|
|
54
|
+
return clazz->getStaticFieldValue(fieldAUTO);
|
|
55
|
+
default:
|
|
56
|
+
std::string stringValue = std::to_string(static_cast<int>(value));
|
|
57
|
+
throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
} // namespace margelo::nitro::nitrogeolocation
|