react-native-nitro-geolocation 1.1.4 → 1.2.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.
Files changed (186) hide show
  1. package/README.md +97 -9
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/AndroidAccuracy.kt +105 -0
  4. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/AndroidHeadingManager.kt +313 -0
  5. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/AndroidLocationSettings.kt +313 -0
  6. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/GetCurrentPosition.kt +46 -45
  7. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/LocationMetadata.kt +26 -0
  8. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/LocationValues.kt +31 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocation.kt +1027 -140
  10. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocationCompat.kt +11 -11
  11. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/RequestAuthorization.kt +6 -6
  12. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/WatchPosition.kt +46 -45
  13. package/ios/CLLocation+GeolocationMetadata.swift +32 -0
  14. package/ios/LocationManager.swift +205 -51
  15. package/ios/NitroGeolocation.swift +949 -110
  16. package/ios/NitroGeolocationCompat.swift +7 -7
  17. package/nitrogen/generated/android/c++/JAccuracyAuthorization.hpp +61 -0
  18. package/nitrogen/generated/android/c++/JAndroidAccuracyPreset.hpp +64 -0
  19. package/nitrogen/generated/android/c++/JAndroidGranularity.hpp +61 -0
  20. package/nitrogen/generated/android/c++/{JRNConfigurationInternal.hpp → JCompatGeolocationConfigurationInternal.hpp} +10 -10
  21. package/nitrogen/generated/android/c++/{JGeolocationError.hpp → JCompatGeolocationError.hpp} +10 -10
  22. package/nitrogen/generated/android/c++/JCompatGeolocationOptions.hpp +105 -0
  23. package/nitrogen/generated/android/c++/JCompatGeolocationResponse.hpp +67 -0
  24. package/nitrogen/generated/android/c++/JFunc_void_AccuracyAuthorization.hpp +77 -0
  25. package/nitrogen/generated/android/c++/JFunc_void_CompatGeolocationError.hpp +78 -0
  26. package/nitrogen/generated/android/c++/JFunc_void_CompatGeolocationResponse.hpp +84 -0
  27. package/nitrogen/generated/android/c++/JFunc_void_GeolocationResponse.hpp +2 -0
  28. package/nitrogen/generated/android/c++/JFunc_void_Heading.hpp +78 -0
  29. package/nitrogen/generated/android/c++/JFunc_void_LocationProviderStatus.hpp +78 -0
  30. package/nitrogen/generated/android/c++/JFunc_void_PermissionStatus.hpp +77 -0
  31. package/nitrogen/generated/android/c++/JFunc_void_std__vector_GeocodedLocation_.hpp +97 -0
  32. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ReverseGeocodedAddress_.hpp +98 -0
  33. package/nitrogen/generated/android/c++/JGeocodedLocation.hpp +65 -0
  34. package/nitrogen/generated/android/c++/JGeocodingCoordinates.hpp +61 -0
  35. package/nitrogen/generated/android/c++/{JModernGeolocationConfiguration.hpp → JGeolocationConfiguration.hpp} +10 -10
  36. package/nitrogen/generated/android/c++/JGeolocationResponse.hpp +13 -3
  37. package/nitrogen/generated/android/c++/JHeading.hpp +69 -0
  38. package/nitrogen/generated/android/c++/JHeadingOptions.hpp +57 -0
  39. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.cpp +46 -30
  40. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.hpp +4 -4
  41. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.cpp +169 -33
  42. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.hpp +14 -3
  43. package/nitrogen/generated/android/c++/JIOSAccuracyPreset.hpp +73 -0
  44. package/nitrogen/generated/android/c++/JIOSActivityType.hpp +67 -0
  45. package/nitrogen/generated/android/c++/JLocationAccuracyOptions.hpp +65 -0
  46. package/nitrogen/generated/android/c++/JLocationAvailability.hpp +62 -0
  47. package/nitrogen/generated/android/c++/JLocationProviderStatus.hpp +77 -0
  48. package/nitrogen/generated/android/c++/JLocationProviderUsed.hpp +67 -0
  49. package/nitrogen/generated/android/c++/JLocationRequestOptions.hpp +49 -3
  50. package/nitrogen/generated/android/c++/{JGeolocationOptions.hpp → JLocationSettingsOptions.hpp} +28 -22
  51. package/nitrogen/generated/android/c++/JReverseGeocodedAddress.hpp +82 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AccuracyAuthorization.kt +24 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AndroidAccuracyPreset.kt +25 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AndroidGranularity.kt +24 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{RNConfigurationInternal.kt → CompatGeolocationConfigurationInternal.kt} +5 -5
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{GeolocationError.kt → CompatGeolocationError.kt} +5 -5
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/CompatGeolocationOptions.kt +68 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/CompatGeolocationResponse.kt +41 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_AccuracyAuthorization.kt +80 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{Func_void_GeolocationError.kt → Func_void_CompatGeolocationError.kt} +9 -9
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_CompatGeolocationResponse.kt +80 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_Heading.kt +80 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_LocationProviderStatus.kt +80 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_PermissionStatus.kt +80 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_std__vector_GeocodedLocation_.kt +80 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_std__vector_ReverseGeocodedAddress_.kt +80 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeocodedLocation.kt +44 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeocodingCoordinates.kt +41 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{ModernGeolocationConfiguration.kt → GeolocationConfiguration.kt} +5 -5
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationResponse.kt +9 -3
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Heading.kt +47 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HeadingOptions.kt +38 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationCompatSpec.kt +7 -7
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationSpec.kt +92 -3
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/IOSAccuracyPreset.kt +28 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/IOSActivityType.kt +26 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationAccuracyOptions.kt +41 -0
  78. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationAvailability.kt +41 -0
  79. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderStatus.kt +53 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderUsed.kt +26 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationRequestOptions.kt +30 -3
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{GeolocationOptions.kt → LocationSettingsOptions.kt} +11 -11
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/ReverseGeocodedAddress.kt +56 -0
  84. package/nitrogen/generated/android/nitrogeolocationOnLoad.cpp +18 -4
  85. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.cpp +76 -12
  86. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.hpp +519 -77
  87. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Umbrella.hpp +61 -12
  88. package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.hpp +28 -16
  89. package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.hpp +131 -13
  90. package/nitrogen/generated/ios/swift/AccuracyAuthorization.swift +44 -0
  91. package/nitrogen/generated/ios/swift/AndroidAccuracyPreset.swift +48 -0
  92. package/nitrogen/generated/ios/swift/AndroidGranularity.swift +44 -0
  93. package/nitrogen/generated/ios/swift/{RNConfigurationInternal.swift → CompatGeolocationConfigurationInternal.swift} +5 -5
  94. package/nitrogen/generated/ios/swift/{GeolocationError.swift → CompatGeolocationError.swift} +5 -5
  95. package/nitrogen/generated/ios/swift/CompatGeolocationOptions.swift +208 -0
  96. package/nitrogen/generated/ios/swift/CompatGeolocationResponse.swift +34 -0
  97. package/nitrogen/generated/ios/swift/Func_void_AccuracyAuthorization.swift +46 -0
  98. package/nitrogen/generated/ios/swift/Func_void_CompatGeolocationError.swift +46 -0
  99. package/nitrogen/generated/ios/swift/Func_void_CompatGeolocationResponse.swift +46 -0
  100. package/nitrogen/generated/ios/swift/{Func_void_GeolocationError.swift → Func_void_Heading.swift} +11 -11
  101. package/nitrogen/generated/ios/swift/Func_void_LocationAvailability.swift +46 -0
  102. package/nitrogen/generated/ios/swift/Func_void_LocationProviderStatus.swift +46 -0
  103. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  104. package/nitrogen/generated/ios/swift/Func_void_std__vector_GeocodedLocation_.swift +46 -0
  105. package/nitrogen/generated/ios/swift/Func_void_std__vector_ReverseGeocodedAddress_.swift +46 -0
  106. package/nitrogen/generated/ios/swift/GeocodedLocation.swift +52 -0
  107. package/nitrogen/generated/ios/swift/GeocodingCoordinates.swift +34 -0
  108. package/nitrogen/generated/ios/swift/{ModernGeolocationConfiguration.swift → GeolocationConfiguration.swift} +5 -5
  109. package/nitrogen/generated/ios/swift/GeolocationResponse.swift +31 -2
  110. package/nitrogen/generated/ios/swift/Heading.swift +70 -0
  111. package/nitrogen/generated/ios/swift/HeadingOptions.swift +42 -0
  112. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec.swift +4 -4
  113. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec_cxx.swift +28 -28
  114. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec.swift +14 -3
  115. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec_cxx.swift +318 -15
  116. package/nitrogen/generated/ios/swift/IOSAccuracyPreset.swift +60 -0
  117. package/nitrogen/generated/ios/swift/IOSActivityType.swift +52 -0
  118. package/nitrogen/generated/ios/swift/LocationAccuracyOptions.swift +46 -0
  119. package/nitrogen/generated/ios/swift/LocationAvailability.swift +47 -0
  120. package/nitrogen/generated/ios/swift/LocationProviderStatus.swift +106 -0
  121. package/nitrogen/generated/ios/swift/LocationProviderUsed.swift +52 -0
  122. package/nitrogen/generated/ios/swift/LocationRequestOptions.swift +142 -1
  123. package/nitrogen/generated/ios/swift/{GeolocationOptions.swift → LocationSettingsOptions.swift} +39 -46
  124. package/nitrogen/generated/ios/swift/ReverseGeocodedAddress.swift +150 -0
  125. package/nitrogen/generated/shared/c++/AccuracyAuthorization.hpp +80 -0
  126. package/nitrogen/generated/shared/c++/AndroidAccuracyPreset.hpp +84 -0
  127. package/nitrogen/generated/shared/c++/AndroidGranularity.hpp +80 -0
  128. package/nitrogen/generated/shared/c++/{RNConfigurationInternal.hpp → CompatGeolocationConfigurationInternal.hpp} +11 -11
  129. package/nitrogen/generated/shared/c++/{GeolocationError.hpp → CompatGeolocationError.hpp} +11 -11
  130. package/nitrogen/generated/shared/c++/CompatGeolocationOptions.hpp +128 -0
  131. package/nitrogen/generated/shared/c++/CompatGeolocationResponse.hpp +88 -0
  132. package/nitrogen/generated/shared/c++/GeocodedLocation.hpp +91 -0
  133. package/nitrogen/generated/shared/c++/GeocodingCoordinates.hpp +87 -0
  134. package/nitrogen/generated/shared/c++/{ModernGeolocationConfiguration.hpp → GeolocationConfiguration.hpp} +11 -11
  135. package/nitrogen/generated/shared/c++/GeolocationResponse.hpp +14 -2
  136. package/nitrogen/generated/shared/c++/Heading.hpp +95 -0
  137. package/nitrogen/generated/shared/c++/HeadingOptions.hpp +83 -0
  138. package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.hpp +16 -16
  139. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.cpp +11 -0
  140. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.hpp +51 -12
  141. package/nitrogen/generated/shared/c++/IOSAccuracyPreset.hpp +96 -0
  142. package/nitrogen/generated/shared/c++/IOSActivityType.hpp +88 -0
  143. package/nitrogen/generated/shared/c++/LocationAccuracyOptions.hpp +92 -0
  144. package/nitrogen/generated/shared/c++/LocationAvailability.hpp +88 -0
  145. package/nitrogen/generated/shared/c++/LocationProviderStatus.hpp +103 -0
  146. package/nitrogen/generated/shared/c++/LocationProviderUsed.hpp +88 -0
  147. package/nitrogen/generated/shared/c++/LocationRequestOptions.hpp +47 -3
  148. package/nitrogen/generated/shared/c++/{GeolocationOptions.hpp → LocationSettingsOptions.hpp} +26 -24
  149. package/nitrogen/generated/shared/c++/ReverseGeocodedAddress.hpp +108 -0
  150. package/package.json +1 -1
  151. package/src/NitroGeolocation.nitro.ts +292 -18
  152. package/src/NitroGeolocationCompat.nitro.ts +12 -12
  153. package/src/api/geocode.ts +18 -0
  154. package/src/api/getAccuracyAuthorization.ts +12 -0
  155. package/src/api/getCurrentPosition.ts +5 -3
  156. package/src/api/getHeading.ts +13 -0
  157. package/src/api/getLastKnownPosition.ts +28 -0
  158. package/src/api/getLocationAvailability.ts +11 -0
  159. package/src/api/getProviderStatus.ts +16 -0
  160. package/src/api/hasServicesEnabled.ts +13 -0
  161. package/src/api/index.ts +11 -0
  162. package/src/api/requestLocationSettings.ts +29 -0
  163. package/src/api/requestPermission.ts +3 -1
  164. package/src/api/requestTemporaryFullAccuracy.ts +21 -0
  165. package/src/api/reverseGeocode.ts +23 -0
  166. package/src/api/setConfiguration.ts +8 -4
  167. package/src/api/watchHeading.ts +19 -0
  168. package/src/api/watchPosition.ts +2 -2
  169. package/src/compat/getCurrentPosition.ts +7 -7
  170. package/src/compat/index.tsx +5 -5
  171. package/src/compat/requestAuthorization.ts +2 -2
  172. package/src/compat/setRNConfiguration.ts +7 -5
  173. package/src/compat/watchPosition.ts +7 -7
  174. package/src/devtools/getCurrentPosition.ts +5 -3
  175. package/src/devtools/index.ts +1 -1
  176. package/src/devtools/watchPosition.ts +6 -7
  177. package/src/hooks/useWatchPosition.ts +2 -2
  178. package/src/index.tsx +35 -6
  179. package/src/publicTypes.ts +108 -0
  180. package/src/types.ts +113 -37
  181. package/src/utils/errors.test.ts +65 -0
  182. package/src/utils/errors.ts +45 -18
  183. package/src/utils/index.ts +2 -2
  184. package/src/utils/provider.test.ts +172 -1
  185. package/src/utils/provider.ts +50 -5
  186. package/nitrogen/generated/android/c++/JFunc_void_GeolocationError.hpp +0 -78
@@ -3,6 +3,8 @@ package com.margelo.nitro.nitrogeolocation
3
3
  import android.Manifest
4
4
  import android.content.Context
5
5
  import android.content.pm.PackageManager
6
+ import android.location.Address
7
+ import android.location.Geocoder
6
8
  import android.location.Location
7
9
  import android.location.LocationListener
8
10
  import android.location.LocationManager as AndroidLocationManager
@@ -15,19 +17,19 @@ import androidx.core.app.ActivityCompat
15
17
  import androidx.core.content.ContextCompat
16
18
  import com.facebook.proguard.annotations.DoNotStrip
17
19
  import com.facebook.react.bridge.ReactApplicationContext
18
- import com.margelo.nitro.core.Promise
20
+ import com.google.android.gms.common.ConnectionResult
21
+ import com.google.android.gms.common.GoogleApiAvailability
22
+ import com.google.android.gms.location.LocationCallback
23
+ import com.google.android.gms.location.LocationRequest as GmsLocationRequest
24
+ import com.google.android.gms.location.LocationResult
25
+ import com.google.android.gms.location.LocationServices
19
26
  import com.margelo.nitro.NitroModules
27
+ import com.margelo.nitro.core.Promise
28
+ import java.io.IOException
29
+ import java.util.Locale
20
30
  import java.util.UUID
21
31
  import java.util.concurrent.ConcurrentHashMap
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)
32
+ import java.util.concurrent.atomic.AtomicBoolean
31
33
 
32
34
  private const val NO_LOCATION_PROVIDER_AVAILABLE_MESSAGE = "No location provider available"
33
35
  private const val NO_APPROXIMATE_LOCATION_PROVIDER_AVAILABLE_MESSAGE =
@@ -35,10 +37,10 @@ private const val NO_APPROXIMATE_LOCATION_PROVIDER_AVAILABLE_MESSAGE =
35
37
  "ACCESS_COARSE_LOCATION is granted, but no enabled coarse-compatible provider is available."
36
38
 
37
39
  /**
38
- * Modern Geolocation implementation for Android.
40
+ * Geolocation implementation for Android.
39
41
  *
40
42
  * Key features:
41
- * - Promise-based permission and getCurrentPosition
43
+ * - Callback-based native permission and getCurrentPosition for structured errors
42
44
  * - Token-based watch subscriptions (first-class functions!)
43
45
  * - WatchPositionResult discriminated union
44
46
  * - Automatic subscription management
@@ -53,10 +55,15 @@ class NitroGeolocation(
53
55
  private data class ParsedOptions(
54
56
  val timeout: Double,
55
57
  val maximumAge: Double,
56
- val enableHighAccuracy: Boolean,
58
+ val androidAccuracy: AndroidAccuracyResolution,
57
59
  val interval: Double,
58
60
  val fastestInterval: Double,
59
- val distanceFilter: Double
61
+ val distanceFilter: Double,
62
+ val granularity: AndroidGranularity,
63
+ val waitForAccurateLocation: Boolean,
64
+ val maxUpdateAge: Double?,
65
+ val maxUpdateDelay: Double,
66
+ val maxUpdates: Int?
60
67
  ) {
61
68
  companion object {
62
69
  private const val DEFAULT_TIMEOUT = 10.0 * 60 * 1000 // 10 minutes in ms
@@ -64,17 +71,42 @@ class NitroGeolocation(
64
71
  private const val DEFAULT_INTERVAL = 1000.0
65
72
  private const val DEFAULT_FASTEST_INTERVAL = 100.0
66
73
  private const val DEFAULT_DISTANCE_FILTER = 0.0
74
+ private const val DEFAULT_MAX_UPDATE_DELAY = 0.0
75
+
76
+ fun parse(
77
+ options: LocationRequestOptions?,
78
+ defaultMaximumAge: Double = DEFAULT_MAXIMUM_AGE
79
+ ): ParsedOptions {
80
+ val enableHighAccuracy = options?.enableHighAccuracy ?: false
81
+ val maxUpdates = options?.maxUpdates?.let { value ->
82
+ if (!value.isFinite()) {
83
+ 0
84
+ } else {
85
+ value.toInt()
86
+ }
87
+ }
67
88
 
68
- fun parse(options: LocationRequestOptions?): ParsedOptions {
69
89
  return ParsedOptions(
70
90
  timeout = options?.timeout ?: DEFAULT_TIMEOUT,
71
- maximumAge = options?.maximumAge ?: DEFAULT_MAXIMUM_AGE,
72
- enableHighAccuracy = options?.enableHighAccuracy ?: false,
91
+ maximumAge = options?.maximumAge ?: defaultMaximumAge,
92
+ androidAccuracy = resolveAndroidAccuracy(
93
+ options?.accuracy,
94
+ enableHighAccuracy
95
+ ),
73
96
  interval = options?.interval ?: DEFAULT_INTERVAL,
74
97
  fastestInterval = options?.fastestInterval ?: DEFAULT_FASTEST_INTERVAL,
75
- distanceFilter = options?.distanceFilter ?: DEFAULT_DISTANCE_FILTER
98
+ distanceFilter = options?.distanceFilter ?: DEFAULT_DISTANCE_FILTER,
99
+ granularity = options?.granularity ?: AndroidGranularity.PERMISSION,
100
+ waitForAccurateLocation = options?.waitForAccurateLocation ?: false,
101
+ maxUpdateAge = options?.maxUpdateAge,
102
+ maxUpdateDelay = options?.maxUpdateDelay ?: DEFAULT_MAX_UPDATE_DELAY,
103
+ maxUpdates = maxUpdates
76
104
  )
77
105
  }
106
+
107
+ fun parseLastKnown(options: LocationRequestOptions?): ParsedOptions {
108
+ return parse(options, defaultMaximumAge = Double.POSITIVE_INFINITY)
109
+ }
78
110
  }
79
111
  }
80
112
 
@@ -82,12 +114,18 @@ class NitroGeolocation(
82
114
  val token: String,
83
115
  val success: (GeolocationResponse) -> Unit,
84
116
  val error: ((LocationError) -> Unit)?,
85
- val options: ParsedOptions
117
+ val options: ParsedOptions,
118
+ var deliveredUpdates: Int = 0
86
119
  )
87
120
 
121
+ private sealed interface PositionResult {
122
+ data class Success(val position: GeolocationResponse) : PositionResult
123
+ data class Failure(val error: LocationError) : PositionResult
124
+ }
125
+
88
126
  private data class PositionRequest(
89
127
  val id: UUID,
90
- val resolver: (Result<GeolocationResponse>) -> Unit,
128
+ val resolver: (PositionResult) -> Unit,
91
129
  val options: ParsedOptions,
92
130
  val handler: Handler,
93
131
  val providers: List<String>,
@@ -102,13 +140,37 @@ class NitroGeolocation(
102
140
 
103
141
  // MARK: - Properties
104
142
 
105
- private var configuration: ModernGeolocationConfiguration? = null
143
+ private var configuration: GeolocationConfiguration? = null
106
144
  private val locationManager: AndroidLocationManager by lazy {
107
145
  reactContext.getSystemService(Context.LOCATION_SERVICE) as AndroidLocationManager
108
146
  }
147
+ private val locationSettings: AndroidLocationSettings by lazy {
148
+ AndroidLocationSettings(
149
+ reactContext = reactContext,
150
+ locationManager = locationManager,
151
+ createLocationError = ::createLocationError,
152
+ createPlayServicesUnavailableError = ::createPlayServicesUnavailableError
153
+ )
154
+ }
155
+ private val fusedLocationClient by lazy {
156
+ LocationServices.getFusedLocationProviderClient(reactContext)
157
+ }
158
+ private val headingManager: AndroidHeadingManager by lazy {
159
+ AndroidHeadingManager(
160
+ context = reactContext,
161
+ createLocationError = ::createLocationError,
162
+ getReferenceLocation = {
163
+ lastLocation ?: getBestCachedLocation(
164
+ getValidProviders(resolveAndroidAccuracy(null, enableHighAccuracy = false)),
165
+ ParsedOptions.parseLastKnown(null)
166
+ )
167
+ }
168
+ )
169
+ }
170
+ private var lastLocation: Location? = null
109
171
 
110
- // Permission promise resolvers
111
- private val pendingPermissionResolvers = mutableListOf<(Result<PermissionStatus>) -> Unit>()
172
+ // Permission callbacks
173
+ private val pendingPermissionResolvers = mutableListOf<(PermissionStatus) -> Unit>()
112
174
 
113
175
  // getCurrentPosition requests
114
176
  private val pendingPositionRequests = ConcurrentHashMap<UUID, PositionRequest>()
@@ -118,20 +180,24 @@ class NitroGeolocation(
118
180
 
119
181
  // Location listener for watch subscriptions
120
182
  private var watchLocationListener: LocationListener? = null
183
+ private var fusedWatchLocationCallback: LocationCallback? = null
121
184
  private var currentWatchProvider: String? = null
122
185
 
123
186
  // Error codes
187
+ private val INTERNAL_ERROR = -1.0
124
188
  private val PERMISSION_DENIED = 1.0
125
189
  private val POSITION_UNAVAILABLE = 2.0
126
190
  private val TIMEOUT = 3.0
191
+ private val PLAY_SERVICE_NOT_AVAILABLE = 4.0
192
+ private val SETTINGS_NOT_SATISFIED = 5.0
127
193
 
128
194
  // MARK: - Configuration
129
195
 
130
- override fun setConfiguration(config: ModernGeolocationConfiguration) {
196
+ override fun setConfiguration(config: GeolocationConfiguration) {
131
197
  this.configuration = config
132
198
  }
133
199
 
134
- // MARK: - Permission API (Promise-based)
200
+ // MARK: - Permission API
135
201
 
136
202
  override fun checkPermission(): Promise<PermissionStatus> {
137
203
  return Promise.async {
@@ -140,30 +206,29 @@ class NitroGeolocation(
140
206
  }
141
207
  }
142
208
 
143
- override fun requestPermission(): Promise<PermissionStatus> {
144
- val promise = Promise<PermissionStatus>()
145
-
209
+ override fun requestPermission(
210
+ success: (PermissionStatus) -> Unit,
211
+ error: ((LocationError) -> Unit)?
212
+ ): Unit {
146
213
  // Check if already determined
147
214
  val currentStatus = getCurrentPermissionStatus()
148
215
  if (currentStatus != PermissionStatus.UNDETERMINED) {
149
- promise.resolve(currentStatus)
150
- return promise
216
+ success(currentStatus)
217
+ return
151
218
  }
152
219
 
153
220
  // Check if we have an activity
154
221
  val activity = reactContext.currentActivity
155
222
  if (activity == null) {
156
- promise.reject(Exception("No activity available"))
157
- return promise
223
+ error?.invoke(createLocationError(
224
+ INTERNAL_ERROR,
225
+ "No activity available"
226
+ ))
227
+ return
158
228
  }
159
229
 
160
230
  // Queue resolver
161
- pendingPermissionResolvers.add { result ->
162
- result.fold(
163
- onSuccess = { promise.resolve(it) },
164
- onFailure = { promise.reject(it) }
165
- )
166
- }
231
+ pendingPermissionResolvers.add(success)
167
232
 
168
233
  // Request permission
169
234
  val permissions = arrayOf(
@@ -176,47 +241,259 @@ class NitroGeolocation(
176
241
  permissions,
177
242
  PERMISSION_REQUEST_CODE
178
243
  )
244
+ }
179
245
 
246
+ // MARK: - Provider/Settings API
247
+
248
+ override fun hasServicesEnabled(): Promise<Boolean> {
249
+ return Promise.async {
250
+ locationSettings.hasServicesEnabled()
251
+ }
252
+ }
253
+
254
+ override fun getProviderStatus(): Promise<LocationProviderStatus> {
255
+ val promise = Promise<LocationProviderStatus>()
256
+ locationSettings.getProviderStatus { status ->
257
+ promise.resolve(status)
258
+ }
259
+ return promise
260
+ }
261
+
262
+ override fun getLocationAvailability(): Promise<LocationAvailability> {
263
+ val promise = Promise<LocationAvailability>()
264
+
265
+ if (!hasLocationPermission()) {
266
+ promise.resolve(createLocationAvailability(false, "permissionDenied"))
267
+ return promise
268
+ }
269
+
270
+ if (!locationSettings.hasServicesEnabled()) {
271
+ promise.resolve(createLocationAvailability(false, "locationServicesDisabled"))
272
+ return promise
273
+ }
274
+
275
+ if (requiresPlayServices()) {
276
+ if (!isGooglePlayServicesAvailable()) {
277
+ promise.resolve(createLocationAvailability(false, "playServicesUnavailable"))
278
+ return promise
279
+ }
280
+
281
+ fusedLocationClient.locationAvailability
282
+ .addOnSuccessListener { availability ->
283
+ promise.resolve(
284
+ createLocationAvailability(
285
+ availability.isLocationAvailable,
286
+ if (availability.isLocationAvailable) null else "fusedLocationUnavailable"
287
+ )
288
+ )
289
+ }
290
+ .addOnFailureListener { exception ->
291
+ promise.resolve(createLocationAvailability(
292
+ false,
293
+ "fusedLocationUnavailable: ${exception.message ?: "unknown error"}"
294
+ ))
295
+ }
296
+ .addOnCanceledListener {
297
+ promise.resolve(createLocationAvailability(false, "fusedLocationUnavailable"))
298
+ }
299
+ return promise
300
+ }
301
+
302
+ val providers = getValidProviders(resolveAndroidAccuracy(null, enableHighAccuracy = false))
303
+ val reason = if (providers.isEmpty()) "noLocationProvider" else null
304
+ promise.resolve(createLocationAvailability(providers.isNotEmpty(), reason))
180
305
  return promise
181
306
  }
182
307
 
183
- // MARK: - Get Current Position (Promise-based)
308
+ override fun requestLocationSettings(
309
+ success: (LocationProviderStatus) -> Unit,
310
+ error: ((LocationError) -> Unit)?,
311
+ options: LocationSettingsOptions?
312
+ ) {
313
+ locationSettings.requestLocationSettings(success, error, options)
314
+ }
315
+
316
+ override fun getAccuracyAuthorization(): Promise<AccuracyAuthorization> {
317
+ return Promise.async {
318
+ getCurrentAccuracyAuthorization()
319
+ }
320
+ }
184
321
 
185
- override fun getCurrentPosition(options: LocationRequestOptions?): Promise<GeolocationResponse> {
186
- val promise = Promise<GeolocationResponse>()
322
+ override fun requestTemporaryFullAccuracy(
323
+ purposeKey: String,
324
+ success: (AccuracyAuthorization) -> Unit,
325
+ error: ((LocationError) -> Unit)?
326
+ ) {
327
+ if (purposeKey.isBlank()) {
328
+ error?.invoke(createLocationError(
329
+ INTERNAL_ERROR,
330
+ "purposeKey must not be empty."
331
+ ))
332
+ return
333
+ }
187
334
 
335
+ success(getCurrentAccuracyAuthorization())
336
+ }
337
+
338
+ // MARK: - Get Current Position
339
+
340
+ override fun getCurrentPosition(
341
+ success: (GeolocationResponse) -> Unit,
342
+ error: ((LocationError) -> Unit)?,
343
+ options: LocationRequestOptions?
344
+ ): Unit {
188
345
  // Check permission
189
346
  if (!hasLocationPermission()) {
190
- promise.reject(createLocationError(
347
+ error?.invoke(createLocationError(
191
348
  PERMISSION_DENIED,
192
349
  "Location permission not granted"
193
350
  ))
194
- return promise
351
+ return
195
352
  }
196
353
 
197
354
  val parsedOptions = ParsedOptions.parse(options)
355
+ val validationError = validateParsedOptions(parsedOptions)
356
+ if (validationError != null) {
357
+ error?.invoke(validationError)
358
+ return
359
+ }
360
+ val permissionError = validateRequestPermission(parsedOptions)
361
+ if (permissionError != null) {
362
+ error?.invoke(permissionError)
363
+ return
364
+ }
365
+ if (requiresPlayServices() && !isGooglePlayServicesAvailable()) {
366
+ error?.invoke(createPlayServicesUnavailableError())
367
+ return
368
+ }
198
369
 
199
- val providers = getValidProviders(parsedOptions.enableHighAccuracy)
370
+ if (requiresPlayServices()) {
371
+ getCurrentPositionWithFused(success, error, parsedOptions)
372
+ return
373
+ }
374
+
375
+ val providers = getValidProviders(parsedOptions)
200
376
  if (providers.isEmpty()) {
201
- promise.reject(createNoLocationProviderError(parsedOptions))
202
- return promise
377
+ error?.invoke(createNoLocationProviderError(parsedOptions))
378
+ return
203
379
  }
204
380
 
205
381
  val cachedLocation = getBestCachedLocation(providers, parsedOptions)
206
382
  if (cachedLocation != null) {
207
- promise.resolve(locationToPosition(cachedLocation))
208
- return promise
383
+ success(locationToPosition(cachedLocation))
384
+ return
209
385
  }
210
386
 
211
387
  // Request fresh location
212
388
  requestFreshLocation(providers, parsedOptions) { result ->
213
- result.fold(
214
- onSuccess = { promise.resolve(it) },
215
- onFailure = { promise.reject(it) }
216
- )
389
+ when (result) {
390
+ is PositionResult.Success -> success(result.position)
391
+ is PositionResult.Failure -> error?.invoke(result.error)
392
+ }
217
393
  }
394
+ }
218
395
 
219
- return promise
396
+ override fun getLastKnownPosition(
397
+ success: (GeolocationResponse) -> Unit,
398
+ error: ((LocationError) -> Unit)?,
399
+ options: LocationRequestOptions?
400
+ ) {
401
+ if (!hasLocationPermission()) {
402
+ error?.invoke(createLocationError(
403
+ PERMISSION_DENIED,
404
+ "Location permission not granted"
405
+ ))
406
+ return
407
+ }
408
+
409
+ val parsedOptions = ParsedOptions.parseLastKnown(options)
410
+ val validationError = validateParsedOptions(parsedOptions)
411
+ if (validationError != null) {
412
+ error?.invoke(validationError)
413
+ return
414
+ }
415
+ val permissionError = validateRequestPermission(parsedOptions)
416
+ if (permissionError != null) {
417
+ error?.invoke(permissionError)
418
+ return
419
+ }
420
+ if (requiresPlayServices() && !isGooglePlayServicesAvailable()) {
421
+ error?.invoke(createPlayServicesUnavailableError())
422
+ return
423
+ }
424
+
425
+ if (requiresPlayServices()) {
426
+ getLastKnownPositionWithFused(success, error, parsedOptions)
427
+ return
428
+ }
429
+
430
+ val providers = getValidProviders(parsedOptions)
431
+ if (providers.isEmpty()) {
432
+ error?.invoke(createNoLocationProviderError(parsedOptions))
433
+ return
434
+ }
435
+
436
+ val cachedLocation = getBestCachedLocation(providers, parsedOptions)
437
+ if (cachedLocation != null) {
438
+ success(locationToPosition(cachedLocation))
439
+ return
440
+ }
441
+
442
+ error?.invoke(createLocationError(
443
+ POSITION_UNAVAILABLE,
444
+ "No cached location available"
445
+ ))
446
+ }
447
+
448
+ // MARK: - Geocoding
449
+
450
+ override fun geocode(
451
+ address: String,
452
+ success: (Array<GeocodedLocation>) -> Unit,
453
+ error: ((LocationError) -> Unit)?
454
+ ) {
455
+ val query = address.trim()
456
+ if (query.isEmpty()) {
457
+ error?.invoke(createLocationError(
458
+ INTERNAL_ERROR,
459
+ "address must not be empty."
460
+ ))
461
+ return
462
+ }
463
+
464
+ runGeocoderOperation(success, error, "Unable to geocode address") {
465
+ val geocoder = Geocoder(reactContext, Locale.getDefault())
466
+ @Suppress("DEPRECATION")
467
+ geocoder.getFromLocationName(query, GEOCODER_MAX_RESULTS)
468
+ .orEmpty()
469
+ .mapNotNull { geocodedAddressToLocation(it) }
470
+ .toTypedArray()
471
+ }
472
+ }
473
+
474
+ override fun reverseGeocode(
475
+ coords: GeocodingCoordinates,
476
+ success: (Array<ReverseGeocodedAddress>) -> Unit,
477
+ error: ((LocationError) -> Unit)?
478
+ ) {
479
+ val validationError = validateGeocodingCoordinates(coords)
480
+ if (validationError != null) {
481
+ error?.invoke(validationError)
482
+ return
483
+ }
484
+
485
+ runGeocoderOperation(success, error, "Unable to reverse geocode coordinates") {
486
+ val geocoder = Geocoder(reactContext, Locale.getDefault())
487
+ @Suppress("DEPRECATION")
488
+ geocoder.getFromLocation(
489
+ coords.latitude,
490
+ coords.longitude,
491
+ GEOCODER_MAX_RESULTS
492
+ )
493
+ .orEmpty()
494
+ .map { addressToReverseGeocodedAddress(it) }
495
+ .toTypedArray()
496
+ }
220
497
  }
221
498
 
222
499
  // MARK: - Watch Position (Callback-based with tokens)
@@ -228,6 +505,23 @@ class NitroGeolocation(
228
505
  ): String {
229
506
  val token = UUID.randomUUID().toString()
230
507
  val parsedOptions = ParsedOptions.parse(options)
508
+ val validationError = validateParsedOptions(parsedOptions)
509
+ if (validationError != null) {
510
+ error?.invoke(validationError)
511
+ return token
512
+ }
513
+ val permissionError = if (!hasLocationPermission()) {
514
+ createLocationError(
515
+ PERMISSION_DENIED,
516
+ "Location permission not granted"
517
+ )
518
+ } else {
519
+ validateRequestPermission(parsedOptions)
520
+ }
521
+ if (permissionError != null) {
522
+ error?.invoke(permissionError)
523
+ return token
524
+ }
231
525
 
232
526
  val subscription = WatchSubscription(
233
527
  token = token,
@@ -241,22 +535,64 @@ class NitroGeolocation(
241
535
  // Start watching if first subscriber
242
536
  if (watchSubscriptions.size == 1) {
243
537
  startWatchingLocation()
538
+ } else {
539
+ restartWatchingLocation()
244
540
  }
245
541
 
246
542
  return token
247
543
  }
248
544
 
545
+ override fun getHeading(
546
+ success: (Heading) -> Unit,
547
+ error: ((LocationError) -> Unit)?
548
+ ) {
549
+ if (!hasLocationPermission()) {
550
+ error?.invoke(createLocationError(
551
+ PERMISSION_DENIED,
552
+ "Location permission not granted"
553
+ ))
554
+ return
555
+ }
556
+
557
+ headingManager.getHeading(success, error)
558
+ }
559
+
560
+ override fun watchHeading(
561
+ success: (Heading) -> Unit,
562
+ error: ((LocationError) -> Unit)?,
563
+ options: HeadingOptions?
564
+ ): String {
565
+ if (!hasLocationPermission()) {
566
+ val token = UUID.randomUUID().toString()
567
+ error?.invoke(createLocationError(
568
+ PERMISSION_DENIED,
569
+ "Location permission not granted"
570
+ ))
571
+ return token
572
+ }
573
+
574
+ return headingManager.watchHeading(success, error, options)
575
+ }
576
+
249
577
  override fun unwatch(token: String) {
250
- watchSubscriptions.remove(token)
578
+ val didRemoveLocationSubscription = watchSubscriptions.remove(token) != null
579
+ headingManager.unwatch(token)
580
+
581
+ if (!didRemoveLocationSubscription) {
582
+ return
583
+ }
251
584
 
252
585
  // Stop watching if no more subscribers
253
586
  if (watchSubscriptions.isEmpty()) {
254
587
  stopWatchingLocation()
588
+ } else {
589
+ restartWatchingLocation()
255
590
  }
256
591
  }
257
592
 
258
593
  override fun stopObserving() {
259
594
  watchSubscriptions.clear()
595
+ headingManager.stopObserving()
260
596
  stopWatchingLocation()
261
597
  }
262
598
 
@@ -306,6 +642,94 @@ class NitroGeolocation(
306
642
  ) == PackageManager.PERMISSION_GRANTED
307
643
  }
308
644
 
645
+ private fun getCurrentAccuracyAuthorization(): AccuracyAuthorization {
646
+ return when {
647
+ hasFineLocationPermission() -> AccuracyAuthorization.FULL
648
+ hasCoarseLocationPermission() -> AccuracyAuthorization.REDUCED
649
+ else -> AccuracyAuthorization.UNKNOWN
650
+ }
651
+ }
652
+
653
+ private fun validateParsedOptions(options: ParsedOptions): LocationError? {
654
+ if (!options.timeout.isFinite() || options.timeout < 0.0) {
655
+ return createLocationError(
656
+ INTERNAL_ERROR,
657
+ "timeout must be a finite number greater than or equal to 0."
658
+ )
659
+ }
660
+
661
+ if (!options.maximumAge.isFinite() && options.maximumAge != Double.POSITIVE_INFINITY) {
662
+ return createLocationError(
663
+ INTERNAL_ERROR,
664
+ "maximumAge must be a finite number greater than or equal to 0."
665
+ )
666
+ }
667
+
668
+ if (options.maximumAge < 0.0) {
669
+ return createLocationError(
670
+ INTERNAL_ERROR,
671
+ "maximumAge must be greater than or equal to 0."
672
+ )
673
+ }
674
+
675
+ if (!options.interval.isFinite() || options.interval <= 0.0) {
676
+ return createLocationError(
677
+ INTERNAL_ERROR,
678
+ "interval must be a finite number greater than 0."
679
+ )
680
+ }
681
+
682
+ if (!options.fastestInterval.isFinite() || options.fastestInterval <= 0.0) {
683
+ return createLocationError(
684
+ INTERNAL_ERROR,
685
+ "fastestInterval must be a finite number greater than 0."
686
+ )
687
+ }
688
+
689
+ if (!options.distanceFilter.isFinite() || options.distanceFilter < 0.0) {
690
+ return createLocationError(
691
+ INTERNAL_ERROR,
692
+ "distanceFilter must be a finite number greater than or equal to 0."
693
+ )
694
+ }
695
+
696
+ val maxUpdateAge = options.maxUpdateAge
697
+ if (maxUpdateAge != null && (!maxUpdateAge.isFinite() || maxUpdateAge < 0.0)) {
698
+ return createLocationError(
699
+ INTERNAL_ERROR,
700
+ "maxUpdateAge must be a finite number greater than or equal to 0."
701
+ )
702
+ }
703
+
704
+ if (!options.maxUpdateDelay.isFinite() || options.maxUpdateDelay < 0.0) {
705
+ return createLocationError(
706
+ INTERNAL_ERROR,
707
+ "maxUpdateDelay must be a finite number greater than or equal to 0."
708
+ )
709
+ }
710
+
711
+ val maxUpdates = options.maxUpdates
712
+ if (maxUpdates != null && maxUpdates < 1) {
713
+ return createLocationError(
714
+ INTERNAL_ERROR,
715
+ "maxUpdates must be greater than or equal to 1."
716
+ )
717
+ }
718
+
719
+ return null
720
+ }
721
+
722
+ private fun validateRequestPermission(options: ParsedOptions): LocationError? {
723
+ if (options.granularity == AndroidGranularity.FINE && !hasFineLocationPermission()) {
724
+ return createLocationError(
725
+ PERMISSION_DENIED,
726
+ "Fine location permission is required for granularity=fine."
727
+ )
728
+ }
729
+
730
+ return null
731
+ }
732
+
309
733
  // Handle permission request result (called from Activity)
310
734
  fun onPermissionResult(requestCode: Int, grantResults: IntArray) {
311
735
  if (requestCode != PERMISSION_REQUEST_CODE) return
@@ -315,29 +739,39 @@ class NitroGeolocation(
315
739
 
316
740
  // Resolve all pending permission requests
317
741
  for (resolver in pendingPermissionResolvers) {
318
- resolver(Result.success(status))
742
+ resolver(status)
319
743
  }
320
744
  pendingPermissionResolvers.clear()
321
745
  }
322
746
 
323
747
  // MARK: - Helper Functions - Provider Selection
324
748
 
325
- private fun getValidProvider(highAccuracy: Boolean): String? {
326
- return getValidProviders(highAccuracy).firstOrNull()
749
+ private fun requiresPlayServices(): Boolean {
750
+ // TODO: Switch auto/default Android provider selection to prefer
751
+ // Google Play Services when available.
752
+ return configuration?.locationProvider == LocationProvider.PLAYSERVICES
753
+ }
754
+
755
+ private fun isGooglePlayServicesAvailable(): Boolean {
756
+ return GoogleApiAvailability.getInstance()
757
+ .isGooglePlayServicesAvailable(reactContext) == ConnectionResult.SUCCESS
758
+ }
759
+
760
+ private fun getValidProvider(accuracy: AndroidAccuracyResolution): String? {
761
+ return getValidProviders(accuracy).firstOrNull()
327
762
  }
328
763
 
329
- private fun getValidProviders(highAccuracy: Boolean): List<String> {
330
- val preferredProvider = if (highAccuracy)
331
- AndroidLocationManager.GPS_PROVIDER
332
- else
333
- AndroidLocationManager.NETWORK_PROVIDER
764
+ private fun getValidProvider(options: ParsedOptions): String? {
765
+ return getValidProviders(options).firstOrNull()
766
+ }
334
767
 
335
- val fallbackProvider = if (highAccuracy)
336
- AndroidLocationManager.NETWORK_PROVIDER
337
- else
338
- AndroidLocationManager.GPS_PROVIDER
768
+ private fun getValidProviders(options: ParsedOptions): List<String> {
769
+ return getValidProviders(options.androidAccuracy)
770
+ .filter { provider -> options.granularity.allowsProvider(provider) }
771
+ }
339
772
 
340
- return listOf(preferredProvider, fallbackProvider)
773
+ private fun getValidProviders(accuracy: AndroidAccuracyResolution): List<String> {
774
+ return accuracy.providerOrder()
341
775
  .distinct()
342
776
  .filter { provider -> isProviderValid(provider) }
343
777
  }
@@ -349,6 +783,7 @@ class NitroGeolocation(
349
783
  when (provider) {
350
784
  AndroidLocationManager.GPS_PROVIDER -> hasFineLocationPermission()
351
785
  AndroidLocationManager.NETWORK_PROVIDER -> hasCoarseLocationPermission() || hasFineLocationPermission()
786
+ AndroidLocationManager.PASSIVE_PROVIDER -> hasLocationPermission()
352
787
  else -> hasLocationPermission()
353
788
  }
354
789
  } catch (e: Exception) {
@@ -356,16 +791,16 @@ class NitroGeolocation(
356
791
  }
357
792
  }
358
793
 
359
- private fun createNoLocationProviderError(options: ParsedOptions): Exception {
794
+ private fun createNoLocationProviderError(options: ParsedOptions): LocationError {
360
795
  return createLocationError(
361
- POSITION_UNAVAILABLE,
796
+ SETTINGS_NOT_SATISFIED,
362
797
  getNoLocationProviderMessage(options)
363
798
  )
364
799
  }
365
800
 
366
801
  private fun getNoLocationProviderMessage(options: ParsedOptions): String {
367
802
  if (
368
- !options.enableHighAccuracy &&
803
+ options.androidAccuracy.mode != AndroidAccuracyMode.HIGH &&
369
804
  hasCoarseLocationPermission() &&
370
805
  !hasFineLocationPermission()
371
806
  ) {
@@ -378,8 +813,37 @@ class NitroGeolocation(
378
813
  // MARK: - Helper Functions - Cache Validation
379
814
 
380
815
  private fun isCachedLocationValid(location: Location, options: ParsedOptions): Boolean {
816
+ val maximumAge = effectiveMaximumAge(options)
817
+ if (maximumAge <= 0.0) return false
818
+
381
819
  val locationAge = SystemClock.elapsedRealtime() - location.elapsedRealtimeNanos / 1_000_000
382
- return locationAge < options.maximumAge
820
+ if (locationAge.coerceAtLeast(0L) >= maximumAge) {
821
+ return false
822
+ }
823
+
824
+ if (options.waitForAccurateLocation && !isLocationAccurateEnough(location, options)) {
825
+ return false
826
+ }
827
+
828
+ return true
829
+ }
830
+
831
+ private fun effectiveMaximumAge(options: ParsedOptions): Double {
832
+ val maxUpdateAge = options.maxUpdateAge ?: return options.maximumAge
833
+ return minOf(options.maximumAge, maxUpdateAge)
834
+ }
835
+
836
+ private fun isLocationAccurateEnough(location: Location, options: ParsedOptions): Boolean {
837
+ if (!location.hasAccuracy()) return false
838
+
839
+ val requiredAccuracy = when (options.androidAccuracy.mode) {
840
+ AndroidAccuracyMode.HIGH -> 25f
841
+ AndroidAccuracyMode.BALANCED -> 100f
842
+ AndroidAccuracyMode.LOW -> 500f
843
+ AndroidAccuracyMode.PASSIVE -> Float.MAX_VALUE
844
+ }
845
+
846
+ return location.accuracy <= requiredAccuracy
383
847
  }
384
848
 
385
849
  private fun getBestCachedLocation(providers: List<String>, options: ParsedOptions): Location? {
@@ -395,7 +859,7 @@ class NitroGeolocation(
395
859
  if (
396
860
  lastKnownLocation != null &&
397
861
  (isCachedLocationValid(lastKnownLocation, options) ||
398
- options.maximumAge == Double.POSITIVE_INFINITY)
862
+ (options.maximumAge == Double.POSITIVE_INFINITY && options.maxUpdateAge == null))
399
863
  ) {
400
864
  bestLocation = selectBestLocation(lastKnownLocation, bestLocation)
401
865
  }
@@ -404,12 +868,139 @@ class NitroGeolocation(
404
868
  return bestLocation
405
869
  }
406
870
 
871
+ private fun getCurrentPositionWithFused(
872
+ success: (GeolocationResponse) -> Unit,
873
+ error: ((LocationError) -> Unit)?,
874
+ options: ParsedOptions
875
+ ) {
876
+ if (effectiveMaximumAge(options) > 0.0) {
877
+ getFusedCachedLocation(options) { cachedLocation ->
878
+ if (cachedLocation != null) {
879
+ success(locationToPosition(cachedLocation))
880
+ return@getFusedCachedLocation
881
+ }
882
+
883
+ requestFusedFreshLocation(success, error, options)
884
+ }
885
+ return
886
+ }
887
+
888
+ requestFusedFreshLocation(success, error, options)
889
+ }
890
+
891
+ private fun getLastKnownPositionWithFused(
892
+ success: (GeolocationResponse) -> Unit,
893
+ error: ((LocationError) -> Unit)?,
894
+ options: ParsedOptions
895
+ ) {
896
+ getFusedCachedLocation(options) { cachedLocation ->
897
+ if (cachedLocation != null) {
898
+ success(locationToPosition(cachedLocation))
899
+ return@getFusedCachedLocation
900
+ }
901
+
902
+ error?.invoke(createLocationError(
903
+ POSITION_UNAVAILABLE,
904
+ "No cached location available"
905
+ ))
906
+ }
907
+ }
908
+
909
+ private fun getFusedCachedLocation(
910
+ options: ParsedOptions,
911
+ completion: (Location?) -> Unit
912
+ ) {
913
+ // Fused lastLocation is not requested with LocationRequest granularity,
914
+ // so it cannot prove that a cached fix satisfies coarse-only callers.
915
+ if (options.granularity == AndroidGranularity.COARSE) {
916
+ completion(null)
917
+ return
918
+ }
919
+
920
+ try {
921
+ fusedLocationClient.lastLocation
922
+ .addOnSuccessListener { location ->
923
+ completion(location?.takeIf { isCachedLocationValid(it, options) })
924
+ }
925
+ .addOnFailureListener {
926
+ completion(null)
927
+ }
928
+ .addOnCanceledListener {
929
+ completion(null)
930
+ }
931
+ } catch (e: SecurityException) {
932
+ completion(null)
933
+ }
934
+ }
935
+
936
+ private fun requestFusedFreshLocation(
937
+ success: (GeolocationResponse) -> Unit,
938
+ error: ((LocationError) -> Unit)?,
939
+ options: ParsedOptions
940
+ ) {
941
+ val handler = Handler(Looper.getMainLooper())
942
+ val didComplete = AtomicBoolean(false)
943
+ lateinit var callback: LocationCallback
944
+
945
+ fun complete(result: PositionResult) {
946
+ if (!didComplete.compareAndSet(false, true)) return
947
+
948
+ handler.removeCallbacksAndMessages(null)
949
+ try {
950
+ fusedLocationClient.removeLocationUpdates(callback)
951
+ } catch (_: Exception) {
952
+ // Ignore cleanup races.
953
+ }
954
+
955
+ when (result) {
956
+ is PositionResult.Success -> success(result.position)
957
+ is PositionResult.Failure -> error?.invoke(result.error)
958
+ }
959
+ }
960
+
961
+ callback = object : LocationCallback() {
962
+ override fun onLocationResult(result: LocationResult) {
963
+ val location = result.lastLocation
964
+ if (location != null) {
965
+ complete(PositionResult.Success(locationToPosition(location)))
966
+ }
967
+ }
968
+ }
969
+
970
+ val timeoutRunnable = Runnable {
971
+ complete(PositionResult.Failure(createPositionTimeoutError(options)))
972
+ }
973
+
974
+ try {
975
+ fusedLocationClient.requestLocationUpdates(
976
+ buildFusedLocationRequest(
977
+ options,
978
+ maxUpdatesOverride = 1,
979
+ includeDistanceFilter = false
980
+ ),
981
+ callback,
982
+ Looper.getMainLooper()
983
+ )
984
+ handler.postDelayed(timeoutRunnable, coerceTimeoutMillis(options.timeout))
985
+ } catch (e: SecurityException) {
986
+ complete(PositionResult.Failure(createLocationError(
987
+ PERMISSION_DENIED,
988
+ "Security exception: ${e.message}"
989
+ )))
990
+ } catch (e: Exception) {
991
+ complete(PositionResult.Failure(createLocationError(
992
+ POSITION_UNAVAILABLE,
993
+ "Unable to request fused location: ${e.message}"
994
+ )))
995
+ }
996
+ }
997
+
407
998
  // MARK: - Helper Functions - Request Fresh Location
408
999
 
409
1000
  private fun requestFreshLocation(
410
1001
  providers: List<String>,
411
1002
  options: ParsedOptions,
412
- resolver: (Result<GeolocationResponse>) -> Unit
1003
+ resolver: (PositionResult) -> Unit
413
1004
  ) {
414
1005
  val id = UUID.randomUUID()
415
1006
  val handler = Handler(Looper.getMainLooper())
@@ -433,21 +1024,22 @@ class NitroGeolocation(
433
1024
  val remainingTimeoutMillis = request.remainingTimeoutMillis()
434
1025
 
435
1026
  if (provider == null) {
436
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
437
- createNoLocationProviderError(request.options)
438
- ))
1027
+ pendingPositionRequests.remove(requestId)?.resolver(
1028
+ PositionResult.Failure(createNoLocationProviderError(request.options))
1029
+ )
439
1030
  return
440
1031
  }
441
1032
 
442
1033
  if (remainingTimeoutMillis <= 0L) {
443
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
444
- createPositionTimeoutError(request.options)
445
- ))
1034
+ pendingPositionRequests.remove(requestId)?.resolver(
1035
+ PositionResult.Failure(createPositionTimeoutError(request.options))
1036
+ )
446
1037
  return
447
1038
  }
448
1039
 
449
- // Use modern API on Android 11+
450
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1040
+ // Android's getCurrentLocation may resolve a recent historical fix. A maximumAge of 0
1041
+ // means callers explicitly asked us to wait for a fresh provider update.
1042
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && request.options.maximumAge > 0.0) {
451
1043
  requestCurrentLocationModern(provider, requestId, request.handler, remainingTimeoutMillis)
452
1044
  } else {
453
1045
  requestCurrentLocationLegacy(provider, requestId, request.handler, remainingTimeoutMillis)
@@ -481,7 +1073,7 @@ class NitroGeolocation(
481
1073
  if (location != null) {
482
1074
  pendingPositionRequests.remove(requestId)
483
1075
  val position = locationToPosition(location)
484
- request.resolver(Result.success(position))
1076
+ request.resolver(PositionResult.Success(position))
485
1077
  } else {
486
1078
  handleProviderFailure(requestId, createLocationError(
487
1079
  POSITION_UNAVAILABLE,
@@ -531,7 +1123,7 @@ class NitroGeolocation(
531
1123
  val request = pendingPositionRequests.remove(requestId)
532
1124
  if (request != null) {
533
1125
  val position = locationToPosition(location)
534
- request.resolver(Result.success(position))
1126
+ request.resolver(PositionResult.Success(position))
535
1127
  }
536
1128
  }
537
1129
  oldLocation = location
@@ -579,7 +1171,7 @@ class NitroGeolocation(
579
1171
  }
580
1172
  }
581
1173
 
582
- private fun handleProviderFailure(requestId: UUID, error: Exception) {
1174
+ private fun handleProviderFailure(requestId: UUID, error: LocationError) {
583
1175
  val request = pendingPositionRequests[requestId] ?: return
584
1176
 
585
1177
  request.cancellationSignal?.cancel()
@@ -588,9 +1180,9 @@ class NitroGeolocation(
588
1180
 
589
1181
  if (request.providerIndex < request.providers.size) {
590
1182
  if (request.remainingTimeoutMillis() <= 0L) {
591
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
592
- createPositionTimeoutError(request.options)
593
- ))
1183
+ pendingPositionRequests.remove(requestId)?.resolver(
1184
+ PositionResult.Failure(createPositionTimeoutError(request.options))
1185
+ )
594
1186
  return
595
1187
  }
596
1188
 
@@ -598,7 +1190,7 @@ class NitroGeolocation(
598
1190
  return
599
1191
  }
600
1192
 
601
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(error))
1193
+ pendingPositionRequests.remove(requestId)?.resolver(PositionResult.Failure(error))
602
1194
  }
603
1195
 
604
1196
  private fun selectBestLocation(newLocation: Location, currentBest: Location?): Location {
@@ -633,29 +1225,27 @@ class NitroGeolocation(
633
1225
  request.cancellationSignal?.cancel()
634
1226
  request.cancellationSignal = null
635
1227
 
636
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
637
- createPositionTimeoutError(request.options)
638
- ))
1228
+ pendingPositionRequests.remove(requestId)?.resolver(
1229
+ PositionResult.Failure(createPositionTimeoutError(request.options))
1230
+ )
639
1231
  }
640
1232
  }
641
1233
 
642
1234
  // MARK: - Helper Functions - Watch Position
643
1235
 
644
1236
  private fun startWatchingLocation() {
645
- // Determine best provider and options from all subscriptions
646
- var useHighAccuracy = false
647
- var smallestInterval = Double.MAX_VALUE
648
- var smallestDistanceFilter = Float.MAX_VALUE
1237
+ if (requiresPlayServices() && !isGooglePlayServicesAvailable()) {
1238
+ notifyWatchPlayServicesUnavailable()
1239
+ return
1240
+ }
649
1241
 
650
- for ((_, subscription) in watchSubscriptions) {
651
- if (subscription.options.enableHighAccuracy) {
652
- useHighAccuracy = true
653
- }
654
- smallestInterval = minOf(smallestInterval, subscription.options.interval)
655
- smallestDistanceFilter = minOf(smallestDistanceFilter, subscription.options.distanceFilter.toFloat())
1242
+ if (requiresPlayServices()) {
1243
+ startWatchingFusedLocation()
1244
+ return
656
1245
  }
657
1246
 
658
- val provider = getValidProvider(useHighAccuracy)
1247
+ val mergedOptions = mergeWatchOptions()
1248
+ val provider = getValidProvider(mergedOptions)
659
1249
  if (provider == null) {
660
1250
  notifyWatchProviderUnavailable()
661
1251
  return
@@ -665,16 +1255,12 @@ class NitroGeolocation(
665
1255
  val listener = object : LocationListener {
666
1256
  override fun onLocationChanged(location: Location) {
667
1257
  val position = locationToPosition(location)
668
-
669
- // Notify all subscribers
670
- for ((_, subscription) in watchSubscriptions) {
671
- subscription.success(position)
672
- }
1258
+ deliverWatchPosition(position)
673
1259
  }
674
1260
 
675
1261
  override fun onProviderDisabled(provider: String) {
676
1262
  val error = LocationError(
677
- code = POSITION_UNAVAILABLE,
1263
+ code = SETTINGS_NOT_SATISFIED,
678
1264
  message = "Provider disabled: $provider"
679
1265
  )
680
1266
 
@@ -693,8 +1279,8 @@ class NitroGeolocation(
693
1279
  try {
694
1280
  locationManager.requestLocationUpdates(
695
1281
  provider,
696
- smallestInterval.toLong(),
697
- smallestDistanceFilter,
1282
+ mergedOptions.interval.toLong(),
1283
+ mergedOptions.distanceFilter.toFloat(),
698
1284
  listener,
699
1285
  Looper.getMainLooper()
700
1286
  )
@@ -710,15 +1296,157 @@ class NitroGeolocation(
710
1296
  }
711
1297
  }
712
1298
 
1299
+ private fun startWatchingFusedLocation() {
1300
+ val mergedOptions = mergeWatchOptions()
1301
+ val callback = object : LocationCallback() {
1302
+ override fun onLocationResult(result: LocationResult) {
1303
+ val location = result.lastLocation ?: return
1304
+ deliverWatchPosition(locationToPosition(location))
1305
+ }
1306
+ }
1307
+
1308
+ fusedWatchLocationCallback = callback
1309
+
1310
+ try {
1311
+ fusedLocationClient.requestLocationUpdates(
1312
+ buildFusedLocationRequest(mergedOptions),
1313
+ callback,
1314
+ Looper.getMainLooper()
1315
+ )
1316
+ } catch (e: SecurityException) {
1317
+ val error = LocationError(
1318
+ code = PERMISSION_DENIED,
1319
+ message = "Permission denied: ${e.message}"
1320
+ )
1321
+
1322
+ for ((_, subscription) in watchSubscriptions) {
1323
+ subscription.error?.invoke(error)
1324
+ }
1325
+ } catch (e: Exception) {
1326
+ val error = LocationError(
1327
+ code = POSITION_UNAVAILABLE,
1328
+ message = "Unable to request fused location updates: ${e.message}"
1329
+ )
1330
+
1331
+ for ((_, subscription) in watchSubscriptions) {
1332
+ subscription.error?.invoke(error)
1333
+ }
1334
+ }
1335
+ }
1336
+
1337
+ private fun mergeWatchOptions(): ParsedOptions {
1338
+ var androidAccuracy: AndroidAccuracyResolution? = null
1339
+ var smallestInterval = Double.MAX_VALUE
1340
+ var smallestFastestInterval = Double.MAX_VALUE
1341
+ var smallestDistanceFilter = Double.MAX_VALUE
1342
+ var granularity = AndroidGranularity.PERMISSION
1343
+ var waitForAccurateLocation = false
1344
+ var maxUpdateAge: Double? = null
1345
+ var smallestMaxUpdateDelay = Double.MAX_VALUE
1346
+
1347
+ for ((_, subscription) in watchSubscriptions) {
1348
+ androidAccuracy = mostDemandingAndroidAccuracy(
1349
+ androidAccuracy,
1350
+ subscription.options.androidAccuracy
1351
+ )
1352
+ smallestInterval = minOf(smallestInterval, subscription.options.interval)
1353
+ smallestFastestInterval = minOf(
1354
+ smallestFastestInterval,
1355
+ subscription.options.fastestInterval
1356
+ )
1357
+ smallestDistanceFilter = minOf(
1358
+ smallestDistanceFilter,
1359
+ subscription.options.distanceFilter
1360
+ )
1361
+ granularity = mergeWatchGranularity(granularity, subscription.options.granularity)
1362
+ waitForAccurateLocation = waitForAccurateLocation ||
1363
+ subscription.options.waitForAccurateLocation
1364
+ maxUpdateAge = mergeNullableMinimum(maxUpdateAge, subscription.options.maxUpdateAge)
1365
+ smallestMaxUpdateDelay = minOf(
1366
+ smallestMaxUpdateDelay,
1367
+ subscription.options.maxUpdateDelay
1368
+ )
1369
+ }
1370
+
1371
+ return ParsedOptions(
1372
+ timeout = Double.POSITIVE_INFINITY,
1373
+ maximumAge = 0.0,
1374
+ androidAccuracy = androidAccuracy ?: resolveAndroidAccuracy(null, enableHighAccuracy = false),
1375
+ interval = smallestInterval,
1376
+ fastestInterval = smallestFastestInterval,
1377
+ distanceFilter = smallestDistanceFilter,
1378
+ granularity = granularity,
1379
+ waitForAccurateLocation = waitForAccurateLocation,
1380
+ maxUpdateAge = maxUpdateAge,
1381
+ maxUpdateDelay = if (smallestMaxUpdateDelay == Double.MAX_VALUE) 0.0 else smallestMaxUpdateDelay,
1382
+ maxUpdates = null
1383
+ )
1384
+ }
1385
+
1386
+ private fun deliverWatchPosition(position: GeolocationResponse) {
1387
+ val tokensToRemove = mutableListOf<String>()
1388
+
1389
+ for ((token, subscription) in watchSubscriptions) {
1390
+ subscription.success(position)
1391
+ subscription.deliveredUpdates += 1
1392
+
1393
+ val maxUpdates = subscription.options.maxUpdates
1394
+ if (maxUpdates != null && subscription.deliveredUpdates >= maxUpdates) {
1395
+ tokensToRemove.add(token)
1396
+ }
1397
+ }
1398
+
1399
+ for (token in tokensToRemove) {
1400
+ watchSubscriptions.remove(token)
1401
+ }
1402
+
1403
+ if (tokensToRemove.isNotEmpty()) {
1404
+ if (watchSubscriptions.isEmpty()) {
1405
+ stopWatchingLocation()
1406
+ } else {
1407
+ restartWatchingLocation()
1408
+ }
1409
+ }
1410
+ }
1411
+
713
1412
  private fun notifyWatchProviderUnavailable() {
714
1413
  for ((_, subscription) in watchSubscriptions) {
715
1414
  subscription.error?.invoke(LocationError(
716
- code = POSITION_UNAVAILABLE,
1415
+ code = SETTINGS_NOT_SATISFIED,
717
1416
  message = getNoLocationProviderMessage(subscription.options)
718
1417
  ))
719
1418
  }
720
1419
  }
721
1420
 
1421
+ private fun notifyWatchPlayServicesUnavailable() {
1422
+ for ((_, subscription) in watchSubscriptions) {
1423
+ subscription.error?.invoke(createPlayServicesUnavailableError())
1424
+ }
1425
+ }
1426
+
1427
+ private fun mergeWatchGranularity(
1428
+ current: AndroidGranularity,
1429
+ next: AndroidGranularity
1430
+ ): AndroidGranularity {
1431
+ return when {
1432
+ current == AndroidGranularity.COARSE || next == AndroidGranularity.COARSE -> {
1433
+ AndroidGranularity.COARSE
1434
+ }
1435
+ current == AndroidGranularity.FINE || next == AndroidGranularity.FINE -> {
1436
+ AndroidGranularity.FINE
1437
+ }
1438
+ else -> AndroidGranularity.PERMISSION
1439
+ }
1440
+ }
1441
+
1442
+ private fun mergeNullableMinimum(current: Double?, next: Double?): Double? {
1443
+ return when {
1444
+ current == null -> next
1445
+ next == null -> current
1446
+ else -> minOf(current, next)
1447
+ }
1448
+ }
1449
+
722
1450
  private fun stopWatchingLocation() {
723
1451
  watchLocationListener?.let { listener ->
724
1452
  try {
@@ -727,59 +1455,169 @@ class NitroGeolocation(
727
1455
  // Ignore
728
1456
  }
729
1457
  }
1458
+ fusedWatchLocationCallback?.let { callback ->
1459
+ try {
1460
+ fusedLocationClient.removeLocationUpdates(callback)
1461
+ } catch (e: Exception) {
1462
+ // Ignore
1463
+ }
1464
+ }
730
1465
  watchLocationListener = null
1466
+ fusedWatchLocationCallback = null
731
1467
  currentWatchProvider = null
732
1468
  }
733
1469
 
1470
+ private fun restartWatchingLocation() {
1471
+ stopWatchingLocation()
1472
+ startWatchingLocation()
1473
+ }
1474
+
734
1475
  // MARK: - Helper Functions - Conversion
735
1476
 
736
1477
  private fun locationToPosition(location: Location): GeolocationResponse {
737
- val altitude = if (location.hasAltitude()) {
738
- NullableDouble.create(location.altitude)
739
- } else {
740
- null
741
- }
742
- val altitudeAccuracy = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && location.hasVerticalAccuracy()) {
743
- NullableDouble.create(location.verticalAccuracyMeters.toDouble())
744
- } else {
745
- null
746
- }
747
- val heading = if (location.hasBearing()) {
748
- NullableDouble.create(location.bearing.toDouble())
749
- } else {
750
- null
751
- }
752
- val speed = if (location.hasSpeed()) {
753
- NullableDouble.create(location.speed.toDouble())
754
- } else {
755
- null
756
- }
1478
+ lastLocation = location
757
1479
 
758
1480
  val coords = GeolocationCoordinates(
759
1481
  latitude = location.latitude,
760
1482
  longitude = location.longitude,
761
- altitude = altitude,
1483
+ altitude = location.altitudeValue(),
762
1484
  accuracy = location.accuracy.toDouble(),
763
- altitudeAccuracy = altitudeAccuracy,
764
- heading = heading,
765
- speed = speed
1485
+ altitudeAccuracy = location.altitudeAccuracyValue(),
1486
+ heading = location.headingValue(),
1487
+ speed = location.speedValue()
766
1488
  )
767
1489
 
768
1490
  return GeolocationResponse(
769
1491
  coords = coords,
770
- timestamp = location.time.toDouble()
1492
+ timestamp = location.time.toDouble(),
1493
+ mocked = location.isMocked(),
1494
+ provider = location.providerUsed()
771
1495
  )
772
1496
  }
773
1497
 
774
- private fun createLocationError(code: Double, message: String): Exception {
775
- val locationError = LocationError(
1498
+ private fun geocodedAddressToLocation(address: Address): GeocodedLocation? {
1499
+ if (!address.hasLatitude() || !address.hasLongitude()) {
1500
+ return null
1501
+ }
1502
+
1503
+ return GeocodedLocation(
1504
+ latitude = address.latitude,
1505
+ longitude = address.longitude,
1506
+ accuracy = null
1507
+ )
1508
+ }
1509
+
1510
+ private fun addressToReverseGeocodedAddress(address: Address): ReverseGeocodedAddress {
1511
+ return ReverseGeocodedAddress(
1512
+ country = address.countryName.nonBlankOrNull(),
1513
+ region = address.adminArea.nonBlankOrNull(),
1514
+ city = (address.locality ?: address.subAdminArea).nonBlankOrNull(),
1515
+ district = address.subLocality.nonBlankOrNull(),
1516
+ street = formatStreet(address),
1517
+ postalCode = address.postalCode.nonBlankOrNull(),
1518
+ formattedAddress = formatAddressLines(address)
1519
+ )
1520
+ }
1521
+
1522
+ private fun formatStreet(address: Address): String? {
1523
+ return listOf(address.subThoroughfare, address.thoroughfare)
1524
+ .mapNotNull { it.nonBlankOrNull() }
1525
+ .joinToString(" ")
1526
+ .nonBlankOrNull()
1527
+ }
1528
+
1529
+ private fun formatAddressLines(address: Address): String? {
1530
+ if (address.maxAddressLineIndex < 0) {
1531
+ return null
1532
+ }
1533
+
1534
+ return (0..address.maxAddressLineIndex)
1535
+ .mapNotNull { index -> address.getAddressLine(index).nonBlankOrNull() }
1536
+ .joinToString(", ")
1537
+ .nonBlankOrNull()
1538
+ }
1539
+
1540
+ private fun validateGeocodingCoordinates(coords: GeocodingCoordinates): LocationError? {
1541
+ if (!coords.latitude.isFinite() || coords.latitude < -90.0 || coords.latitude > 90.0) {
1542
+ return createLocationError(
1543
+ INTERNAL_ERROR,
1544
+ "latitude must be a finite number between -90 and 90."
1545
+ )
1546
+ }
1547
+
1548
+ if (!coords.longitude.isFinite() || coords.longitude < -180.0 || coords.longitude > 180.0) {
1549
+ return createLocationError(
1550
+ INTERNAL_ERROR,
1551
+ "longitude must be a finite number between -180 and 180."
1552
+ )
1553
+ }
1554
+
1555
+ return null
1556
+ }
1557
+
1558
+ private fun <T> runGeocoderOperation(
1559
+ success: (Array<T>) -> Unit,
1560
+ error: ((LocationError) -> Unit)?,
1561
+ failurePrefix: String,
1562
+ operation: () -> Array<T>
1563
+ ) {
1564
+ if (!Geocoder.isPresent()) {
1565
+ error?.invoke(createLocationError(
1566
+ POSITION_UNAVAILABLE,
1567
+ "Platform geocoder is not available."
1568
+ ))
1569
+ return
1570
+ }
1571
+
1572
+ val handler = Handler(Looper.getMainLooper())
1573
+
1574
+ Thread {
1575
+ try {
1576
+ val results = operation()
1577
+ handler.post { success(results) }
1578
+ } catch (e: IOException) {
1579
+ handler.post {
1580
+ error?.invoke(createLocationError(
1581
+ POSITION_UNAVAILABLE,
1582
+ "$failurePrefix: ${e.message ?: "geocoder service unavailable"}"
1583
+ ))
1584
+ }
1585
+ } catch (e: Exception) {
1586
+ handler.post {
1587
+ error?.invoke(createLocationError(
1588
+ INTERNAL_ERROR,
1589
+ "$failurePrefix: ${e.message ?: "unknown error"}"
1590
+ ))
1591
+ }
1592
+ }
1593
+ }.start()
1594
+ }
1595
+
1596
+ private fun createLocationAvailability(
1597
+ available: Boolean,
1598
+ reason: String?
1599
+ ): LocationAvailability {
1600
+ return LocationAvailability(
1601
+ available = available,
1602
+ reason = reason
1603
+ )
1604
+ }
1605
+
1606
+ private fun createLocationError(code: Double, message: String): LocationError {
1607
+ return LocationError(
776
1608
  code = code,
777
1609
  message = message
778
1610
  )
779
- return GeolocationErrorException(locationError)
780
1611
  }
781
1612
 
782
- private fun createPositionTimeoutError(options: ParsedOptions): Exception {
1613
+ private fun createPlayServicesUnavailableError(): LocationError {
1614
+ return createLocationError(
1615
+ PLAY_SERVICE_NOT_AVAILABLE,
1616
+ "Google Play Services location provider is not available."
1617
+ )
1618
+ }
1619
+
1620
+ private fun createPositionTimeoutError(options: ParsedOptions): LocationError {
783
1621
  val timeoutSeconds = options.timeout / 1000.0
784
1622
  val message = String.format("Unable to fetch location within %.1fs.", timeoutSeconds)
785
1623
  return createLocationError(TIMEOUT, message)
@@ -805,8 +1643,57 @@ class NitroGeolocation(
805
1643
  }
806
1644
  }
807
1645
 
1646
+ private fun buildFusedLocationRequest(
1647
+ options: ParsedOptions,
1648
+ maxUpdatesOverride: Int? = null,
1649
+ includeDistanceFilter: Boolean = true
1650
+ ): GmsLocationRequest {
1651
+ val builder = GmsLocationRequest
1652
+ .Builder(options.androidAccuracy.gmsPriority(), coercePositiveMillis(options.interval))
1653
+ .setMinUpdateIntervalMillis(coercePositiveMillis(options.fastestInterval))
1654
+ .setGranularity(options.granularity.gmsGranularity())
1655
+ .setWaitForAccurateLocation(options.waitForAccurateLocation)
1656
+ .setMaxUpdateDelayMillis(coerceNonNegativeMillis(options.maxUpdateDelay))
1657
+
1658
+ if (includeDistanceFilter) {
1659
+ builder.setMinUpdateDistanceMeters(options.distanceFilter.toFloat())
1660
+ }
1661
+
1662
+ options.maxUpdateAge?.let { value ->
1663
+ builder.setMaxUpdateAgeMillis(coerceNonNegativeMillis(value))
1664
+ }
1665
+
1666
+ val maxUpdates = maxUpdatesOverride ?: options.maxUpdates
1667
+ if (maxUpdates != null) {
1668
+ builder.setMaxUpdates(maxUpdates)
1669
+ }
1670
+
1671
+ return builder.build()
1672
+ }
1673
+
1674
+ private fun coercePositiveMillis(value: Double): Long {
1675
+ return when {
1676
+ value.isNaN() || value <= 0.0 -> 1L
1677
+ value.isInfinite() || value >= Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
1678
+ else -> value.toLong()
1679
+ }
1680
+ }
1681
+
1682
+ private fun coerceNonNegativeMillis(value: Double): Long {
1683
+ return when {
1684
+ value.isNaN() || value <= 0.0 -> 0L
1685
+ value.isInfinite() || value >= Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
1686
+ else -> value.toLong()
1687
+ }
1688
+ }
1689
+
808
1690
  companion object {
809
1691
  private const val PERMISSION_REQUEST_CODE = 8947
1692
+ private const val GEOCODER_MAX_RESULTS = 5
810
1693
  private const val TWO_MINUTES_MS = 2 * 60 * 1000L
811
1694
  }
812
1695
  }
1696
+
1697
+ private fun String?.nonBlankOrNull(): String? {
1698
+ return this?.trim()?.takeIf { it.isNotEmpty() }
1699
+ }