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
@@ -0,0 +1,313 @@
1
+ package com.margelo.nitro.nitrogeolocation
2
+
3
+ import android.content.Context
4
+ import android.hardware.GeomagneticField
5
+ import android.hardware.Sensor
6
+ import android.hardware.SensorEvent
7
+ import android.hardware.SensorEventListener
8
+ import android.hardware.SensorManager
9
+ import android.location.Location
10
+ import android.os.Handler
11
+ import android.os.Looper
12
+ import java.util.UUID
13
+ import java.util.concurrent.ConcurrentHashMap
14
+ import kotlin.math.abs
15
+
16
+ private const val DEFAULT_HEADING_TIMEOUT_MS = 10_000L
17
+
18
+ internal class AndroidHeadingManager(
19
+ context: Context,
20
+ private val createLocationError: (Double, String) -> LocationError,
21
+ private val getReferenceLocation: () -> Location?
22
+ ) {
23
+ private data class PendingHeadingRequest(
24
+ val id: UUID,
25
+ val success: (Heading) -> Unit,
26
+ val error: ((LocationError) -> Unit)?,
27
+ val timeoutRunnable: Runnable
28
+ )
29
+
30
+ private data class HeadingSubscription(
31
+ val token: String,
32
+ val success: (Heading) -> Unit,
33
+ val error: ((LocationError) -> Unit)?,
34
+ val headingFilter: Double,
35
+ var lastDeliveredHeading: Double?
36
+ )
37
+
38
+ private val sensorManager =
39
+ context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
40
+ private val mainHandler = Handler(Looper.getMainLooper())
41
+ private val pendingRequests = ConcurrentHashMap<UUID, PendingHeadingRequest>()
42
+ private val subscriptions = ConcurrentHashMap<String, HeadingSubscription>()
43
+ private val rotationVectorSensor: Sensor? =
44
+ sensorManager?.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
45
+ ?: sensorManager?.getDefaultSensor(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR)
46
+ private val accelerometerSensor: Sensor? =
47
+ sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
48
+ private val magneticFieldSensor: Sensor? =
49
+ sensorManager?.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
50
+
51
+ private var isListening = false
52
+ private var sensorAccuracy: Int? = null
53
+ private var gravityValues: FloatArray? = null
54
+ private var magneticValues: FloatArray? = null
55
+
56
+ private val sensorListener = object : SensorEventListener {
57
+ override fun onSensorChanged(event: SensorEvent) {
58
+ val magneticHeading = when (event.sensor.type) {
59
+ Sensor.TYPE_ROTATION_VECTOR,
60
+ Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR -> {
61
+ headingFromRotationVector(event.values)
62
+ }
63
+ Sensor.TYPE_ACCELEROMETER -> {
64
+ gravityValues = event.values.clone()
65
+ headingFromAccelerationAndMagnetometer()
66
+ }
67
+ Sensor.TYPE_MAGNETIC_FIELD -> {
68
+ magneticValues = event.values.clone()
69
+ headingFromAccelerationAndMagnetometer()
70
+ }
71
+ else -> null
72
+ } ?: return
73
+
74
+ emitHeading(createHeading(magneticHeading))
75
+ }
76
+
77
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
78
+ sensorAccuracy = accuracy
79
+ }
80
+ }
81
+
82
+ fun getHeading(
83
+ success: (Heading) -> Unit,
84
+ error: ((LocationError) -> Unit)?
85
+ ) {
86
+ if (!hasHeadingSensor()) {
87
+ error?.invoke(createUnavailableError())
88
+ return
89
+ }
90
+
91
+ val id = UUID.randomUUID()
92
+ val timeoutRunnable = Runnable {
93
+ pendingRequests.remove(id)?.error?.invoke(
94
+ createLocationError(
95
+ TIMEOUT,
96
+ "Unable to fetch heading within ${DEFAULT_HEADING_TIMEOUT_MS / 1000.0}s."
97
+ )
98
+ )
99
+ stopIfIdle()
100
+ }
101
+
102
+ pendingRequests[id] = PendingHeadingRequest(
103
+ id = id,
104
+ success = success,
105
+ error = error,
106
+ timeoutRunnable = timeoutRunnable
107
+ )
108
+ mainHandler.postDelayed(timeoutRunnable, DEFAULT_HEADING_TIMEOUT_MS)
109
+ startIfNeeded()
110
+ }
111
+
112
+ fun watchHeading(
113
+ success: (Heading) -> Unit,
114
+ error: ((LocationError) -> Unit)?,
115
+ options: HeadingOptions?
116
+ ): String {
117
+ val token = UUID.randomUUID().toString()
118
+ val headingFilter = options?.headingFilter ?: 0.0
119
+
120
+ if (!headingFilter.isFinite() || headingFilter < 0.0) {
121
+ error?.invoke(createLocationError(
122
+ INTERNAL_ERROR,
123
+ "headingFilter must be a finite number greater than or equal to 0."
124
+ ))
125
+ return token
126
+ }
127
+
128
+ if (!hasHeadingSensor()) {
129
+ error?.invoke(createUnavailableError())
130
+ return token
131
+ }
132
+
133
+ subscriptions[token] = HeadingSubscription(
134
+ token = token,
135
+ success = success,
136
+ error = error,
137
+ headingFilter = headingFilter,
138
+ lastDeliveredHeading = null
139
+ )
140
+ startIfNeeded()
141
+
142
+ return token
143
+ }
144
+
145
+ fun unwatch(token: String) {
146
+ subscriptions.remove(token)
147
+ stopIfIdle()
148
+ }
149
+
150
+ fun stopObserving() {
151
+ pendingRequests.values.forEach { request ->
152
+ mainHandler.removeCallbacks(request.timeoutRunnable)
153
+ }
154
+ pendingRequests.clear()
155
+ subscriptions.clear()
156
+ stopListening()
157
+ }
158
+
159
+ fun hasActiveWork(): Boolean {
160
+ return pendingRequests.isNotEmpty() || subscriptions.isNotEmpty()
161
+ }
162
+
163
+ private fun hasHeadingSensor(): Boolean {
164
+ return sensorManager != null &&
165
+ (rotationVectorSensor != null || (accelerometerSensor != null && magneticFieldSensor != null))
166
+ }
167
+
168
+ private fun startIfNeeded() {
169
+ if (isListening) return
170
+
171
+ val manager = sensorManager ?: return
172
+ val didRegister = if (rotationVectorSensor != null) {
173
+ manager.registerListener(
174
+ sensorListener,
175
+ rotationVectorSensor,
176
+ SensorManager.SENSOR_DELAY_UI,
177
+ mainHandler
178
+ )
179
+ } else {
180
+ val accelerometerRegistered = manager.registerListener(
181
+ sensorListener,
182
+ accelerometerSensor!!,
183
+ SensorManager.SENSOR_DELAY_UI,
184
+ mainHandler
185
+ )
186
+ val magneticRegistered = manager.registerListener(
187
+ sensorListener,
188
+ magneticFieldSensor!!,
189
+ SensorManager.SENSOR_DELAY_UI,
190
+ mainHandler
191
+ )
192
+ accelerometerRegistered && magneticRegistered
193
+ }
194
+
195
+ isListening = didRegister
196
+ if (!didRegister) {
197
+ val error = createUnavailableError()
198
+ pendingRequests.values.forEach { it.error?.invoke(error) }
199
+ subscriptions.values.forEach { it.error?.invoke(error) }
200
+ stopObserving()
201
+ }
202
+ }
203
+
204
+ private fun stopIfIdle() {
205
+ if (!hasActiveWork()) {
206
+ stopListening()
207
+ }
208
+ }
209
+
210
+ private fun stopListening() {
211
+ if (!isListening) return
212
+
213
+ try {
214
+ sensorManager?.unregisterListener(sensorListener)
215
+ } catch (_: Exception) {
216
+ // Ignore unregister races.
217
+ }
218
+ isListening = false
219
+ gravityValues = null
220
+ magneticValues = null
221
+ }
222
+
223
+ private fun headingFromRotationVector(values: FloatArray): Double? {
224
+ val rotationMatrix = FloatArray(9)
225
+ val orientation = FloatArray(3)
226
+ SensorManager.getRotationMatrixFromVector(rotationMatrix, values)
227
+ SensorManager.getOrientation(rotationMatrix, orientation)
228
+ return normalizeHeading(Math.toDegrees(orientation[0].toDouble()))
229
+ }
230
+
231
+ private fun headingFromAccelerationAndMagnetometer(): Double? {
232
+ val gravity = gravityValues ?: return null
233
+ val magnetic = magneticValues ?: return null
234
+ val rotationMatrix = FloatArray(9)
235
+ val inclinationMatrix = FloatArray(9)
236
+
237
+ if (!SensorManager.getRotationMatrix(rotationMatrix, inclinationMatrix, gravity, magnetic)) {
238
+ return null
239
+ }
240
+
241
+ val orientation = FloatArray(3)
242
+ SensorManager.getOrientation(rotationMatrix, orientation)
243
+ return normalizeHeading(Math.toDegrees(orientation[0].toDouble()))
244
+ }
245
+
246
+ private fun createHeading(magneticHeading: Double): Heading {
247
+ val referenceLocation = getReferenceLocation()
248
+ val trueHeading = referenceLocation?.let { location ->
249
+ val field = GeomagneticField(
250
+ location.latitude.toFloat(),
251
+ location.longitude.toFloat(),
252
+ if (location.hasAltitude()) location.altitude.toFloat() else 0f,
253
+ location.time
254
+ )
255
+ normalizeHeading(magneticHeading + field.declination)
256
+ }
257
+
258
+ return Heading(
259
+ magneticHeading = magneticHeading,
260
+ trueHeading = trueHeading,
261
+ accuracy = sensorAccuracy?.takeIf {
262
+ it != SensorManager.SENSOR_STATUS_UNRELIABLE
263
+ }?.toDouble(),
264
+ timestamp = System.currentTimeMillis().toDouble()
265
+ )
266
+ }
267
+
268
+ private fun emitHeading(heading: Heading) {
269
+ val pendingSnapshot = pendingRequests.values.toList()
270
+ pendingSnapshot.forEach { request ->
271
+ mainHandler.removeCallbacks(request.timeoutRunnable)
272
+ if (pendingRequests.remove(request.id) != null) {
273
+ request.success(heading)
274
+ }
275
+ }
276
+
277
+ subscriptions.values.forEach { subscription ->
278
+ val lastHeading = subscription.lastDeliveredHeading
279
+ if (
280
+ lastHeading == null ||
281
+ angularDistance(lastHeading, heading.magneticHeading) >= subscription.headingFilter
282
+ ) {
283
+ subscription.lastDeliveredHeading = heading.magneticHeading
284
+ subscription.success(heading)
285
+ }
286
+ }
287
+
288
+ stopIfIdle()
289
+ }
290
+
291
+ private fun angularDistance(first: Double, second: Double): Double {
292
+ val distance = abs(first - second) % 360.0
293
+ return if (distance > 180.0) 360.0 - distance else distance
294
+ }
295
+
296
+ private fun normalizeHeading(value: Double): Double {
297
+ val normalized = value % 360.0
298
+ return if (normalized < 0.0) normalized + 360.0 else normalized
299
+ }
300
+
301
+ private fun createUnavailableError(): LocationError {
302
+ return createLocationError(
303
+ POSITION_UNAVAILABLE,
304
+ "Heading sensor is not available."
305
+ )
306
+ }
307
+
308
+ private companion object {
309
+ private const val INTERNAL_ERROR = -1.0
310
+ private const val POSITION_UNAVAILABLE = 2.0
311
+ private const val TIMEOUT = 3.0
312
+ }
313
+ }
@@ -0,0 +1,313 @@
1
+ package com.margelo.nitro.nitrogeolocation
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.IntentSender
8
+ import android.content.pm.PackageManager
9
+ import android.location.LocationManager as AndroidLocationManager
10
+ import android.os.Build
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import androidx.core.content.ContextCompat
14
+ import com.facebook.react.bridge.BaseActivityEventListener
15
+ import com.facebook.react.bridge.ReactApplicationContext
16
+ import com.google.android.gms.common.ConnectionResult
17
+ import com.google.android.gms.common.GoogleApiAvailability
18
+ import com.google.android.gms.common.api.ResolvableApiException
19
+ import com.google.android.gms.location.LocationRequest as GmsLocationRequest
20
+ import com.google.android.gms.location.LocationServices
21
+ import com.google.android.gms.location.LocationSettingsRequest
22
+ import com.google.android.gms.location.Priority
23
+ import java.util.concurrent.atomic.AtomicBoolean
24
+
25
+ private const val LOCATION_SETTINGS_REQUEST_CODE = 8948
26
+ private const val GOOGLE_LOCATION_ACCURACY_TIMEOUT_MS = 2_000L
27
+
28
+ internal class AndroidLocationSettings(
29
+ private val reactContext: ReactApplicationContext,
30
+ private val locationManager: AndroidLocationManager,
31
+ private val createLocationError: (Double, String) -> LocationError,
32
+ private val createPlayServicesUnavailableError: () -> LocationError
33
+ ) {
34
+ private data class ParsedSettingsOptions(
35
+ val androidAccuracy: AndroidAccuracyResolution,
36
+ val intervalMillis: Long,
37
+ val fastestIntervalMillis: Long,
38
+ val distanceFilterMeters: Float,
39
+ val alwaysShow: Boolean,
40
+ val needBle: Boolean
41
+ ) {
42
+ companion object {
43
+ private const val DEFAULT_INTERVAL_MS = 5_000.0
44
+ private const val DEFAULT_FASTEST_INTERVAL_MS = 1_000.0
45
+ private const val DEFAULT_DISTANCE_FILTER_METERS = 0.0
46
+
47
+ fun parse(options: LocationSettingsOptions?): ParsedSettingsOptions {
48
+ val enableHighAccuracy = options?.enableHighAccuracy ?: true
49
+ return ParsedSettingsOptions(
50
+ androidAccuracy = resolveAndroidAccuracy(
51
+ options?.accuracy,
52
+ enableHighAccuracy
53
+ ),
54
+ intervalMillis = coercePositiveMillis(
55
+ options?.interval,
56
+ DEFAULT_INTERVAL_MS
57
+ ),
58
+ fastestIntervalMillis = coercePositiveMillis(
59
+ options?.fastestInterval,
60
+ DEFAULT_FASTEST_INTERVAL_MS
61
+ ),
62
+ distanceFilterMeters = (options?.distanceFilter
63
+ ?: DEFAULT_DISTANCE_FILTER_METERS)
64
+ .coerceAtLeast(0.0)
65
+ .toFloat(),
66
+ alwaysShow = options?.alwaysShow ?: true,
67
+ needBle = options?.needBle ?: false
68
+ )
69
+ }
70
+
71
+ private fun coercePositiveMillis(value: Double?, defaultValue: Double): Long {
72
+ val nextValue = value ?: defaultValue
73
+ return when {
74
+ nextValue.isNaN() || nextValue <= 0.0 -> defaultValue.toLong()
75
+ nextValue.isInfinite() || nextValue >= Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
76
+ else -> nextValue.toLong()
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ private data class PendingLocationSettingsRequest(
83
+ val success: (LocationProviderStatus) -> Unit,
84
+ val error: ((LocationError) -> Unit)?,
85
+ val options: ParsedSettingsOptions
86
+ )
87
+
88
+ private var pendingLocationSettingsRequest: PendingLocationSettingsRequest? = null
89
+ private val mainHandler = Handler(Looper.getMainLooper())
90
+
91
+ private val activityEventListener = object : BaseActivityEventListener() {
92
+ override fun onActivityResult(
93
+ activity: Activity,
94
+ requestCode: Int,
95
+ resultCode: Int,
96
+ data: Intent?
97
+ ) {
98
+ if (requestCode != LOCATION_SETTINGS_REQUEST_CODE) return
99
+
100
+ val pendingRequest = pendingLocationSettingsRequest ?: return
101
+ pendingLocationSettingsRequest = null
102
+
103
+ if (resultCode == Activity.RESULT_OK) {
104
+ checkLocationSettings(pendingRequest, shouldShowResolution = false)
105
+ return
106
+ }
107
+
108
+ pendingRequest.error?.invoke(createLocationError(
109
+ SETTINGS_NOT_SATISFIED,
110
+ "Location settings change was cancelled."
111
+ ))
112
+ }
113
+ }
114
+
115
+ init {
116
+ reactContext.addActivityEventListener(activityEventListener)
117
+ }
118
+
119
+ fun hasServicesEnabled(): Boolean {
120
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
121
+ locationManager.isLocationEnabled
122
+ } else {
123
+ isProviderEnabled(AndroidLocationManager.GPS_PROVIDER) ||
124
+ isProviderEnabled(AndroidLocationManager.NETWORK_PROVIDER)
125
+ }
126
+ }
127
+
128
+ fun getProviderStatus(success: (LocationProviderStatus) -> Unit) {
129
+ getGoogleLocationAccuracyEnabled { googleLocationAccuracyEnabled ->
130
+ success(createProviderStatus(googleLocationAccuracyEnabled))
131
+ }
132
+ }
133
+
134
+ private fun createProviderStatus(
135
+ googleLocationAccuracyEnabled: Boolean?
136
+ ): LocationProviderStatus {
137
+ return LocationProviderStatus(
138
+ locationServicesEnabled = hasServicesEnabled(),
139
+ backgroundModeEnabled = hasBackgroundLocationPermission(),
140
+ gpsAvailable = isProviderEnabled(AndroidLocationManager.GPS_PROVIDER),
141
+ networkAvailable = isProviderEnabled(AndroidLocationManager.NETWORK_PROVIDER),
142
+ passiveAvailable = isProviderEnabled(AndroidLocationManager.PASSIVE_PROVIDER),
143
+ googleLocationAccuracyEnabled = googleLocationAccuracyEnabled
144
+ )
145
+ }
146
+
147
+ fun requestLocationSettings(
148
+ success: (LocationProviderStatus) -> Unit,
149
+ error: ((LocationError) -> Unit)?,
150
+ options: LocationSettingsOptions?
151
+ ) {
152
+ if (!isGooglePlayServicesAvailable()) {
153
+ error?.invoke(createPlayServicesUnavailableError())
154
+ return
155
+ }
156
+
157
+ if (pendingLocationSettingsRequest != null) {
158
+ error?.invoke(createLocationError(
159
+ INTERNAL_ERROR,
160
+ "A location settings request is already in progress."
161
+ ))
162
+ return
163
+ }
164
+
165
+ val pendingRequest = PendingLocationSettingsRequest(
166
+ success = success,
167
+ error = error,
168
+ options = ParsedSettingsOptions.parse(options)
169
+ )
170
+
171
+ checkLocationSettings(pendingRequest, shouldShowResolution = true)
172
+ }
173
+
174
+ private fun checkLocationSettings(
175
+ pendingRequest: PendingLocationSettingsRequest,
176
+ shouldShowResolution: Boolean
177
+ ) {
178
+ val settingsClient = LocationServices.getSettingsClient(reactContext)
179
+ settingsClient
180
+ .checkLocationSettings(buildLocationSettingsRequest(pendingRequest.options))
181
+ .addOnSuccessListener {
182
+ getProviderStatus(pendingRequest.success)
183
+ }
184
+ .addOnFailureListener { exception ->
185
+ if (shouldShowResolution && exception is ResolvableApiException) {
186
+ showResolutionDialog(exception, pendingRequest)
187
+ return@addOnFailureListener
188
+ }
189
+
190
+ pendingRequest.error?.invoke(createLocationError(
191
+ SETTINGS_NOT_SATISFIED,
192
+ "Location settings do not satisfy the requested options."
193
+ ))
194
+ }
195
+ }
196
+
197
+ private fun showResolutionDialog(
198
+ exception: ResolvableApiException,
199
+ pendingRequest: PendingLocationSettingsRequest
200
+ ) {
201
+ val activity = reactContext.currentActivity
202
+ if (activity == null) {
203
+ pendingRequest.error?.invoke(createLocationError(
204
+ INTERNAL_ERROR,
205
+ "No activity available to request location settings."
206
+ ))
207
+ return
208
+ }
209
+
210
+ pendingLocationSettingsRequest = pendingRequest
211
+
212
+ try {
213
+ exception.startResolutionForResult(activity, LOCATION_SETTINGS_REQUEST_CODE)
214
+ } catch (e: IntentSender.SendIntentException) {
215
+ pendingLocationSettingsRequest = null
216
+ pendingRequest.error?.invoke(createLocationError(
217
+ INTERNAL_ERROR,
218
+ "Failed to show location settings dialog: ${e.message}"
219
+ ))
220
+ }
221
+ }
222
+
223
+ private fun buildLocationSettingsRequest(
224
+ options: ParsedSettingsOptions
225
+ ): LocationSettingsRequest {
226
+ val priority = when (options.androidAccuracy.mode) {
227
+ AndroidAccuracyMode.HIGH -> Priority.PRIORITY_HIGH_ACCURACY
228
+ AndroidAccuracyMode.BALANCED -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
229
+ AndroidAccuracyMode.LOW -> Priority.PRIORITY_LOW_POWER
230
+ AndroidAccuracyMode.PASSIVE -> Priority.PRIORITY_PASSIVE
231
+ }
232
+
233
+ val request = GmsLocationRequest
234
+ .Builder(priority, options.intervalMillis)
235
+ .setMinUpdateIntervalMillis(options.fastestIntervalMillis)
236
+ .setMinUpdateDistanceMeters(options.distanceFilterMeters)
237
+ .build()
238
+
239
+ return LocationSettingsRequest
240
+ .Builder()
241
+ .addLocationRequest(request)
242
+ .setAlwaysShow(options.alwaysShow)
243
+ .setNeedBle(options.needBle)
244
+ .build()
245
+ }
246
+
247
+ private fun isProviderEnabled(provider: String): Boolean {
248
+ return try {
249
+ locationManager.isProviderEnabled(provider)
250
+ } catch (e: Exception) {
251
+ false
252
+ }
253
+ }
254
+
255
+ private fun hasBackgroundLocationPermission(): Boolean {
256
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true
257
+
258
+ return ContextCompat.checkSelfPermission(
259
+ reactContext,
260
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
261
+ ) == PackageManager.PERMISSION_GRANTED
262
+ }
263
+
264
+ private fun getGoogleLocationAccuracyEnabled(success: (Boolean?) -> Unit) {
265
+ if (!isGooglePlayServicesAvailable()) {
266
+ success(null)
267
+ return
268
+ }
269
+
270
+ val didComplete = AtomicBoolean(false)
271
+ val timeoutRunnable = Runnable {
272
+ if (didComplete.compareAndSet(false, true)) {
273
+ success(null)
274
+ }
275
+ }
276
+
277
+ fun complete(value: Boolean?) {
278
+ if (didComplete.compareAndSet(false, true)) {
279
+ mainHandler.removeCallbacks(timeoutRunnable)
280
+ success(value)
281
+ }
282
+ }
283
+
284
+ mainHandler.postDelayed(timeoutRunnable, GOOGLE_LOCATION_ACCURACY_TIMEOUT_MS)
285
+
286
+ try {
287
+ LocationServices
288
+ .getSettingsClient(reactContext)
289
+ .isGoogleLocationAccuracyEnabled
290
+ .addOnSuccessListener { enabled ->
291
+ complete(enabled)
292
+ }
293
+ .addOnFailureListener {
294
+ complete(null)
295
+ }
296
+ .addOnCanceledListener {
297
+ complete(null)
298
+ }
299
+ } catch (e: Exception) {
300
+ complete(null)
301
+ }
302
+ }
303
+
304
+ private fun isGooglePlayServicesAvailable(): Boolean {
305
+ return GoogleApiAvailability.getInstance()
306
+ .isGooglePlayServicesAvailable(reactContext) == ConnectionResult.SUCCESS
307
+ }
308
+
309
+ private companion object {
310
+ private const val INTERNAL_ERROR = -1.0
311
+ private const val SETTINGS_NOT_SATISFIED = 5.0
312
+ }
313
+ }