@nitra/geolocation 7.1.5 → 8.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.
@@ -1,7 +1,14 @@
1
1
  package com.capacitorjs.plugins.geolocation
2
2
 
3
3
  import android.Manifest
4
+ import android.content.Context
5
+ import android.location.Location
6
+ import android.location.LocationListener
7
+ import android.location.LocationManager
8
+ import android.location.OnNmeaMessageListener
4
9
  import android.os.Build
10
+ import android.os.Handler
11
+ import android.os.Looper
5
12
  import androidx.activity.result.contract.ActivityResultContracts
6
13
  import com.getcapacitor.JSObject
7
14
  import com.getcapacitor.PermissionState
@@ -11,15 +18,22 @@ import com.getcapacitor.PluginMethod
11
18
  import com.getcapacitor.annotation.CapacitorPlugin
12
19
  import com.getcapacitor.annotation.Permission
13
20
  import com.getcapacitor.annotation.PermissionCallback
14
- import com.google.android.gms.location.LocationServices
15
21
  import io.ionic.libs.iongeolocationlib.controller.IONGLOCController
16
22
  import io.ionic.libs.iongeolocationlib.model.IONGLOCException
17
23
  import io.ionic.libs.iongeolocationlib.model.IONGLOCLocationOptions
18
24
  import io.ionic.libs.iongeolocationlib.model.IONGLOCLocationResult
25
+ import kotlinx.coroutines.async
19
26
  import kotlinx.coroutines.CoroutineScope
20
27
  import kotlinx.coroutines.Dispatchers
21
28
  import kotlinx.coroutines.cancel
22
29
  import kotlinx.coroutines.launch
30
+ import kotlinx.coroutines.suspendCancellableCoroutine
31
+ import kotlinx.coroutines.withTimeout
32
+ import java.util.Calendar
33
+ import java.util.Locale
34
+ import java.util.TimeZone
35
+ import kotlin.coroutines.resume
36
+ import org.json.JSONObject
23
37
 
24
38
  @CapacitorPlugin(
25
39
  name = "Geolocation",
@@ -37,9 +51,16 @@ class GeolocationPlugin : Plugin() {
37
51
  private lateinit var coroutineScope: CoroutineScope
38
52
  private val watchingCalls: MutableMap<String, PluginCall> = mutableMapOf()
39
53
 
54
+ private data class GnssTimeResult(
55
+ val timestamp: Long,
56
+ val status: String?,
57
+ val nmea: String
58
+ )
59
+
40
60
  companion object {
41
61
  const val LOCATION_ALIAS: String = "location"
42
62
  const val COARSE_LOCATION_ALIAS: String = "coarseLocation"
63
+ const val WATCH_SATELLITE_TIME_TIMEOUT_MS: Long = 1000
43
64
  }
44
65
 
45
66
  override fun load() {
@@ -54,11 +75,7 @@ class GeolocationPlugin : Plugin() {
54
75
  }
55
76
  }
56
77
 
57
- this.controller = IONGLOCController(
58
- LocationServices.getFusedLocationProviderClient(context),
59
- activityLauncher
60
- )
61
-
78
+ this.controller = IONGLOCController(context, activityLauncher)
62
79
  }
63
80
 
64
81
  override fun handleOnDestroy() {
@@ -82,7 +99,7 @@ class GeolocationPlugin : Plugin() {
82
99
  * @param onLocationEnabled lambda function to use in case location services are enabled
83
100
  */
84
101
  private fun checkLocationState(call: PluginCall, onLocationEnabled: () -> Unit) {
85
- if (controller.areLocationServicesEnabled(context)) {
102
+ if (controller.areLocationServicesEnabled()) {
86
103
  onLocationEnabled()
87
104
  } else {
88
105
  call.sendError(GeolocationErrors.LOCATION_DISABLED)
@@ -120,7 +137,10 @@ class GeolocationPlugin : Plugin() {
120
137
  callbackName: String,
121
138
  onPermissionGranted: () -> Unit
122
139
  ) {
123
- val alias = getAlias(call)
140
+ val alias = getAlias(call) ?: run {
141
+ call.sendError(GeolocationErrors.LOCATION_MANIFEST_PERMISSIONS_MISSING)
142
+ return
143
+ }
124
144
  if (getPermissionState(alias) != PermissionState.GRANTED) {
125
145
  requestPermissionForAlias(alias, call, callbackName)
126
146
  } else {
@@ -161,6 +181,37 @@ class GeolocationPlugin : Plugin() {
161
181
  }
162
182
  }
163
183
 
184
+ /**
185
+ * Requests precise location permission. GNSS NMEA data should not be exposed through coarse permission.
186
+ * @param call the PluginCall to use in case we want to send an error
187
+ * @param callbackName a string identifying the callback to call once the permission prompt is answered
188
+ * @param onPermissionGranted lambda function to use in case the permission is enabled
189
+ */
190
+ private fun handlePrecisePermissionRequest(
191
+ call: PluginCall,
192
+ callbackName: String,
193
+ onPermissionGranted: () -> Unit
194
+ ) {
195
+ if (getPermissionState(LOCATION_ALIAS) != PermissionState.GRANTED) {
196
+ requestPermissionForAlias(LOCATION_ALIAS, call, callbackName)
197
+ } else {
198
+ onPermissionGranted()
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Handles precise location permission results.
204
+ * @param call the PluginCall to use in case we want to send an error
205
+ * @param onPermissionGranted lambda function to use in case the permission was granted
206
+ */
207
+ private fun handlePrecisePermissionResult(call: PluginCall, onPermissionGranted: () -> Unit) {
208
+ if (getPermissionState(LOCATION_ALIAS) == PermissionState.GRANTED) {
209
+ onPermissionGranted()
210
+ } else {
211
+ call.sendError(GeolocationErrors.LOCATION_PERMISSIONS_DENIED)
212
+ }
213
+ }
214
+
164
215
  /**
165
216
  * Clears the watch, removing location updates.
166
217
  * @param call the plugin call
@@ -182,19 +233,20 @@ class GeolocationPlugin : Plugin() {
182
233
  }
183
234
 
184
235
  /**
185
- * Gets the appropriate permission alias
236
+ * Gets the appropriate permission alias, based on the Android version,
237
+ * the permissions declared in the manifest
238
+ * and the enableHighAccuracy option provided by the caller.
186
239
  * @param call the plugin call
187
- * @return String with correct alias
240
+ * @return String with correct alias or null if no permissions can be requested
188
241
  */
189
- private fun getAlias(call: PluginCall): String {
190
- var alias = LOCATION_ALIAS
191
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
192
- val enableHighAccuracy = call.getBoolean("enableHighAccuracy") ?: false
193
- if (!enableHighAccuracy) {
194
- alias = COARSE_LOCATION_ALIAS
195
- }
196
- }
197
- return alias
242
+ private fun getAlias(call: PluginCall): String? {
243
+ val hasFine = isPermissionDeclared(LOCATION_ALIAS)
244
+ val hasCoarse = isPermissionDeclared(COARSE_LOCATION_ALIAS)
245
+ if (!hasFine && !hasCoarse) return null
246
+ val enableHighAccuracy = call.getBoolean("enableHighAccuracy") ?: false
247
+ val shouldRequestFine =
248
+ hasFine && (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || enableHighAccuracy)
249
+ return if (shouldRequestFine) LOCATION_ALIAS else COARSE_LOCATION_ALIAS
198
250
  }
199
251
 
200
252
  /**
@@ -204,20 +256,152 @@ class GeolocationPlugin : Plugin() {
204
256
  private fun getPosition(call: PluginCall) {
205
257
  coroutineScope.launch {
206
258
  val locationOptions = createOptions(call)
259
+ val gnssTime = async {
260
+ getGnssTimeOrNull(locationOptions.timeout)
261
+ }
207
262
 
208
263
  // call getCurrentPosition method from controller
209
264
  val locationResult = controller.getCurrentPosition(activity, locationOptions)
210
265
 
211
266
  locationResult
212
267
  .onSuccess { location ->
213
- call.sendSuccess(getJSObjectForLocation(location))
268
+ call.sendSuccess(getJSObjectForLocation(location, gnssTime.await()))
214
269
  }
215
270
  .onFailure { exception ->
271
+ gnssTime.cancel()
216
272
  onLocationError(exception, call)
217
273
  }
218
274
  }
219
275
  }
220
276
 
277
+ /**
278
+ * Waits until the GNSS receiver emits an RMC NMEA sentence containing date and UTC time.
279
+ * @return satellite time parsed from NMEA
280
+ */
281
+ private suspend fun awaitSatelliteTime(): GnssTimeResult {
282
+ val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
283
+
284
+ if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
285
+ throw IllegalStateException("GPS provider is disabled.")
286
+ }
287
+
288
+ return suspendCancellableCoroutine { continuation ->
289
+ lateinit var listener: OnNmeaMessageListener
290
+ val locationListener = object : LocationListener {
291
+ override fun onLocationChanged(location: Location) = Unit
292
+ }
293
+
294
+ fun cleanup() {
295
+ locationManager.removeNmeaListener(listener)
296
+ locationManager.removeUpdates(locationListener)
297
+ }
298
+
299
+ listener = OnNmeaMessageListener { message, _ ->
300
+ val gnssTime = parseSatelliteTime(message)
301
+ if (gnssTime != null && continuation.isActive) {
302
+ cleanup()
303
+ continuation.resume(gnssTime)
304
+ }
305
+ }
306
+
307
+ val registered = locationManager.addNmeaListener(listener, Handler(Looper.getMainLooper()))
308
+ if (!registered && continuation.isActive) {
309
+ continuation.cancel(IllegalStateException("Unable to register NMEA listener."))
310
+ return@suspendCancellableCoroutine
311
+ }
312
+
313
+ try {
314
+ locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 0f, locationListener)
315
+ } catch (exception: Throwable) {
316
+ cleanup()
317
+ continuation.cancel(exception)
318
+ return@suspendCancellableCoroutine
319
+ }
320
+
321
+ continuation.invokeOnCancellation {
322
+ cleanup()
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Returns satellite time if GNSS RMC NMEA data is available within the timeout.
329
+ * Position responses use null instead of failing when GNSS time is unavailable.
330
+ * @param timeout maximum wait time in milliseconds
331
+ * @return satellite time parsed from NMEA or null
332
+ */
333
+ private suspend fun getGnssTimeOrNull(timeout: Long): GnssTimeResult? {
334
+ if (timeout <= 0) {
335
+ return null
336
+ }
337
+
338
+ return try {
339
+ withTimeout(timeout) {
340
+ awaitSatelliteTime()
341
+ }
342
+ } catch (_: Throwable) {
343
+ null
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Parses UTC date/time from RMC NMEA sentences such as GPRMC, GNRMC, or GARMC.
349
+ * @param message raw NMEA sentence
350
+ * @return satellite time parsed from NMEA, or null if the sentence does not contain time
351
+ */
352
+ private fun parseSatelliteTime(message: String): GnssTimeResult? {
353
+ val sentence = message.trim()
354
+ val withoutChecksum = sentence.substringBefore("*")
355
+ val fields = withoutChecksum.split(",")
356
+ if (fields.size <= 9 || !fields[0].endsWith("RMC")) {
357
+ return null
358
+ }
359
+
360
+ val time = fields[1]
361
+ val status = fields[2].ifBlank { null }
362
+ val date = fields[9]
363
+
364
+ if (status != "A") {
365
+ return null
366
+ }
367
+
368
+ if (time.length < 6 || date.length != 6) {
369
+ return null
370
+ }
371
+
372
+ val hour = time.substring(0, 2).toIntOrNull() ?: return null
373
+ val minute = time.substring(2, 4).toIntOrNull() ?: return null
374
+ val second = time.substring(4, 6).toIntOrNull() ?: return null
375
+ val millisecond = time.substringAfter(".", "").take(3).padEnd(3, '0').toIntOrNull() ?: 0
376
+ val day = date.substring(0, 2).toIntOrNull() ?: return null
377
+ val month = date.substring(2, 4).toIntOrNull() ?: return null
378
+ val yearSuffix = date.substring(4, 6).toIntOrNull() ?: return null
379
+ val year = if (yearSuffix >= 80) 1900 + yearSuffix else 2000 + yearSuffix
380
+
381
+ val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.US).apply {
382
+ clear()
383
+ isLenient = false
384
+ set(Calendar.YEAR, year)
385
+ set(Calendar.MONTH, month - 1)
386
+ set(Calendar.DAY_OF_MONTH, day)
387
+ set(Calendar.HOUR_OF_DAY, hour)
388
+ set(Calendar.MINUTE, minute)
389
+ set(Calendar.SECOND, second)
390
+ set(Calendar.MILLISECOND, millisecond)
391
+ }
392
+
393
+ val timestamp = try {
394
+ calendar.timeInMillis
395
+ } catch (_: IllegalArgumentException) {
396
+ return null
397
+ }
398
+ return GnssTimeResult(
399
+ timestamp = timestamp,
400
+ status = status,
401
+ nmea = sentence
402
+ )
403
+ }
404
+
221
405
  /**
222
406
  * Starts watching the device's location by requesting location updates
223
407
  * @param call the plugin call
@@ -232,8 +416,9 @@ class GeolocationPlugin : Plugin() {
232
416
  controller.addWatch(activity, locationOptions, watchId).collect { result ->
233
417
  result.onSuccess { locationList ->
234
418
  locationList.forEach { locationResult ->
419
+ val gnssTime = getGnssTimeOrNull(WATCH_SATELLITE_TIME_TIMEOUT_MS)
235
420
  call.sendSuccess(
236
- result = getJSObjectForLocation(locationResult),
421
+ result = getJSObjectForLocation(locationResult, gnssTime),
237
422
  keepCallback = true)
238
423
  }
239
424
  }
@@ -250,7 +435,10 @@ class GeolocationPlugin : Plugin() {
250
435
  * @param locationResult IONGLOCLocationResult object with the location to convert
251
436
  * @return JSObject with converted JSON object
252
437
  */
253
- private fun getJSObjectForLocation(locationResult: IONGLOCLocationResult): JSObject {
438
+ private fun getJSObjectForLocation(
439
+ locationResult: IONGLOCLocationResult,
440
+ gnssTime: GnssTimeResult?
441
+ ): JSObject {
254
442
  val coords = JSObject().apply {
255
443
  put("latitude", locationResult.latitude)
256
444
  put("longitude", locationResult.longitude)
@@ -258,12 +446,12 @@ class GeolocationPlugin : Plugin() {
258
446
  put("altitude", locationResult.altitude)
259
447
  locationResult.altitudeAccuracy?.let { put("altitudeAccuracy", it) }
260
448
  put("speed", locationResult.speed)
261
- put("heading", locationResult.heading)
449
+ put("heading", if (locationResult.heading != -1f) locationResult.heading else null)
262
450
  put("isMock", locationResult.isMock)
263
451
  put("provider", locationResult.provider)
264
452
  }
265
453
  return JSObject().apply {
266
- put("timestamp", locationResult.timestamp)
454
+ put("timestamp", gnssTime?.timestamp ?: JSONObject.NULL)
267
455
  put("coords", coords)
268
456
  }
269
457
  }
@@ -294,6 +482,9 @@ class GeolocationPlugin : Plugin() {
294
482
  is IONGLOCException.IONGLOCLocationRetrievalTimeoutException -> {
295
483
  call.sendError(GeolocationErrors.GET_LOCATION_TIMEOUT)
296
484
  }
485
+ is IONGLOCException.IONGLOCLocationAndNetworkDisabledException -> {
486
+ call.sendError(GeolocationErrors.NETWORK_LOCATION_DISABLED_ERROR)
487
+ }
297
488
  else -> {
298
489
  call.sendError(GeolocationErrors.POSITION_UNAVAILABLE)
299
490
  }
@@ -315,7 +506,7 @@ class GeolocationPlugin : Plugin() {
315
506
  }
316
507
 
317
508
  /**
318
- * Extension function to return a unsuccessful plugin result
509
+ * Extension function to return an unsuccessful plugin result
319
510
  * @param error error class representing the error to return, containing a code and message
320
511
  */
321
512
  private fun PluginCall.sendError(error: GeolocationErrors.ErrorInfo) {
@@ -328,13 +519,26 @@ class GeolocationPlugin : Plugin() {
328
519
  * @return IONGLOCLocationOptions object
329
520
  */
330
521
  private fun createOptions(call: PluginCall): IONGLOCLocationOptions {
331
- val timeout = call.getLong("timeout", 10000) ?: 10000
332
- val maximumAge = call.getLong("maximumAge", 0) ?: 0
522
+ val timeout = call.getNumber("timeout", 10000)
523
+ val maximumAge = call.getNumber("maximumAge", 0)
333
524
  val enableHighAccuracy = call.getBoolean("enableHighAccuracy", false) ?: false
334
- val minimumUpdateInterval = call.getLong("minimumUpdateInterval", 5000) ?: 5000
335
-
336
- val locationOptions = IONGLOCLocationOptions(timeout, maximumAge, enableHighAccuracy, minimumUpdateInterval)
525
+ val minimumUpdateInterval = call.getNumber("minimumUpdateInterval", 5000)
526
+ val enableLocationFallback = call.getBoolean("enableLocationFallback", true) ?: true
527
+ val interval = call.getNumber("interval", -1).let {
528
+ // using "< 0" and not "<= 0" because 0 is a valid value for interval
529
+ if (it < 0) timeout else it
530
+ }
337
531
 
338
- return locationOptions
532
+ return IONGLOCLocationOptions(
533
+ timeout = timeout,
534
+ maximumAge = maximumAge,
535
+ enableHighAccuracy = enableHighAccuracy,
536
+ enableLocationManagerFallback = enableLocationFallback,
537
+ interval = interval,
538
+ minUpdateInterval = minimumUpdateInterval
539
+ )
339
540
  }
340
- }
541
+
542
+ private fun PluginCall.getNumber(name: String, defaultValue: Long): Long =
543
+ getLong(name) ?: getInt(name)?.toLong() ?: defaultValue
544
+ }
package/dist/docs.json CHANGED
@@ -142,9 +142,9 @@
142
142
  "name": "since"
143
143
  }
144
144
  ],
145
- "docs": "Creation timestamp for coords",
145
+ "docs": "GNSS satellite timestamp for coords, in milliseconds since the Unix epoch.\n\nOn Android, this is populated only when a GNSS RMC NMEA message is available.\nOn iOS and web, this is always `null`.",
146
146
  "complexTypes": [],
147
- "type": "number"
147
+ "type": "number | null"
148
148
  },
149
149
  {
150
150
  "name": "coords",
@@ -156,7 +156,7 @@
156
156
  ],
157
157
  "docs": "The GPS coordinates along with the accuracy of the data",
158
158
  "complexTypes": [],
159
- "type": "{ latitude: number; longitude: number; accuracy: number; altitudeAccuracy: number | null | undefined; altitude: number | null; speed: number | null; heading: number | null; }"
159
+ "type": "{ latitude: number; longitude: number; accuracy: number; altitudeAccuracy: number | null | undefined; altitude: number | null; speed: number | null; heading: number | null; magneticHeading: number | null | undefined; trueHeading: number | null | undefined; headingAccuracy: number | null | undefined; course: number | null | undefined; isMock?: boolean | undefined; provider?: string | undefined; }"
160
160
  }
161
161
  ]
162
162
  },
@@ -195,7 +195,7 @@
195
195
  "name": "since"
196
196
  }
197
197
  ],
198
- "docs": "The maximum wait time in milliseconds for location updates.\n\nIn Android, since version 7.1.0 of the plugin, it is also used to determine the\ninterval of location updates for `watchPosition`.",
198
+ "docs": "The maximum wait time in milliseconds for location updates.",
199
199
  "complexTypes": [],
200
200
  "type": "number | undefined"
201
201
  },
@@ -227,9 +227,41 @@
227
227
  "name": "since"
228
228
  }
229
229
  ],
230
- "docs": "The minumum update interval for location updates.\n\nIf location updates are available faster than this interval then an update\nwill only occur if the minimum update interval has expired since the last location update.\n\nThis parameter is only available for Android. It has no effect on iOS or Web platforms.",
230
+ "docs": "The minimum update interval for `watchPosition`. Not to be confused with `interval`.\n\nIf location updates are available faster than this interval then an update\nwill only occur if the minimum update interval has expired since the last location update.\n\nThis parameter is only available for Android. It has no effect on iOS or Web platforms.",
231
231
  "complexTypes": [],
232
232
  "type": "number | undefined"
233
+ },
234
+ {
235
+ "name": "interval",
236
+ "tags": [
237
+ {
238
+ "text": "`timeout`",
239
+ "name": "default"
240
+ },
241
+ {
242
+ "text": "8.0.0",
243
+ "name": "since"
244
+ }
245
+ ],
246
+ "docs": "Desired interval in milliseconds to receive location updates in `watchPosition`.\n\nFor very low values of `interval` (a couple seconds or less),\nthe platform may not guarantee timely location updates - they may take longer than specified.\nThe platform may also be able to provide location updates faster than `interval`.\nYou may use `minimumUpdateInterval` to control that behavior.\n\nFor backwards compatibility with version 7.1.x, if no value is passed,\nthe default value of this parameter is that of `timeout`.\n\nThis parameter is only available for Android. It has no effect on iOS or Web platforms.",
247
+ "complexTypes": [],
248
+ "type": "number | undefined"
249
+ },
250
+ {
251
+ "name": "enableLocationFallback",
252
+ "tags": [
253
+ {
254
+ "text": "true",
255
+ "name": "default"
256
+ },
257
+ {
258
+ "text": "8.0.0",
259
+ "name": "since"
260
+ }
261
+ ],
262
+ "docs": "Whether to fall back to the Android framework's `LocationManager` in case Google Play Service's location settings checks fail.\nThis can happen for multiple reasons - e.g. device has no Play Services or device has no network connection (Airplane Mode)\nIf set to `false`, failures are propagated to the caller.\nNote that `LocationManager` may not be as effective as Google Play Services implementation.\nIf the device's in airplane mode, only the GPS provider is used, which may take longer to return a location, depending on GPS signal.\nThis means that to receive location in such circumstances, you may need to provide a higher timeout.\n\nThis parameter is only available for Android. It has no effect on iOS or Web platforms.",
263
+ "complexTypes": [],
264
+ "type": "boolean | undefined"
233
265
  }
234
266
  ]
235
267
  },
@@ -71,11 +71,14 @@ export interface ClearWatchOptions {
71
71
  }
72
72
  export interface Position {
73
73
  /**
74
- * Creation timestamp for coords
74
+ * GNSS satellite timestamp for coords, in milliseconds since the Unix epoch.
75
+ *
76
+ * On Android, this is populated only when a GNSS RMC NMEA message is available.
77
+ * On iOS and web, this is always `null`.
75
78
  *
76
79
  * @since 1.0.0
77
80
  */
78
- timestamp: number;
81
+ timestamp: number | null;
79
82
  /**
80
83
  * The GPS coordinates along with the accuracy of the data
81
84
  *
@@ -121,11 +124,60 @@ export interface Position {
121
124
  */
122
125
  speed: number | null;
123
126
  /**
124
- * The heading the user is facing (if available)
127
+ * The heading the user is facing (if available).
128
+ *
129
+ * Historically, this field returned the direction of travel (course) on iOS and Android.
130
+ * It now prioritizes the compass heading (true or magnetic) if available, falling back
131
+ * to the direction of travel (course).
125
132
  *
126
133
  * @since 1.0.0
127
134
  */
128
135
  heading: number | null;
136
+ /**
137
+ * The heading (measured in degrees) relative to magnetic north.
138
+ *
139
+ * Only available when using `watchPosition`.
140
+ *
141
+ * @since 8.2.0
142
+ */
143
+ magneticHeading: number | null | undefined;
144
+ /**
145
+ * The heading (measured in degrees) relative to true north.
146
+ *
147
+ * Only available when using `watchPosition`.
148
+ *
149
+ * @since 8.2.0
150
+ */
151
+ trueHeading: number | null | undefined;
152
+ /**
153
+ * The maximum deviation (measured in degrees) between the reported heading and the true geomagnetic heading.
154
+ *
155
+ * Only available when using `watchPosition`.
156
+ *
157
+ * @since 8.2.0
158
+ */
159
+ headingAccuracy: number | null | undefined;
160
+ /**
161
+ * The direction in which the device is travelling, measured in degrees and relative to due north.
162
+ *
163
+ * Only available when using `watchPosition`.
164
+ *
165
+ * @since 8.2.0
166
+ */
167
+ course: number | null | undefined;
168
+ /**
169
+ * Whether the location was generated by a mock provider (e.g. a fake GPS app).
170
+ *
171
+ * On Android, available on API 31+ (uses `isMock()`), falls back to `isFromMockProvider()` on older versions.
172
+ * On iOS, detected via `CLLocationSourceInformation` on iOS 15+.
173
+ */
174
+ isMock?: boolean;
175
+ /**
176
+ * The location provider that generated this location (Android only).
177
+ *
178
+ * Common values: `"gps"`, `"network"`, `"fused"`.
179
+ */
180
+ provider?: string;
129
181
  };
130
182
  }
131
183
  export interface PositionOptions {
@@ -142,9 +194,6 @@ export interface PositionOptions {
142
194
  /**
143
195
  * The maximum wait time in milliseconds for location updates.
144
196
  *
145
- * In Android, since version 7.1.0 of the plugin, it is also used to determine the
146
- * interval of location updates for `watchPosition`.
147
- *
148
197
  * @default 10000
149
198
  * @since 1.0.0
150
199
  */
@@ -157,7 +206,7 @@ export interface PositionOptions {
157
206
  */
158
207
  maximumAge?: number;
159
208
  /**
160
- * The minumum update interval for location updates.
209
+ * The minimum update interval for `watchPosition`. Not to be confused with `interval`.
161
210
  *
162
211
  * If location updates are available faster than this interval then an update
163
212
  * will only occur if the minimum update interval has expired since the last location update.
@@ -168,6 +217,37 @@ export interface PositionOptions {
168
217
  * @since 6.1.0
169
218
  */
170
219
  minimumUpdateInterval?: number;
220
+ /**
221
+ * Desired interval in milliseconds to receive location updates in `watchPosition`.
222
+ *
223
+ * For very low values of `interval` (a couple seconds or less),
224
+ * the platform may not guarantee timely location updates - they may take longer than specified.
225
+ * The platform may also be able to provide location updates faster than `interval`.
226
+ * You may use `minimumUpdateInterval` to control that behavior.
227
+ *
228
+ * For backwards compatibility with version 7.1.x, if no value is passed,
229
+ * the default value of this parameter is that of `timeout`.
230
+ *
231
+ * This parameter is only available for Android. It has no effect on iOS or Web platforms.
232
+ *
233
+ * @default `timeout`
234
+ * @since 8.0.0
235
+ */
236
+ interval?: number;
237
+ /**
238
+ * Whether to fall back to the Android framework's `LocationManager` in case Google Play Service's location settings checks fail.
239
+ * This can happen for multiple reasons - e.g. device has no Play Services or device has no network connection (Airplane Mode)
240
+ * If set to `false`, failures are propagated to the caller.
241
+ * Note that `LocationManager` may not be as effective as Google Play Services implementation.
242
+ * If the device's in airplane mode, only the GPS provider is used, which may take longer to return a location, depending on GPS signal.
243
+ * This means that to receive location in such circumstances, you may need to provide a higher timeout.
244
+ *
245
+ * This parameter is only available for Android. It has no effect on iOS or Web platforms.
246
+ *
247
+ * @default true
248
+ * @since 8.0.0
249
+ */
250
+ enableLocationFallback?: boolean;
171
251
  }
172
252
  export type WatchPositionCallback = (position: Position | null, err?: any) => void;
173
253
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState } from '@capacitor/core';\n\nexport type CallbackID = string;\n\nexport interface PermissionStatus {\n /**\n * Permission state for location alias.\n *\n * On Android it requests/checks both ACCESS_COARSE_LOCATION and\n * ACCESS_FINE_LOCATION permissions.\n *\n * On iOS and web it requests/checks location permission.\n *\n * @since 1.0.0\n */\n location: PermissionState;\n\n /**\n * Permission state for coarseLocation alias.\n *\n * On Android it requests/checks ACCESS_COARSE_LOCATION.\n *\n * On Android 12+, users can choose between Approximate location (ACCESS_COARSE_LOCATION) or\n * Precise location (ACCESS_FINE_LOCATION), so this alias can be used if the app doesn't\n * need high accuracy.\n *\n * On iOS and web it will have the same value as location alias.\n *\n * @since 1.2.0\n */\n coarseLocation: PermissionState;\n}\n\nexport type GeolocationPermissionType = 'location' | 'coarseLocation';\n\nexport interface GeolocationPluginPermissions {\n permissions: GeolocationPermissionType[];\n}\n\nexport interface GeolocationPlugin {\n /**\n * Get the current GPS location of the device\n *\n * @since 1.0.0\n */\n getCurrentPosition(options?: PositionOptions): Promise<Position>;\n\n /**\n * Set up a watch for location changes. Note that watching for location changes\n * can consume a large amount of energy. Be smart about listening only when you need to.\n *\n * @since 1.0.0\n */\n watchPosition(options: PositionOptions, callback: WatchPositionCallback): Promise<CallbackID>;\n\n /**\n * Clear a given watch\n *\n * @since 1.0.0\n */\n clearWatch(options: ClearWatchOptions): Promise<void>;\n\n /**\n * Check location permissions. Will throw if system location services are disabled.\n *\n * @since 1.0.0\n */\n checkPermissions(): Promise<PermissionStatus>;\n\n /**\n * Request location permissions. Will throw if system location services are disabled.\n *\n * Not available on web.\n *\n * @since 1.0.0\n */\n requestPermissions(permissions?: GeolocationPluginPermissions): Promise<PermissionStatus>;\n}\n\nexport interface ClearWatchOptions {\n id: CallbackID;\n}\n\nexport interface Position {\n /**\n * Creation timestamp for coords\n *\n * @since 1.0.0\n */\n timestamp: number;\n\n /**\n * The GPS coordinates along with the accuracy of the data\n *\n * @since 1.0.0\n */\n coords: {\n /**\n * Latitude in decimal degrees\n *\n * @since 1.0.0\n */\n latitude: number;\n\n /**\n * longitude in decimal degrees\n *\n * @since 1.0.0\n */\n longitude: number;\n\n /**\n * Accuracy level of the latitude and longitude coordinates in meters\n *\n * @since 1.0.0\n */\n accuracy: number;\n\n /**\n * Accuracy level of the altitude coordinate in meters, if available.\n *\n * Available on all iOS versions and on Android 8.0+.\n *\n * @since 1.0.0\n */\n altitudeAccuracy: number | null | undefined;\n\n /**\n * The altitude the user is at (if available)\n *\n * @since 1.0.0\n */\n altitude: number | null;\n\n /**\n * The speed the user is traveling (if available)\n *\n * @since 1.0.0\n */\n speed: number | null;\n\n /**\n * The heading the user is facing (if available)\n *\n * @since 1.0.0\n */\n heading: number | null;\n };\n}\n\nexport interface PositionOptions {\n /**\n * High accuracy mode (such as GPS, if available)\n *\n * On Android 12+ devices it will be ignored if users didn't grant\n * ACCESS_FINE_LOCATION permissions (can be checked with location alias).\n *\n * @default false\n * @since 1.0.0\n */\n enableHighAccuracy?: boolean;\n\n /**\n * The maximum wait time in milliseconds for location updates.\n *\n * In Android, since version 7.1.0 of the plugin, it is also used to determine the\n * interval of location updates for `watchPosition`.\n *\n * @default 10000\n * @since 1.0.0\n */\n timeout?: number;\n\n /**\n * The maximum age in milliseconds of a possible cached position that is acceptable to return\n *\n * @default 0\n * @since 1.0.0\n */\n maximumAge?: number;\n\n /**\n * The minumum update interval for location updates.\n *\n * If location updates are available faster than this interval then an update\n * will only occur if the minimum update interval has expired since the last location update.\n *\n * This parameter is only available for Android. It has no effect on iOS or Web platforms.\n *\n * @default 5000\n * @since 6.1.0\n */\n minimumUpdateInterval?: number;\n}\n\nexport type WatchPositionCallback = (position: Position | null, err?: any) => void;\n\n/**\n * @deprecated Use `PositionOptions`.\n * @since 1.0.0\n */\nexport type GeolocationOptions = PositionOptions;\n\n/**\n * @deprecated Use `WatchPositionCallback`.\n * @since 1.0.0\n */\nexport type GeolocationWatchCallback = WatchPositionCallback;\n\n/**\n * @deprecated Use `Position`.\n * @since 1.0.0\n */\nexport type GeolocationPosition = Position;\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState } from '@capacitor/core';\n\nexport type CallbackID = string;\n\nexport interface PermissionStatus {\n /**\n * Permission state for location alias.\n *\n * On Android it requests/checks both ACCESS_COARSE_LOCATION and\n * ACCESS_FINE_LOCATION permissions.\n *\n * On iOS and web it requests/checks location permission.\n *\n * @since 1.0.0\n */\n location: PermissionState;\n\n /**\n * Permission state for coarseLocation alias.\n *\n * On Android it requests/checks ACCESS_COARSE_LOCATION.\n *\n * On Android 12+, users can choose between Approximate location (ACCESS_COARSE_LOCATION) or\n * Precise location (ACCESS_FINE_LOCATION), so this alias can be used if the app doesn't\n * need high accuracy.\n *\n * On iOS and web it will have the same value as location alias.\n *\n * @since 1.2.0\n */\n coarseLocation: PermissionState;\n}\n\nexport type GeolocationPermissionType = 'location' | 'coarseLocation';\n\nexport interface GeolocationPluginPermissions {\n permissions: GeolocationPermissionType[];\n}\n\nexport interface GeolocationPlugin {\n /**\n * Get the current GPS location of the device\n *\n * @since 1.0.0\n */\n getCurrentPosition(options?: PositionOptions): Promise<Position>;\n\n /**\n * Set up a watch for location changes. Note that watching for location changes\n * can consume a large amount of energy. Be smart about listening only when you need to.\n *\n * @since 1.0.0\n */\n watchPosition(options: PositionOptions, callback: WatchPositionCallback): Promise<CallbackID>;\n\n /**\n * Clear a given watch\n *\n * @since 1.0.0\n */\n clearWatch(options: ClearWatchOptions): Promise<void>;\n\n /**\n * Check location permissions. Will throw if system location services are disabled.\n *\n * @since 1.0.0\n */\n checkPermissions(): Promise<PermissionStatus>;\n\n /**\n * Request location permissions. Will throw if system location services are disabled.\n *\n * Not available on web.\n *\n * @since 1.0.0\n */\n requestPermissions(permissions?: GeolocationPluginPermissions): Promise<PermissionStatus>;\n}\n\nexport interface ClearWatchOptions {\n id: CallbackID;\n}\n\nexport interface Position {\n /**\n * GNSS satellite timestamp for coords, in milliseconds since the Unix epoch.\n *\n * On Android, this is populated only when a GNSS RMC NMEA message is available.\n * On iOS and web, this is always `null`.\n *\n * @since 1.0.0\n */\n timestamp: number | null;\n\n /**\n * The GPS coordinates along with the accuracy of the data\n *\n * @since 1.0.0\n */\n coords: {\n /**\n * Latitude in decimal degrees\n *\n * @since 1.0.0\n */\n latitude: number;\n\n /**\n * longitude in decimal degrees\n *\n * @since 1.0.0\n */\n longitude: number;\n\n /**\n * Accuracy level of the latitude and longitude coordinates in meters\n *\n * @since 1.0.0\n */\n accuracy: number;\n\n /**\n * Accuracy level of the altitude coordinate in meters, if available.\n *\n * Available on all iOS versions and on Android 8.0+.\n *\n * @since 1.0.0\n */\n altitudeAccuracy: number | null | undefined;\n\n /**\n * The altitude the user is at (if available)\n *\n * @since 1.0.0\n */\n altitude: number | null;\n\n /**\n * The speed the user is traveling (if available)\n *\n * @since 1.0.0\n */\n speed: number | null;\n\n /**\n * The heading the user is facing (if available).\n *\n * Historically, this field returned the direction of travel (course) on iOS and Android.\n * It now prioritizes the compass heading (true or magnetic) if available, falling back\n * to the direction of travel (course).\n *\n * @since 1.0.0\n */\n heading: number | null;\n\n /**\n * The heading (measured in degrees) relative to magnetic north.\n *\n * Only available when using `watchPosition`.\n *\n * @since 8.2.0\n */\n magneticHeading: number | null | undefined;\n\n /**\n * The heading (measured in degrees) relative to true north.\n *\n * Only available when using `watchPosition`.\n *\n * @since 8.2.0\n */\n trueHeading: number | null | undefined;\n\n /**\n * The maximum deviation (measured in degrees) between the reported heading and the true geomagnetic heading.\n *\n * Only available when using `watchPosition`.\n *\n * @since 8.2.0\n */\n headingAccuracy: number | null | undefined;\n\n /**\n * The direction in which the device is travelling, measured in degrees and relative to due north.\n *\n * Only available when using `watchPosition`.\n *\n * @since 8.2.0\n */\n course: number | null | undefined;\n\n /**\n * Whether the location was generated by a mock provider (e.g. a fake GPS app).\n *\n * On Android, available on API 31+ (uses `isMock()`), falls back to `isFromMockProvider()` on older versions.\n * On iOS, detected via `CLLocationSourceInformation` on iOS 15+.\n */\n isMock?: boolean;\n\n /**\n * The location provider that generated this location (Android only).\n *\n * Common values: `\"gps\"`, `\"network\"`, `\"fused\"`.\n */\n provider?: string;\n };\n}\n\nexport interface PositionOptions {\n /**\n * High accuracy mode (such as GPS, if available)\n *\n * On Android 12+ devices it will be ignored if users didn't grant\n * ACCESS_FINE_LOCATION permissions (can be checked with location alias).\n *\n * @default false\n * @since 1.0.0\n */\n enableHighAccuracy?: boolean;\n\n /**\n * The maximum wait time in milliseconds for location updates.\n *\n * @default 10000\n * @since 1.0.0\n */\n timeout?: number;\n\n /**\n * The maximum age in milliseconds of a possible cached position that is acceptable to return\n *\n * @default 0\n * @since 1.0.0\n */\n maximumAge?: number;\n\n /**\n * The minimum update interval for `watchPosition`. Not to be confused with `interval`.\n *\n * If location updates are available faster than this interval then an update\n * will only occur if the minimum update interval has expired since the last location update.\n *\n * This parameter is only available for Android. It has no effect on iOS or Web platforms.\n *\n * @default 5000\n * @since 6.1.0\n */\n minimumUpdateInterval?: number;\n\n /**\n * Desired interval in milliseconds to receive location updates in `watchPosition`.\n *\n * For very low values of `interval` (a couple seconds or less),\n * the platform may not guarantee timely location updates - they may take longer than specified.\n * The platform may also be able to provide location updates faster than `interval`.\n * You may use `minimumUpdateInterval` to control that behavior.\n *\n * For backwards compatibility with version 7.1.x, if no value is passed,\n * the default value of this parameter is that of `timeout`.\n *\n * This parameter is only available for Android. It has no effect on iOS or Web platforms.\n *\n * @default `timeout`\n * @since 8.0.0\n */\n interval?: number;\n\n /**\n * Whether to fall back to the Android framework's `LocationManager` in case Google Play Service's location settings checks fail.\n * This can happen for multiple reasons - e.g. device has no Play Services or device has no network connection (Airplane Mode)\n * If set to `false`, failures are propagated to the caller.\n * Note that `LocationManager` may not be as effective as Google Play Services implementation.\n * If the device's in airplane mode, only the GPS provider is used, which may take longer to return a location, depending on GPS signal.\n * This means that to receive location in such circumstances, you may need to provide a higher timeout.\n *\n * This parameter is only available for Android. It has no effect on iOS or Web platforms.\n *\n * @default true\n * @since 8.0.0\n */\n enableLocationFallback?: boolean;\n}\n\nexport type WatchPositionCallback = (position: Position | null, err?: any) => void;\n\n/**\n * @deprecated Use `PositionOptions`.\n * @since 1.0.0\n */\nexport type GeolocationOptions = PositionOptions;\n\n/**\n * @deprecated Use `WatchPositionCallback`.\n * @since 1.0.0\n */\nexport type GeolocationWatchCallback = WatchPositionCallback;\n\n/**\n * @deprecated Use `Position`.\n * @since 1.0.0\n */\nexport type GeolocationPosition = Position;\n"]}
package/dist/esm/web.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
2
  import type { CallbackID, GeolocationPlugin, PermissionStatus, Position, PositionOptions, WatchPositionCallback } from './definitions';
3
3
  export declare class GeolocationWeb extends WebPlugin implements GeolocationPlugin {
4
+ private latestOrientation;
5
+ constructor();
6
+ private updateOrientation;
7
+ private augmentPosition;
4
8
  getCurrentPosition(options?: PositionOptions): Promise<Position>;
5
9
  watchPosition(options: PositionOptions, callback: WatchPositionCallback): Promise<CallbackID>;
6
10
  clearWatch(options: {