react-native-nitro-geolocation 0.0.1 → 0.1.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 (79) hide show
  1. package/LICENSE +4 -1
  2. package/README.md +598 -0
  3. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/GetCurrentPosition.kt +341 -0
  4. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocation.kt +76 -5
  5. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/RequestAuthorization.kt +164 -0
  6. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/WatchPosition.kt +228 -0
  7. package/ios/LocationManager.swift +529 -0
  8. package/ios/NitroGeolocation.swift +96 -2
  9. package/nitrogen/generated/.gitattributes +1 -0
  10. package/nitrogen/generated/android/c++/JAuthorizationLevelInternal.hpp +62 -0
  11. package/nitrogen/generated/android/c++/JFunc_void.hpp +74 -0
  12. package/nitrogen/generated/android/c++/JFunc_void_GeolocationError.hpp +77 -0
  13. package/nitrogen/generated/android/c++/JFunc_void_GeolocationResponse.hpp +79 -0
  14. package/nitrogen/generated/android/c++/JGeolocationCoordinates.hpp +77 -0
  15. package/nitrogen/generated/android/c++/JGeolocationError.hpp +69 -0
  16. package/nitrogen/generated/android/c++/JGeolocationOptions.hpp +77 -0
  17. package/nitrogen/generated/android/c++/JGeolocationResponse.hpp +59 -0
  18. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.cpp +98 -0
  19. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.hpp +69 -0
  20. package/nitrogen/generated/android/c++/JLocationProviderInternal.hpp +62 -0
  21. package/nitrogen/generated/android/c++/JRNConfigurationInternal.hpp +69 -0
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevelInternal.kt +22 -0
  23. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void.kt +81 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationError.kt +81 -0
  25. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationResponse.kt +81 -0
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationCoordinates.kt +47 -0
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationError.kt +41 -0
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationOptions.kt +47 -0
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationResponse.kt +32 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationSpec.kt +87 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderInternal.kt +22 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/RNConfigurationInternal.kt +38 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/nitrogeolocationOnLoad.kt +35 -0
  34. package/nitrogen/generated/android/nitrogeolocation+autolinking.cmake +81 -0
  35. package/nitrogen/generated/android/nitrogeolocation+autolinking.gradle +27 -0
  36. package/nitrogen/generated/android/nitrogeolocationOnLoad.cpp +50 -0
  37. package/nitrogen/generated/android/nitrogeolocationOnLoad.hpp +25 -0
  38. package/nitrogen/generated/ios/NitroGeolocation+autolinking.rb +60 -0
  39. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.cpp +56 -0
  40. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.hpp +252 -0
  41. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Umbrella.hpp +67 -0
  42. package/nitrogen/generated/ios/NitroGeolocationAutolinking.mm +33 -0
  43. package/nitrogen/generated/ios/NitroGeolocationAutolinking.swift +25 -0
  44. package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.cpp +11 -0
  45. package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.hpp +125 -0
  46. package/nitrogen/generated/ios/swift/AuthorizationLevelInternal.swift +44 -0
  47. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  48. package/nitrogen/generated/ios/swift/Func_void_GeolocationError.swift +47 -0
  49. package/nitrogen/generated/ios/swift/Func_void_GeolocationResponse.swift +47 -0
  50. package/nitrogen/generated/ios/swift/GeolocationCoordinates.swift +149 -0
  51. package/nitrogen/generated/ios/swift/GeolocationError.swift +79 -0
  52. package/nitrogen/generated/ios/swift/GeolocationOptions.swift +185 -0
  53. package/nitrogen/generated/ios/swift/GeolocationResponse.swift +46 -0
  54. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec.swift +54 -0
  55. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec_cxx.swift +236 -0
  56. package/nitrogen/generated/ios/swift/LocationProviderInternal.swift +44 -0
  57. package/nitrogen/generated/ios/swift/RNConfigurationInternal.swift +104 -0
  58. package/nitrogen/generated/shared/c++/AuthorizationLevelInternal.hpp +80 -0
  59. package/nitrogen/generated/shared/c++/GeolocationCoordinates.hpp +91 -0
  60. package/nitrogen/generated/shared/c++/GeolocationError.hpp +83 -0
  61. package/nitrogen/generated/shared/c++/GeolocationOptions.hpp +91 -0
  62. package/nitrogen/generated/shared/c++/GeolocationResponse.hpp +72 -0
  63. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.cpp +26 -0
  64. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.hpp +79 -0
  65. package/nitrogen/generated/shared/c++/LocationProviderInternal.hpp +80 -0
  66. package/nitrogen/generated/shared/c++/RNConfigurationInternal.hpp +84 -0
  67. package/package.json +34 -10
  68. package/src/NitroGeolocation.nitro.ts +38 -3
  69. package/src/NitroGeolocationModule.ts +5 -0
  70. package/src/clearWatch.ts +13 -0
  71. package/src/getCurrentPosition.ts +14 -0
  72. package/src/index.tsx +32 -7
  73. package/src/requestAuthorization.ts +9 -0
  74. package/src/setRNConfiguration.ts +22 -0
  75. package/src/stopObserving.ts +12 -0
  76. package/src/types.ts +43 -0
  77. package/src/watchPosition.ts +26 -0
  78. package/nitro.json +0 -17
  79. package/turbo.json +0 -42
@@ -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
+ }
@@ -1,10 +1,81 @@
1
1
  package com.margelo.nitro.nitrogeolocation
2
-
2
+
3
3
  import com.facebook.proguard.annotations.DoNotStrip
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.margelo.nitro.NitroModules
4
6
 
5
7
  @DoNotStrip
6
- class NitroGeolocation : HybridNitroGeolocationSpec() {
7
- override fun multiply(a: Double, b: Double): Double {
8
- return a * b
9
- }
8
+ class NitroGeolocation(
9
+ private val reactContext: ReactApplicationContext = NitroModules.applicationContext!!
10
+ ) : HybridNitroGeolocationSpec() {
11
+ private var configuration: RNConfigurationInternal =
12
+ RNConfigurationInternal(
13
+ skipPermissionRequests = false,
14
+ authorizationLevel = null,
15
+ enableBackgroundLocationUpdates = null,
16
+ locationProvider = null
17
+ )
18
+
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
+ }
33
+ )
34
+ }
35
+
36
+ private val watchPositionHandler by lazy { WatchPosition(reactContext) }
37
+
38
+ override fun setRNConfiguration(config: RNConfigurationInternal) {
39
+ configuration = config
40
+ }
41
+
42
+ override fun requestAuthorization(
43
+ success: (() -> Unit)?,
44
+ error: ((error: GeolocationError) -> Unit)?
45
+ ) {
46
+ requestAuthorizationHandler.execute(success, error)
47
+ }
48
+
49
+ override fun getCurrentPosition(
50
+ success: (position: GeolocationResponse) -> Unit,
51
+ error: ((error: GeolocationError) -> Unit)?,
52
+ options: GeolocationOptions?
53
+ ) {
54
+ GetCurrentPosition(reactContext).execute(success, error, options)
55
+ }
56
+
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()
63
+ }
64
+
65
+ override fun clearWatch(watchId: Double) {
66
+ watchPositionHandler.clearWatch(watchId.toInt())
67
+ }
68
+
69
+ override fun stopObserving() {
70
+ watchPositionHandler.stopObserving()
71
+ }
72
+
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()
80
+ )
10
81
  }
@@ -0,0 +1,164 @@
1
+ package com.margelo.nitro.nitrogeolocation
2
+
3
+ import android.Manifest
4
+ import android.content.pm.PackageManager
5
+ import android.os.Build
6
+ import androidx.core.app.ActivityCompat
7
+ import androidx.core.content.ContextCompat
8
+ import com.facebook.react.bridge.ReactApplicationContext
9
+ import com.facebook.react.modules.core.PermissionAwareActivity
10
+ import com.facebook.react.modules.core.PermissionListener
11
+
12
+ // ===== Data Models =====
13
+ sealed class PermissionState {
14
+ object LegacyAndroid : PermissionState()
15
+ object AlreadyGranted : PermissionState()
16
+ object NeedsRequest : PermissionState()
17
+ data class Error(val message: String) : PermissionState()
18
+ }
19
+
20
+ sealed class PermissionResult {
21
+ object Granted : PermissionResult()
22
+ object Denied : PermissionResult()
23
+ }
24
+
25
+ class RequestAuthorization(
26
+ private val reactContext: ReactApplicationContext,
27
+ private val onPermissionResult:
28
+ (PermissionResult, (() -> Unit)?, ((GeolocationError) -> Unit)?) -> Unit
29
+ ) {
30
+ private var pendingAuthSuccess: (() -> Unit)? = null
31
+ private var pendingAuthError: ((error: GeolocationError) -> Unit)? = null
32
+
33
+ fun execute(success: (() -> Unit)?, error: ((error: GeolocationError) -> Unit)?) {
34
+ val state = determinePermissionState()
35
+ executePermissionAction(state, success, error)
36
+ }
37
+
38
+ // ===== State Determination (Pure Functions) =====
39
+ private fun determinePermissionState(): PermissionState =
40
+ when {
41
+ isLegacyAndroid() -> PermissionState.LegacyAndroid
42
+ else -> determineModernAndroidState()
43
+ }
44
+
45
+ private fun determineModernAndroidState(): PermissionState =
46
+ when {
47
+ hasLocationPermission(reactContext) -> PermissionState.AlreadyGranted
48
+ reactContext.currentActivity == null ->
49
+ PermissionState.Error("Current activity is null")
50
+ else -> PermissionState.NeedsRequest
51
+ }
52
+
53
+ private fun isLegacyAndroid(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.M
54
+
55
+ private fun hasLocationPermission(context: ReactApplicationContext): Boolean =
56
+ LOCATION_PERMISSIONS.any { permission ->
57
+ ContextCompat.checkSelfPermission(context, permission) ==
58
+ PackageManager.PERMISSION_GRANTED
59
+ }
60
+
61
+ private fun determinePermissionResult(
62
+ permissions: Array<String>,
63
+ grantResults: IntArray
64
+ ): PermissionResult {
65
+ val hasGrantedPermission =
66
+ permissions.indices.any { i ->
67
+ isLocationPermission(permissions[i]) &&
68
+ grantResults[i] == PackageManager.PERMISSION_GRANTED
69
+ }
70
+
71
+ return when {
72
+ hasGrantedPermission -> PermissionResult.Granted
73
+ else -> PermissionResult.Denied
74
+ }
75
+ }
76
+
77
+ private fun isLocationPermission(permission: String): Boolean =
78
+ permission in LOCATION_PERMISSIONS
79
+
80
+ // ===== Actions (Side Effects) =====
81
+ private fun executePermissionAction(
82
+ state: PermissionState,
83
+ success: (() -> Unit)?,
84
+ error: ((error: GeolocationError) -> Unit)?
85
+ ) {
86
+ when (state) {
87
+ is PermissionState.LegacyAndroid -> success?.invoke()
88
+ is PermissionState.AlreadyGranted -> success?.invoke()
89
+ is PermissionState.NeedsRequest -> showPermissionDialog(success, error)
90
+ is PermissionState.Error -> error?.invoke(createPermissionError(state.message))
91
+ }
92
+ }
93
+
94
+ private fun showPermissionDialog(
95
+ success: (() -> Unit)?,
96
+ error: ((error: GeolocationError) -> Unit)?
97
+ ) {
98
+ pendingAuthSuccess = success
99
+ pendingAuthError = error
100
+
101
+ reactContext.currentActivity?.let { activity ->
102
+ requestPermissionsFromActivity(activity)
103
+ }
104
+ }
105
+
106
+ private fun requestPermissionsFromActivity(activity: android.app.Activity) {
107
+ val permissions = LOCATION_PERMISSIONS.toTypedArray()
108
+
109
+ when (val permissionAware = activity as? PermissionAwareActivity) {
110
+ null ->
111
+ ActivityCompat.requestPermissions(
112
+ activity,
113
+ permissions,
114
+ PERMISSION_REQUEST_CODE
115
+ )
116
+ else ->
117
+ permissionAware.requestPermissions(
118
+ permissions,
119
+ PERMISSION_REQUEST_CODE,
120
+ createPermissionListener()
121
+ )
122
+ }
123
+ }
124
+
125
+ private fun createPermissionListener() =
126
+ PermissionListener { requestCode, permissions, grantResults ->
127
+ when (requestCode) {
128
+ PERMISSION_REQUEST_CODE -> {
129
+ val result = determinePermissionResult(permissions, grantResults)
130
+ onPermissionResult(result, pendingAuthSuccess, pendingAuthError)
131
+ clearPendingCallbacks()
132
+ true
133
+ }
134
+ else -> false
135
+ }
136
+ }
137
+
138
+ private fun clearPendingCallbacks() {
139
+ pendingAuthSuccess = null
140
+ pendingAuthError = null
141
+ }
142
+
143
+ private fun createPermissionError(message: String) =
144
+ GeolocationError(
145
+ code = PERMISSION_DENIED.toDouble(),
146
+ message = message,
147
+ PERMISSION_DENIED = PERMISSION_DENIED.toDouble(),
148
+ POSITION_UNAVAILABLE = POSITION_UNAVAILABLE.toDouble(),
149
+ TIMEOUT = TIMEOUT.toDouble()
150
+ )
151
+
152
+ companion object {
153
+ const val PERMISSION_DENIED = 1
154
+ const val POSITION_UNAVAILABLE = 2
155
+ const val TIMEOUT = 3
156
+ const val PERMISSION_REQUEST_CODE = 2025
157
+
158
+ private val LOCATION_PERMISSIONS =
159
+ listOf(
160
+ Manifest.permission.ACCESS_FINE_LOCATION,
161
+ Manifest.permission.ACCESS_COARSE_LOCATION
162
+ )
163
+ }
164
+ }