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