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
- // 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
- }
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
- // Request fresh location
211
- if (provider == null) {
212
- promise.reject(createLocationError(
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
- requestFreshLocation(provider, parsedOptions) { result ->
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 when {
330
- isProviderValid(preferredProvider) -> preferredProvider
331
- isProviderValid(fallbackProvider) -> fallbackProvider
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
- provider: String,
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, options, id, handler)
451
+ requestCurrentLocationModern(provider, requestId, request.handler, remainingTimeoutMillis)
373
452
  } else {
374
- requestCurrentLocationLegacy(provider, options, id, handler)
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.remove(requestId)
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
- request.resolver(Result.failure(createLocationError(
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, options.timeout.toLong())
494
+ handler.postDelayed(timeoutRunnable, timeoutMillis)
415
495
 
416
496
  pendingPositionRequests[requestId]?.cancellationSignal = cancellationSignal
417
497
 
418
498
  } catch (e: SecurityException) {
419
- pendingPositionRequests.remove(requestId)
420
- val request = pendingPositionRequests[requestId]
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, options.timeout.toLong())
572
+ handler.postDelayed(timeoutRunnable, timeoutMillis)
494
573
 
495
574
  } catch (e: SecurityException) {
496
- pendingPositionRequests.remove(requestId)?.resolver(Result.failure(createLocationError(
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.remove(requestId)
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
- 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))
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) ?: return
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.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": "^5.9.3"
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
+ });
@@ -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
+ });
@@ -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
+ }