@bricks-soft/capacitor-location-tracking 8.0.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +375 -124
  2. package/android/src/main/AndroidManifest.xml +2 -0
  3. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +4 -1
  4. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +14 -5
  5. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +10 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +70 -15
  7. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +6 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +8 -3
  9. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +10 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/config/SecretStore.kt +188 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +188 -17
  12. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +11 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +21 -5
  14. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +20 -9
  15. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +24 -2
  16. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +43 -1
  17. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +17 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceDetails.kt +167 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +191 -34
  20. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +6 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +3 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +52 -8
  23. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +22 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +78 -27
  25. package/android/src/main/java/com/brickssoft/locationtracking/http/ConfigChecks.kt +140 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +50 -12
  27. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +17 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +507 -92
  29. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +6 -6
  30. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +23 -1
  31. package/android/src/main/java/com/brickssoft/locationtracking/http/UploadJobs.kt +217 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/http/UploadWait.kt +184 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +66 -7
  34. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogRedaction.kt +31 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +44 -1
  36. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +79 -8
  37. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +9 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +12 -2
  39. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +10 -2
  40. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +24 -1
  41. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +31 -10
  42. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +48 -3
  43. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +60 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/provider/LocationSubscriptions.kt +180 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +200 -126
  46. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +20 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +39 -51
  48. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +8 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +47 -53
  50. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +34 -2
  51. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +19 -1
  52. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +7 -0
  53. package/android/src/main/res/xml/lt_file_paths.xml +1 -1
  54. package/dist/esm/definitions.d.ts +107 -4
  55. package/dist/esm/definitions.js.map +1 -1
  56. package/dist/esm/events.d.ts +8 -2
  57. package/dist/esm/events.js +7 -1
  58. package/dist/esm/events.js.map +1 -1
  59. package/dist/esm/index.d.ts +1 -1
  60. package/dist/esm/index.js +1 -1
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/web/config.js +10 -0
  63. package/dist/esm/web/config.js.map +1 -1
  64. package/dist/esm/web/device.d.ts +1 -1
  65. package/dist/esm/web/device.js +8 -1
  66. package/dist/esm/web/device.js.map +1 -1
  67. package/dist/esm/web.d.ts +0 -8
  68. package/dist/esm/web.js +12 -3
  69. package/dist/esm/web.js.map +1 -1
  70. package/dist/plugin.cjs.js +39 -5
  71. package/dist/plugin.cjs.js.map +1 -1
  72. package/dist/plugin.js +39 -5
  73. package/dist/plugin.js.map +1 -1
  74. package/package.json +2 -2
package/README.md CHANGED
@@ -28,6 +28,8 @@ whether the user really kept tracking on.
28
28
  - [Companion plugins (native API)](#companion-plugins-native-api)
29
29
  - [Error codes](#error-codes)
30
30
  - [Web stub](#web-stub)
31
+ - [Stored data and backups](#stored-data-and-backups)
32
+ - [Device information on records](#device-information-on-records)
31
33
  - [Server side](#server-side)
32
34
  - [Example app](#example-app)
33
35
  - [Field-force example](#field-force-example)
@@ -143,11 +145,22 @@ does not use it. The `locationProvider` values:
143
145
  - `'android'`: always the Android `LocationManager`.
144
146
 
145
147
  The backend is chosen once per process and again whenever `locationProvider` changes (while tracking, the location
146
- request, activity updates and geofences move to the new backend). `getState().backend` and `getDeviceInfo()`
147
- (`gmsAvailable`, `hmsAvailable`, `packagedProviders`) show what was selected.
148
+ request, activity updates and geofences move to the new backend). With `'auto'`, the plugin also checks when tracking
149
+ starts, on every `providerchange` and on every heartbeat whether a better backend has become available (for example
150
+ Google Play services finished an update), and then moves to it. `getState().backend` and `getDeviceInfo()` show what
151
+ was selected: `gmsAvailable`, `hmsAvailable`, `packagedProviders`, and why a better backend was not used
152
+ (`backendFallbackReason`, with the raw availability codes `gmsStatus` and `hmsStatus`). `hmsCoreOutdated` is `true`
153
+ when HMS Core on the phone is older than 6.3.0.301, which Huawei names for Location Kit 6.x (the plugin still uses it
154
+ and logs a warning). `locationAvailableHint` is the last `LocationAvailability` from GMS or HMS; Google documents that
155
+ it can be wrong in both directions, so it is a hint only.
148
156
 
149
157
  The `android` backend has two limitations: it has **no activity recognition**, so motion detection relies on distance
150
158
  only, and **no native geofence dwell**, so the plugin synthesizes `DWELL` itself (see [Geofencing](#geofencing)).
159
+ For `'high'` and `'balanced'` accuracy it registers both the `gps` and the `network` provider, so a phone indoors
160
+ without satellites still gets network fixes; while a GPS fix arrived in the last 10 s (or two update intervals, if
161
+ longer), network fixes are dropped, so GPS and network points do not alternate. A fix without an accuracy value is
162
+ treated as 1,000 km inaccurate, so `filter.trackingAccuracyThreshold` drops it. `getCurrentPosition()` asks the
163
+ enabled providers at the same time and keeps the most accurate fix that arrives within 3 s of the first one.
151
164
 
152
165
  **Release builds (R8 / minify).** Nothing to add: the plugin ships consumer ProGuard/R8 rules that keep its provider
153
166
  bundles and the GMS/HMS class names it probes by reflection, so a minified release app still detects Google Play
@@ -345,6 +358,17 @@ If you don't want a permission, remove it in your app's manifest and turn off th
345
358
  </manifest>
346
359
  ```
347
360
 
361
+ **Apps that track only while they are in use.** Because the plugin's manifest declares `ACCESS_BACKGROUND_LOCATION`,
362
+ Google Play asks every app that uses the plugin for the background-location declaration. An app that never asks for
363
+ "Allow all the time" should remove the permission, so that Play does not ask:
364
+
365
+ ```xml
366
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" tools:node="remove"/>
367
+ ```
368
+
369
+ Without it, geofences do not fire in the background, and on Android 11 and newer tracking is not resumed from the
370
+ background (after a reboot, an app update or process death); it resumes when the app opens and calls `ready()`.
371
+
348
372
  ### 5. Runtime permission flow
349
373
 
350
374
  `requestPermissions()` asks, by default, for all four permissions in this order: `location`, `notifications`,
@@ -365,7 +389,10 @@ resolves with the resulting `PermissionStatus`. Overlapping calls run one after
365
389
  `backgroundPermissionRationale`; if the user declines it, or no Activity is visible, the step is skipped. Google
366
390
  Play requires your own in-app disclosure before this, plus a declaration in the Play Console.
367
391
  - **Android 12+.** The user may grant only *approximate* location. `getProviderState().accuracy` then reports
368
- `'approximate'`.
392
+ `'approximate'`. Approximate fixes are accurate to only a few kilometres, so `filter.trackingAccuracyThreshold`
393
+ (default 100 m) drops all of them: no `location` or `motionchange` records with coords are created, and only
394
+ heartbeats and audit records (`tracking_start`, `tracking_stop`, `providerchange`) are uploaded. Check
395
+ `getProviderState().accuracy` and ask the user for precise location if your app needs positions.
369
396
  - **Android 13+.** `POST_NOTIFICATIONS` is a runtime permission. If it is denied, tracking still works, but the
370
397
  foreground-service notification is hidden.
371
398
  - **Below Android 10**, there is no separate background permission (it is reported the same as `location`), and
@@ -373,8 +400,10 @@ resolves with the resulting `PermissionStatus`. Overlapping calls run one after
373
400
  also reported as `'granted'`.
374
401
  - A permission whose Android permission you removed from the app's manifest (`tools:node="remove"`) is not requested.
375
402
  - Background location is needed for geofencing and for resuming tracking after a reboot, an app update or process
376
- death. On Android 14+ a resume from the background without it ends with a `tracking_stop` whose reason is
377
- `service_start_failed`.
403
+ death. On Android 11 and newer, a resume from the background without it ends with a `tracking_stop` whose reason
404
+ is `service_start_failed`: Android 14+ refuses to start the service, and on Android 11 to 13 the service would start
405
+ but get no location, so the plugin does not start it. [`notification.resume`](#confignotificationresume) lets the
406
+ user resume with a tap. Opening the app (`ready()`) also resumes tracking.
378
407
  - Revoking a location permission in Settings kills the app's process. While tracking is on, the plugin records a
379
408
  `providerchange` the next time it runs; if location permission is gone completely, it also stops tracking with a
380
409
  `tracking_stop` whose reason is `permission_denied`.
@@ -488,9 +517,24 @@ instead.
488
517
 
489
518
  ## Configuration reference
490
519
 
491
- All options are optional. `getState().config` returns the full config with every default filled in (including the
492
- JWT tokens). Out-of-range values are clamped to the limits given below, and every clamp is logged as a warning;
493
- numbers that are not finite become the default.
520
+ All options are optional. `getState().config` returns the full config with every default filled in. The secret
521
+ values of `http.authorization` are masked there (see [Tokens](#tokens-storage-and-masking)). Out-of-range values are
522
+ clamped to the limits given below, and every clamp is logged as a warning; numbers that are not finite become the
523
+ default.
524
+
525
+ `ready()`, `setConfig()` and `reset()` also check the values that would otherwise fail only at upload time, and
526
+ reject with `INVALID_ARGUMENT` (nothing is applied):
527
+
528
+ - `http.url` (when set): an `http` or `https` URL with a host, without user info (`user:password@`) and without a
529
+ fragment (`#...`).
530
+ - `http.headers` and `http.authorization.refreshHeaders`: names and values that HTTP allows (ASCII, no line breaks).
531
+ The error names the header but never repeats its value.
532
+ - `http.locationTemplate` and `http.geofenceTemplate`: every `<%= name %>` placeholder exists, and the template
533
+ renders to a JSON object or array, both for a record with every field set and for one with no location.
534
+
535
+ Values that are accepted but probably wrong are listed in the `warnings` array of the `State` these calls resolve
536
+ with. Today the only warning is an `http://` URL: Android 9+ blocks cleartext traffic unless the app's network
537
+ security config allows that host.
494
538
 
495
539
  ### `config` (top level)
496
540
 
@@ -510,7 +554,7 @@ numbers that are not finite become the default.
510
554
  | `elasticityMultiplier` | number (≥ 0) | `1` | Elastic filter: `distanceFilter × max(1, round(speed / 5) × elasticityMultiplier)`. |
511
555
  | `stationaryRadius` | number (m, ≥ 1) | `25` | While stationary: the stationary geofence has a radius of `max(stationaryRadius, 150)` m, and a fix whose distance from the stop point minus its accuracy is more than `stationaryRadius` (with an accuracy no worse than `filter.trackingAccuracyThreshold`) switches to moving. While moving, a displacement beyond it counts as evidence of motion for stop detection. |
512
556
  | `stopTimeout` | number (min, ≥ 0) | `5` | Minutes without evidence of motion (a displacement beyond `stationaryRadius`, or a confident moving activity) before switching to stationary. Values below 1 act as 1. |
513
- | `stopAfterElapsedMinutes` | number (min, ≥ 0) | `0` (off) | Stops tracking automatically this many minutes after it started (`tracking_stop`, reason `stop_after_elapsed`). While the phone sleeps, the stop can come later: see [Field-force example](#field-force-example). If the deadline passes while the phone is off or the app is not running, the stop is recorded when the plugin next runs, and tracking does not resume. |
557
+ | `stopAfterElapsedMinutes` | number (min, ≥ 0) | `0` (off) | Stops tracking automatically this many minutes after it started (`tracking_stop`, reason `stop_after_elapsed`). While the phone sleeps, the stop can come later: see [Field-force example](#field-force-example). If the deadline passes while the phone is off or the app is not running, the stop is recorded when the plugin next runs, and tracking does not resume. The time is counted on the phone's elapsed-time clock, so changing the wall clock does not move the stop; after a reboot it is counted from the wall-clock start time. |
514
558
  | `stopOnStationary` | boolean | `false` | Stops tracking when stop detection switches the device to stationary (`tracking_stop`, reason `stop_on_stationary`). `changePace({ isMoving: false })` does not stop tracking. |
515
559
  | `locationTimeout` | number (ms, > 0) | `30000` | Default timeout of `getCurrentPosition()`, and of the first fix after `start()`. |
516
560
  | `filter` | object | see below | Location filters. |
@@ -543,12 +587,13 @@ numbers that are not finite become the default.
543
587
  | `enabled` | boolean | `true` | Creates `heartbeat` records while tracking is on. |
544
588
  | `minInterval` | number (s, ≥ 60) | `180` | A heartbeat is due when no record has been created for this long. |
545
589
  | `maxInterval` | number (s, ≥ `minInterval`) | `300` | Upper end of the heartbeat window: the heartbeat should exist by then. A heartbeat Android delivers later is still created, and the lateness is logged. |
590
+ | `uploadKeepAwake` | number (s, 0–60) | `10` | After a heartbeat is created, the plugin keeps the CPU awake (a partial wake lock) until the upload that sends it has finished, for at most this many seconds. So the heartbeat reaches the server when it is created, not when the phone next wakes up. `0` turns it off. |
546
591
 
547
592
  ### `config.http`
548
593
 
549
594
  | Option | Type | Default | Description |
550
595
  |---|---|---|---|
551
- | `url` | string \| null | none | Upload endpoint. Without a URL (or with one that isn't a valid `http(s)` URL) nothing is uploaded, and records stay queued. |
596
+ | `url` | string \| null | none | Upload endpoint. Without a URL nothing is uploaded, and records stay queued. An invalid URL is rejected by `setConfig()` (see above); an `http://` URL gives a warning. |
552
597
  | `method` | `'POST' \| 'PUT' \| 'PATCH'` | `'POST'` | HTTP method. |
553
598
  | `headers` | object | `{}` | Extra request headers. They can override `Content-Type`; an `Authorization` header turns the JWT handling off. |
554
599
  | `params` | object | `{}` | Merged into the **root** of every request body. Keys that the body already has are not overwritten. Ignored for a batch with `rootProperty: '.'`. |
@@ -562,6 +607,7 @@ numbers that are not finite become the default.
562
607
  | `locationTemplate` | string \| null | `null` | JSON text with `<%= name %>` placeholders that replaces the default record shape. |
563
608
  | `geofenceTemplate` | string \| null | `null` | Template for `geofence` records. Falls back to `locationTemplate`. |
564
609
  | `timeout` | number (ms, > 0) | `60000` | Request timeout (call, read and write). |
610
+ | `maxRetryAfter` | number (s, ≥ 0) | `3600` | Longest wait the plugin accepts from a `Retry-After` header of a `429` or `503` response. A longer value is cut to this. |
565
611
  | `authorization` | object \| null | `null` | JWT settings, see below. |
566
612
 
567
613
  The upload rules (priority records, retries, templates, body shapes) are described in
@@ -580,6 +626,38 @@ The upload rules (priority records, retries, templates, body shapes) are describ
580
626
  | `refreshPayloadEncoding` | `'json' \| 'form'` | `'json'` | Body encoding of the refresh request. |
581
627
  | `expires` | number (epoch ms) | `-1` | Access-token expiry. `-1` means unknown. The token is refreshed 60 s before it expires, and after a `401` (at most one refresh per upload). Updated by every refresh. |
582
628
 
629
+ #### Tokens: storage and masking
630
+
631
+ - `accessToken`, `refreshToken`, `refreshPayload` and `refreshHeaders` are stored encrypted (AES-256-GCM with a key
632
+ that stays in the Android Keystore) in a file under `noBackupFilesDir`, not in the SharedPreferences file. Tokens
633
+ that an older version stored in plain text are moved there on the first start.
634
+ - If the stored tokens cannot be decrypted (for example the Keystore key is gone), they read as unset: uploads that
635
+ need them wait, an `authorization` event reports the error, and new tokens from `setConfig()` end the wait.
636
+ - `getState()` and the `State` of `ready()`, `setConfig()` and `reset()` return these values masked: `"***"`, or
637
+ `"***"` plus the last 4 characters for values longer than 8 characters. The string values of `refreshPayload` are
638
+ masked one by one; a value that contains `{refreshToken}` is shown as it is. The `authorization` event still carries the full
639
+ server response, which apps need to read new tokens.
640
+ - A masked value given back unchanged in `setConfig()` (for example the whole `getState().config`) keeps the stored
641
+ value. Any other value replaces it.
642
+ - One upload uses one token: the token read when the upload starts, or the token refreshed from it after a `401`.
643
+ If the app changes the authorization during an upload, the upload stops and the records stay queued. If
644
+ `http.authorization` is still set, a new upload with the new tokens starts about 10 s later (also when tracking is
645
+ off or the app's process has ended, through the upload job); automatic uploads wait until then, `sync()` does not. If the app removed the authorization (step 3 below),
646
+ nothing starts until the next trigger (a new record, the network returning, `sync()`). So an app that sets the next
647
+ user's tokens without the logout steps below gets the queued records uploaded with them. A refresh that finishes
648
+ after the app set new tokens does not overwrite them.
649
+
650
+ #### Logging out and switching users
651
+
652
+ The queue does not remember which user recorded a record. To make sure one user's records are never uploaded with
653
+ another user's token or `params`, log out in this order:
654
+
655
+ 1. `await LocationTracking.stop()`;
656
+ 2. `await LocationTracking.sync()` to upload the queued records with the current user's settings, or
657
+ `await LocationTracking.destroyLocations()` to delete them;
658
+ 3. remove the authorization: `await LocationTracking.setConfig({ http: { authorization: null } })`;
659
+ 4. configure the new user (`setConfig` with the new token, `params` and `extras`), then `start()` again.
660
+
583
661
  ### `config.persistence`
584
662
 
585
663
  | Option | Type | Default | Description |
@@ -645,7 +723,12 @@ session resumes with the persisted config and its original `stopAfterElapsedMinu
645
723
  | Option | Type | Default | Description |
646
724
  |---|---|---|---|
647
725
  | `logLevel` | `'off' \| 'error' \| 'warn' \| 'info' \| 'debug' \| 'verbose'` | `'info'` | Minimum level written to the plugin log. |
648
- | `logMaxDays` | number (≥ 1) | `3` | Days of log files to keep. |
726
+ | `logMaxDays` | number (≥ 1) | `3` | Days of log files to keep. All log files together are also kept under 10 MB: the oldest daily files are deleted first (never today's). |
727
+
728
+ Text that the app writes with `log()` is cleaned before it reaches logcat and the log files (which `emailLog()` and
729
+ `uploadLog()` send): values of credential fields (`accessToken`, `refresh_token`, `password`, `secret`, `apiKey`,
730
+ `authorization`, …) become `[redacted]`, `Bearer <token>` becomes `Bearer [redacted]`, JSON Web Tokens become `[jwt]`
731
+ and URLs become `[url]`.
649
732
 
650
733
  ### `config.backgroundPermissionRationale`
651
734
 
@@ -683,6 +766,11 @@ The dialog shown before the Android 11+ "Allow all the time" settings page.
683
766
  one twice does nothing.
684
767
  - `stop()` removes the location and activity updates, records a `tracking_stop` (reason `stop`) while the foreground
685
768
  service is still running, then stops the service, the heartbeat and the OS geofences, and emits `enabledchange`.
769
+ The change of `enabled` is written to disk before `stop()` resolves, so a process killed right after `stop()` does
770
+ not bring tracking back.
771
+ - Every `enabledchange` event carries the `reason` of the change (the `tracking_start` or `tracking_stop` reason, for
772
+ example `start`, `stop`, `stop_after_elapsed` or `service_start_failed`). `getState()` returns the reason and time
773
+ of the last `tracking_stop` as `lastStopReason` and `lastStopAt`.
686
774
  - Tracking also stops by itself: `stopOnStationary`, `stopAfterElapsedMinutes`, `stopOnTerminate` (app swiped away),
687
775
  a revoked location permission (`permission_denied`), or Android refusing the foreground service
688
776
  (`service_start_failed`). Each case records a `tracking_stop` with that reason and emits `enabledchange`. When
@@ -690,7 +778,13 @@ The dialog shown before the Android 11+ "Allow all the time" settings page.
690
778
  permission is no longer granted, and `service_start_failed` otherwise.
691
779
  - Tracking resumes by itself after the process was killed (reason `restore`), after a reboot (`boot`) and after an
692
780
  app update (`package_replaced`, both only with `app.startOnBoot`), and when `ready()` finds it enabled but not
693
- running. See [docs/heartbeat.md](docs/heartbeat.md#android-reliability) for what Android allows. When Android
781
+ running.
782
+ - **User stop.** After the user stops the app (Settings → Force stop, or the Stop button of the Android 13+ Task
783
+ Manager), tracking stays off until the app is opened again: a broadcast, alarm or service restart that starts the
784
+ process does not resume it. The plugin records one `tracking_stop` with reason `user_stopped` and `stopped_at` (the
785
+ time of the stop, from Android's exit record), releases the leftover activity, geofence and heartbeat
786
+ registrations, and keeps `enabled` set, so `ready()` resumes tracking when the app opens. A boot or app-update
787
+ restore is not checked (on Android 11 and 12 an update can be recorded like a user stop). See [docs/heartbeat.md](docs/heartbeat.md#android-reliability) for what Android allows. When Android
694
788
  refuses such a restore, [`notification.resume`](#confignotificationresume) can offer a tap that resumes it
695
789
  (`resume_notification`).
696
790
  - A boot broadcast restores tracking once per boot. The plugin reads the phone's boot counter
@@ -720,13 +814,17 @@ The dialog shown before the Android 11+ "Allow all the time" settings page.
720
814
  last evidence of motion, even if no more fixes arrive.
721
815
  - Each transition records a `motionchange` with the new `is_moving`. `changePace({ isMoving })` forces a transition;
722
816
  it is ignored while tracking is off or in geofences-only mode.
817
+ - **Location request errors.** If the OS rejects the location request (for example a missing permission, or GMS or
818
+ HMS reporting an error later), the plugin emits a `locationerror` event and requests it again after 10 s, doubling
819
+ the wait up to 5 minutes; the next fix resets the wait. Tracking stays on.
723
820
  - Without activity recognition (the `android` backend, `disableMotionActivityUpdates`, or no activity permission),
724
821
  motion is detected from the stationary region and from distance only.
725
822
 
726
823
  **Positions.** `getCurrentPosition()` works whether or not tracking is on. It needs location permission
727
824
  (`PERMISSION_DENIED`) and enabled location services (`LOCATION_DISABLED`). It takes up to `samples` fixes, stops
728
825
  early on a fix of 10 m or better, and resolves with the most accurate one; at the timeout it resolves with the best
729
- fix so far, or rejects with `TIMEOUT` if there is none. With `maximumAge`, a recent enough known fix is returned
826
+ fix so far, or rejects with `TIMEOUT` if there is none. If the OS rejects the location request, it rejects at once
827
+ with that error (for example `PERMISSION_DENIED` or `UNAVAILABLE`) instead of waiting for the timeout. With `maximumAge`, a recent enough known fix is returned
730
828
  right away. With `persist: true` (default) the fix is stored as a `current_position` record, which is uploaded,
731
829
  emitted as a `location` event and restarts the heartbeat window. `watchPosition()` reports each fix (or an error,
732
830
  such as `PERMISSION_DENIED`) to its callback until `clearWatch()`; it doesn't report disabled location services as
@@ -856,12 +954,27 @@ How to measure the cost on a real phone: procedure M-04 in the
856
954
  [docs/wire-format.md](docs/wire-format.md#when-uploads-happen). Priority records (`heartbeat`, `tracking_start`,
857
955
  `tracking_stop`, `providerchange`) are uploaded right away and take the older queued records along.
858
956
  - A request answered `5xx` or `429`, or without an answer (timeout, network error), is **tried again in the same
859
- upload** after 2, 4 and 8 seconds (at most 4 tries). Other answers are not tried again; a `401` refreshes the JWT
860
- and retries once ([details](docs/wire-format.md#response-handling-and-retries)).
861
- - With `syncInterval: 0` (the default) there is **no retry timer** for failed uploads: queued records are retried when
862
- a record is inserted (at least every heartbeat while tracking), when the network comes back, when tracking starts,
863
- and on `sync()`. With `syncInterval` above 0, a timer uploads normal records that have become due, and retries them
864
- once per `syncInterval` after a failed upload (see below).
957
+ upload** after about 2, 4 and 8 seconds (at most 4 tries; each wait is moved by up to 20 % up or down, so many
958
+ phones do not retry at the same moment). A `429` or `503` with a `Retry-After` longer than the next wait ends the
959
+ upload. Other answers are not tried again; a `401` refreshes the JWT and retries once
960
+ ([details](docs/wire-format.md#response-handling-and-retries)).
961
+ - Redirects are not followed: a `3xx` answer is a failed upload and the records stay queued. Only the first 16 KB of
962
+ an upload answer are read (for the `http` event's `responseText`).
963
+ - A `413` (request too large) for a batch halves the batch size and sends the same records again in smaller batches.
964
+ - **Wait after a failed upload.** When the last try of an automatic upload fails, the plugin stores the earliest time
965
+ of the next automatic upload of normal records: 30 s after the first failure, doubling with each further failure up
966
+ to 15 minutes (each moved by up to 20 %), and at least the server's `Retry-After` (up to `http.maxRetryAfter`).
967
+ After a `401` that a token refresh cannot fix, the wait is 15 minutes, and new tokens from `setConfig()` end it at
968
+ once. The stored time survives an app restart. The network coming back ends a server wait (not an authorization
969
+ wait), and a successful upload ends both. Priority records and `sync()` are not held back.
970
+ - **Uploads when the app is not running.** The plugin schedules an Android job (JobScheduler) for queued records: when
971
+ the device is offline it runs as soon as a network is available, after a failed upload it runs at the stored next
972
+ time, and when tracking stops, or an upload starts while tracking is off, it is scheduled to run right away once. So
973
+ records are uploaded also after the app's process has ended, or when Android freezes it in the background.
974
+ On Android 12+ the job is expedited when it should run at once.
975
+ - Records are uploaded when a record is inserted (at least every heartbeat while tracking), when the network comes
976
+ back, when tracking starts, by the upload job, and on `sync()`. With `syncInterval` above 0, a timer also uploads
977
+ normal records that have become due (see below).
865
978
  - `getLocations({ limit })` returns queued records, oldest first; `getCount()` counts them; `destroyLocations()` and
866
979
  `destroyLocation({ uuid })` delete them without uploading.
867
980
  - `sync()` uploads the whole queue now, ignoring `autoSync`, `autoSyncThreshold` and `disableAutoSyncOnCellular`, and
@@ -869,8 +982,8 @@ How to measure the cost on a real phone: procedure M-04 in the
869
982
  `NETWORK_ERROR` at the first failed upload, after its tries (records uploaded before the failure are already
870
983
  deleted).
871
984
  - `insertLocation({ location })` stores a record of your own (default `event: 'location'`) and uploads it like the
872
- others. It emits **no** event and does **not** restart the heartbeat window, because it is not evidence that
873
- tracking runs.
985
+ others. `location.coords` needs `latitude`, `longitude` and an `accuracy` (m) greater than 0. It emits **no**
986
+ event and does **not** restart the heartbeat window, because it is not evidence that tracking runs.
874
987
  - `persistence.maxDaysToPersist` and `maxRecordsToPersist` prune the queue, including heartbeats and audit records.
875
988
 
876
989
  ### Live location: `http.syncInterval`
@@ -940,9 +1053,11 @@ tracking has been started (or resumed) in the app's process.
940
1053
  | `http` | `{ success, status, responseText, uuids }` | An upload request finished. There is one event per HTTP request: a `401` followed by a token refresh and a retry gives two, and every try of an upload that is tried again gives one. `status` is `0` for a network error. |
941
1054
  | `connectivitychange` | `{ connected, type }` | The network connection changed. A network blocked for the app by Doze or Data Saver counts as disconnected. |
942
1055
  | `powersavechange` | `{ isPowerSaveMode }` | Battery saver was turned on or off. |
943
- | `enabledchange` | `{ enabled }` | Tracking was started or stopped, including automatic stops (`stopOnStationary`, `stopAfterElapsedMinutes`, `terminate`, `permission_denied`, `service_start_failed`, `reboot`, `package_replaced`). Not emitted when tracking resumes in a new process. |
1056
+ | `enabledchange` | `{ enabled, reason }` | Tracking was started or stopped, including automatic stops. `reason` is the reason of the `tracking_start` or `tracking_stop` record: `start`, `start_geofences`, `resume_notification`, `stop`, `stop_on_stationary`, `stop_after_elapsed`, `terminate`, `permission_denied`, `service_start_failed`, `reboot` or `package_replaced`. Not emitted when tracking resumes in a new process. |
944
1057
  | `notificationaction` | `{ id }` | A notification action button was tapped. |
945
- | `authorization` | `{ success, status, error?, response? }` | A JWT refresh was attempted, successfully or not. `response` is the parsed JSON body of the refresh response. |
1058
+ | `authorization` | `{ success, status, error?, response? }` | A JWT refresh was attempted, successfully or not, or the stored tokens cannot be decrypted (`success: false`, `status: 0`, `error` says to set the tokens again). `response` is the parsed JSON body of the refresh response. |
1059
+ | `locationerror` | `{ code, message, backend, retryInMs }` | The OS rejected the location request (`code` is an [error code](#error-codes) such as `PERMISSION_DENIED` or `UNAVAILABLE`). Tracking stays on and the request is made again after `retryInMs` (10 s, doubling up to 5 minutes; `null` when no retry is scheduled). |
1060
+ | `storagefull` | `{ uuid, event, diskFreeBytes }` | A record could not be stored because the phone's storage is full. `diskFreeBytes` is the free space of the app's storage, or `null`. Show the user a "storage full" message: until space is freed, records are lost. `insertLocation()` sends no event; it rejects with `IO_ERROR`. |
946
1061
 
947
1062
  ## Companion plugins (native API)
948
1063
 
@@ -1071,6 +1186,40 @@ nothing and cannot track.
1071
1186
  (the status stays `'prompt'`).
1072
1187
  - **Everything else** rejects with `UNIMPLEMENTED`.
1073
1188
 
1189
+ ## Stored data and backups
1190
+
1191
+ The plugin keeps its SQLite queue (`location_tracking.db`), its log files and the encrypted tokens under the app's
1192
+ `noBackupFilesDir`, so Android Auto Backup and device-to-device transfer never copy them. Files of an older version are
1193
+ moved there on the first start, without losing records. The SharedPreferences file (config and runtime state) stays
1194
+ where it was and can be part of a backup or transfer; it holds no tokens.
1195
+
1196
+ When that file is restored onto another installation (a backup restore or a new phone), the plugin notices it (the
1197
+ stored install time differs from the app's) and never turns tracking on by itself: `enabled` is cleared, the
1198
+ authorization tokens are removed and the boot counters are reset. The rest of the config (URL, intervals, ...) is
1199
+ kept. The app logs the user in again and calls `start()`.
1200
+
1201
+ ## Device information on records
1202
+
1203
+ Some records carry readings that explain gaps in tracking (all without extra permissions):
1204
+
1205
+ | Field | On | Content |
1206
+ |---|---|---|
1207
+ | `network` | every record | Connection type when the record was created: `wifi`, `cellular`, `ethernet`, `other` or `none`. |
1208
+ | `device.device_id` | every record | The device id: on Android, `Settings.Secure.ANDROID_ID`; other platforms will fill it with their own id. Read once per process. Not in records queued by an older version. |
1209
+ | `device.background_restricted`, `device.standby_bucket` | `heartbeat` | The user set battery use to "Restricted"; the app standby bucket (`active`, `working_set`, `frequent`, `rare`, `restricted`). Android 9+. |
1210
+ | `device.disk_free_bytes` | `heartbeat` | Free space of the storage that holds the app's files. |
1211
+ | `device.thermal_status` | `heartbeat` | `none`, `light`, `moderate`, `severe`, `critical`, `emergency` or `shutdown`. Android 10+. |
1212
+ | `battery.temperature`, `battery.health`, `battery.plugged` | `heartbeat` | °C; `good`, `overheat`, `dead`, `over_voltage`, `cold`, `failure`, `unknown`; `ac`, `usb`, `wireless`, `dock`, `none`. |
1213
+ | `device.manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes` | `tracking_start` | The phone, its Android version and its memory. |
1214
+
1215
+ A reading that the Android version does not have, or that fails, is left out. Templates use them through the
1216
+ placeholders listed in [docs/wire-format.md](docs/wire-format.md#templates).
1217
+
1218
+ **Google Play Data safety.** Every record includes `device.device_id` (`Settings.Secure.ANDROID_ID` on Android), an ID
1219
+ that identifies the phone for your app. An app that uploads records must declare **"Device or other IDs"** in the Data safety
1220
+ form of the Google Play Console (collected, together with the purpose, for example "App functionality" or
1221
+ "Analytics").
1222
+
1074
1223
  ## Server side
1075
1224
 
1076
1225
  - [docs/wire-format.md](docs/wire-format.md) describes every record variant, batching, templates, upload and retry
@@ -1081,7 +1230,7 @@ nothing and cannot track.
1081
1230
  to audit tracking gaps on the server.
1082
1231
  - [docs/device-test-checklist.md](docs/device-test-checklist.md) is a manual test plan for real GMS, HMS and
1083
1232
  non-GMS phones; [docs/e2e-runbook.md](docs/e2e-runbook.md) covers the emulator tests and the real-phone procedures
1084
- M-01 … M-08.
1233
+ M-01 … M-12.
1085
1234
 
1086
1235
  In short, store records idempotently by `uuid` and answer `2xx` quickly (also for records you will never accept, or
1087
1236
  they are retried until pruned). While tracking is on, expect some record at least every `maxInterval` seconds (about
@@ -1091,11 +1240,15 @@ next, so a normal 9-minute gap in Doze is not flagged as a failure (see
1091
1240
  [docs/heartbeat.md](docs/heartbeat.md#heartbeat-metadata)). A back office can show tracking as **online** while the
1092
1241
  latest record is not older than that expected gap. A `tracking_stop` with reason `service_start_failed` means Android
1093
1242
  refused the tracking foreground service (a `start()` from the background, or a restart in the background); the app
1094
- has to call `start()` again while it is visible.
1243
+ has to call `start()` again while it is visible. A `tracking_stop` with reason `user_stopped` means the user stopped
1244
+ the app (Settings → Force stop, or the Task Manager); its `stopped_at` is the time of the stop, which can be well
1245
+ before `recorded_at`. Records from a phone that is offline for a long time arrive late; the `heartbeat` records'
1246
+ `device` and `battery` readings (standby bucket, "Restricted" battery setting, free disk, temperature) help explain
1247
+ gaps.
1095
1248
 
1096
1249
  ## Example app
1097
1250
 
1098
- [`example/`](example/) is a plain HTML/JS Capacitor app, with a button for every method and a live log of all 13
1251
+ [`example/`](example/) is a plain HTML/JS Capacitor app, with a button for every method and a live log of all 15
1099
1252
  events:
1100
1253
 
1101
1254
  ```bash
@@ -1184,7 +1337,7 @@ The plugin is tested end to end on an Android emulator (AVD), in GitHub Actions
1184
1337
 
1185
1338
  | Part | Where | What |
1186
1339
  |---|---|---|
1187
- | Plugin suite | [`e2e/plugin/`](e2e/plugin/) | 34 scenarios against the plugin's [example app](#example-app): lifecycle (foreground-service start crashes, kills, reboots, updates), heartbeat and power (stationary GPS off, Doze, clock changes, offline, server errors, `syncInterval`), permissions, providers and geofences. |
1340
+ | Plugin suite | [`e2e/plugin/`](e2e/plugin/) | 47 scenarios against the plugin's [example app](#example-app): lifecycle (foreground-service start crashes, kills, reboots, updates), heartbeat and power (stationary GPS off, Doze, clock changes, offline, server errors, `syncInterval`), permissions, providers and geofences, and the round-3 behavior (restores without background location, the upload job, user stop, data migration and backup, encrypted tokens, upload rules, token races). |
1188
1341
  | Field-force suite | [`examples/field-force/e2e/`](examples/field-force/e2e/) | 12 scenarios against the [field-force example](#field-force-example): auto start, 02:00 stop, live location, route and odometer, online/offline audit, and PremiseMonitor receiving every record natively (also after a kill and a reboot). |
1189
1342
  | Test kit | [`testing/e2e-kit/`](testing/e2e-kit/) | adb, WebView and debug-command helpers, a mock back office, fixtures and assertions, on Node 22 without runtime dependencies. |
1190
1343
  | Manual procedures | [docs/e2e-runbook.md](docs/e2e-runbook.md) | M-01 … M-08: HMS on a Huawei phone, phone makers' task killers, a real drive, a 12-hour battery measurement, a real 02:00 stop, a real Android 14 boot, 16 KB alignment, real overnight Doze. |
@@ -1238,15 +1391,24 @@ npm ci && npm run build && npm test
1238
1391
  npm publish # prepare builds again; publishConfig.access makes it public
1239
1392
  ```
1240
1393
 
1241
- Each later release:
1242
-
1243
- 1. Run `npm version <patch|minor|major>`. It sets the version in `package.json` and `package-lock.json`, copies it to
1244
- `PLUGIN_VERSION` in `src/web/device.ts` (`npm test` checks this; `android/build.gradle` reads `package.json`
1245
- itself), and commits and tags. Give the `CHANGELOG.md` section that version and the date.
1246
- 2. Merge to `master` and create a GitHub release with a tag such as `v8.0.1`. The release starts the workflow (it can
1247
- also be started by hand from the Actions tab). The workflow builds, runs `npm test` and runs `npm stage publish`.
1248
- 3. In the package's **Staged Packages** tab on npmjs.com, approve the staged version (or run
1394
+ Each later release, with `scripts/release.sh` (bash; run it from the repository root on `master`):
1395
+
1396
+ 1. While you work, write the changes under `## [Unreleased]` in `CHANGELOG.md`.
1397
+ 2. `scripts/release.sh check <version>` runs every check and changes nothing: `master` is clean and equal to
1398
+ `origin/master`; the version is new and greater than the one on npm, and its major version is Capacitor's; the
1399
+ versions in `package.json`, `package-lock.json` and `src/web/device.ts` agree; `[Unreleased]` is not empty; git
1400
+ can sign; `npm run docgen` leaves the README unchanged; the changes since the last tag contain none of the
1401
+ private-name patterns (a file outside the repository, see the script); the steps of `ci.yml` pass on this
1402
+ computer; and `npm pack` contains only the files listed in `package.json`.
1403
+ 3. `scripts/release.sh run <version>` runs the checks, renames `[Unreleased]` to `[<version>] - <date>`, runs
1404
+ `npm version <version> --no-git-tag-version` (it sets `package.json`, `package-lock.json` and `PLUGIN_VERSION` in
1405
+ `src/web/device.ts`; `android/build.gradle` reads `package.json` itself), and makes a signed commit and a signed tag
1406
+ `v<version>`. After you type `yes`, it pushes both and creates the GitHub release with the CHANGELOG section as
1407
+ notes. The release starts the workflow (it can also be started by hand from the Actions tab), which builds, runs
1408
+ `npm test` and runs `npm stage publish`; the script waits for it.
1409
+ 4. In the package's **Staged Packages** tab on npmjs.com, approve the staged version (or run
1249
1410
  `npm stage approve <stage-id>`) with 2FA. Only then can apps install it.
1411
+ 5. `scripts/release.sh verify <version>` checks that npm serves it as `latest`, with a provenance statement.
1250
1412
 
1251
1413
  Versions published by the workflow carry an npm provenance statement (the repository is public), which links each
1252
1414
  version to the commit and workflow run that built it. The first version, published by hand, has none.
@@ -1313,6 +1475,8 @@ version to the commit and workflow run that built it. The first version, publish
1313
1475
  * [`addListener('enabledchange', ...)`](#addlistenerenabledchange-)
1314
1476
  * [`addListener('notificationaction', ...)`](#addlistenernotificationaction-)
1315
1477
  * [`addListener('authorization', ...)`](#addlistenerauthorization-)
1478
+ * [`addListener('locationerror', ...)`](#addlistenerlocationerror-)
1479
+ * [`addListener('storagefull', ...)`](#addlistenerstoragefull-)
1316
1480
  * [`removeAllListeners()`](#removealllisteners)
1317
1481
  * [Interfaces](#interfaces)
1318
1482
  * [Type Aliases](#type-aliases)
@@ -2114,6 +2278,38 @@ addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) =>
2114
2278
  --------------------
2115
2279
 
2116
2280
 
2281
+ ### addListener('locationerror', ...)
2282
+
2283
+ ```typescript
2284
+ addListener(eventName: 'locationerror', listenerFunc: (e: LocationErrorEvent) => void) => Promise<PluginListenerHandle>
2285
+ ```
2286
+
2287
+ | Param | Type |
2288
+ | ------------------ | --------------------------------------------------------------------------------- |
2289
+ | **`eventName`** | <code>'locationerror'</code> |
2290
+ | **`listenerFunc`** | <code>(e: <a href="#locationerrorevent">LocationErrorEvent</a>) =&gt; void</code> |
2291
+
2292
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
2293
+
2294
+ --------------------
2295
+
2296
+
2297
+ ### addListener('storagefull', ...)
2298
+
2299
+ ```typescript
2300
+ addListener(eventName: 'storagefull', listenerFunc: (e: StorageFullEvent) => void) => Promise<PluginListenerHandle>
2301
+ ```
2302
+
2303
+ | Param | Type |
2304
+ | ------------------ | ----------------------------------------------------------------------------- |
2305
+ | **`eventName`** | <code>'storagefull'</code> |
2306
+ | **`listenerFunc`** | <code>(e: <a href="#storagefullevent">StorageFullEvent</a>) =&gt; void</code> |
2307
+
2308
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
2309
+
2310
+ --------------------
2311
+
2312
+
2117
2313
  ### removeAllListeners()
2118
2314
 
2119
2315
  ```typescript
@@ -2128,15 +2324,18 @@ removeAllListeners() => Promise<void>
2128
2324
 
2129
2325
  #### State
2130
2326
 
2131
- | Prop | Type | Description |
2132
- | ------------------ | ----------------------------------------------------------- | ----------------------------- |
2133
- | **`enabled`** | <code>boolean</code> | |
2134
- | **`trackingMode`** | <code><a href="#trackingmode">TrackingMode</a></code> | |
2135
- | **`isMoving`** | <code>boolean</code> | |
2136
- | **`odometer`** | <code>number</code> | |
2137
- | **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> | |
2138
- | **`lastRecordAt`** | <code>string \| null</code> | |
2139
- | **`config`** | <code><a href="#config">Config</a></code> | fully populated with defaults |
2327
+ | Prop | Type | Description |
2328
+ | -------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2329
+ | **`enabled`** | <code>boolean</code> | |
2330
+ | **`trackingMode`** | <code><a href="#trackingmode">TrackingMode</a></code> | |
2331
+ | **`isMoving`** | <code>boolean</code> | |
2332
+ | **`odometer`** | <code>number</code> | |
2333
+ | **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> | |
2334
+ | **`lastRecordAt`** | <code>string \| null</code> | |
2335
+ | **`lastStopReason`** | <code>string \| null</code> | reason of the last `tracking_stop` record (the `tracking_stop` reasons of <a href="#location">`Location.reason`</a>); null if none |
2336
+ | **`lastStopAt`** | <code>string \| null</code> | when tracking stopped for `lastStopReason` (ISO-8601 UTC ms); null if none |
2337
+ | **`config`** | <code><a href="#config">Config</a></code> | fully populated with defaults. On Android the `http.authorization` secrets (`accessToken`, `refreshToken`, the values of `refreshPayload` and `refreshHeaders`) are masked: `***`, or `***` plus the last 4 characters for values longer than 8 characters; a `refreshPayload` value that contains `{refreshToken}` is shown. Passing a masked value back to `setConfig` keeps the stored one. The web stub returns them as given. |
2338
+ | **`warnings`** | <code>string[]</code> | `ready()`, `setConfig()` and `reset()` only: what the given config contains that was accepted but is probably wrong (for example an `http://` upload URL). |
2140
2339
 
2141
2340
 
2142
2341
  #### Config
@@ -2199,32 +2398,34 @@ removeAllListeners() => Promise<void>
2199
2398
 
2200
2399
  #### HeartbeatConfig
2201
2400
 
2202
- | Prop | Type | Description | Default |
2203
- | ----------------- | -------------------- | -------------------------- | ----------------- |
2204
- | **`enabled`** | <code>boolean</code> | | <code>true</code> |
2205
- | **`minInterval`** | <code>number</code> | seconds, min 60 | <code>180</code> |
2206
- | **`maxInterval`** | <code>number</code> | seconds, &gt;= minInterval | <code>300</code> |
2401
+ | Prop | Type | Description | Default |
2402
+ | --------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
2403
+ | **`enabled`** | <code>boolean</code> | | <code>true</code> |
2404
+ | **`minInterval`** | <code>number</code> | seconds, min 60 | <code>180</code> |
2405
+ | **`maxInterval`** | <code>number</code> | seconds, &gt;= minInterval | <code>300</code> |
2406
+ | **`uploadKeepAwake`** | <code>number</code> | seconds, 0..60. After a heartbeat is queued, keep the phone awake until its upload has finished, or at most this long, so it reaches the server on time while tracking is on. 0 = do not keep the phone awake. | <code>10</code> |
2207
2407
 
2208
2408
 
2209
2409
  #### HttpConfig
2210
2410
 
2211
- | Prop | Type | Description | Default |
2212
- | ------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------- |
2213
- | **`url`** | <code>string \| null</code> | No url =&gt; nothing is uploaded (records stay queued). | |
2214
- | **`method`** | <code><a href="#httpmethod">HttpMethod</a></code> | | <code>'POST'</code> |
2215
- | **`headers`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | | <code>{}</code> |
2216
- | **`params`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> | merged into the ROOT of every request body | <code>{}</code> |
2217
- | **`autoSync`** | <code>boolean</code> | | <code>true</code> |
2218
- | **`autoSyncThreshold`** | <code>number</code> | upload when queue &gt;= threshold; 0 = every record | <code>0</code> |
2219
- | **`syncInterval`** | <code>number</code> | s; upload normal records once the oldest queued one is this old; 0 = off | <code>0</code> |
2220
- | **`batchSync`** | <code>boolean</code> | | <code>false</code> |
2221
- | **`maxBatchSize`** | <code>number</code> | | <code>100</code> |
2222
- | **`disableAutoSyncOnCellular`** | <code>boolean</code> | | <code>false</code> |
2223
- | **`rootProperty`** | <code>string</code> | '.' = no wrapping | <code>'location'</code> |
2224
- | **`locationTemplate`** | <code>string \| null</code> | JSON text with `&lt;%= name %&gt;` placeholders | |
2225
- | **`geofenceTemplate`** | <code>string \| null</code> | used for event 'geofence'; falls back to locationTemplate | |
2226
- | **`timeout`** | <code>number</code> | ms | <code>60000</code> |
2227
- | **`authorization`** | <code><a href="#authorizationconfig">AuthorizationConfig</a> \| null</code> | | |
2411
+ | Prop | Type | Description | Default |
2412
+ | ------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- |
2413
+ | **`url`** | <code>string \| null</code> | No url =&gt; nothing is uploaded (records stay queued). | |
2414
+ | **`method`** | <code><a href="#httpmethod">HttpMethod</a></code> | | <code>'POST'</code> |
2415
+ | **`headers`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | | <code>{}</code> |
2416
+ | **`params`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> | merged into the ROOT of every request body | <code>{}</code> |
2417
+ | **`autoSync`** | <code>boolean</code> | | <code>true</code> |
2418
+ | **`autoSyncThreshold`** | <code>number</code> | upload when queue &gt;= threshold; 0 = every record | <code>0</code> |
2419
+ | **`syncInterval`** | <code>number</code> | s; upload normal records once the oldest queued one is this old; 0 = off | <code>0</code> |
2420
+ | **`batchSync`** | <code>boolean</code> | | <code>false</code> |
2421
+ | **`maxBatchSize`** | <code>number</code> | | <code>100</code> |
2422
+ | **`disableAutoSyncOnCellular`** | <code>boolean</code> | | <code>false</code> |
2423
+ | **`rootProperty`** | <code>string</code> | '.' = no wrapping | <code>'location'</code> |
2424
+ | **`locationTemplate`** | <code>string \| null</code> | JSON text with `&lt;%= name %&gt;` placeholders | |
2425
+ | **`geofenceTemplate`** | <code>string \| null</code> | used for event 'geofence'; falls back to locationTemplate | |
2426
+ | **`timeout`** | <code>number</code> | ms | <code>60000</code> |
2427
+ | **`maxRetryAfter`** | <code>number</code> | seconds, &gt;= 0. The longest wait that a `Retry-After` header of a 429 or 503 answer can impose on automatic uploads. | <code>3600</code> |
2428
+ | **`authorization`** | <code><a href="#authorizationconfig">AuthorizationConfig</a> \| null</code> | | |
2228
2429
 
2229
2430
 
2230
2431
  #### AuthorizationConfig
@@ -2331,27 +2532,30 @@ restart), typically on Android 14+ without "Allow all the time". A tap resumes t
2331
2532
 
2332
2533
  #### Location
2333
2534
 
2334
- | Prop | Type | Description |
2335
- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2336
- | **`uuid`** | <code>string</code> | |
2337
- | **`event`** | <code><a href="#recordevent">RecordEvent</a></code> | |
2338
- | **`timestamp`** | <code>string \| null</code> | fix time (ISO-8601 UTC ms); null if no location ever known |
2339
- | **`recorded_at`** | <code>string</code> | record creation time |
2340
- | **`sent_at`** | <code>string</code> | only present in HTTP bodies |
2341
- | **`elapsed_realtime_ms`** | <code>number</code> | |
2342
- | **`boot_count`** | <code>number</code> | -1 if unavailable |
2343
- | **`is_moving`** | <code>boolean</code> | |
2344
- | **`odometer`** | <code>number</code> | |
2345
- | **`mock`** | <code>boolean</code> | |
2346
- | **`coords`** | <code><a href="#coords">Coords</a> \| null</code> | |
2347
- | **`activity`** | <code>{ type: <a href="#activitytype">ActivityType</a>; confidence: number; }</code> | |
2348
- | **`battery`** | <code>{ level: number; is_charging: boolean; }</code> | level 0..1, -1 unknown |
2349
- | **`backend`** | <code><a href="#locationbackend">LocationBackend</a> \| null</code> | |
2350
- | **`extras`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> | |
2351
- | **`geofence`** | <code>{ identifier: string; action: <a href="#geofenceaction">GeofenceAction</a>; extras?: <a href="#record">Record</a>&lt;string, unknown&gt;; }</code> | event 'geofence' only |
2352
- | **`provider`** | <code><a href="#providerstate">ProviderState</a></code> | provider state when the record was created; the new state for event 'providerchange' |
2353
- | **`reason`** | <code>string</code> | tracking_start: start\|start_geofences\|boot\|restore\|package_replaced\|resume_notification; tracking_stop: stop\|stop_on_stationary\|stop_after_elapsed\|terminate\|permission_denied\|service_start_failed\|reboot\|package_replaced |
2354
- | **`heartbeat`** | <code><a href="#heartbeatmeta">HeartbeatMeta</a></code> | event 'heartbeat' only, optional: how the heartbeat is scheduled |
2535
+ | Prop | Type | Description |
2536
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2537
+ | **`uuid`** | <code>string</code> | |
2538
+ | **`event`** | <code><a href="#recordevent">RecordEvent</a></code> | |
2539
+ | **`timestamp`** | <code>string \| null</code> | fix time (ISO-8601 UTC ms); null if no location ever known |
2540
+ | **`recorded_at`** | <code>string</code> | record creation time |
2541
+ | **`sent_at`** | <code>string</code> | only present in HTTP bodies |
2542
+ | **`elapsed_realtime_ms`** | <code>number</code> | |
2543
+ | **`boot_count`** | <code>number</code> | -1 if unavailable |
2544
+ | **`is_moving`** | <code>boolean</code> | |
2545
+ | **`odometer`** | <code>number</code> | |
2546
+ | **`mock`** | <code>boolean</code> | |
2547
+ | **`coords`** | <code><a href="#coords">Coords</a> \| null</code> | |
2548
+ | **`activity`** | <code>{ type: <a href="#activitytype">ActivityType</a>; confidence: number; }</code> | |
2549
+ | **`battery`** | <code>{ level: number; is_charging: boolean; temperature?: number; health?: 'unknown' \| 'good' \| 'overheat' \| 'dead' \| 'over_voltage' \| 'cold' \| 'failure'; plugged?: 'none' \| 'ac' \| 'usb' \| 'wireless' \| 'dock' \| 'other'; }</code> | level 0..1, -1 unknown. Heartbeat records also carry `temperature` (°C), `health` and `plugged` when the device reports them. |
2550
+ | **`backend`** | <code><a href="#locationbackend">LocationBackend</a> \| null</code> | |
2551
+ | **`extras`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> | |
2552
+ | **`geofence`** | <code>{ identifier: string; action: <a href="#geofenceaction">GeofenceAction</a>; extras?: <a href="#record">Record</a>&lt;string, unknown&gt;; }</code> | event 'geofence' only |
2553
+ | **`provider`** | <code><a href="#providerstate">ProviderState</a></code> | provider state when the record was created; the new state for event 'providerchange' |
2554
+ | **`reason`** | <code>string</code> | tracking_start: start\|start_geofences\|boot\|restore\|package_replaced\|resume_notification; tracking_stop: stop\|stop_on_stationary\|stop_after_elapsed\|terminate\|permission_denied\|service_start_failed\|reboot\|package_replaced\|user_stopped |
2555
+ | **`stopped_at`** | <code>string</code> | event 'tracking_stop' with reason 'user_stopped' only: when the user stopped the app (ISO-8601 UTC ms) |
2556
+ | **`heartbeat`** | <code><a href="#heartbeatmeta">HeartbeatMeta</a></code> | event 'heartbeat' only, optional: how the heartbeat is scheduled |
2557
+ | **`network`** | <code><a href="#connectivitytype">ConnectivityType</a></code> | connection type when the record was created |
2558
+ | **`device`** | <code><a href="#devicereadings">DeviceReadings</a></code> | the device id on every record; more readings on 'heartbeat' and 'tracking_start' records (see <a href="#devicereadings">`DeviceReadings`</a>) |
2355
2559
 
2356
2560
 
2357
2561
  #### Coords
@@ -2395,6 +2599,28 @@ Scheduling metadata of a `heartbeat` record (snake_case = wire format).
2395
2599
  | **`device_idle`** | <code>boolean</code> | the device was in deep Doze |
2396
2600
 
2397
2601
 
2602
+ #### DeviceReadings
2603
+
2604
+ The `device` object of records; a reading is present only when the device provides it.
2605
+ Every record: `device_id`.
2606
+ Heartbeat records: `background_restricted`, `standby_bucket`, `disk_free_bytes`, `thermal_status`.
2607
+ `tracking_start` records: `manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes`.
2608
+
2609
+ | Prop | Type | Description |
2610
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2611
+ | **`device_id`** | <code>string</code> | the device id: on Android, `Settings.Secure.ANDROID_ID`; other platforms will fill it with their own id. Absent in records queued by an older version. Declare "Device or other IDs" in Google Play's Data safety form. |
2612
+ | **`background_restricted`** | <code>boolean</code> | the user set the app's battery use to "Restricted" (Android 9+) |
2613
+ | **`standby_bucket`** | <code>'exempted' \| 'active' \| 'working_set' \| 'frequent' \| 'rare' \| 'restricted'</code> | App Standby bucket (Android 9+) |
2614
+ | **`disk_free_bytes`** | <code>number</code> | free space on the storage that holds the app's files |
2615
+ | **`thermal_status`** | <code>'none' \| 'unknown' \| 'light' \| 'moderate' \| 'severe' \| 'critical' \| 'emergency' \| 'shutdown'</code> | Android 10+ |
2616
+ | **`manufacturer`** | <code>string</code> | |
2617
+ | **`model`** | <code>string</code> | |
2618
+ | **`os_version`** | <code>string</code> | Android version, e.g. "14" |
2619
+ | **`sdk_int`** | <code>number</code> | |
2620
+ | **`low_ram_device`** | <code>boolean</code> | |
2621
+ | **`ram_total_bytes`** | <code>number</code> | |
2622
+
2623
+
2398
2624
  #### CurrentPositionOptions
2399
2625
 
2400
2626
  | Prop | Type | Description | Default |
@@ -2419,13 +2645,13 @@ Scheduling metadata of a `heartbeat` record (snake_case = wire format).
2419
2645
 
2420
2646
  #### InsertLocationInput
2421
2647
 
2422
- | Prop | Type |
2423
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2424
- | **`coords`** | <code><a href="#pick">Pick</a>&lt;<a href="#coords">Coords</a>, 'latitude' \| 'longitude'&gt; & <a href="#partial">Partial</a>&lt;<a href="#coords">Coords</a>&gt;</code> |
2425
- | **`timestamp`** | <code>string</code> |
2426
- | **`event`** | <code><a href="#recordevent">RecordEvent</a></code> |
2427
- | **`is_moving`** | <code>boolean</code> |
2428
- | **`extras`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> |
2648
+ | Prop | Type | Description |
2649
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
2650
+ | **`coords`** | <code><a href="#pick">Pick</a>&lt;<a href="#coords">Coords</a>, 'latitude' \| 'longitude' \| 'accuracy'&gt; & <a href="#partial">Partial</a>&lt;<a href="#coords">Coords</a>&gt;</code> | `accuracy` (m) is required and must be greater than 0; otherwise the call rejects with `INVALID_ARGUMENT`. |
2651
+ | **`timestamp`** | <code>string</code> | |
2652
+ | **`event`** | <code><a href="#recordevent">RecordEvent</a></code> | |
2653
+ | **`is_moving`** | <code>boolean</code> | |
2654
+ | **`extras`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> | |
2429
2655
 
2430
2656
 
2431
2657
  #### Geofence
@@ -2484,19 +2710,26 @@ the computed enclosing circle.
2484
2710
 
2485
2711
  #### DeviceInfo
2486
2712
 
2487
- | Prop | Type |
2488
- | ----------------------- | ----------------------------------------------------------- |
2489
- | **`platform`** | <code>'android' \| 'web'</code> |
2490
- | **`manufacturer`** | <code>string</code> |
2491
- | **`model`** | <code>string</code> |
2492
- | **`brand`** | <code>string</code> |
2493
- | **`osVersion`** | <code>string</code> |
2494
- | **`sdkInt`** | <code>number</code> |
2495
- | **`pluginVersion`** | <code>string</code> |
2496
- | **`gmsAvailable`** | <code>boolean</code> |
2497
- | **`hmsAvailable`** | <code>boolean</code> |
2498
- | **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> |
2499
- | **`packagedProviders`** | <code>string[]</code> |
2713
+ | Prop | Type | Description |
2714
+ | --------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
2715
+ | **`platform`** | <code>'android' \| 'web'</code> | |
2716
+ | **`manufacturer`** | <code>string</code> | |
2717
+ | **`model`** | <code>string</code> | |
2718
+ | **`brand`** | <code>string</code> | |
2719
+ | **`osVersion`** | <code>string</code> | |
2720
+ | **`sdkInt`** | <code>number</code> | |
2721
+ | **`pluginVersion`** | <code>string</code> | |
2722
+ | **`gmsAvailable`** | <code>boolean</code> | |
2723
+ | **`hmsAvailable`** | <code>boolean</code> | |
2724
+ | **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> | |
2725
+ | **`packagedProviders`** | <code>string[]</code> | |
2726
+ | **`backendFallbackReason`** | <code>string \| null</code> | why a better location backend is not used (for example "gms: unavailable (status 2)"); null when none is better |
2727
+ | **`gmsStatus`** | <code>number \| null</code> | raw GoogleApiAvailability result (0 = success); null if GMS is not packaged or not checked yet |
2728
+ | **`hmsStatus`** | <code>number \| null</code> | raw HuaweiApiAvailability result (0 = success); null if HMS is not packaged or not checked yet |
2729
+ | **`hmsCoreOutdated`** | <code>boolean \| null</code> | true when HMS Core is older than 6.3.0.301; null if HMS is not packaged |
2730
+ | **`locationAvailableHint`** | <code>boolean \| null</code> | last LocationAvailability reported by GMS or HMS; only a hint (it can be wrong both ways); null if none |
2731
+ | **`cpuCores`** | <code>number</code> | number of CPU cores |
2732
+ | **`supportedAbis`** | <code>string[]</code> | CPU types (Build.SUPPORTED_ABIS), e.g. ["arm64-v8a"] |
2500
2733
 
2501
2734
 
2502
2735
  #### Sensors
@@ -2608,9 +2841,10 @@ the computed enclosing circle.
2608
2841
 
2609
2842
  #### EnabledChangeEvent
2610
2843
 
2611
- | Prop | Type |
2612
- | ------------- | -------------------- |
2613
- | **`enabled`** | <code>boolean</code> |
2844
+ | Prop | Type | Description |
2845
+ | ------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2846
+ | **`enabled`** | <code>boolean</code> | |
2847
+ | **`reason`** | <code>string \| null</code> | why: a `tracking_start` reason when tracking was switched on (start, start_geofences, resume_notification), a `tracking_stop` reason when it was switched off; the same values as <a href="#location">`Location.reason`</a> |
2614
2848
 
2615
2849
 
2616
2850
  #### NotificationActionEvent
@@ -2630,6 +2864,29 @@ the computed enclosing circle.
2630
2864
  | **`response`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> |
2631
2865
 
2632
2866
 
2867
+ #### LocationErrorEvent
2868
+
2869
+ The location request failed (no permission, Play services or HMS Core refused it, no usable provider).
2870
+
2871
+ | Prop | Type | Description |
2872
+ | --------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
2873
+ | **`code`** | <code><a href="#errorcode">ErrorCode</a></code> | |
2874
+ | **`message`** | <code>string</code> | |
2875
+ | **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> | |
2876
+ | **`retryInMs`** | <code>number \| null</code> | the plugin requests location updates again after this many ms; null if it does not |
2877
+
2878
+
2879
+ #### StorageFullEvent
2880
+
2881
+ A record could not be stored because the device storage is full.
2882
+
2883
+ | Prop | Type |
2884
+ | ------------------- | --------------------------------------------------- |
2885
+ | **`uuid`** | <code>string</code> |
2886
+ | **`event`** | <code><a href="#recordevent">RecordEvent</a></code> |
2887
+ | **`diskFreeBytes`** | <code>number \| null</code> |
2888
+
2889
+
2633
2890
  ### Type Aliases
2634
2891
 
2635
2892
 
@@ -2657,9 +2914,7 @@ the computed enclosing circle.
2657
2914
 
2658
2915
  Construct a type with a set of properties K of type T
2659
2916
 
2660
- <code>{
2661
- [P in K]: T;
2662
- }</code>
2917
+ <code>{ [P in K]: T; }</code>
2663
2918
 
2664
2919
 
2665
2920
  #### NotificationPriority
@@ -2692,6 +2947,11 @@ Construct a type with a set of properties K of type T
2692
2947
  <code>'ENTER' | 'EXIT' | 'DWELL'</code>
2693
2948
 
2694
2949
 
2950
+ #### ConnectivityType
2951
+
2952
+ <code>'wifi' | 'cellular' | 'ethernet' | 'other' | 'none'</code>
2953
+
2954
+
2695
2955
  #### WatchPositionCallback
2696
2956
 
2697
2957
  <code>(location: <a href="#location">Location</a> | null, error?: { code: <a href="#errorcode">ErrorCode</a>; message: string; }): void</code>
@@ -2713,18 +2973,14 @@ Rejection `code` of every failed promise.
2713
2973
 
2714
2974
  From T, pick a set of properties whose keys are in the union K
2715
2975
 
2716
- <code>{
2717
- [P in K]: T[P];
2718
- }</code>
2976
+ <code>{ [P in K]: T[P]; }</code>
2719
2977
 
2720
2978
 
2721
2979
  #### Partial
2722
2980
 
2723
2981
  Make all properties in T optional
2724
2982
 
2725
- <code>{
2726
- [P in keyof T]?: T[P];
2727
- }</code>
2983
+ <code>{ [P in keyof T]?: T[P]; }</code>
2728
2984
 
2729
2985
 
2730
2986
  #### HeartbeatStrategy
@@ -2748,11 +3004,6 @@ Make all properties in T optional
2748
3004
 
2749
3005
  <code>T extends U ? never : T</code>
2750
3006
 
2751
-
2752
- #### ConnectivityType
2753
-
2754
- <code>'wifi' | 'cellular' | 'ethernet' | 'other' | 'none'</code>
2755
-
2756
3007
  </docgen-api>
2757
3008
 
2758
3009
  ## License