react-native-nitro-geolocation 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/GetCurrentPosition.kt +4 -4
  2. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocation.kt +642 -50
  3. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocationCompat.kt +81 -0
  4. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/WatchPosition.kt +4 -4
  5. package/ios/LocationManager.swift +4 -4
  6. package/ios/NitroGeolocation.swift +543 -68
  7. package/ios/NitroGeolocationCompat.swift +109 -0
  8. package/nitrogen/generated/android/c++/JAuthorizationLevel.hpp +61 -0
  9. package/nitrogen/generated/android/c++/JAuthorizationLevelInternal.hpp +3 -4
  10. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -1
  11. package/nitrogen/generated/android/c++/JFunc_void_GeolocationError.hpp +2 -1
  12. package/nitrogen/generated/android/c++/JFunc_void_GeolocationResponse.hpp +6 -1
  13. package/nitrogen/generated/android/c++/JFunc_void_LocationError.hpp +78 -0
  14. package/nitrogen/generated/android/c++/JGeolocationCoordinates.hpp +25 -17
  15. package/nitrogen/generated/android/c++/JGeolocationError.hpp +5 -1
  16. package/nitrogen/generated/android/c++/JGeolocationOptions.hpp +5 -1
  17. package/nitrogen/generated/android/c++/JGeolocationResponse.hpp +9 -1
  18. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.cpp +109 -0
  19. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.hpp +70 -0
  20. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.cpp +98 -42
  21. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.hpp +7 -5
  22. package/nitrogen/generated/android/c++/JLocationError.hpp +61 -0
  23. package/nitrogen/generated/android/c++/JLocationProvider.hpp +61 -0
  24. package/nitrogen/generated/android/c++/JLocationProviderInternal.hpp +3 -4
  25. package/nitrogen/generated/android/c++/JLocationRequestOptions.hpp +81 -0
  26. package/nitrogen/generated/android/c++/JModernGeolocationConfiguration.hpp +73 -0
  27. package/nitrogen/generated/android/c++/JNullableDouble.cpp +26 -0
  28. package/nitrogen/generated/android/c++/JNullableDouble.hpp +69 -0
  29. package/nitrogen/generated/android/c++/JPermissionStatus.hpp +64 -0
  30. package/nitrogen/generated/android/c++/JRNConfigurationInternal.hpp +5 -1
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevel.kt +24 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevelInternal.kt +2 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void.kt +0 -1
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationError.kt +0 -1
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationResponse.kt +0 -1
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_LocationError.kt +80 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationCoordinates.kt +34 -25
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationError.kt +27 -18
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationOptions.kt +33 -24
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationResponse.kt +18 -9
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationCompatSpec.kt +92 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationSpec.kt +17 -17
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationError.kt +41 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProvider.kt +24 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderInternal.kt +2 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationRequestOptions.kt +56 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/ModernGeolocationConfiguration.kt +47 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/NullableDouble.kt +59 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/PermissionStatus.kt +25 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/RNConfigurationInternal.kt +24 -15
  51. package/nitrogen/generated/android/nitrogeolocation+autolinking.cmake +3 -0
  52. package/nitrogen/generated/android/nitrogeolocationOnLoad.cpp +14 -2
  53. package/nitrogen/generated/ios/NitroGeolocation+autolinking.rb +1 -1
  54. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.cpp +55 -13
  55. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.hpp +325 -68
  56. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Umbrella.hpp +26 -0
  57. package/nitrogen/generated/ios/NitroGeolocationAutolinking.mm +8 -0
  58. package/nitrogen/generated/ios/NitroGeolocationAutolinking.swift +15 -0
  59. package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.cpp +11 -0
  60. package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.hpp +130 -0
  61. package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.hpp +47 -26
  62. package/nitrogen/generated/ios/swift/AuthorizationLevel.swift +44 -0
  63. package/nitrogen/generated/ios/swift/Func_void.swift +1 -1
  64. package/nitrogen/generated/ios/swift/Func_void_GeolocationError.swift +1 -1
  65. package/nitrogen/generated/ios/swift/Func_void_GeolocationResponse.swift +1 -1
  66. package/nitrogen/generated/ios/swift/Func_void_LocationError.swift +47 -0
  67. package/nitrogen/generated/ios/swift/Func_void_PermissionStatus.swift +47 -0
  68. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  69. package/nitrogen/generated/ios/swift/GeolocationCoordinates.swift +132 -93
  70. package/nitrogen/generated/ios/swift/GeolocationError.swift +11 -40
  71. package/nitrogen/generated/ios/swift/GeolocationOptions.swift +29 -98
  72. package/nitrogen/generated/ios/swift/GeolocationResponse.swift +5 -16
  73. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec.swift +61 -0
  74. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec_cxx.swift +244 -0
  75. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec.swift +13 -5
  76. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec_cxx.swift +66 -64
  77. package/nitrogen/generated/ios/swift/LocationError.swift +35 -0
  78. package/nitrogen/generated/ios/swift/LocationProvider.swift +44 -0
  79. package/nitrogen/generated/ios/swift/LocationRequestOptions.swift +116 -0
  80. package/nitrogen/generated/ios/swift/ModernGeolocationConfiguration.swift +83 -0
  81. package/nitrogen/generated/ios/swift/NullableDouble.swift +18 -0
  82. package/nitrogen/generated/ios/swift/PermissionStatus.swift +48 -0
  83. package/nitrogen/generated/ios/swift/RNConfigurationInternal.swift +16 -50
  84. package/nitrogen/generated/shared/c++/AuthorizationLevel.hpp +80 -0
  85. package/nitrogen/generated/shared/c++/GeolocationCoordinates.hpp +45 -27
  86. package/nitrogen/generated/shared/c++/GeolocationError.hpp +32 -16
  87. package/nitrogen/generated/shared/c++/GeolocationOptions.hpp +38 -22
  88. package/nitrogen/generated/shared/c++/GeolocationResponse.hpp +23 -7
  89. package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.cpp +26 -0
  90. package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.hpp +79 -0
  91. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.cpp +4 -3
  92. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.hpp +22 -16
  93. package/nitrogen/generated/shared/c++/LocationError.hpp +87 -0
  94. package/nitrogen/generated/shared/c++/LocationProvider.hpp +80 -0
  95. package/nitrogen/generated/shared/c++/LocationRequestOptions.hpp +107 -0
  96. package/nitrogen/generated/shared/c++/ModernGeolocationConfiguration.hpp +100 -0
  97. package/nitrogen/generated/shared/c++/PermissionStatus.hpp +84 -0
  98. package/nitrogen/generated/shared/c++/RNConfigurationInternal.hpp +29 -13
  99. package/package.json +16 -7
  100. package/src/GeolocationClient.ts +116 -0
  101. package/src/NitroGeolocation.nitro.ts +177 -30
  102. package/src/NitroGeolocationCompat.nitro.ts +41 -0
  103. package/src/NitroGeolocationModule.ts +6 -0
  104. package/src/compat/clearWatch.ts +9 -0
  105. package/src/{getCurrentPosition.ts → compat/getCurrentPosition.ts} +7 -3
  106. package/src/compat/index.tsx +34 -0
  107. package/src/compat/requestAuthorization.ts +9 -0
  108. package/src/{setRNConfiguration.ts → compat/setRNConfiguration.ts} +6 -4
  109. package/src/{stopObserving.ts → compat/stopObserving.ts} +4 -2
  110. package/src/{watchPosition.ts → compat/watchPosition.ts} +7 -6
  111. package/src/components/GeolocationProvider.tsx +91 -0
  112. package/src/components/index.ts +13 -0
  113. package/src/hooks/index.ts +9 -0
  114. package/src/hooks/useCheckPermission.ts +46 -0
  115. package/src/hooks/useGetCurrentPosition.ts +44 -0
  116. package/src/hooks/useRequestPermission.ts +38 -0
  117. package/src/hooks/useWatchPosition.ts +127 -0
  118. package/src/index.tsx +72 -27
  119. package/src/types.ts +32 -4
  120. package/src/utils/cache.ts +25 -0
  121. package/src/utils/config.ts +93 -0
  122. package/src/utils/errors.ts +82 -0
  123. package/src/utils/index.ts +27 -0
  124. package/src/utils/provider.ts +61 -0
  125. package/src/utils/quality.ts +98 -0
  126. package/src/clearWatch.ts +0 -13
  127. package/src/requestAuthorization.ts +0 -9
@@ -1,81 +1,673 @@
1
1
  package com.margelo.nitro.nitrogeolocation
2
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 as AndroidLocationManager
9
+ import android.os.Build
10
+ import android.os.CancellationSignal
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import android.os.SystemClock
14
+ import androidx.core.app.ActivityCompat
15
+ import androidx.core.content.ContextCompat
3
16
  import com.facebook.proguard.annotations.DoNotStrip
4
17
  import com.facebook.react.bridge.ReactApplicationContext
18
+ import com.margelo.nitro.core.Promise
5
19
  import com.margelo.nitro.NitroModules
20
+ import java.util.UUID
21
+ import java.util.concurrent.ConcurrentHashMap
6
22
 
23
+ /**
24
+ * Exception wrapper for LocationError struct.
25
+ * Nitrogen-generated LocationError doesn't extend Exception,
26
+ * so we need to wrap it for Promise.reject().
27
+ */
28
+ private class GeolocationErrorException(
29
+ val locationError: LocationError
30
+ ) : Exception(locationError.message)
31
+
32
+ /**
33
+ * Modern Geolocation implementation for Android.
34
+ *
35
+ * Key features:
36
+ * - Promise-based permission and getCurrentPosition
37
+ * - Token-based watch subscriptions (first-class functions!)
38
+ * - WatchPositionResult discriminated union
39
+ * - Automatic subscription management
40
+ */
7
41
  @DoNotStrip
8
42
  class NitroGeolocation(
9
- private val reactContext: ReactApplicationContext = NitroModules.applicationContext!!
43
+ private val reactContext: ReactApplicationContext = NitroModules.applicationContext!!
10
44
  ) : HybridNitroGeolocationSpec() {
11
- private var configuration: RNConfigurationInternal =
12
- RNConfigurationInternal(
13
- skipPermissionRequests = false,
14
- authorizationLevel = null,
15
- enableBackgroundLocationUpdates = null,
16
- locationProvider = null
45
+
46
+ // MARK: - Types
47
+
48
+ private data class ParsedOptions(
49
+ val timeout: Double,
50
+ val maximumAge: Double,
51
+ val enableHighAccuracy: Boolean,
52
+ val interval: Double,
53
+ val fastestInterval: Double,
54
+ val distanceFilter: Double
55
+ ) {
56
+ companion object {
57
+ private const val DEFAULT_TIMEOUT = 10.0 * 60 * 1000 // 10 minutes in ms
58
+ private const val DEFAULT_MAXIMUM_AGE = 0.0
59
+ private const val DEFAULT_INTERVAL = 1000.0
60
+ private const val DEFAULT_FASTEST_INTERVAL = 100.0
61
+ private const val DEFAULT_DISTANCE_FILTER = 0.0
62
+
63
+ fun parse(options: LocationRequestOptions?): ParsedOptions {
64
+ return ParsedOptions(
65
+ timeout = options?.timeout ?: DEFAULT_TIMEOUT,
66
+ maximumAge = options?.maximumAge ?: DEFAULT_MAXIMUM_AGE,
67
+ enableHighAccuracy = options?.enableHighAccuracy ?: false,
68
+ interval = options?.interval ?: DEFAULT_INTERVAL,
69
+ fastestInterval = options?.fastestInterval ?: DEFAULT_FASTEST_INTERVAL,
70
+ distanceFilter = options?.distanceFilter ?: DEFAULT_DISTANCE_FILTER
71
+ )
72
+ }
73
+ }
74
+ }
75
+
76
+ private data class WatchSubscription(
77
+ val token: String,
78
+ val success: (GeolocationResponse) -> Unit,
79
+ val error: ((LocationError) -> Unit)?,
80
+ val options: ParsedOptions
81
+ )
82
+
83
+ private data class PositionRequest(
84
+ val id: UUID,
85
+ val resolver: (Result<GeolocationResponse>) -> Unit,
86
+ val options: ParsedOptions,
87
+ val handler: Handler,
88
+ var cancellationSignal: CancellationSignal? = null
89
+ )
90
+
91
+ // MARK: - Properties
92
+
93
+ private var configuration: ModernGeolocationConfiguration? = null
94
+ private val locationManager: AndroidLocationManager by lazy {
95
+ reactContext.getSystemService(Context.LOCATION_SERVICE) as AndroidLocationManager
96
+ }
97
+
98
+ // Permission promise resolvers
99
+ private val pendingPermissionResolvers = mutableListOf<(Result<PermissionStatus>) -> Unit>()
100
+
101
+ // getCurrentPosition requests
102
+ private val pendingPositionRequests = ConcurrentHashMap<UUID, PositionRequest>()
103
+
104
+ // Watch subscriptions (token -> callback)
105
+ private val watchSubscriptions = ConcurrentHashMap<String, WatchSubscription>()
106
+
107
+ // Location listener for watch subscriptions
108
+ private var watchLocationListener: LocationListener? = null
109
+ private var currentWatchProvider: String? = null
110
+
111
+ // Error codes
112
+ private val PERMISSION_DENIED = 1.0
113
+ private val POSITION_UNAVAILABLE = 2.0
114
+ private val TIMEOUT = 3.0
115
+
116
+ // MARK: - Configuration
117
+
118
+ override fun setConfiguration(config: ModernGeolocationConfiguration) {
119
+ this.configuration = config
120
+ }
121
+
122
+ // MARK: - Permission API (Promise-based)
123
+
124
+ override fun checkPermission(): Promise<PermissionStatus> {
125
+ return Promise.async {
126
+ val status = getCurrentPermissionStatus()
127
+ status
128
+ }
129
+ }
130
+
131
+ override fun requestPermission(): Promise<PermissionStatus> {
132
+ val promise = Promise<PermissionStatus>()
133
+
134
+ // Check if already determined
135
+ val currentStatus = getCurrentPermissionStatus()
136
+ if (currentStatus != PermissionStatus.UNDETERMINED) {
137
+ promise.resolve(currentStatus)
138
+ return promise
139
+ }
140
+
141
+ // Check if we have an activity
142
+ val activity = reactContext.currentActivity
143
+ if (activity == null) {
144
+ promise.reject(Exception("No activity available"))
145
+ return promise
146
+ }
147
+
148
+ // Queue resolver
149
+ pendingPermissionResolvers.add { result ->
150
+ result.fold(
151
+ onSuccess = { promise.resolve(it) },
152
+ onFailure = { promise.reject(it) }
17
153
  )
154
+ }
18
155
 
19
- private val requestAuthorizationHandler by lazy {
20
- RequestAuthorization(
21
- reactContext = reactContext,
22
- onPermissionResult = { result, success, error ->
23
- when (result) {
24
- is PermissionResult.Granted -> success?.invoke()
25
- is PermissionResult.Denied ->
26
- error?.invoke(
27
- createPermissionError(
28
- "Location permission was not granted."
29
- )
30
- )
31
- }
32
- }
156
+ // Request permission
157
+ val permissions = arrayOf(
158
+ Manifest.permission.ACCESS_FINE_LOCATION,
159
+ Manifest.permission.ACCESS_COARSE_LOCATION
160
+ )
161
+
162
+ ActivityCompat.requestPermissions(
163
+ activity,
164
+ permissions,
165
+ PERMISSION_REQUEST_CODE
33
166
  )
167
+
168
+ return promise
34
169
  }
35
170
 
36
- private val watchPositionHandler by lazy { WatchPosition(reactContext) }
171
+ // MARK: - Get Current Position (Promise-based)
172
+
173
+ override fun getCurrentPosition(options: LocationRequestOptions?): Promise<GeolocationResponse> {
174
+ val promise = Promise<GeolocationResponse>()
175
+
176
+ // Check permission
177
+ if (!hasLocationPermission()) {
178
+ promise.reject(createLocationError(
179
+ PERMISSION_DENIED,
180
+ "Location permission not granted"
181
+ ))
182
+ return promise
183
+ }
184
+
185
+ val parsedOptions = ParsedOptions.parse(options)
186
+
187
+ // Check cached location
188
+ val provider = getValidProvider(parsedOptions.enableHighAccuracy)
189
+ if (provider != null) {
190
+ val lastKnownLocation = try {
191
+ locationManager.getLastKnownLocation(provider)
192
+ } catch (e: SecurityException) {
193
+ null
194
+ }
195
+
196
+ if (lastKnownLocation != null && isCachedLocationValid(lastKnownLocation, parsedOptions)) {
197
+ val position = locationToPosition(lastKnownLocation)
198
+ promise.resolve(position)
199
+ return promise
200
+ }
37
201
 
38
- override fun setRNConfiguration(config: RNConfigurationInternal) {
39
- configuration = config
202
+ // maximumAge is Infinity -> use cached if available
203
+ if (lastKnownLocation != null && parsedOptions.maximumAge == Double.POSITIVE_INFINITY) {
204
+ val position = locationToPosition(lastKnownLocation)
205
+ promise.resolve(position)
206
+ return promise
207
+ }
208
+ }
209
+
210
+ // Request fresh location
211
+ if (provider == null) {
212
+ promise.reject(createLocationError(
213
+ POSITION_UNAVAILABLE,
214
+ "No location provider available"
215
+ ))
216
+ return promise
217
+ }
218
+
219
+ requestFreshLocation(provider, parsedOptions) { result ->
220
+ result.fold(
221
+ onSuccess = { promise.resolve(it) },
222
+ onFailure = { promise.reject(it) }
223
+ )
224
+ }
225
+
226
+ return promise
227
+ }
228
+
229
+ // MARK: - Watch Position (Callback-based with tokens)
230
+
231
+ override fun watchPosition(
232
+ success: (GeolocationResponse) -> Unit,
233
+ error: ((LocationError) -> Unit)?,
234
+ options: LocationRequestOptions?
235
+ ): String {
236
+ val token = UUID.randomUUID().toString()
237
+ val parsedOptions = ParsedOptions.parse(options)
238
+
239
+ val subscription = WatchSubscription(
240
+ token = token,
241
+ success = success,
242
+ error = error,
243
+ options = parsedOptions
244
+ )
245
+
246
+ watchSubscriptions[token] = subscription
247
+
248
+ // Start watching if first subscriber
249
+ if (watchSubscriptions.size == 1) {
250
+ startWatchingLocation()
251
+ }
252
+
253
+ return token
40
254
  }
41
255
 
42
- override fun requestAuthorization(
43
- success: (() -> Unit)?,
44
- error: ((error: GeolocationError) -> Unit)?
256
+ override fun unwatch(token: String) {
257
+ watchSubscriptions.remove(token)
258
+
259
+ // Stop watching if no more subscribers
260
+ if (watchSubscriptions.isEmpty()) {
261
+ stopWatchingLocation()
262
+ }
263
+ }
264
+
265
+ override fun stopObserving() {
266
+ watchSubscriptions.clear()
267
+ stopWatchingLocation()
268
+ }
269
+
270
+ // MARK: - Helper Functions - Permission
271
+
272
+ private fun getCurrentPermissionStatus(): PermissionStatus {
273
+ // Legacy Android (< 6.0)
274
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
275
+ return PermissionStatus.GRANTED
276
+ }
277
+
278
+ val fineLocationGranted = ContextCompat.checkSelfPermission(
279
+ reactContext,
280
+ Manifest.permission.ACCESS_FINE_LOCATION
281
+ ) == PackageManager.PERMISSION_GRANTED
282
+
283
+ val coarseLocationGranted = ContextCompat.checkSelfPermission(
284
+ reactContext,
285
+ Manifest.permission.ACCESS_COARSE_LOCATION
286
+ ) == PackageManager.PERMISSION_GRANTED
287
+
288
+ return when {
289
+ fineLocationGranted || coarseLocationGranted -> PermissionStatus.GRANTED
290
+ else -> {
291
+ // On Android, there's no "restricted" state like iOS
292
+ // We could check if permission was previously denied, but for simplicity:
293
+ PermissionStatus.DENIED
294
+ }
295
+ }
296
+ }
297
+
298
+ private fun hasLocationPermission(): Boolean {
299
+ return getCurrentPermissionStatus() == PermissionStatus.GRANTED
300
+ }
301
+
302
+ // Handle permission request result (called from Activity)
303
+ fun onPermissionResult(requestCode: Int, grantResults: IntArray) {
304
+ if (requestCode != PERMISSION_REQUEST_CODE) return
305
+
306
+ val granted = grantResults.isNotEmpty() && grantResults.any { it == PackageManager.PERMISSION_GRANTED }
307
+ val status = if (granted) PermissionStatus.GRANTED else PermissionStatus.DENIED
308
+
309
+ // Resolve all pending permission requests
310
+ for (resolver in pendingPermissionResolvers) {
311
+ resolver(Result.success(status))
312
+ }
313
+ pendingPermissionResolvers.clear()
314
+ }
315
+
316
+ // MARK: - Helper Functions - Provider Selection
317
+
318
+ private fun getValidProvider(highAccuracy: Boolean): String? {
319
+ val preferredProvider = if (highAccuracy)
320
+ AndroidLocationManager.GPS_PROVIDER
321
+ else
322
+ AndroidLocationManager.NETWORK_PROVIDER
323
+
324
+ val fallbackProvider = if (highAccuracy)
325
+ AndroidLocationManager.NETWORK_PROVIDER
326
+ else
327
+ AndroidLocationManager.GPS_PROVIDER
328
+
329
+ return when {
330
+ isProviderValid(preferredProvider) -> preferredProvider
331
+ isProviderValid(fallbackProvider) -> fallbackProvider
332
+ else -> null
333
+ }
334
+ }
335
+
336
+ private fun isProviderValid(provider: String): Boolean {
337
+ return try {
338
+ locationManager.isProviderEnabled(provider)
339
+ } catch (e: Exception) {
340
+ false
341
+ }
342
+ }
343
+
344
+ // MARK: - Helper Functions - Cache Validation
345
+
346
+ private fun isCachedLocationValid(location: Location, options: ParsedOptions): Boolean {
347
+ val locationAge = SystemClock.elapsedRealtime() - location.elapsedRealtimeNanos / 1_000_000
348
+ return locationAge < options.maximumAge
349
+ }
350
+
351
+ // MARK: - Helper Functions - Request Fresh Location
352
+
353
+ private fun requestFreshLocation(
354
+ provider: String,
355
+ options: ParsedOptions,
356
+ resolver: (Result<GeolocationResponse>) -> Unit
45
357
  ) {
46
- requestAuthorizationHandler.execute(success, error)
358
+ val id = UUID.randomUUID()
359
+ val handler = Handler(Looper.getMainLooper())
360
+
361
+ val request = PositionRequest(
362
+ id = id,
363
+ resolver = resolver,
364
+ options = options,
365
+ handler = handler
366
+ )
367
+
368
+ pendingPositionRequests[id] = request
369
+
370
+ // Use modern API on Android 11+
371
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
372
+ requestCurrentLocationModern(provider, options, id, handler)
373
+ } else {
374
+ requestCurrentLocationLegacy(provider, options, id, handler)
375
+ }
47
376
  }
48
377
 
49
- override fun getCurrentPosition(
50
- success: (position: GeolocationResponse) -> Unit,
51
- error: ((error: GeolocationError) -> Unit)?,
52
- options: GeolocationOptions?
378
+ @androidx.annotation.RequiresApi(Build.VERSION_CODES.R)
379
+ private fun requestCurrentLocationModern(
380
+ provider: String,
381
+ options: ParsedOptions,
382
+ requestId: UUID,
383
+ handler: Handler
53
384
  ) {
54
- GetCurrentPosition(reactContext).execute(success, error, options)
385
+ val cancellationSignal = CancellationSignal()
386
+
387
+ // Timeout handler
388
+ val timeoutRunnable = Runnable {
389
+ handlePositionTimeout(requestId)
390
+ }
391
+
392
+ try {
393
+ locationManager.getCurrentLocation(
394
+ provider,
395
+ cancellationSignal,
396
+ { runnable -> handler.post(runnable) }
397
+ ) { location ->
398
+ handler.removeCallbacks(timeoutRunnable)
399
+
400
+ val request = pendingPositionRequests.remove(requestId)
401
+ if (request != null) {
402
+ if (location != null) {
403
+ val position = locationToPosition(location)
404
+ request.resolver(Result.success(position))
405
+ } else {
406
+ request.resolver(Result.failure(createLocationError(
407
+ POSITION_UNAVAILABLE,
408
+ "Unable to get location"
409
+ )))
410
+ }
411
+ }
412
+ }
413
+
414
+ handler.postDelayed(timeoutRunnable, options.timeout.toLong())
415
+
416
+ pendingPositionRequests[requestId]?.cancellationSignal = cancellationSignal
417
+
418
+ } catch (e: SecurityException) {
419
+ pendingPositionRequests.remove(requestId)
420
+ val request = pendingPositionRequests[requestId]
421
+ request?.resolver(Result.failure(createLocationError(
422
+ PERMISSION_DENIED,
423
+ "Security exception: ${e.message}"
424
+ )))
425
+ }
55
426
  }
56
427
 
57
- override fun watchPosition(
58
- success: (position: GeolocationResponse) -> Unit,
59
- error: ((error: GeolocationError) -> Unit)?,
60
- options: GeolocationOptions?
61
- ): Double {
62
- return watchPositionHandler.watch(success, error, options).toDouble()
428
+ private fun requestCurrentLocationLegacy(
429
+ provider: String,
430
+ options: ParsedOptions,
431
+ requestId: UUID,
432
+ handler: Handler
433
+ ) {
434
+ var isResolved = false
435
+ var oldLocation: Location? = null
436
+
437
+ val listener = object : LocationListener {
438
+ override fun onLocationChanged(location: Location) {
439
+ synchronized(this) {
440
+ if (!isResolved) {
441
+ val bestLocation = selectBestLocation(location, oldLocation)
442
+ if (bestLocation == location) {
443
+ isResolved = true
444
+ handler.removeCallbacksAndMessages(null)
445
+
446
+ try {
447
+ locationManager.removeUpdates(this)
448
+ } catch (e: Exception) {
449
+ // Ignore
450
+ }
451
+
452
+ val request = pendingPositionRequests.remove(requestId)
453
+ if (request != null) {
454
+ val position = locationToPosition(location)
455
+ request.resolver(Result.success(position))
456
+ }
457
+ }
458
+ oldLocation = location
459
+ }
460
+ }
461
+ }
462
+
463
+ override fun onProviderDisabled(provider: String) {}
464
+ override fun onProviderEnabled(provider: String) {}
465
+ @Deprecated("Deprecated in Java")
466
+ override fun onStatusChanged(provider: String?, status: Int, extras: android.os.Bundle?) {}
467
+ }
468
+
469
+ // Timeout handler
470
+ val timeoutRunnable = Runnable {
471
+ synchronized(listener) {
472
+ if (!isResolved) {
473
+ isResolved = true
474
+ try {
475
+ locationManager.removeUpdates(listener)
476
+ } catch (e: Exception) {
477
+ // Ignore
478
+ }
479
+ handlePositionTimeout(requestId)
480
+ }
481
+ }
482
+ }
483
+
484
+ try {
485
+ locationManager.requestLocationUpdates(
486
+ provider,
487
+ 100, // min time (ms)
488
+ 1f, // min distance (m)
489
+ listener,
490
+ Looper.getMainLooper()
491
+ )
492
+
493
+ handler.postDelayed(timeoutRunnable, options.timeout.toLong())
494
+
495
+ } catch (e: SecurityException) {
496
+ pendingPositionRequests.remove(requestId)?.resolver(Result.failure(createLocationError(
497
+ PERMISSION_DENIED,
498
+ "Security exception: ${e.message}"
499
+ )))
500
+ }
63
501
  }
64
502
 
65
- override fun clearWatch(watchId: Double) {
66
- watchPositionHandler.clearWatch(watchId.toInt())
503
+ private fun selectBestLocation(newLocation: Location, currentBest: Location?): Location {
504
+ if (currentBest == null) return newLocation
505
+
506
+ val timeDelta = newLocation.time - currentBest.time
507
+ val isSignificantlyNewer = timeDelta > TWO_MINUTES_MS
508
+ val isSignificantlyOlder = timeDelta < -TWO_MINUTES_MS
509
+
510
+ if (isSignificantlyNewer) return newLocation
511
+ if (isSignificantlyOlder) return currentBest
512
+
513
+ val accuracyDelta = (newLocation.accuracy - currentBest.accuracy).toInt()
514
+ val isMoreAccurate = accuracyDelta < 0
515
+ val isSignificantlyLessAccurate = accuracyDelta > 200
516
+ val isNewer = timeDelta > 0
517
+ val isLessAccurate = accuracyDelta > 0
518
+ val isFromSameProvider = newLocation.provider == currentBest.provider
519
+
520
+ return when {
521
+ isMoreAccurate -> newLocation
522
+ isNewer && !isLessAccurate -> newLocation
523
+ isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> newLocation
524
+ else -> currentBest
525
+ }
67
526
  }
68
527
 
69
- override fun stopObserving() {
70
- watchPositionHandler.stopObserving()
528
+ private fun handlePositionTimeout(requestId: UUID) {
529
+ val request = pendingPositionRequests.remove(requestId)
530
+ if (request != null) {
531
+ request.cancellationSignal?.cancel()
532
+ request.handler.removeCallbacksAndMessages(null)
533
+
534
+ val timeoutSeconds = request.options.timeout / 1000.0
535
+ val message = String.format("Unable to fetch location within %.1fs.", timeoutSeconds)
536
+ val error = createLocationError(TIMEOUT, message)
537
+
538
+ request.resolver(Result.failure(error))
539
+ }
71
540
  }
72
541
 
73
- private fun createPermissionError(message: String) =
74
- GeolocationError(
75
- code = RequestAuthorization.PERMISSION_DENIED.toDouble(),
76
- message = message,
77
- PERMISSION_DENIED = RequestAuthorization.PERMISSION_DENIED.toDouble(),
78
- POSITION_UNAVAILABLE = RequestAuthorization.POSITION_UNAVAILABLE.toDouble(),
79
- TIMEOUT = RequestAuthorization.TIMEOUT.toDouble()
542
+ // MARK: - Helper Functions - Watch Position
543
+
544
+ private fun startWatchingLocation() {
545
+ // Determine best provider and options from all subscriptions
546
+ var useHighAccuracy = false
547
+ var smallestInterval = Double.MAX_VALUE
548
+ var smallestDistanceFilter = Float.MAX_VALUE
549
+
550
+ for ((_, subscription) in watchSubscriptions) {
551
+ if (subscription.options.enableHighAccuracy) {
552
+ useHighAccuracy = true
553
+ }
554
+ smallestInterval = minOf(smallestInterval, subscription.options.interval)
555
+ smallestDistanceFilter = minOf(smallestDistanceFilter, subscription.options.distanceFilter.toFloat())
556
+ }
557
+
558
+ val provider = getValidProvider(useHighAccuracy) ?: return
559
+ currentWatchProvider = provider
560
+
561
+ val listener = object : LocationListener {
562
+ override fun onLocationChanged(location: Location) {
563
+ val position = locationToPosition(location)
564
+
565
+ // Notify all subscribers
566
+ for ((_, subscription) in watchSubscriptions) {
567
+ subscription.success(position)
568
+ }
569
+ }
570
+
571
+ override fun onProviderDisabled(provider: String) {
572
+ val error = LocationError(
573
+ code = POSITION_UNAVAILABLE,
574
+ message = "Provider disabled: $provider"
575
+ )
576
+
577
+ for ((_, subscription) in watchSubscriptions) {
578
+ subscription.error?.invoke(error)
579
+ }
580
+ }
581
+
582
+ override fun onProviderEnabled(provider: String) {}
583
+ @Deprecated("Deprecated in Java")
584
+ override fun onStatusChanged(provider: String?, status: Int, extras: android.os.Bundle?) {}
585
+ }
586
+
587
+ watchLocationListener = listener
588
+
589
+ try {
590
+ locationManager.requestLocationUpdates(
591
+ provider,
592
+ smallestInterval.toLong(),
593
+ smallestDistanceFilter,
594
+ listener,
595
+ Looper.getMainLooper()
80
596
  )
597
+ } catch (e: SecurityException) {
598
+ val error = LocationError(
599
+ code = PERMISSION_DENIED,
600
+ message = "Permission denied: ${e.message}"
601
+ )
602
+
603
+ for ((_, subscription) in watchSubscriptions) {
604
+ subscription.error?.invoke(error)
605
+ }
606
+ }
607
+ }
608
+
609
+ private fun stopWatchingLocation() {
610
+ watchLocationListener?.let { listener ->
611
+ try {
612
+ locationManager.removeUpdates(listener)
613
+ } catch (e: Exception) {
614
+ // Ignore
615
+ }
616
+ }
617
+ watchLocationListener = null
618
+ currentWatchProvider = null
619
+ }
620
+
621
+ // MARK: - Helper Functions - Conversion
622
+
623
+ private fun locationToPosition(location: Location): GeolocationResponse {
624
+ val altitude = if (location.hasAltitude()) {
625
+ NullableDouble.create(location.altitude)
626
+ } else {
627
+ null
628
+ }
629
+ val altitudeAccuracy = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && location.hasVerticalAccuracy()) {
630
+ NullableDouble.create(location.verticalAccuracyMeters.toDouble())
631
+ } else {
632
+ null
633
+ }
634
+ val heading = if (location.hasBearing()) {
635
+ NullableDouble.create(location.bearing.toDouble())
636
+ } else {
637
+ null
638
+ }
639
+ val speed = if (location.hasSpeed()) {
640
+ NullableDouble.create(location.speed.toDouble())
641
+ } else {
642
+ null
643
+ }
644
+
645
+ val coords = GeolocationCoordinates(
646
+ latitude = location.latitude,
647
+ longitude = location.longitude,
648
+ altitude = altitude,
649
+ accuracy = location.accuracy.toDouble(),
650
+ altitudeAccuracy = altitudeAccuracy,
651
+ heading = heading,
652
+ speed = speed
653
+ )
654
+
655
+ return GeolocationResponse(
656
+ coords = coords,
657
+ timestamp = location.time.toDouble()
658
+ )
659
+ }
660
+
661
+ private fun createLocationError(code: Double, message: String): Exception {
662
+ val locationError = LocationError(
663
+ code = code,
664
+ message = message
665
+ )
666
+ return GeolocationErrorException(locationError)
667
+ }
668
+
669
+ companion object {
670
+ private const val PERMISSION_REQUEST_CODE = 8947
671
+ private const val TWO_MINUTES_MS = 2 * 60 * 1000L
672
+ }
81
673
  }