@bricks-soft/capacitor-location-tracking 8.0.1 → 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.
- package/BricksSoftCapacitorLocationTracking.podspec +18 -0
- package/Package.swift +13 -0
- package/README.md +476 -147
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +4 -1
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +14 -5
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +10 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +70 -15
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +6 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +8 -3
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +10 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SecretStore.kt +188 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +188 -17
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +11 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +21 -5
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +20 -9
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +24 -2
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +43 -1
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +17 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceDetails.kt +167 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +191 -34
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +6 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +3 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +52 -8
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +78 -27
- package/android/src/main/java/com/brickssoft/locationtracking/http/ConfigChecks.kt +140 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +50 -12
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +17 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +507 -92
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +6 -6
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +23 -1
- package/android/src/main/java/com/brickssoft/locationtracking/http/UploadJobs.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/UploadWait.kt +184 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +66 -7
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogRedaction.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +44 -1
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +79 -8
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +12 -2
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +10 -2
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +24 -1
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +31 -10
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +48 -3
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/LocationSubscriptions.kt +180 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +200 -126
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +20 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +39 -51
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +8 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +47 -53
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +34 -2
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +19 -1
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +7 -0
- package/android/src/main/res/xml/lt_file_paths.xml +1 -1
- package/dist/esm/definitions.d.ts +117 -8
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/events.d.ts +8 -2
- package/dist/esm/events.js +7 -1
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web/config.js +10 -0
- package/dist/esm/web/config.js.map +1 -1
- package/dist/esm/web/device.d.ts +1 -1
- package/dist/esm/web/device.js +8 -1
- package/dist/esm/web/device.js.map +1 -1
- package/dist/esm/web.d.ts +0 -8
- package/dist/esm/web.js +12 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +39 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +39 -5
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
- package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
- package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
- package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
- package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
- package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
- package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
- package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
- package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
- package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
- package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
- package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
- package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
- package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
- package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
- package/package.json +10 -4
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)
|
|
@@ -72,7 +74,7 @@ whether the user really kept tracking on.
|
|
|
72
74
|
| Platform | Status |
|
|
73
75
|
|---|---|
|
|
74
76
|
| Android (API 24+, Kotlin) | ✔ Supported |
|
|
75
|
-
| iOS |
|
|
77
|
+
| iOS 15+ | Native route tracking, heartbeat, restore, positions and HTTP sync. Geofencing on iOS 17+, up to 20 geofences. |
|
|
76
78
|
| Web | Stub for development only: positions through `navigator.geolocation`, an in-memory config, permissions and device info. Nothing is stored or uploaded, and tracking can't be started. Everything else rejects with `UNIMPLEMENTED`. See [Web stub](#web-stub). |
|
|
77
79
|
|
|
78
80
|
## Installation
|
|
@@ -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). `
|
|
147
|
-
|
|
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
|
|
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
|
|
492
|
-
|
|
493
|
-
numbers that are not finite become the
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
@@ -816,7 +914,7 @@ How to measure the cost on a real phone: procedure M-04 in the
|
|
|
816
914
|
|
|
817
915
|
## Geofencing
|
|
818
916
|
|
|
819
|
-
- Up to **100** geofences (`TOO_MANY_GEOFENCES` beyond that). An identifier is 1–100 characters; adding a geofence
|
|
917
|
+
- Up to **100** geofences on Android and **20** on iOS (`TOO_MANY_GEOFENCES` beyond that). An identifier is 1–100 characters; adding a geofence
|
|
820
918
|
with an existing identifier replaces it. `loiteringDelay` must be ≥ 0. The identifier `__lt_stationary__` is
|
|
821
919
|
reserved for the plugin's stationary region: `addGeofence` rejects it with `INVALID_ARGUMENT`.
|
|
822
920
|
- The stationary region (see [Battery](#battery)) does not count against the 100. GMS accepts at most 100 geofences
|
|
@@ -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
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -1035,7 +1150,7 @@ Every rejected promise has a `code` (see the `ErrorCode` type) and a `message`.
|
|
|
1035
1150
|
| `NO_URL` | `sync()` was called without a valid `http.url`. |
|
|
1036
1151
|
| `HTTP_ERROR` | `sync()` got a non-2xx response. |
|
|
1037
1152
|
| `NETWORK_ERROR` | `sync()` failed with a network or I/O error. |
|
|
1038
|
-
| `TOO_MANY_GEOFENCES` | Adding the geofences would exceed 100 (or the OS geofence limit reported by GMS/HMS). |
|
|
1153
|
+
| `TOO_MANY_GEOFENCES` | Adding the geofences would exceed 100 on Android, 20 on iOS (or the OS geofence limit reported by GMS/HMS). |
|
|
1039
1154
|
| `NO_ACTIVITY` | `emailLog()` was called while no Activity is attached. (The `open*Settings` methods work without one.) |
|
|
1040
1155
|
| `IO_ERROR` | Storage or file error (database, log files). |
|
|
1041
1156
|
| `UNIMPLEMENTED` | Not available on this platform (web stub, iOS). |
|
|
@@ -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-
|
|
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
|
|
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/) |
|
|
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. |
|
|
@@ -1206,10 +1359,13 @@ Node ignores it.)
|
|
|
1206
1359
|
CI runs the emulator suites through `.github/scripts/run-e2e.sh` in `.github/workflows/e2e-android.yml`, only when
|
|
1207
1360
|
started by hand (Actions tab or `gh workflow run`; pushes and pull requests start no run, to save Actions minutes):
|
|
1208
1361
|
the plugin suite on API 34 (with smaller runs on API 29 and 35), P-P08 on an image without Google Play services, the
|
|
1209
|
-
field-force suite on API 34, and the long scenarios (with the `include-long` input).
|
|
1210
|
-
`.github/workflows/
|
|
1211
|
-
|
|
1212
|
-
|
|
1362
|
+
field-force suite on API 34, and the long scenarios (with the `include-long` input).
|
|
1363
|
+
`.github/workflows/typescript.yml` runs on every push: it type-checks the kit and runs its unit tests, type-checks and
|
|
1364
|
+
dry-runs both suites, and runs the field-force Node tests. `.github/workflows/android.yml`, also started by hand only,
|
|
1365
|
+
runs the Android unit tests, builds the example apps and checks that the Google Play build of the field-force app
|
|
1366
|
+
supports 16 KB memory pages (`.github/scripts/check-16kb.py`). `.github/workflows/ios.yml`, started by hand only, runs
|
|
1367
|
+
XCTest, the UI tests and the shared scenarios P-S01 to P-S07 on iOS simulators; on a Mac the same steps run with
|
|
1368
|
+
`.github/scripts/ios/check.sh all` ([docs/e2e-runbook.md](docs/e2e-runbook.md), 6.11).
|
|
1213
1369
|
|
|
1214
1370
|
The field-force suite, together with the field-force app and the kit, is written so that it can move into the Bricks
|
|
1215
1371
|
app as its integration test. The contract behind the tests (scenario ids, debug commands, mock back office) is
|
|
@@ -1238,15 +1394,26 @@ npm ci && npm run build && npm test
|
|
|
1238
1394
|
npm publish # prepare builds again; publishConfig.access makes it public
|
|
1239
1395
|
```
|
|
1240
1396
|
|
|
1241
|
-
Each later release:
|
|
1242
|
-
|
|
1243
|
-
1.
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1397
|
+
Each later release, with `scripts/release.sh` (bash; run it from the repository root on `master`):
|
|
1398
|
+
|
|
1399
|
+
1. While you work, write the changes under `## [Unreleased]` in `CHANGELOG.md`.
|
|
1400
|
+
2. `scripts/release.sh check <version>` runs every check and changes nothing: `master` is clean and equal to
|
|
1401
|
+
`origin/master`; the version is new and greater than the one on npm, and its major version is Capacitor's; the
|
|
1402
|
+
versions in `package.json`, `package-lock.json`, `src/web/device.ts` and
|
|
1403
|
+
`ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift` agree; `[Unreleased]` is not empty; git
|
|
1404
|
+
can sign; `npm run docgen` leaves the README unchanged; the changes since the last tag contain none of the
|
|
1405
|
+
private-name patterns (a file outside the repository, see the script); the steps of `typescript.yml` and
|
|
1406
|
+
`android.yml` pass on this computer, and on macOS those of `ios.yml` (elsewhere it warns that the iOS checks were
|
|
1407
|
+
skipped); and `npm pack` contains only the files listed in `package.json`.
|
|
1408
|
+
3. `scripts/release.sh run <version>` runs the checks, renames `[Unreleased]` to `[<version>] - <date>`, runs
|
|
1409
|
+
`npm version <version> --no-git-tag-version` (it sets `package.json`, `package-lock.json`, `PLUGIN_VERSION` in
|
|
1410
|
+
`src/web/device.ts` and `PluginInfo.version` in Swift; `android/build.gradle` reads `package.json` itself), and makes a signed commit and a signed tag
|
|
1411
|
+
`v<version>`. After you type `yes`, it pushes both and creates the GitHub release with the CHANGELOG section as
|
|
1412
|
+
notes. The release starts the workflow (it can also be started by hand from the Actions tab), which builds, runs
|
|
1413
|
+
`npm test` and runs `npm stage publish`; the script waits for it.
|
|
1414
|
+
4. In the package's **Staged Packages** tab on npmjs.com, approve the staged version (or run
|
|
1249
1415
|
`npm stage approve <stage-id>`) with 2FA. Only then can apps install it.
|
|
1416
|
+
5. `scripts/release.sh verify <version>` checks that npm serves it as `latest`, with a provenance statement.
|
|
1250
1417
|
|
|
1251
1418
|
Versions published by the workflow carry an npm provenance statement (the repository is public), which links each
|
|
1252
1419
|
version to the commit and workflow run that built it. The first version, published by hand, has none.
|
|
@@ -1313,6 +1480,8 @@ version to the commit and workflow run that built it. The first version, publish
|
|
|
1313
1480
|
* [`addListener('enabledchange', ...)`](#addlistenerenabledchange-)
|
|
1314
1481
|
* [`addListener('notificationaction', ...)`](#addlistenernotificationaction-)
|
|
1315
1482
|
* [`addListener('authorization', ...)`](#addlistenerauthorization-)
|
|
1483
|
+
* [`addListener('locationerror', ...)`](#addlistenerlocationerror-)
|
|
1484
|
+
* [`addListener('storagefull', ...)`](#addlistenerstoragefull-)
|
|
1316
1485
|
* [`removeAllListeners()`](#removealllisteners)
|
|
1317
1486
|
* [Interfaces](#interfaces)
|
|
1318
1487
|
* [Type Aliases](#type-aliases)
|
|
@@ -2114,6 +2283,38 @@ addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) =>
|
|
|
2114
2283
|
--------------------
|
|
2115
2284
|
|
|
2116
2285
|
|
|
2286
|
+
### addListener('locationerror', ...)
|
|
2287
|
+
|
|
2288
|
+
```typescript
|
|
2289
|
+
addListener(eventName: 'locationerror', listenerFunc: (e: LocationErrorEvent) => void) => Promise<PluginListenerHandle>
|
|
2290
|
+
```
|
|
2291
|
+
|
|
2292
|
+
| Param | Type |
|
|
2293
|
+
| ------------------ | --------------------------------------------------------------------------------- |
|
|
2294
|
+
| **`eventName`** | <code>'locationerror'</code> |
|
|
2295
|
+
| **`listenerFunc`** | <code>(e: <a href="#locationerrorevent">LocationErrorEvent</a>) => void</code> |
|
|
2296
|
+
|
|
2297
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2298
|
+
|
|
2299
|
+
--------------------
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
### addListener('storagefull', ...)
|
|
2303
|
+
|
|
2304
|
+
```typescript
|
|
2305
|
+
addListener(eventName: 'storagefull', listenerFunc: (e: StorageFullEvent) => void) => Promise<PluginListenerHandle>
|
|
2306
|
+
```
|
|
2307
|
+
|
|
2308
|
+
| Param | Type |
|
|
2309
|
+
| ------------------ | ----------------------------------------------------------------------------- |
|
|
2310
|
+
| **`eventName`** | <code>'storagefull'</code> |
|
|
2311
|
+
| **`listenerFunc`** | <code>(e: <a href="#storagefullevent">StorageFullEvent</a>) => void</code> |
|
|
2312
|
+
|
|
2313
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2314
|
+
|
|
2315
|
+
--------------------
|
|
2316
|
+
|
|
2317
|
+
|
|
2117
2318
|
### removeAllListeners()
|
|
2118
2319
|
|
|
2119
2320
|
```typescript
|
|
@@ -2128,15 +2329,18 @@ removeAllListeners() => Promise<void>
|
|
|
2128
2329
|
|
|
2129
2330
|
#### State
|
|
2130
2331
|
|
|
2131
|
-
| Prop
|
|
2132
|
-
|
|
|
2133
|
-
| **`enabled`**
|
|
2134
|
-
| **`trackingMode`**
|
|
2135
|
-
| **`isMoving`**
|
|
2136
|
-
| **`odometer`**
|
|
2137
|
-
| **`backend`**
|
|
2138
|
-
| **`lastRecordAt`**
|
|
2139
|
-
| **`
|
|
2332
|
+
| Prop | Type | Description |
|
|
2333
|
+
| -------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2334
|
+
| **`enabled`** | <code>boolean</code> | |
|
|
2335
|
+
| **`trackingMode`** | <code><a href="#trackingmode">TrackingMode</a></code> | |
|
|
2336
|
+
| **`isMoving`** | <code>boolean</code> | |
|
|
2337
|
+
| **`odometer`** | <code>number</code> | |
|
|
2338
|
+
| **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> | |
|
|
2339
|
+
| **`lastRecordAt`** | <code>string \| null</code> | |
|
|
2340
|
+
| **`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 |
|
|
2341
|
+
| **`lastStopAt`** | <code>string \| null</code> | when tracking stopped for `lastStopReason` (ISO-8601 UTC ms); null if none |
|
|
2342
|
+
| **`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. |
|
|
2343
|
+
| **`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
2344
|
|
|
2141
2345
|
|
|
2142
2346
|
#### Config
|
|
@@ -2199,32 +2403,34 @@ removeAllListeners() => Promise<void>
|
|
|
2199
2403
|
|
|
2200
2404
|
#### HeartbeatConfig
|
|
2201
2405
|
|
|
2202
|
-
| Prop
|
|
2203
|
-
|
|
|
2204
|
-
| **`enabled`**
|
|
2205
|
-
| **`minInterval`**
|
|
2206
|
-
| **`maxInterval`**
|
|
2406
|
+
| Prop | Type | Description | Default |
|
|
2407
|
+
| --------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
|
2408
|
+
| **`enabled`** | <code>boolean</code> | | <code>true</code> |
|
|
2409
|
+
| **`minInterval`** | <code>number</code> | seconds, min 60 | <code>180</code> |
|
|
2410
|
+
| **`maxInterval`** | <code>number</code> | seconds, >= minInterval | <code>300</code> |
|
|
2411
|
+
| **`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
2412
|
|
|
2208
2413
|
|
|
2209
2414
|
#### HttpConfig
|
|
2210
2415
|
|
|
2211
|
-
| Prop | Type | Description
|
|
2212
|
-
| ------------------------------- | --------------------------------------------------------------------------- |
|
|
2213
|
-
| **`url`** | <code>string \| null</code> | No url => nothing is uploaded (records stay queued).
|
|
2214
|
-
| **`method`** | <code><a href="#httpmethod">HttpMethod</a></code> |
|
|
2215
|
-
| **`headers`** | <code><a href="#record">Record</a><string, string></code> |
|
|
2216
|
-
| **`params`** | <code><a href="#record">Record</a><string, unknown></code> | merged into the ROOT of every request body
|
|
2217
|
-
| **`autoSync`** | <code>boolean</code> |
|
|
2218
|
-
| **`autoSyncThreshold`** | <code>number</code> | upload when queue >= threshold; 0 = every record
|
|
2219
|
-
| **`syncInterval`** | <code>number</code> | s; upload normal records once the oldest queued one is this old; 0 = off
|
|
2220
|
-
| **`batchSync`** | <code>boolean</code> |
|
|
2221
|
-
| **`maxBatchSize`** | <code>number</code> |
|
|
2222
|
-
| **`disableAutoSyncOnCellular`** | <code>boolean</code> |
|
|
2223
|
-
| **`rootProperty`** | <code>string</code> | '.' = no wrapping
|
|
2224
|
-
| **`locationTemplate`** | <code>string \| null</code> | JSON text with `<%= name %>` placeholders
|
|
2225
|
-
| **`geofenceTemplate`** | <code>string \| null</code> | used for event 'geofence'; falls back to locationTemplate
|
|
2226
|
-
| **`timeout`** | <code>number</code> | ms
|
|
2227
|
-
| **`
|
|
2416
|
+
| Prop | Type | Description | Default |
|
|
2417
|
+
| ------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
|
2418
|
+
| **`url`** | <code>string \| null</code> | No url => nothing is uploaded (records stay queued). | |
|
|
2419
|
+
| **`method`** | <code><a href="#httpmethod">HttpMethod</a></code> | | <code>'POST'</code> |
|
|
2420
|
+
| **`headers`** | <code><a href="#record">Record</a><string, string></code> | | <code>{}</code> |
|
|
2421
|
+
| **`params`** | <code><a href="#record">Record</a><string, unknown></code> | merged into the ROOT of every request body | <code>{}</code> |
|
|
2422
|
+
| **`autoSync`** | <code>boolean</code> | | <code>true</code> |
|
|
2423
|
+
| **`autoSyncThreshold`** | <code>number</code> | upload when queue >= threshold; 0 = every record | <code>0</code> |
|
|
2424
|
+
| **`syncInterval`** | <code>number</code> | s; upload normal records once the oldest queued one is this old; 0 = off | <code>0</code> |
|
|
2425
|
+
| **`batchSync`** | <code>boolean</code> | | <code>false</code> |
|
|
2426
|
+
| **`maxBatchSize`** | <code>number</code> | | <code>100</code> |
|
|
2427
|
+
| **`disableAutoSyncOnCellular`** | <code>boolean</code> | | <code>false</code> |
|
|
2428
|
+
| **`rootProperty`** | <code>string</code> | '.' = no wrapping | <code>'location'</code> |
|
|
2429
|
+
| **`locationTemplate`** | <code>string \| null</code> | JSON text with `<%= name %>` placeholders | |
|
|
2430
|
+
| **`geofenceTemplate`** | <code>string \| null</code> | used for event 'geofence'; falls back to locationTemplate | |
|
|
2431
|
+
| **`timeout`** | <code>number</code> | ms | <code>60000</code> |
|
|
2432
|
+
| **`maxRetryAfter`** | <code>number</code> | seconds, >= 0. The longest wait that a `Retry-After` header of a 429 or 503 answer can impose on automatic uploads. | <code>3600</code> |
|
|
2433
|
+
| **`authorization`** | <code><a href="#authorizationconfig">AuthorizationConfig</a> \| null</code> | | |
|
|
2228
2434
|
|
|
2229
2435
|
|
|
2230
2436
|
#### AuthorizationConfig
|
|
@@ -2288,12 +2494,13 @@ Posted when Android refuses to restore tracking from the background (after a reb
|
|
|
2288
2494
|
restart), typically on Android 14+ without "Allow all the time". A tap resumes the session
|
|
2289
2495
|
(`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.
|
|
2290
2496
|
|
|
2291
|
-
| Prop | Type | Default |
|
|
2292
|
-
| ----------------- | -------------------- | ---------------------------------------------------------- |
|
|
2293
|
-
| **`
|
|
2294
|
-
| **`
|
|
2295
|
-
| **`
|
|
2296
|
-
| **`
|
|
2497
|
+
| Prop | Type | Description | Default |
|
|
2498
|
+
| ----------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
2499
|
+
| **`delay`** | <code>number</code> | iOS only (Android warns about an unknown key): seconds from the last activity to the check-in notification; raised to `heartbeat.maxInterval + 60` when lower. | <code>600</code> |
|
|
2500
|
+
| **`enabled`** | <code>boolean</code> | | <code>false</code> |
|
|
2501
|
+
| **`title`** | <code>string</code> | | <code>app label</code> |
|
|
2502
|
+
| **`text`** | <code>string</code> | | <code>'Location tracking is paused. Tap to resume.'</code> |
|
|
2503
|
+
| **`channelName`** | <code>string</code> | | <code>'Paused location tracking'</code> |
|
|
2297
2504
|
|
|
2298
2505
|
|
|
2299
2506
|
#### GeofenceConfig
|
|
@@ -2331,27 +2538,30 @@ restart), typically on Android 14+ without "Allow all the time". A tap resumes t
|
|
|
2331
2538
|
|
|
2332
2539
|
#### Location
|
|
2333
2540
|
|
|
2334
|
-
| Prop | Type
|
|
2335
|
-
| ------------------------- |
|
|
2336
|
-
| **`uuid`** | <code>string</code>
|
|
2337
|
-
| **`event`** | <code><a href="#recordevent">RecordEvent</a></code>
|
|
2338
|
-
| **`timestamp`** | <code>string \| null</code>
|
|
2339
|
-
| **`recorded_at`** | <code>string</code>
|
|
2340
|
-
| **`sent_at`** | <code>string</code>
|
|
2341
|
-
| **`elapsed_realtime_ms`** | <code>number</code>
|
|
2342
|
-
| **`boot_count`** | <code>number</code>
|
|
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>
|
|
2349
|
-
| **`backend`** | <code><a href="#locationbackend">LocationBackend</a> \| null</code>
|
|
2350
|
-
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code>
|
|
2351
|
-
| **`geofence`** | <code>{ identifier: string; action: <a href="#geofenceaction">GeofenceAction</a>; extras?: <a href="#record">Record</a><string, unknown>; }</code>
|
|
2352
|
-
| **`provider`** | <code><a href="#providerstate">ProviderState</a></code>
|
|
2353
|
-
| **`reason`** | <code>string</code>
|
|
2354
|
-
| **`
|
|
2541
|
+
| Prop | Type | Description |
|
|
2542
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2543
|
+
| **`uuid`** | <code>string</code> | |
|
|
2544
|
+
| **`event`** | <code><a href="#recordevent">RecordEvent</a></code> | |
|
|
2545
|
+
| **`timestamp`** | <code>string \| null</code> | fix time (ISO-8601 UTC ms); null if no location ever known |
|
|
2546
|
+
| **`recorded_at`** | <code>string</code> | record creation time |
|
|
2547
|
+
| **`sent_at`** | <code>string</code> | only present in HTTP bodies |
|
|
2548
|
+
| **`elapsed_realtime_ms`** | <code>number</code> | |
|
|
2549
|
+
| **`boot_count`** | <code>number</code> | -1 if unavailable |
|
|
2550
|
+
| **`is_moving`** | <code>boolean</code> | |
|
|
2551
|
+
| **`odometer`** | <code>number</code> | |
|
|
2552
|
+
| **`mock`** | <code>boolean</code> | |
|
|
2553
|
+
| **`coords`** | <code><a href="#coords">Coords</a> \| null</code> | |
|
|
2554
|
+
| **`activity`** | <code>{ type: <a href="#activitytype">ActivityType</a>; confidence: number; }</code> | |
|
|
2555
|
+
| **`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. |
|
|
2556
|
+
| **`backend`** | <code><a href="#locationbackend">LocationBackend</a> \| null</code> | |
|
|
2557
|
+
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code> | |
|
|
2558
|
+
| **`geofence`** | <code>{ identifier: string; action: <a href="#geofenceaction">GeofenceAction</a>; extras?: <a href="#record">Record</a><string, unknown>; }</code> | event 'geofence' only |
|
|
2559
|
+
| **`provider`** | <code><a href="#providerstate">ProviderState</a></code> | provider state when the record was created; the new state for event 'providerchange' |
|
|
2560
|
+
| **`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 |
|
|
2561
|
+
| **`stopped_at`** | <code>string</code> | event 'tracking_stop' with reason 'user_stopped' only: when the user stopped the app (ISO-8601 UTC ms) |
|
|
2562
|
+
| **`heartbeat`** | <code><a href="#heartbeatmeta">HeartbeatMeta</a></code> | event 'heartbeat' only, optional: how the heartbeat is scheduled |
|
|
2563
|
+
| **`network`** | <code><a href="#connectivitytype">ConnectivityType</a></code> | connection type when the record was created |
|
|
2564
|
+
| **`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
2565
|
|
|
2356
2566
|
|
|
2357
2567
|
#### Coords
|
|
@@ -2385,14 +2595,36 @@ restart), typically on Android 14+ without "Allow all the time". A tap resumes t
|
|
|
2385
2595
|
|
|
2386
2596
|
Scheduling metadata of a `heartbeat` record (snake_case = wire format).
|
|
2387
2597
|
|
|
2388
|
-
| Prop | Type
|
|
2389
|
-
| -------------------- |
|
|
2390
|
-
| **`strategy`** | <code>'exact' \| 'listener_with_backup' \| 'idle_paced'</code> | |
|
|
2391
|
-
| **`min_interval`** | <code>number</code>
|
|
2392
|
-
| **`max_interval`** | <code>number</code>
|
|
2393
|
-
| **`next_at`** | <code>string \| null</code>
|
|
2394
|
-
| **`battery_exempt`** | <code>boolean</code>
|
|
2395
|
-
| **`device_idle`** | <code>boolean</code>
|
|
2598
|
+
| Prop | Type | Description |
|
|
2599
|
+
| -------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
2600
|
+
| **`strategy`** | <code>'exact' \| 'listener_with_backup' \| 'idle_paced' \| 'timer'</code> | |
|
|
2601
|
+
| **`min_interval`** | <code>number</code> | heartbeat.minInterval (s) when the heartbeat was created |
|
|
2602
|
+
| **`max_interval`** | <code>number</code> | heartbeat.maxInterval (s) when the heartbeat was created |
|
|
2603
|
+
| **`next_at`** | <code>string \| null</code> | when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown |
|
|
2604
|
+
| **`battery_exempt`** | <code>boolean</code> | the app is exempt from battery optimization |
|
|
2605
|
+
| **`device_idle`** | <code>boolean</code> | the device was in deep Doze |
|
|
2606
|
+
|
|
2607
|
+
|
|
2608
|
+
#### DeviceReadings
|
|
2609
|
+
|
|
2610
|
+
The `device` object of records; a reading is present only when the device provides it.
|
|
2611
|
+
Every record: `device_id`.
|
|
2612
|
+
Heartbeat records: `background_restricted`, `standby_bucket`, `disk_free_bytes`, `thermal_status`.
|
|
2613
|
+
`tracking_start` records: `manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes`.
|
|
2614
|
+
|
|
2615
|
+
| Prop | Type | Description |
|
|
2616
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2617
|
+
| **`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. |
|
|
2618
|
+
| **`background_restricted`** | <code>boolean</code> | the user set the app's battery use to "Restricted" (Android 9+) |
|
|
2619
|
+
| **`standby_bucket`** | <code>'exempted' \| 'active' \| 'working_set' \| 'frequent' \| 'rare' \| 'restricted'</code> | App Standby bucket (Android 9+) |
|
|
2620
|
+
| **`disk_free_bytes`** | <code>number</code> | free space on the storage that holds the app's files |
|
|
2621
|
+
| **`thermal_status`** | <code>'none' \| 'unknown' \| 'light' \| 'moderate' \| 'severe' \| 'critical' \| 'emergency' \| 'shutdown'</code> | Android 10+ |
|
|
2622
|
+
| **`manufacturer`** | <code>string</code> | |
|
|
2623
|
+
| **`model`** | <code>string</code> | |
|
|
2624
|
+
| **`os_version`** | <code>string</code> | Android version, e.g. "14" |
|
|
2625
|
+
| **`sdk_int`** | <code>number</code> | |
|
|
2626
|
+
| **`low_ram_device`** | <code>boolean</code> | |
|
|
2627
|
+
| **`ram_total_bytes`** | <code>number</code> | |
|
|
2396
2628
|
|
|
2397
2629
|
|
|
2398
2630
|
#### CurrentPositionOptions
|
|
@@ -2419,13 +2651,13 @@ Scheduling metadata of a `heartbeat` record (snake_case = wire format).
|
|
|
2419
2651
|
|
|
2420
2652
|
#### InsertLocationInput
|
|
2421
2653
|
|
|
2422
|
-
| Prop | Type
|
|
2423
|
-
| --------------- |
|
|
2424
|
-
| **`coords`** | <code><a href="#pick">Pick</a><<a href="#coords">Coords</a>, 'latitude' \| 'longitude'> & <a href="#partial">Partial</a><<a href="#coords">Coords</a>></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><string, unknown></code>
|
|
2654
|
+
| Prop | Type | Description |
|
|
2655
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
2656
|
+
| **`coords`** | <code><a href="#pick">Pick</a><<a href="#coords">Coords</a>, 'latitude' \| 'longitude' \| 'accuracy'> & <a href="#partial">Partial</a><<a href="#coords">Coords</a>></code> | `accuracy` (m) is required and must be greater than 0; otherwise the call rejects with `INVALID_ARGUMENT`. |
|
|
2657
|
+
| **`timestamp`** | <code>string</code> | |
|
|
2658
|
+
| **`event`** | <code><a href="#recordevent">RecordEvent</a></code> | |
|
|
2659
|
+
| **`is_moving`** | <code>boolean</code> | |
|
|
2660
|
+
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code> | |
|
|
2429
2661
|
|
|
2430
2662
|
|
|
2431
2663
|
#### Geofence
|
|
@@ -2484,19 +2716,26 @@ the computed enclosing circle.
|
|
|
2484
2716
|
|
|
2485
2717
|
#### DeviceInfo
|
|
2486
2718
|
|
|
2487
|
-
| Prop
|
|
2488
|
-
|
|
|
2489
|
-
| **`platform`**
|
|
2490
|
-
| **`manufacturer`**
|
|
2491
|
-
| **`model`**
|
|
2492
|
-
| **`brand`**
|
|
2493
|
-
| **`osVersion`**
|
|
2494
|
-
| **`sdkInt`**
|
|
2495
|
-
| **`pluginVersion`**
|
|
2496
|
-
| **`gmsAvailable`**
|
|
2497
|
-
| **`hmsAvailable`**
|
|
2498
|
-
| **`backend`**
|
|
2499
|
-
| **`packagedProviders`**
|
|
2719
|
+
| Prop | Type | Description |
|
|
2720
|
+
| --------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
2721
|
+
| **`platform`** | <code>'android' \| 'web' \| 'ios'</code> | |
|
|
2722
|
+
| **`manufacturer`** | <code>string</code> | |
|
|
2723
|
+
| **`model`** | <code>string</code> | |
|
|
2724
|
+
| **`brand`** | <code>string</code> | |
|
|
2725
|
+
| **`osVersion`** | <code>string</code> | |
|
|
2726
|
+
| **`sdkInt`** | <code>number</code> | |
|
|
2727
|
+
| **`pluginVersion`** | <code>string</code> | |
|
|
2728
|
+
| **`gmsAvailable`** | <code>boolean</code> | |
|
|
2729
|
+
| **`hmsAvailable`** | <code>boolean</code> | |
|
|
2730
|
+
| **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> | |
|
|
2731
|
+
| **`packagedProviders`** | <code>string[]</code> | |
|
|
2732
|
+
| **`backendFallbackReason`** | <code>string \| null</code> | why a better location backend is not used (for example "gms: unavailable (status 2)"); null when none is better |
|
|
2733
|
+
| **`gmsStatus`** | <code>number \| null</code> | raw GoogleApiAvailability result (0 = success); null if GMS is not packaged or not checked yet |
|
|
2734
|
+
| **`hmsStatus`** | <code>number \| null</code> | raw HuaweiApiAvailability result (0 = success); null if HMS is not packaged or not checked yet |
|
|
2735
|
+
| **`hmsCoreOutdated`** | <code>boolean \| null</code> | true when HMS Core is older than 6.3.0.301; null if HMS is not packaged |
|
|
2736
|
+
| **`locationAvailableHint`** | <code>boolean \| null</code> | last LocationAvailability reported by GMS or HMS; only a hint (it can be wrong both ways); null if none |
|
|
2737
|
+
| **`cpuCores`** | <code>number</code> | number of CPU cores |
|
|
2738
|
+
| **`supportedAbis`** | <code>string[]</code> | CPU types (Build.SUPPORTED_ABIS), e.g. ["arm64-v8a"] |
|
|
2500
2739
|
|
|
2501
2740
|
|
|
2502
2741
|
#### Sensors
|
|
@@ -2608,9 +2847,10 @@ the computed enclosing circle.
|
|
|
2608
2847
|
|
|
2609
2848
|
#### EnabledChangeEvent
|
|
2610
2849
|
|
|
2611
|
-
| Prop | Type
|
|
2612
|
-
| ------------- |
|
|
2613
|
-
| **`enabled`** | <code>boolean</code>
|
|
2850
|
+
| Prop | Type | Description |
|
|
2851
|
+
| ------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2852
|
+
| **`enabled`** | <code>boolean</code> | |
|
|
2853
|
+
| **`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
2854
|
|
|
2615
2855
|
|
|
2616
2856
|
#### NotificationActionEvent
|
|
@@ -2630,6 +2870,29 @@ the computed enclosing circle.
|
|
|
2630
2870
|
| **`response`** | <code><a href="#record">Record</a><string, unknown></code> |
|
|
2631
2871
|
|
|
2632
2872
|
|
|
2873
|
+
#### LocationErrorEvent
|
|
2874
|
+
|
|
2875
|
+
The location request failed (no permission, Play services or HMS Core refused it, no usable provider).
|
|
2876
|
+
|
|
2877
|
+
| Prop | Type | Description |
|
|
2878
|
+
| --------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
2879
|
+
| **`code`** | <code><a href="#errorcode">ErrorCode</a></code> | |
|
|
2880
|
+
| **`message`** | <code>string</code> | |
|
|
2881
|
+
| **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> | |
|
|
2882
|
+
| **`retryInMs`** | <code>number \| null</code> | the plugin requests location updates again after this many ms; null if it does not |
|
|
2883
|
+
|
|
2884
|
+
|
|
2885
|
+
#### StorageFullEvent
|
|
2886
|
+
|
|
2887
|
+
A record could not be stored because the device storage is full.
|
|
2888
|
+
|
|
2889
|
+
| Prop | Type |
|
|
2890
|
+
| ------------------- | --------------------------------------------------- |
|
|
2891
|
+
| **`uuid`** | <code>string</code> |
|
|
2892
|
+
| **`event`** | <code><a href="#recordevent">RecordEvent</a></code> |
|
|
2893
|
+
| **`diskFreeBytes`** | <code>number \| null</code> |
|
|
2894
|
+
|
|
2895
|
+
|
|
2633
2896
|
### Type Aliases
|
|
2634
2897
|
|
|
2635
2898
|
|
|
@@ -2640,7 +2903,7 @@ the computed enclosing circle.
|
|
|
2640
2903
|
|
|
2641
2904
|
#### LocationBackend
|
|
2642
2905
|
|
|
2643
|
-
<code>'gms' | 'hms' | 'android' | 'web'</code>
|
|
2906
|
+
<code>'gms' | 'hms' | 'android' | 'web' | 'ios'</code>
|
|
2644
2907
|
|
|
2645
2908
|
|
|
2646
2909
|
#### DesiredAccuracy
|
|
@@ -2657,9 +2920,7 @@ the computed enclosing circle.
|
|
|
2657
2920
|
|
|
2658
2921
|
Construct a type with a set of properties K of type T
|
|
2659
2922
|
|
|
2660
|
-
<code>{
|
|
2661
|
-
[P in K]: T;
|
|
2662
|
-
}</code>
|
|
2923
|
+
<code>{ [P in K]: T; }</code>
|
|
2663
2924
|
|
|
2664
2925
|
|
|
2665
2926
|
#### NotificationPriority
|
|
@@ -2692,6 +2953,11 @@ Construct a type with a set of properties K of type T
|
|
|
2692
2953
|
<code>'ENTER' | 'EXIT' | 'DWELL'</code>
|
|
2693
2954
|
|
|
2694
2955
|
|
|
2956
|
+
#### ConnectivityType
|
|
2957
|
+
|
|
2958
|
+
<code>'wifi' | 'cellular' | 'ethernet' | 'other' | 'none'</code>
|
|
2959
|
+
|
|
2960
|
+
|
|
2695
2961
|
#### WatchPositionCallback
|
|
2696
2962
|
|
|
2697
2963
|
<code>(location: <a href="#location">Location</a> | null, error?: { code: <a href="#errorcode">ErrorCode</a>; message: string; }): void</code>
|
|
@@ -2713,23 +2979,19 @@ Rejection `code` of every failed promise.
|
|
|
2713
2979
|
|
|
2714
2980
|
From T, pick a set of properties whose keys are in the union K
|
|
2715
2981
|
|
|
2716
|
-
<code>{
|
|
2717
|
-
[P in K]: T[P];
|
|
2718
|
-
}</code>
|
|
2982
|
+
<code>{ [P in K]: T[P]; }</code>
|
|
2719
2983
|
|
|
2720
2984
|
|
|
2721
2985
|
#### Partial
|
|
2722
2986
|
|
|
2723
2987
|
Make all properties in T optional
|
|
2724
2988
|
|
|
2725
|
-
<code>{
|
|
2726
|
-
[P in keyof T]?: T[P];
|
|
2727
|
-
}</code>
|
|
2989
|
+
<code>{ [P in keyof T]?: T[P]; }</code>
|
|
2728
2990
|
|
|
2729
2991
|
|
|
2730
2992
|
#### HeartbeatStrategy
|
|
2731
2993
|
|
|
2732
|
-
<code>'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled'</code>
|
|
2994
|
+
<code>'exact' | 'listener_with_backup' | 'idle_paced' | 'timer' | 'disabled'</code>
|
|
2733
2995
|
|
|
2734
2996
|
|
|
2735
2997
|
#### PermissionState
|
|
@@ -2748,13 +3010,80 @@ Make all properties in T optional
|
|
|
2748
3010
|
|
|
2749
3011
|
<code>T extends U ? never : T</code>
|
|
2750
3012
|
|
|
2751
|
-
|
|
2752
|
-
#### ConnectivityType
|
|
2753
|
-
|
|
2754
|
-
<code>'wifi' | 'cellular' | 'ethernet' | 'other' | 'none'</code>
|
|
2755
|
-
|
|
2756
3013
|
</docgen-api>
|
|
2757
3014
|
|
|
2758
3015
|
## License
|
|
2759
3016
|
|
|
2760
3017
|
MIT, © 2026 Bricks Soft. See [LICENSE](LICENSE).
|
|
3018
|
+
|
|
3019
|
+
## iOS setup
|
|
3020
|
+
|
|
3021
|
+
The iOS implementation supports native route tracking, motion detection, heartbeat audit records, restore, check-in
|
|
3022
|
+
notifications, configuration, SQLite queueing, HTTP sync, permissions, current positions and watches. Geofencing needs
|
|
3023
|
+
iOS 17; below it, `startGeofences` and the geofence methods reject with `UNAVAILABLE`.
|
|
3024
|
+
|
|
3025
|
+
The deployment target is iOS 15. Install `@capacitor/ios` 8 and run `npx cap sync ios`. Both SPM and CocoaPods are supported.
|
|
3026
|
+
In the host app's Info.plist, add:
|
|
3027
|
+
|
|
3028
|
+
```xml
|
|
3029
|
+
<key>UIBackgroundModes</key><array><string>location</string><string>fetch</string></array>
|
|
3030
|
+
<key>BGTaskSchedulerPermittedIdentifiers</key><array><string>com.brickssoft.locationtracking.refresh</string></array>
|
|
3031
|
+
<key>NSLocationWhenInUseUsageDescription</key><string>Explain your foreground tracking purpose.</string>
|
|
3032
|
+
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key><string>Explain your optional background tracking purpose.</string>
|
|
3033
|
+
<key>NSMotionUsageDescription</key><string>Explain how motion selects the tracking profile.</string>
|
|
3034
|
+
```
|
|
3035
|
+
|
|
3036
|
+
Call the native bootstrap before `application(_:didFinishLaunchingWithOptions:)` returns, so the background refresh
|
|
3037
|
+
handler is registered during launch:
|
|
3038
|
+
|
|
3039
|
+
```swift
|
|
3040
|
+
import LocationTrackingPlugin
|
|
3041
|
+
|
|
3042
|
+
LocationTrackingNative.bootstrap()
|
|
3043
|
+
```
|
|
3044
|
+
|
|
3045
|
+
For apps that track only while they are in use, request `{ permissions: ['location'] }`. Always is optional, and the
|
|
3046
|
+
Always purpose string is still required because the library references that API. When the user grants approximate
|
|
3047
|
+
location, `ProviderState.accuracy` is `'approximate'` and a `providerchange` event follows; stored geofences stay inactive
|
|
3048
|
+
and a `PERMISSION_DENIED` `locationerror` says why. Explain it in the app and call `openAppSettings()` so the user can
|
|
3049
|
+
turn on Precise Location, which stays on.
|
|
3050
|
+
|
|
3051
|
+
Queued records and configuration use Application Support with backup exclusion and protection until first unlock.
|
|
3052
|
+
Authorization is held in Keychain with the same unlock requirement. HTTP uses an ephemeral session, refuses redirects,
|
|
3053
|
+
and treats every 2xx as success; keep your upload endpoint on HTTPS. A refresh task is best effort and does not keep
|
|
3054
|
+
the app continuously running. `stop()` leaves position watches active; clear each watch explicitly.
|
|
3055
|
+
|
|
3056
|
+
While moving, the route uses the configured accuracy. While stationary, it keeps continuous 100 m updates so that
|
|
3057
|
+
movement can resume tracking without Motion permission. Core Motion confidence is 33, 66 or 100; the default confidence
|
|
3058
|
+
threshold accepts only high-confidence samples. Location update intervals are controlled by iOS.
|
|
3059
|
+
|
|
3060
|
+
Heartbeats use an in-process timer and are not backfilled after a suspension or kill. When In Use tracking must start
|
|
3061
|
+
in the foreground; enabled intent restores when the app next becomes active. Always authorization adds significant-change
|
|
3062
|
+
monitoring as a relaunch trigger. iOS does not relaunch a force-quit app for location events. If Location Services are
|
|
3063
|
+
switched off for the whole device, enabled tracking keeps its intent and original deadline and resumes when they are
|
|
3064
|
+
switched back on; a revoked app permission still stops it with `permission_denied`.
|
|
3065
|
+
|
|
3066
|
+
Enable `notification.resume` and request `notifications` permission for a pending check-in after tracking goes quiet.
|
|
3067
|
+
Its iOS `delay` defaults to 600 seconds and is raised to `heartbeat.maxInterval + 60` when lower. Fixes and records move
|
|
3068
|
+
one pending notification, at most once a minute. Stop and session expiry cancel it. A tap opens the app and restores the
|
|
3069
|
+
original session deadline. The plugin leaves Capacitor's notification-center delegate in place.
|
|
3070
|
+
|
|
3071
|
+
Geofences use one [`CLMonitor`](https://developer.apple.com/documentation/corelocation/clmonitor), opened again under the
|
|
3072
|
+
same name at every launch, and are registered while tracking runs. A polygon registers its enclosing circle and is
|
|
3073
|
+
hit-tested against fixes while the device is inside that circle. DWELL is synthesized from the time inside, so it fires
|
|
3074
|
+
only while the app runs. With Always authorization on iOS 18 and later, the plugin holds a
|
|
3075
|
+
[`CLServiceSession`](https://developer.apple.com/documentation/corelocation/clservicesession) so events also arrive when
|
|
3076
|
+
the app is not in use; with When In Use they arrive only while it runs. While location accuracy is approximate, stored
|
|
3077
|
+
geofences stay inactive and a `locationerror` with `PERMISSION_DENIED` says so.
|
|
3078
|
+
|
|
3079
|
+
`stopOnTerminate` handles the best-effort termination callback. An OS kill without that callback preserves enabled intent,
|
|
3080
|
+
even when `stopOnTerminate` is true. `startOnBoot` is ignored. Deadlines use the continuous elapsed clock within the process.
|
|
3081
|
+
On restore, matching saved wall/elapsed checkpoints preserve elapsed timing; a detected reboot or clock discontinuity uses
|
|
3082
|
+
the original wall-clock start. A wall-clock change while the process is absent cannot be distinguished reliably from a
|
|
3083
|
+
reboot on current iOS, which [no longer exposes a boot-session identifier](https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-18-release-notes).
|
|
3084
|
+
Fixes are ordered by the elapsed clock at delivery, so a wall-clock correction does not make new fixes look stale.
|
|
3085
|
+
|
|
3086
|
+
`example/ios` is the example host. Run `npm run copy-vendor` and `npx cap sync ios` from `example/` before building.
|
|
3087
|
+
The iOS workflow installs the npm tarball into a fresh app through each package manager and archives both unsigned and ad-hoc signed Release
|
|
3088
|
+
IPAs. The ad-hoc identity is `-`, with no provisioning profile; it does not provide Apple device authorization. Installing it on an iPhone still requires an Apple signing certificate/private key, a matching provisioning profile
|
|
3089
|
+
and entitlements for its bundle identifier, or a device-cloud service that re-signs uploaded apps.
|