react-native-nitro-geolocation 1.1.1 → 1.1.3
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.
|
@@ -29,6 +29,11 @@ private class GeolocationErrorException(
|
|
|
29
29
|
val locationError: LocationError
|
|
30
30
|
) : Exception(locationError.message)
|
|
31
31
|
|
|
32
|
+
private const val NO_LOCATION_PROVIDER_AVAILABLE_MESSAGE = "No location provider available"
|
|
33
|
+
private const val NO_APPROXIMATE_LOCATION_PROVIDER_AVAILABLE_MESSAGE =
|
|
34
|
+
"No location provider is available for approximate location. " +
|
|
35
|
+
"ACCESS_COARSE_LOCATION is granted, but no enabled coarse-compatible provider is available."
|
|
36
|
+
|
|
32
37
|
/**
|
|
33
38
|
* Modern Geolocation implementation for Android.
|
|
34
39
|
*
|
|
@@ -85,8 +90,15 @@ class NitroGeolocation(
|
|
|
85
90
|
val resolver: (Result<GeolocationResponse>) -> Unit,
|
|
86
91
|
val options: ParsedOptions,
|
|
87
92
|
val handler: Handler,
|
|
93
|
+
val providers: List<String>,
|
|
94
|
+
val deadlineElapsedRealtime: Long,
|
|
95
|
+
var providerIndex: Int = 0,
|
|
88
96
|
var cancellationSignal: CancellationSignal? = null
|
|
89
|
-
)
|
|
97
|
+
) {
|
|
98
|
+
fun remainingTimeoutMillis(): Long {
|
|
99
|
+
return (deadlineElapsedRealtime - SystemClock.elapsedRealtime()).coerceAtLeast(0L)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
90
102
|
|
|
91
103
|
// MARK: - Properties
|
|
92
104
|
|
|
@@ -184,39 +196,20 @@ class NitroGeolocation(
|
|
|
184
196
|
|
|
185
197
|
val parsedOptions = ParsedOptions.parse(options)
|
|
186
198
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
}
|
|
201
|
-
|
|
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
|
-
}
|
|
199
|
+
val providers = getValidProviders(parsedOptions.enableHighAccuracy)
|
|
200
|
+
if (providers.isEmpty()) {
|
|
201
|
+
promise.reject(createNoLocationProviderError(parsedOptions))
|
|
202
|
+
return promise
|
|
208
203
|
}
|
|
209
204
|
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
promise.
|
|
213
|
-
POSITION_UNAVAILABLE,
|
|
214
|
-
"No location provider available"
|
|
215
|
-
))
|
|
205
|
+
val cachedLocation = getBestCachedLocation(providers, parsedOptions)
|
|
206
|
+
if (cachedLocation != null) {
|
|
207
|
+
promise.resolve(locationToPosition(cachedLocation))
|
|
216
208
|
return promise
|
|
217
209
|
}
|
|
218
210
|
|
|
219
|
-
|
|
211
|
+
// Request fresh location
|
|
212
|
+
requestFreshLocation(providers, parsedOptions) { result ->
|
|
220
213
|
result.fold(
|
|
221
214
|
onSuccess = { promise.resolve(it) },
|
|
222
215
|
onFailure = { promise.reject(it) }
|
|
@@ -299,6 +292,20 @@ class NitroGeolocation(
|
|
|
299
292
|
return getCurrentPermissionStatus() == PermissionStatus.GRANTED
|
|
300
293
|
}
|
|
301
294
|
|
|
295
|
+
private fun hasFineLocationPermission(): Boolean {
|
|
296
|
+
return ContextCompat.checkSelfPermission(
|
|
297
|
+
reactContext,
|
|
298
|
+
Manifest.permission.ACCESS_FINE_LOCATION
|
|
299
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private fun hasCoarseLocationPermission(): Boolean {
|
|
303
|
+
return ContextCompat.checkSelfPermission(
|
|
304
|
+
reactContext,
|
|
305
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
306
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
307
|
+
}
|
|
308
|
+
|
|
302
309
|
// Handle permission request result (called from Activity)
|
|
303
310
|
fun onPermissionResult(requestCode: Int, grantResults: IntArray) {
|
|
304
311
|
if (requestCode != PERMISSION_REQUEST_CODE) return
|
|
@@ -316,6 +323,10 @@ class NitroGeolocation(
|
|
|
316
323
|
// MARK: - Helper Functions - Provider Selection
|
|
317
324
|
|
|
318
325
|
private fun getValidProvider(highAccuracy: Boolean): String? {
|
|
326
|
+
return getValidProviders(highAccuracy).firstOrNull()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private fun getValidProviders(highAccuracy: Boolean): List<String> {
|
|
319
330
|
val preferredProvider = if (highAccuracy)
|
|
320
331
|
AndroidLocationManager.GPS_PROVIDER
|
|
321
332
|
else
|
|
@@ -326,21 +337,44 @@ class NitroGeolocation(
|
|
|
326
337
|
else
|
|
327
338
|
AndroidLocationManager.GPS_PROVIDER
|
|
328
339
|
|
|
329
|
-
return
|
|
330
|
-
|
|
331
|
-
isProviderValid(
|
|
332
|
-
else -> null
|
|
333
|
-
}
|
|
340
|
+
return listOf(preferredProvider, fallbackProvider)
|
|
341
|
+
.distinct()
|
|
342
|
+
.filter { provider -> isProviderValid(provider) }
|
|
334
343
|
}
|
|
335
344
|
|
|
336
345
|
private fun isProviderValid(provider: String): Boolean {
|
|
337
346
|
return try {
|
|
338
|
-
locationManager.isProviderEnabled(provider)
|
|
347
|
+
if (!locationManager.isProviderEnabled(provider)) return false
|
|
348
|
+
|
|
349
|
+
when (provider) {
|
|
350
|
+
AndroidLocationManager.GPS_PROVIDER -> hasFineLocationPermission()
|
|
351
|
+
AndroidLocationManager.NETWORK_PROVIDER -> hasCoarseLocationPermission() || hasFineLocationPermission()
|
|
352
|
+
else -> hasLocationPermission()
|
|
353
|
+
}
|
|
339
354
|
} catch (e: Exception) {
|
|
340
355
|
false
|
|
341
356
|
}
|
|
342
357
|
}
|
|
343
358
|
|
|
359
|
+
private fun createNoLocationProviderError(options: ParsedOptions): Exception {
|
|
360
|
+
return createLocationError(
|
|
361
|
+
POSITION_UNAVAILABLE,
|
|
362
|
+
getNoLocationProviderMessage(options)
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private fun getNoLocationProviderMessage(options: ParsedOptions): String {
|
|
367
|
+
if (
|
|
368
|
+
!options.enableHighAccuracy &&
|
|
369
|
+
hasCoarseLocationPermission() &&
|
|
370
|
+
!hasFineLocationPermission()
|
|
371
|
+
) {
|
|
372
|
+
return NO_APPROXIMATE_LOCATION_PROVIDER_AVAILABLE_MESSAGE
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return NO_LOCATION_PROVIDER_AVAILABLE_MESSAGE
|
|
376
|
+
}
|
|
377
|
+
|
|
344
378
|
// MARK: - Helper Functions - Cache Validation
|
|
345
379
|
|
|
346
380
|
private fun isCachedLocationValid(location: Location, options: ParsedOptions): Boolean {
|
|
@@ -348,10 +382,32 @@ class NitroGeolocation(
|
|
|
348
382
|
return locationAge < options.maximumAge
|
|
349
383
|
}
|
|
350
384
|
|
|
385
|
+
private fun getBestCachedLocation(providers: List<String>, options: ParsedOptions): Location? {
|
|
386
|
+
var bestLocation: Location? = null
|
|
387
|
+
|
|
388
|
+
for (provider in providers) {
|
|
389
|
+
val lastKnownLocation = try {
|
|
390
|
+
locationManager.getLastKnownLocation(provider)
|
|
391
|
+
} catch (e: SecurityException) {
|
|
392
|
+
null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (
|
|
396
|
+
lastKnownLocation != null &&
|
|
397
|
+
(isCachedLocationValid(lastKnownLocation, options) ||
|
|
398
|
+
options.maximumAge == Double.POSITIVE_INFINITY)
|
|
399
|
+
) {
|
|
400
|
+
bestLocation = selectBestLocation(lastKnownLocation, bestLocation)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return bestLocation
|
|
405
|
+
}
|
|
406
|
+
|
|
351
407
|
// MARK: - Helper Functions - Request Fresh Location
|
|
352
408
|
|
|
353
409
|
private fun requestFreshLocation(
|
|
354
|
-
|
|
410
|
+
providers: List<String>,
|
|
355
411
|
options: ParsedOptions,
|
|
356
412
|
resolver: (Result<GeolocationResponse>) -> Unit
|
|
357
413
|
) {
|
|
@@ -362,25 +418,48 @@ class NitroGeolocation(
|
|
|
362
418
|
id = id,
|
|
363
419
|
resolver = resolver,
|
|
364
420
|
options = options,
|
|
365
|
-
handler = handler
|
|
421
|
+
handler = handler,
|
|
422
|
+
providers = providers,
|
|
423
|
+
deadlineElapsedRealtime = createRequestDeadlineElapsedRealtime(options.timeout)
|
|
366
424
|
)
|
|
367
425
|
|
|
368
426
|
pendingPositionRequests[id] = request
|
|
427
|
+
requestFreshLocationForCurrentProvider(id)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private fun requestFreshLocationForCurrentProvider(requestId: UUID) {
|
|
431
|
+
val request = pendingPositionRequests[requestId] ?: return
|
|
432
|
+
val provider = request.providers.getOrNull(request.providerIndex)
|
|
433
|
+
val remainingTimeoutMillis = request.remainingTimeoutMillis()
|
|
434
|
+
|
|
435
|
+
if (provider == null) {
|
|
436
|
+
pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
|
|
437
|
+
createNoLocationProviderError(request.options)
|
|
438
|
+
))
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (remainingTimeoutMillis <= 0L) {
|
|
443
|
+
pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
|
|
444
|
+
createPositionTimeoutError(request.options)
|
|
445
|
+
))
|
|
446
|
+
return
|
|
447
|
+
}
|
|
369
448
|
|
|
370
449
|
// Use modern API on Android 11+
|
|
371
450
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
372
|
-
requestCurrentLocationModern(provider,
|
|
451
|
+
requestCurrentLocationModern(provider, requestId, request.handler, remainingTimeoutMillis)
|
|
373
452
|
} else {
|
|
374
|
-
requestCurrentLocationLegacy(provider,
|
|
453
|
+
requestCurrentLocationLegacy(provider, requestId, request.handler, remainingTimeoutMillis)
|
|
375
454
|
}
|
|
376
455
|
}
|
|
377
456
|
|
|
378
457
|
@androidx.annotation.RequiresApi(Build.VERSION_CODES.R)
|
|
379
458
|
private fun requestCurrentLocationModern(
|
|
380
459
|
provider: String,
|
|
381
|
-
options: ParsedOptions,
|
|
382
460
|
requestId: UUID,
|
|
383
|
-
handler: Handler
|
|
461
|
+
handler: Handler,
|
|
462
|
+
timeoutMillis: Long
|
|
384
463
|
) {
|
|
385
464
|
val cancellationSignal = CancellationSignal()
|
|
386
465
|
|
|
@@ -397,39 +476,39 @@ class NitroGeolocation(
|
|
|
397
476
|
) { location ->
|
|
398
477
|
handler.removeCallbacks(timeoutRunnable)
|
|
399
478
|
|
|
400
|
-
val request = pendingPositionRequests
|
|
479
|
+
val request = pendingPositionRequests[requestId]
|
|
401
480
|
if (request != null) {
|
|
402
481
|
if (location != null) {
|
|
482
|
+
pendingPositionRequests.remove(requestId)
|
|
403
483
|
val position = locationToPosition(location)
|
|
404
484
|
request.resolver(Result.success(position))
|
|
405
485
|
} else {
|
|
406
|
-
|
|
486
|
+
handleProviderFailure(requestId, createLocationError(
|
|
407
487
|
POSITION_UNAVAILABLE,
|
|
408
488
|
"Unable to get location"
|
|
409
|
-
))
|
|
489
|
+
))
|
|
410
490
|
}
|
|
411
491
|
}
|
|
412
492
|
}
|
|
413
493
|
|
|
414
|
-
handler.postDelayed(timeoutRunnable,
|
|
494
|
+
handler.postDelayed(timeoutRunnable, timeoutMillis)
|
|
415
495
|
|
|
416
496
|
pendingPositionRequests[requestId]?.cancellationSignal = cancellationSignal
|
|
417
497
|
|
|
418
498
|
} catch (e: SecurityException) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
request?.resolver(Result.failure(createLocationError(
|
|
499
|
+
handler.removeCallbacks(timeoutRunnable)
|
|
500
|
+
handleProviderFailure(requestId, createLocationError(
|
|
422
501
|
PERMISSION_DENIED,
|
|
423
502
|
"Security exception: ${e.message}"
|
|
424
|
-
))
|
|
503
|
+
))
|
|
425
504
|
}
|
|
426
505
|
}
|
|
427
506
|
|
|
428
507
|
private fun requestCurrentLocationLegacy(
|
|
429
508
|
provider: String,
|
|
430
|
-
options: ParsedOptions,
|
|
431
509
|
requestId: UUID,
|
|
432
|
-
handler: Handler
|
|
510
|
+
handler: Handler,
|
|
511
|
+
timeoutMillis: Long
|
|
433
512
|
) {
|
|
434
513
|
var isResolved = false
|
|
435
514
|
var oldLocation: Location? = null
|
|
@@ -490,16 +569,38 @@ class NitroGeolocation(
|
|
|
490
569
|
Looper.getMainLooper()
|
|
491
570
|
)
|
|
492
571
|
|
|
493
|
-
handler.postDelayed(timeoutRunnable,
|
|
572
|
+
handler.postDelayed(timeoutRunnable, timeoutMillis)
|
|
494
573
|
|
|
495
574
|
} catch (e: SecurityException) {
|
|
496
|
-
|
|
575
|
+
handleProviderFailure(requestId, createLocationError(
|
|
497
576
|
PERMISSION_DENIED,
|
|
498
577
|
"Security exception: ${e.message}"
|
|
499
|
-
))
|
|
578
|
+
))
|
|
500
579
|
}
|
|
501
580
|
}
|
|
502
581
|
|
|
582
|
+
private fun handleProviderFailure(requestId: UUID, error: Exception) {
|
|
583
|
+
val request = pendingPositionRequests[requestId] ?: return
|
|
584
|
+
|
|
585
|
+
request.cancellationSignal?.cancel()
|
|
586
|
+
request.cancellationSignal = null
|
|
587
|
+
request.providerIndex += 1
|
|
588
|
+
|
|
589
|
+
if (request.providerIndex < request.providers.size) {
|
|
590
|
+
if (request.remainingTimeoutMillis() <= 0L) {
|
|
591
|
+
pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
|
|
592
|
+
createPositionTimeoutError(request.options)
|
|
593
|
+
))
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
requestFreshLocationForCurrentProvider(requestId)
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
pendingPositionRequests.remove(requestId)?.resolver(Result.failure(error))
|
|
602
|
+
}
|
|
603
|
+
|
|
503
604
|
private fun selectBestLocation(newLocation: Location, currentBest: Location?): Location {
|
|
504
605
|
if (currentBest == null) return newLocation
|
|
505
606
|
|
|
@@ -526,16 +627,15 @@ class NitroGeolocation(
|
|
|
526
627
|
}
|
|
527
628
|
|
|
528
629
|
private fun handlePositionTimeout(requestId: UUID) {
|
|
529
|
-
val request = pendingPositionRequests
|
|
630
|
+
val request = pendingPositionRequests[requestId]
|
|
530
631
|
if (request != null) {
|
|
531
|
-
request.cancellationSignal?.cancel()
|
|
532
632
|
request.handler.removeCallbacksAndMessages(null)
|
|
633
|
+
request.cancellationSignal?.cancel()
|
|
634
|
+
request.cancellationSignal = null
|
|
533
635
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
request.resolver(Result.failure(error))
|
|
636
|
+
pendingPositionRequests.remove(requestId)?.resolver(Result.failure(
|
|
637
|
+
createPositionTimeoutError(request.options)
|
|
638
|
+
))
|
|
539
639
|
}
|
|
540
640
|
}
|
|
541
641
|
|
|
@@ -555,7 +655,11 @@ class NitroGeolocation(
|
|
|
555
655
|
smallestDistanceFilter = minOf(smallestDistanceFilter, subscription.options.distanceFilter.toFloat())
|
|
556
656
|
}
|
|
557
657
|
|
|
558
|
-
val provider = getValidProvider(useHighAccuracy)
|
|
658
|
+
val provider = getValidProvider(useHighAccuracy)
|
|
659
|
+
if (provider == null) {
|
|
660
|
+
notifyWatchProviderUnavailable()
|
|
661
|
+
return
|
|
662
|
+
}
|
|
559
663
|
currentWatchProvider = provider
|
|
560
664
|
|
|
561
665
|
val listener = object : LocationListener {
|
|
@@ -606,6 +710,15 @@ class NitroGeolocation(
|
|
|
606
710
|
}
|
|
607
711
|
}
|
|
608
712
|
|
|
713
|
+
private fun notifyWatchProviderUnavailable() {
|
|
714
|
+
for ((_, subscription) in watchSubscriptions) {
|
|
715
|
+
subscription.error?.invoke(LocationError(
|
|
716
|
+
code = POSITION_UNAVAILABLE,
|
|
717
|
+
message = getNoLocationProviderMessage(subscription.options)
|
|
718
|
+
))
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
609
722
|
private fun stopWatchingLocation() {
|
|
610
723
|
watchLocationListener?.let { listener ->
|
|
611
724
|
try {
|
|
@@ -666,6 +779,32 @@ class NitroGeolocation(
|
|
|
666
779
|
return GeolocationErrorException(locationError)
|
|
667
780
|
}
|
|
668
781
|
|
|
782
|
+
private fun createPositionTimeoutError(options: ParsedOptions): Exception {
|
|
783
|
+
val timeoutSeconds = options.timeout / 1000.0
|
|
784
|
+
val message = String.format("Unable to fetch location within %.1fs.", timeoutSeconds)
|
|
785
|
+
return createLocationError(TIMEOUT, message)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private fun createRequestDeadlineElapsedRealtime(timeout: Double): Long {
|
|
789
|
+
val now = SystemClock.elapsedRealtime()
|
|
790
|
+
val timeoutMillis = coerceTimeoutMillis(timeout)
|
|
791
|
+
val maxTimeoutMillis = Long.MAX_VALUE - now
|
|
792
|
+
|
|
793
|
+
return if (timeoutMillis >= maxTimeoutMillis) {
|
|
794
|
+
Long.MAX_VALUE
|
|
795
|
+
} else {
|
|
796
|
+
now + timeoutMillis
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private fun coerceTimeoutMillis(timeout: Double): Long {
|
|
801
|
+
return when {
|
|
802
|
+
timeout.isNaN() || timeout <= 0.0 -> 0L
|
|
803
|
+
timeout.isInfinite() || timeout >= Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
|
|
804
|
+
else -> timeout.toLong()
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
669
808
|
companion object {
|
|
670
809
|
private const val PERMISSION_REQUEST_CODE = 8947
|
|
671
810
|
private const val TWO_MINUTES_MS = 2 * 60 * 1000L
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-geolocation",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "⚡🚀Blazing-fast geolocation for React Native powered by Nitro Modules",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"source": "src/index",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"jsSrcsDir": "src"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"typecheck": "tsc --noEmit"
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest --run",
|
|
25
|
+
"test:watch": "vitest"
|
|
24
26
|
},
|
|
25
27
|
"author": "jingjing2222",
|
|
26
28
|
"license": "MIT",
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
"README.md"
|
|
51
53
|
],
|
|
52
54
|
"devDependencies": {
|
|
55
|
+
"@granite-js/vitest": "1.0.23",
|
|
53
56
|
"@react-native/babel-preset": "0.81.1",
|
|
54
57
|
"@tsconfig/react-native": "^3.0.7",
|
|
55
58
|
"@types/react": "^19.1.0",
|
|
@@ -57,7 +60,8 @@
|
|
|
57
60
|
"react": "19.1.0",
|
|
58
61
|
"react-native": "0.81.1",
|
|
59
62
|
"react-native-nitro-modules": "0.35.0",
|
|
60
|
-
"typescript": "
|
|
63
|
+
"typescript": "5.9.3",
|
|
64
|
+
"vitest": "4.1.5"
|
|
61
65
|
},
|
|
62
66
|
"peerDependencies": {
|
|
63
67
|
"react": ">=18.0.0",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { isDevtoolsEnabled } from "./index";
|
|
3
|
+
|
|
4
|
+
const globalState = globalThis as typeof globalThis & {
|
|
5
|
+
__DEV__?: boolean;
|
|
6
|
+
__geolocationDevToolsEnabled?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const originalState = {
|
|
10
|
+
hasDev: Object.prototype.hasOwnProperty.call(globalState, "__DEV__"),
|
|
11
|
+
dev: globalState.__DEV__,
|
|
12
|
+
hasDevtoolsEnabled: Object.prototype.hasOwnProperty.call(
|
|
13
|
+
globalState,
|
|
14
|
+
"__geolocationDevToolsEnabled"
|
|
15
|
+
),
|
|
16
|
+
devtoolsEnabled: globalState.__geolocationDevToolsEnabled
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("isDevtoolsEnabled", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (originalState.hasDev) {
|
|
22
|
+
globalState.__DEV__ = originalState.dev;
|
|
23
|
+
} else {
|
|
24
|
+
globalState.__DEV__ = undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (originalState.hasDevtoolsEnabled) {
|
|
28
|
+
globalState.__geolocationDevToolsEnabled = originalState.devtoolsEnabled;
|
|
29
|
+
} else {
|
|
30
|
+
globalState.__geolocationDevToolsEnabled = undefined;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("enables devtools only when React Native __DEV__ and the devtools flag are true", () => {
|
|
35
|
+
globalState.__DEV__ = true;
|
|
36
|
+
globalState.__geolocationDevToolsEnabled = true;
|
|
37
|
+
|
|
38
|
+
expect(isDevtoolsEnabled()).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("ignores the devtools flag outside React Native __DEV__", () => {
|
|
42
|
+
globalState.__DEV__ = false;
|
|
43
|
+
globalState.__geolocationDevToolsEnabled = true;
|
|
44
|
+
|
|
45
|
+
expect(isDevtoolsEnabled()).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("stays disabled when __DEV__ is unavailable", () => {
|
|
49
|
+
globalState.__DEV__ = undefined;
|
|
50
|
+
globalState.__geolocationDevToolsEnabled = true;
|
|
51
|
+
|
|
52
|
+
expect(isDevtoolsEnabled()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/devtools/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { GeolocationResponse } from "../types";
|
|
2
2
|
|
|
3
|
+
declare const __DEV__: boolean;
|
|
4
|
+
|
|
3
5
|
declare global {
|
|
4
6
|
var __geolocationDevToolsEnabled: boolean | undefined;
|
|
5
7
|
var __geolocationDevtools: DevtoolsState | undefined;
|
|
@@ -18,6 +20,10 @@ export function getDevtoolsState(): DevtoolsState {
|
|
|
18
20
|
return globalThis.__geolocationDevtools;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function isReactNativeDev(): boolean {
|
|
24
|
+
return typeof __DEV__ !== "undefined" && __DEV__ === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
export function isDevtoolsEnabled(): boolean {
|
|
22
|
-
return globalThis.__geolocationDevToolsEnabled === true;
|
|
28
|
+
return isReactNativeDev() && globalThis.__geolocationDevToolsEnabled === true;
|
|
23
29
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { selectProviderForAndroidPermissions } from "./provider";
|
|
3
|
+
|
|
4
|
+
describe("selectProviderForAndroidPermissions", () => {
|
|
5
|
+
it("prefers the network provider for low-accuracy coarse-only requests", () => {
|
|
6
|
+
expect(
|
|
7
|
+
selectProviderForAndroidPermissions({
|
|
8
|
+
enableHighAccuracy: false,
|
|
9
|
+
providers: {
|
|
10
|
+
gps: true,
|
|
11
|
+
network: true
|
|
12
|
+
},
|
|
13
|
+
permissions: {
|
|
14
|
+
fine: false,
|
|
15
|
+
coarse: true
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
).toBe("network");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not fall back to GPS when only coarse permission is granted", () => {
|
|
22
|
+
expect(
|
|
23
|
+
selectProviderForAndroidPermissions({
|
|
24
|
+
enableHighAccuracy: false,
|
|
25
|
+
providers: {
|
|
26
|
+
gps: true,
|
|
27
|
+
network: false
|
|
28
|
+
},
|
|
29
|
+
permissions: {
|
|
30
|
+
fine: false,
|
|
31
|
+
coarse: true
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not use GPS for high-accuracy coarse-only requests when network is unavailable", () => {
|
|
38
|
+
expect(
|
|
39
|
+
selectProviderForAndroidPermissions({
|
|
40
|
+
enableHighAccuracy: true,
|
|
41
|
+
providers: {
|
|
42
|
+
gps: true,
|
|
43
|
+
network: false
|
|
44
|
+
},
|
|
45
|
+
permissions: {
|
|
46
|
+
fine: false,
|
|
47
|
+
coarse: true
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("can satisfy high-accuracy requests with a coarse-compatible fallback", () => {
|
|
54
|
+
expect(
|
|
55
|
+
selectProviderForAndroidPermissions({
|
|
56
|
+
enableHighAccuracy: true,
|
|
57
|
+
providers: {
|
|
58
|
+
gps: true,
|
|
59
|
+
network: true
|
|
60
|
+
},
|
|
61
|
+
permissions: {
|
|
62
|
+
fine: false,
|
|
63
|
+
coarse: true
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
).toBe("network");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("uses GPS as the low-accuracy fallback only when fine permission is granted", () => {
|
|
70
|
+
expect(
|
|
71
|
+
selectProviderForAndroidPermissions({
|
|
72
|
+
enableHighAccuracy: false,
|
|
73
|
+
providers: {
|
|
74
|
+
gps: true,
|
|
75
|
+
network: false
|
|
76
|
+
},
|
|
77
|
+
permissions: {
|
|
78
|
+
fine: true,
|
|
79
|
+
coarse: true
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
).toBe("gps");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("uses GPS for high-accuracy requests when fine permission is granted", () => {
|
|
86
|
+
expect(
|
|
87
|
+
selectProviderForAndroidPermissions({
|
|
88
|
+
enableHighAccuracy: true,
|
|
89
|
+
providers: {
|
|
90
|
+
gps: true,
|
|
91
|
+
network: true
|
|
92
|
+
},
|
|
93
|
+
permissions: {
|
|
94
|
+
fine: true,
|
|
95
|
+
coarse: true
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
).toBe("gps");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("rejects provider selection when no Android location permission is granted", () => {
|
|
102
|
+
expect(
|
|
103
|
+
selectProviderForAndroidPermissions({
|
|
104
|
+
enableHighAccuracy: false,
|
|
105
|
+
providers: {
|
|
106
|
+
gps: true,
|
|
107
|
+
network: true
|
|
108
|
+
},
|
|
109
|
+
permissions: {
|
|
110
|
+
fine: false,
|
|
111
|
+
coarse: false
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects provider selection when every compatible provider is disabled", () => {
|
|
118
|
+
expect(
|
|
119
|
+
selectProviderForAndroidPermissions({
|
|
120
|
+
enableHighAccuracy: false,
|
|
121
|
+
providers: {
|
|
122
|
+
gps: false,
|
|
123
|
+
network: false
|
|
124
|
+
},
|
|
125
|
+
permissions: {
|
|
126
|
+
fine: true,
|
|
127
|
+
coarse: true
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
package/src/utils/provider.ts
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type Provider = "gps" | "network" | null;
|
|
5
5
|
|
|
6
|
+
export interface AndroidProviderSelectionInput {
|
|
7
|
+
enableHighAccuracy: boolean;
|
|
8
|
+
providers: {
|
|
9
|
+
gps: boolean;
|
|
10
|
+
network: boolean;
|
|
11
|
+
};
|
|
12
|
+
permissions: {
|
|
13
|
+
fine: boolean;
|
|
14
|
+
coarse: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* Selects the best available location provider based on user preferences
|
|
8
20
|
* and provider availability.
|
|
@@ -59,3 +71,15 @@ export function selectProvider(
|
|
|
59
71
|
|
|
60
72
|
return null;
|
|
61
73
|
}
|
|
74
|
+
|
|
75
|
+
export function selectProviderForAndroidPermissions({
|
|
76
|
+
enableHighAccuracy,
|
|
77
|
+
providers,
|
|
78
|
+
permissions
|
|
79
|
+
}: AndroidProviderSelectionInput): Provider {
|
|
80
|
+
const gpsAvailable = providers.gps && permissions.fine;
|
|
81
|
+
const networkAvailable =
|
|
82
|
+
providers.network && (permissions.coarse || permissions.fine);
|
|
83
|
+
|
|
84
|
+
return selectProvider(enableHighAccuracy, gpsAvailable, networkAvailable);
|
|
85
|
+
}
|