react-native-nitro-geolocation 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +316 -0
  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 +1025 -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 +291 -17
  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 +96 -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,37 @@ 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
+ return configuration?.locationProvider == LocationProvider.PLAYSERVICES
751
+ }
752
+
753
+ private fun isGooglePlayServicesAvailable(): Boolean {
754
+ return GoogleApiAvailability.getInstance()
755
+ .isGooglePlayServicesAvailable(reactContext) == ConnectionResult.SUCCESS
756
+ }
757
+
758
+ private fun getValidProvider(accuracy: AndroidAccuracyResolution): String? {
759
+ return getValidProviders(accuracy).firstOrNull()
327
760
  }
328
761
 
329
- private fun getValidProviders(highAccuracy: Boolean): List<String> {
330
- val preferredProvider = if (highAccuracy)
331
- AndroidLocationManager.GPS_PROVIDER
332
- else
333
- AndroidLocationManager.NETWORK_PROVIDER
762
+ private fun getValidProvider(options: ParsedOptions): String? {
763
+ return getValidProviders(options).firstOrNull()
764
+ }
334
765
 
335
- val fallbackProvider = if (highAccuracy)
336
- AndroidLocationManager.NETWORK_PROVIDER
337
- else
338
- AndroidLocationManager.GPS_PROVIDER
766
+ private fun getValidProviders(options: ParsedOptions): List<String> {
767
+ return getValidProviders(options.androidAccuracy)
768
+ .filter { provider -> options.granularity.allowsProvider(provider) }
769
+ }
339
770
 
340
- return listOf(preferredProvider, fallbackProvider)
771
+ private fun getValidProviders(accuracy: AndroidAccuracyResolution): List<String> {
772
+ return accuracy.providerOrder()
341
773
  .distinct()
342
774
  .filter { provider -> isProviderValid(provider) }
343
775
  }
@@ -349,6 +781,7 @@ class NitroGeolocation(
349
781
  when (provider) {
350
782
  AndroidLocationManager.GPS_PROVIDER -> hasFineLocationPermission()
351
783
  AndroidLocationManager.NETWORK_PROVIDER -> hasCoarseLocationPermission() || hasFineLocationPermission()
784
+ AndroidLocationManager.PASSIVE_PROVIDER -> hasLocationPermission()
352
785
  else -> hasLocationPermission()
353
786
  }
354
787
  } catch (e: Exception) {
@@ -356,16 +789,16 @@ class NitroGeolocation(
356
789
  }
357
790
  }
358
791
 
359
- private fun createNoLocationProviderError(options: ParsedOptions): Exception {
792
+ private fun createNoLocationProviderError(options: ParsedOptions): LocationError {
360
793
  return createLocationError(
361
- POSITION_UNAVAILABLE,
794
+ SETTINGS_NOT_SATISFIED,
362
795
  getNoLocationProviderMessage(options)
363
796
  )
364
797
  }
365
798
 
366
799
  private fun getNoLocationProviderMessage(options: ParsedOptions): String {
367
800
  if (
368
- !options.enableHighAccuracy &&
801
+ options.androidAccuracy.mode != AndroidAccuracyMode.HIGH &&
369
802
  hasCoarseLocationPermission() &&
370
803
  !hasFineLocationPermission()
371
804
  ) {
@@ -378,8 +811,37 @@ class NitroGeolocation(
378
811
  // MARK: - Helper Functions - Cache Validation
379
812
 
380
813
  private fun isCachedLocationValid(location: Location, options: ParsedOptions): Boolean {
814
+ val maximumAge = effectiveMaximumAge(options)
815
+ if (maximumAge <= 0.0) return false
816
+
381
817
  val locationAge = SystemClock.elapsedRealtime() - location.elapsedRealtimeNanos / 1_000_000
382
- return locationAge < options.maximumAge
818
+ if (locationAge.coerceAtLeast(0L) >= maximumAge) {
819
+ return false
820
+ }
821
+
822
+ if (options.waitForAccurateLocation && !isLocationAccurateEnough(location, options)) {
823
+ return false
824
+ }
825
+
826
+ return true
827
+ }
828
+
829
+ private fun effectiveMaximumAge(options: ParsedOptions): Double {
830
+ val maxUpdateAge = options.maxUpdateAge ?: return options.maximumAge
831
+ return minOf(options.maximumAge, maxUpdateAge)
832
+ }
833
+
834
+ private fun isLocationAccurateEnough(location: Location, options: ParsedOptions): Boolean {
835
+ if (!location.hasAccuracy()) return false
836
+
837
+ val requiredAccuracy = when (options.androidAccuracy.mode) {
838
+ AndroidAccuracyMode.HIGH -> 25f
839
+ AndroidAccuracyMode.BALANCED -> 100f
840
+ AndroidAccuracyMode.LOW -> 500f
841
+ AndroidAccuracyMode.PASSIVE -> Float.MAX_VALUE
842
+ }
843
+
844
+ return location.accuracy <= requiredAccuracy
383
845
  }
384
846
 
385
847
  private fun getBestCachedLocation(providers: List<String>, options: ParsedOptions): Location? {
@@ -395,7 +857,7 @@ class NitroGeolocation(
395
857
  if (
396
858
  lastKnownLocation != null &&
397
859
  (isCachedLocationValid(lastKnownLocation, options) ||
398
- options.maximumAge == Double.POSITIVE_INFINITY)
860
+ (options.maximumAge == Double.POSITIVE_INFINITY && options.maxUpdateAge == null))
399
861
  ) {
400
862
  bestLocation = selectBestLocation(lastKnownLocation, bestLocation)
401
863
  }
@@ -404,12 +866,139 @@ class NitroGeolocation(
404
866
  return bestLocation
405
867
  }
406
868
 
869
+ private fun getCurrentPositionWithFused(
870
+ success: (GeolocationResponse) -> Unit,
871
+ error: ((LocationError) -> Unit)?,
872
+ options: ParsedOptions
873
+ ) {
874
+ if (effectiveMaximumAge(options) > 0.0) {
875
+ getFusedCachedLocation(options) { cachedLocation ->
876
+ if (cachedLocation != null) {
877
+ success(locationToPosition(cachedLocation))
878
+ return@getFusedCachedLocation
879
+ }
880
+
881
+ requestFusedFreshLocation(success, error, options)
882
+ }
883
+ return
884
+ }
885
+
886
+ requestFusedFreshLocation(success, error, options)
887
+ }
888
+
889
+ private fun getLastKnownPositionWithFused(
890
+ success: (GeolocationResponse) -> Unit,
891
+ error: ((LocationError) -> Unit)?,
892
+ options: ParsedOptions
893
+ ) {
894
+ getFusedCachedLocation(options) { cachedLocation ->
895
+ if (cachedLocation != null) {
896
+ success(locationToPosition(cachedLocation))
897
+ return@getFusedCachedLocation
898
+ }
899
+
900
+ error?.invoke(createLocationError(
901
+ POSITION_UNAVAILABLE,
902
+ "No cached location available"
903
+ ))
904
+ }
905
+ }
906
+
907
+ private fun getFusedCachedLocation(
908
+ options: ParsedOptions,
909
+ completion: (Location?) -> Unit
910
+ ) {
911
+ // Fused lastLocation is not requested with LocationRequest granularity,
912
+ // so it cannot prove that a cached fix satisfies coarse-only callers.
913
+ if (options.granularity == AndroidGranularity.COARSE) {
914
+ completion(null)
915
+ return
916
+ }
917
+
918
+ try {
919
+ fusedLocationClient.lastLocation
920
+ .addOnSuccessListener { location ->
921
+ completion(location?.takeIf { isCachedLocationValid(it, options) })
922
+ }
923
+ .addOnFailureListener {
924
+ completion(null)
925
+ }
926
+ .addOnCanceledListener {
927
+ completion(null)
928
+ }
929
+ } catch (e: SecurityException) {
930
+ completion(null)
931
+ }
932
+ }
933
+
934
+ private fun requestFusedFreshLocation(
935
+ success: (GeolocationResponse) -> Unit,
936
+ error: ((LocationError) -> Unit)?,
937
+ options: ParsedOptions
938
+ ) {
939
+ val handler = Handler(Looper.getMainLooper())
940
+ val didComplete = AtomicBoolean(false)
941
+ lateinit var callback: LocationCallback
942
+
943
+ fun complete(result: PositionResult) {
944
+ if (!didComplete.compareAndSet(false, true)) return
945
+
946
+ handler.removeCallbacksAndMessages(null)
947
+ try {
948
+ fusedLocationClient.removeLocationUpdates(callback)
949
+ } catch (_: Exception) {
950
+ // Ignore cleanup races.
951
+ }
952
+
953
+ when (result) {
954
+ is PositionResult.Success -> success(result.position)
955
+ is PositionResult.Failure -> error?.invoke(result.error)
956
+ }
957
+ }
958
+
959
+ callback = object : LocationCallback() {
960
+ override fun onLocationResult(result: LocationResult) {
961
+ val location = result.lastLocation
962
+ if (location != null) {
963
+ complete(PositionResult.Success(locationToPosition(location)))
964
+ }
965
+ }
966
+ }
967
+
968
+ val timeoutRunnable = Runnable {
969
+ complete(PositionResult.Failure(createPositionTimeoutError(options)))
970
+ }
971
+
972
+ try {
973
+ fusedLocationClient.requestLocationUpdates(
974
+ buildFusedLocationRequest(
975
+ options,
976
+ maxUpdatesOverride = 1,
977
+ includeDistanceFilter = false
978
+ ),
979
+ callback,
980
+ Looper.getMainLooper()
981
+ )
982
+ handler.postDelayed(timeoutRunnable, coerceTimeoutMillis(options.timeout))
983
+ } catch (e: SecurityException) {
984
+ complete(PositionResult.Failure(createLocationError(
985
+ PERMISSION_DENIED,
986
+ "Security exception: ${e.message}"
987
+ )))
988
+ } catch (e: Exception) {
989
+ complete(PositionResult.Failure(createLocationError(
990
+ POSITION_UNAVAILABLE,
991
+ "Unable to request fused location: ${e.message}"
992
+ )))
993
+ }
994
+ }
995
+
407
996
  // MARK: - Helper Functions - Request Fresh Location
408
997
 
409
998
  private fun requestFreshLocation(
410
999
  providers: List<String>,
411
1000
  options: ParsedOptions,
412
- resolver: (Result<GeolocationResponse>) -> Unit
1001
+ resolver: (PositionResult) -> Unit
413
1002
  ) {
414
1003
  val id = UUID.randomUUID()
415
1004
  val handler = Handler(Looper.getMainLooper())
@@ -433,21 +1022,22 @@ class NitroGeolocation(
433
1022
  val remainingTimeoutMillis = request.remainingTimeoutMillis()
434
1023
 
435
1024
  if (provider == null) {
436
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
437
- createNoLocationProviderError(request.options)
438
- ))
1025
+ pendingPositionRequests.remove(requestId)?.resolver(
1026
+ PositionResult.Failure(createNoLocationProviderError(request.options))
1027
+ )
439
1028
  return
440
1029
  }
441
1030
 
442
1031
  if (remainingTimeoutMillis <= 0L) {
443
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
444
- createPositionTimeoutError(request.options)
445
- ))
1032
+ pendingPositionRequests.remove(requestId)?.resolver(
1033
+ PositionResult.Failure(createPositionTimeoutError(request.options))
1034
+ )
446
1035
  return
447
1036
  }
448
1037
 
449
- // Use modern API on Android 11+
450
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1038
+ // Android's getCurrentLocation may resolve a recent historical fix. A maximumAge of 0
1039
+ // means callers explicitly asked us to wait for a fresh provider update.
1040
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && request.options.maximumAge > 0.0) {
451
1041
  requestCurrentLocationModern(provider, requestId, request.handler, remainingTimeoutMillis)
452
1042
  } else {
453
1043
  requestCurrentLocationLegacy(provider, requestId, request.handler, remainingTimeoutMillis)
@@ -481,7 +1071,7 @@ class NitroGeolocation(
481
1071
  if (location != null) {
482
1072
  pendingPositionRequests.remove(requestId)
483
1073
  val position = locationToPosition(location)
484
- request.resolver(Result.success(position))
1074
+ request.resolver(PositionResult.Success(position))
485
1075
  } else {
486
1076
  handleProviderFailure(requestId, createLocationError(
487
1077
  POSITION_UNAVAILABLE,
@@ -531,7 +1121,7 @@ class NitroGeolocation(
531
1121
  val request = pendingPositionRequests.remove(requestId)
532
1122
  if (request != null) {
533
1123
  val position = locationToPosition(location)
534
- request.resolver(Result.success(position))
1124
+ request.resolver(PositionResult.Success(position))
535
1125
  }
536
1126
  }
537
1127
  oldLocation = location
@@ -579,7 +1169,7 @@ class NitroGeolocation(
579
1169
  }
580
1170
  }
581
1171
 
582
- private fun handleProviderFailure(requestId: UUID, error: Exception) {
1172
+ private fun handleProviderFailure(requestId: UUID, error: LocationError) {
583
1173
  val request = pendingPositionRequests[requestId] ?: return
584
1174
 
585
1175
  request.cancellationSignal?.cancel()
@@ -588,9 +1178,9 @@ class NitroGeolocation(
588
1178
 
589
1179
  if (request.providerIndex < request.providers.size) {
590
1180
  if (request.remainingTimeoutMillis() <= 0L) {
591
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
592
- createPositionTimeoutError(request.options)
593
- ))
1181
+ pendingPositionRequests.remove(requestId)?.resolver(
1182
+ PositionResult.Failure(createPositionTimeoutError(request.options))
1183
+ )
594
1184
  return
595
1185
  }
596
1186
 
@@ -598,7 +1188,7 @@ class NitroGeolocation(
598
1188
  return
599
1189
  }
600
1190
 
601
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(error))
1191
+ pendingPositionRequests.remove(requestId)?.resolver(PositionResult.Failure(error))
602
1192
  }
603
1193
 
604
1194
  private fun selectBestLocation(newLocation: Location, currentBest: Location?): Location {
@@ -633,29 +1223,27 @@ class NitroGeolocation(
633
1223
  request.cancellationSignal?.cancel()
634
1224
  request.cancellationSignal = null
635
1225
 
636
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
637
- createPositionTimeoutError(request.options)
638
- ))
1226
+ pendingPositionRequests.remove(requestId)?.resolver(
1227
+ PositionResult.Failure(createPositionTimeoutError(request.options))
1228
+ )
639
1229
  }
640
1230
  }
641
1231
 
642
1232
  // MARK: - Helper Functions - Watch Position
643
1233
 
644
1234
  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
1235
+ if (requiresPlayServices() && !isGooglePlayServicesAvailable()) {
1236
+ notifyWatchPlayServicesUnavailable()
1237
+ return
1238
+ }
649
1239
 
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())
1240
+ if (requiresPlayServices()) {
1241
+ startWatchingFusedLocation()
1242
+ return
656
1243
  }
657
1244
 
658
- val provider = getValidProvider(useHighAccuracy)
1245
+ val mergedOptions = mergeWatchOptions()
1246
+ val provider = getValidProvider(mergedOptions)
659
1247
  if (provider == null) {
660
1248
  notifyWatchProviderUnavailable()
661
1249
  return
@@ -665,16 +1253,12 @@ class NitroGeolocation(
665
1253
  val listener = object : LocationListener {
666
1254
  override fun onLocationChanged(location: Location) {
667
1255
  val position = locationToPosition(location)
668
-
669
- // Notify all subscribers
670
- for ((_, subscription) in watchSubscriptions) {
671
- subscription.success(position)
672
- }
1256
+ deliverWatchPosition(position)
673
1257
  }
674
1258
 
675
1259
  override fun onProviderDisabled(provider: String) {
676
1260
  val error = LocationError(
677
- code = POSITION_UNAVAILABLE,
1261
+ code = SETTINGS_NOT_SATISFIED,
678
1262
  message = "Provider disabled: $provider"
679
1263
  )
680
1264
 
@@ -693,8 +1277,8 @@ class NitroGeolocation(
693
1277
  try {
694
1278
  locationManager.requestLocationUpdates(
695
1279
  provider,
696
- smallestInterval.toLong(),
697
- smallestDistanceFilter,
1280
+ mergedOptions.interval.toLong(),
1281
+ mergedOptions.distanceFilter.toFloat(),
698
1282
  listener,
699
1283
  Looper.getMainLooper()
700
1284
  )
@@ -710,15 +1294,157 @@ class NitroGeolocation(
710
1294
  }
711
1295
  }
712
1296
 
1297
+ private fun startWatchingFusedLocation() {
1298
+ val mergedOptions = mergeWatchOptions()
1299
+ val callback = object : LocationCallback() {
1300
+ override fun onLocationResult(result: LocationResult) {
1301
+ val location = result.lastLocation ?: return
1302
+ deliverWatchPosition(locationToPosition(location))
1303
+ }
1304
+ }
1305
+
1306
+ fusedWatchLocationCallback = callback
1307
+
1308
+ try {
1309
+ fusedLocationClient.requestLocationUpdates(
1310
+ buildFusedLocationRequest(mergedOptions),
1311
+ callback,
1312
+ Looper.getMainLooper()
1313
+ )
1314
+ } catch (e: SecurityException) {
1315
+ val error = LocationError(
1316
+ code = PERMISSION_DENIED,
1317
+ message = "Permission denied: ${e.message}"
1318
+ )
1319
+
1320
+ for ((_, subscription) in watchSubscriptions) {
1321
+ subscription.error?.invoke(error)
1322
+ }
1323
+ } catch (e: Exception) {
1324
+ val error = LocationError(
1325
+ code = POSITION_UNAVAILABLE,
1326
+ message = "Unable to request fused location updates: ${e.message}"
1327
+ )
1328
+
1329
+ for ((_, subscription) in watchSubscriptions) {
1330
+ subscription.error?.invoke(error)
1331
+ }
1332
+ }
1333
+ }
1334
+
1335
+ private fun mergeWatchOptions(): ParsedOptions {
1336
+ var androidAccuracy: AndroidAccuracyResolution? = null
1337
+ var smallestInterval = Double.MAX_VALUE
1338
+ var smallestFastestInterval = Double.MAX_VALUE
1339
+ var smallestDistanceFilter = Double.MAX_VALUE
1340
+ var granularity = AndroidGranularity.PERMISSION
1341
+ var waitForAccurateLocation = false
1342
+ var maxUpdateAge: Double? = null
1343
+ var smallestMaxUpdateDelay = Double.MAX_VALUE
1344
+
1345
+ for ((_, subscription) in watchSubscriptions) {
1346
+ androidAccuracy = mostDemandingAndroidAccuracy(
1347
+ androidAccuracy,
1348
+ subscription.options.androidAccuracy
1349
+ )
1350
+ smallestInterval = minOf(smallestInterval, subscription.options.interval)
1351
+ smallestFastestInterval = minOf(
1352
+ smallestFastestInterval,
1353
+ subscription.options.fastestInterval
1354
+ )
1355
+ smallestDistanceFilter = minOf(
1356
+ smallestDistanceFilter,
1357
+ subscription.options.distanceFilter
1358
+ )
1359
+ granularity = mergeWatchGranularity(granularity, subscription.options.granularity)
1360
+ waitForAccurateLocation = waitForAccurateLocation ||
1361
+ subscription.options.waitForAccurateLocation
1362
+ maxUpdateAge = mergeNullableMinimum(maxUpdateAge, subscription.options.maxUpdateAge)
1363
+ smallestMaxUpdateDelay = minOf(
1364
+ smallestMaxUpdateDelay,
1365
+ subscription.options.maxUpdateDelay
1366
+ )
1367
+ }
1368
+
1369
+ return ParsedOptions(
1370
+ timeout = Double.POSITIVE_INFINITY,
1371
+ maximumAge = 0.0,
1372
+ androidAccuracy = androidAccuracy ?: resolveAndroidAccuracy(null, enableHighAccuracy = false),
1373
+ interval = smallestInterval,
1374
+ fastestInterval = smallestFastestInterval,
1375
+ distanceFilter = smallestDistanceFilter,
1376
+ granularity = granularity,
1377
+ waitForAccurateLocation = waitForAccurateLocation,
1378
+ maxUpdateAge = maxUpdateAge,
1379
+ maxUpdateDelay = if (smallestMaxUpdateDelay == Double.MAX_VALUE) 0.0 else smallestMaxUpdateDelay,
1380
+ maxUpdates = null
1381
+ )
1382
+ }
1383
+
1384
+ private fun deliverWatchPosition(position: GeolocationResponse) {
1385
+ val tokensToRemove = mutableListOf<String>()
1386
+
1387
+ for ((token, subscription) in watchSubscriptions) {
1388
+ subscription.success(position)
1389
+ subscription.deliveredUpdates += 1
1390
+
1391
+ val maxUpdates = subscription.options.maxUpdates
1392
+ if (maxUpdates != null && subscription.deliveredUpdates >= maxUpdates) {
1393
+ tokensToRemove.add(token)
1394
+ }
1395
+ }
1396
+
1397
+ for (token in tokensToRemove) {
1398
+ watchSubscriptions.remove(token)
1399
+ }
1400
+
1401
+ if (tokensToRemove.isNotEmpty()) {
1402
+ if (watchSubscriptions.isEmpty()) {
1403
+ stopWatchingLocation()
1404
+ } else {
1405
+ restartWatchingLocation()
1406
+ }
1407
+ }
1408
+ }
1409
+
713
1410
  private fun notifyWatchProviderUnavailable() {
714
1411
  for ((_, subscription) in watchSubscriptions) {
715
1412
  subscription.error?.invoke(LocationError(
716
- code = POSITION_UNAVAILABLE,
1413
+ code = SETTINGS_NOT_SATISFIED,
717
1414
  message = getNoLocationProviderMessage(subscription.options)
718
1415
  ))
719
1416
  }
720
1417
  }
721
1418
 
1419
+ private fun notifyWatchPlayServicesUnavailable() {
1420
+ for ((_, subscription) in watchSubscriptions) {
1421
+ subscription.error?.invoke(createPlayServicesUnavailableError())
1422
+ }
1423
+ }
1424
+
1425
+ private fun mergeWatchGranularity(
1426
+ current: AndroidGranularity,
1427
+ next: AndroidGranularity
1428
+ ): AndroidGranularity {
1429
+ return when {
1430
+ current == AndroidGranularity.COARSE || next == AndroidGranularity.COARSE -> {
1431
+ AndroidGranularity.COARSE
1432
+ }
1433
+ current == AndroidGranularity.FINE || next == AndroidGranularity.FINE -> {
1434
+ AndroidGranularity.FINE
1435
+ }
1436
+ else -> AndroidGranularity.PERMISSION
1437
+ }
1438
+ }
1439
+
1440
+ private fun mergeNullableMinimum(current: Double?, next: Double?): Double? {
1441
+ return when {
1442
+ current == null -> next
1443
+ next == null -> current
1444
+ else -> minOf(current, next)
1445
+ }
1446
+ }
1447
+
722
1448
  private fun stopWatchingLocation() {
723
1449
  watchLocationListener?.let { listener ->
724
1450
  try {
@@ -727,59 +1453,169 @@ class NitroGeolocation(
727
1453
  // Ignore
728
1454
  }
729
1455
  }
1456
+ fusedWatchLocationCallback?.let { callback ->
1457
+ try {
1458
+ fusedLocationClient.removeLocationUpdates(callback)
1459
+ } catch (e: Exception) {
1460
+ // Ignore
1461
+ }
1462
+ }
730
1463
  watchLocationListener = null
1464
+ fusedWatchLocationCallback = null
731
1465
  currentWatchProvider = null
732
1466
  }
733
1467
 
1468
+ private fun restartWatchingLocation() {
1469
+ stopWatchingLocation()
1470
+ startWatchingLocation()
1471
+ }
1472
+
734
1473
  // MARK: - Helper Functions - Conversion
735
1474
 
736
1475
  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
- }
1476
+ lastLocation = location
757
1477
 
758
1478
  val coords = GeolocationCoordinates(
759
1479
  latitude = location.latitude,
760
1480
  longitude = location.longitude,
761
- altitude = altitude,
1481
+ altitude = location.altitudeValue(),
762
1482
  accuracy = location.accuracy.toDouble(),
763
- altitudeAccuracy = altitudeAccuracy,
764
- heading = heading,
765
- speed = speed
1483
+ altitudeAccuracy = location.altitudeAccuracyValue(),
1484
+ heading = location.headingValue(),
1485
+ speed = location.speedValue()
766
1486
  )
767
1487
 
768
1488
  return GeolocationResponse(
769
1489
  coords = coords,
770
- timestamp = location.time.toDouble()
1490
+ timestamp = location.time.toDouble(),
1491
+ mocked = location.isMocked(),
1492
+ provider = location.providerUsed()
771
1493
  )
772
1494
  }
773
1495
 
774
- private fun createLocationError(code: Double, message: String): Exception {
775
- val locationError = LocationError(
1496
+ private fun geocodedAddressToLocation(address: Address): GeocodedLocation? {
1497
+ if (!address.hasLatitude() || !address.hasLongitude()) {
1498
+ return null
1499
+ }
1500
+
1501
+ return GeocodedLocation(
1502
+ latitude = address.latitude,
1503
+ longitude = address.longitude,
1504
+ accuracy = null
1505
+ )
1506
+ }
1507
+
1508
+ private fun addressToReverseGeocodedAddress(address: Address): ReverseGeocodedAddress {
1509
+ return ReverseGeocodedAddress(
1510
+ country = address.countryName.nonBlankOrNull(),
1511
+ region = address.adminArea.nonBlankOrNull(),
1512
+ city = (address.locality ?: address.subAdminArea).nonBlankOrNull(),
1513
+ district = address.subLocality.nonBlankOrNull(),
1514
+ street = formatStreet(address),
1515
+ postalCode = address.postalCode.nonBlankOrNull(),
1516
+ formattedAddress = formatAddressLines(address)
1517
+ )
1518
+ }
1519
+
1520
+ private fun formatStreet(address: Address): String? {
1521
+ return listOf(address.subThoroughfare, address.thoroughfare)
1522
+ .mapNotNull { it.nonBlankOrNull() }
1523
+ .joinToString(" ")
1524
+ .nonBlankOrNull()
1525
+ }
1526
+
1527
+ private fun formatAddressLines(address: Address): String? {
1528
+ if (address.maxAddressLineIndex < 0) {
1529
+ return null
1530
+ }
1531
+
1532
+ return (0..address.maxAddressLineIndex)
1533
+ .mapNotNull { index -> address.getAddressLine(index).nonBlankOrNull() }
1534
+ .joinToString(", ")
1535
+ .nonBlankOrNull()
1536
+ }
1537
+
1538
+ private fun validateGeocodingCoordinates(coords: GeocodingCoordinates): LocationError? {
1539
+ if (!coords.latitude.isFinite() || coords.latitude < -90.0 || coords.latitude > 90.0) {
1540
+ return createLocationError(
1541
+ INTERNAL_ERROR,
1542
+ "latitude must be a finite number between -90 and 90."
1543
+ )
1544
+ }
1545
+
1546
+ if (!coords.longitude.isFinite() || coords.longitude < -180.0 || coords.longitude > 180.0) {
1547
+ return createLocationError(
1548
+ INTERNAL_ERROR,
1549
+ "longitude must be a finite number between -180 and 180."
1550
+ )
1551
+ }
1552
+
1553
+ return null
1554
+ }
1555
+
1556
+ private fun <T> runGeocoderOperation(
1557
+ success: (Array<T>) -> Unit,
1558
+ error: ((LocationError) -> Unit)?,
1559
+ failurePrefix: String,
1560
+ operation: () -> Array<T>
1561
+ ) {
1562
+ if (!Geocoder.isPresent()) {
1563
+ error?.invoke(createLocationError(
1564
+ POSITION_UNAVAILABLE,
1565
+ "Platform geocoder is not available."
1566
+ ))
1567
+ return
1568
+ }
1569
+
1570
+ val handler = Handler(Looper.getMainLooper())
1571
+
1572
+ Thread {
1573
+ try {
1574
+ val results = operation()
1575
+ handler.post { success(results) }
1576
+ } catch (e: IOException) {
1577
+ handler.post {
1578
+ error?.invoke(createLocationError(
1579
+ POSITION_UNAVAILABLE,
1580
+ "$failurePrefix: ${e.message ?: "geocoder service unavailable"}"
1581
+ ))
1582
+ }
1583
+ } catch (e: Exception) {
1584
+ handler.post {
1585
+ error?.invoke(createLocationError(
1586
+ INTERNAL_ERROR,
1587
+ "$failurePrefix: ${e.message ?: "unknown error"}"
1588
+ ))
1589
+ }
1590
+ }
1591
+ }.start()
1592
+ }
1593
+
1594
+ private fun createLocationAvailability(
1595
+ available: Boolean,
1596
+ reason: String?
1597
+ ): LocationAvailability {
1598
+ return LocationAvailability(
1599
+ available = available,
1600
+ reason = reason
1601
+ )
1602
+ }
1603
+
1604
+ private fun createLocationError(code: Double, message: String): LocationError {
1605
+ return LocationError(
776
1606
  code = code,
777
1607
  message = message
778
1608
  )
779
- return GeolocationErrorException(locationError)
780
1609
  }
781
1610
 
782
- private fun createPositionTimeoutError(options: ParsedOptions): Exception {
1611
+ private fun createPlayServicesUnavailableError(): LocationError {
1612
+ return createLocationError(
1613
+ PLAY_SERVICE_NOT_AVAILABLE,
1614
+ "Google Play Services location provider is not available."
1615
+ )
1616
+ }
1617
+
1618
+ private fun createPositionTimeoutError(options: ParsedOptions): LocationError {
783
1619
  val timeoutSeconds = options.timeout / 1000.0
784
1620
  val message = String.format("Unable to fetch location within %.1fs.", timeoutSeconds)
785
1621
  return createLocationError(TIMEOUT, message)
@@ -805,8 +1641,57 @@ class NitroGeolocation(
805
1641
  }
806
1642
  }
807
1643
 
1644
+ private fun buildFusedLocationRequest(
1645
+ options: ParsedOptions,
1646
+ maxUpdatesOverride: Int? = null,
1647
+ includeDistanceFilter: Boolean = true
1648
+ ): GmsLocationRequest {
1649
+ val builder = GmsLocationRequest
1650
+ .Builder(options.androidAccuracy.gmsPriority(), coercePositiveMillis(options.interval))
1651
+ .setMinUpdateIntervalMillis(coercePositiveMillis(options.fastestInterval))
1652
+ .setGranularity(options.granularity.gmsGranularity())
1653
+ .setWaitForAccurateLocation(options.waitForAccurateLocation)
1654
+ .setMaxUpdateDelayMillis(coerceNonNegativeMillis(options.maxUpdateDelay))
1655
+
1656
+ if (includeDistanceFilter) {
1657
+ builder.setMinUpdateDistanceMeters(options.distanceFilter.toFloat())
1658
+ }
1659
+
1660
+ options.maxUpdateAge?.let { value ->
1661
+ builder.setMaxUpdateAgeMillis(coerceNonNegativeMillis(value))
1662
+ }
1663
+
1664
+ val maxUpdates = maxUpdatesOverride ?: options.maxUpdates
1665
+ if (maxUpdates != null) {
1666
+ builder.setMaxUpdates(maxUpdates)
1667
+ }
1668
+
1669
+ return builder.build()
1670
+ }
1671
+
1672
+ private fun coercePositiveMillis(value: Double): Long {
1673
+ return when {
1674
+ value.isNaN() || value <= 0.0 -> 1L
1675
+ value.isInfinite() || value >= Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
1676
+ else -> value.toLong()
1677
+ }
1678
+ }
1679
+
1680
+ private fun coerceNonNegativeMillis(value: Double): Long {
1681
+ return when {
1682
+ value.isNaN() || value <= 0.0 -> 0L
1683
+ value.isInfinite() || value >= Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
1684
+ else -> value.toLong()
1685
+ }
1686
+ }
1687
+
808
1688
  companion object {
809
1689
  private const val PERMISSION_REQUEST_CODE = 8947
1690
+ private const val GEOCODER_MAX_RESULTS = 5
810
1691
  private const val TWO_MINUTES_MS = 2 * 60 * 1000L
811
1692
  }
812
1693
  }
1694
+
1695
+ private fun String?.nonBlankOrNull(): String? {
1696
+ return this?.trim()?.takeIf { it.isNotEmpty() }
1697
+ }