@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.1
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/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +107 -0
- package/android/consumer-rules.pro +42 -0
- package/android/src/main/AndroidManifest.xml +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
- package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
- package/android/src/main/res/values/lt_strings.xml +11 -0
- package/android/src/main/res/xml/lt_file_paths.xml +5 -0
- package/dist/esm/definitions.d.ts +719 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/events.d.ts +50 -0
- package/dist/esm/events.js +52 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +10 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/web/config.d.ts +19 -0
- package/dist/esm/web/config.js +243 -0
- package/dist/esm/web/config.js.map +1 -0
- package/dist/esm/web/device.d.ts +9 -0
- package/dist/esm/web/device.js +90 -0
- package/dist/esm/web/device.js.map +1 -0
- package/dist/esm/web/errors.d.ts +14 -0
- package/dist/esm/web/errors.js +19 -0
- package/dist/esm/web/errors.js.map +1 -0
- package/dist/esm/web/geolocation.d.ts +26 -0
- package/dist/esm/web/geolocation.js +117 -0
- package/dist/esm/web/geolocation.js.map +1 -0
- package/dist/esm/web/location.d.ts +7 -0
- package/dist/esm/web/location.js +70 -0
- package/dist/esm/web/location.js.map +1 -0
- package/dist/esm/web.d.ts +164 -0
- package/dist/esm/web.js +328 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +945 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +948 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +60 -4
package/README.md
CHANGED
|
@@ -1,3 +1,2760 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bricks-soft/capacitor-location-tracking
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Background location tracking for Capacitor 8 on Android. It records locations with Google Play services (GMS),
|
|
4
|
+
Huawei Location Kit (HMS) or the plain Android `LocationManager`, stores them in SQLite and uploads them to your
|
|
5
|
+
server. While tracking is on, it also sends **heartbeat** audit records every 3–5 minutes, so your server can tell
|
|
6
|
+
whether the user really kept tracking on.
|
|
7
|
+
|
|
8
|
+
> **Clean-room implementation.** This plugin offers a feature set similar to
|
|
9
|
+
> [transistorsoft/capacitor-background-geolocation](https://github.com/transistorsoft/capacitor-background-geolocation),
|
|
10
|
+
> with a new, smaller API. It was written from scratch: it contains no code from that project (whose native engine is
|
|
11
|
+
> closed-source and commercial) and is **not affiliated with, endorsed by or supported by Transistor Software**.
|
|
12
|
+
> Moving an app from its v8 release: [docs/migrating-from-transistor-v8.md](docs/migrating-from-transistor-v8.md).
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Platform status](#platform-status)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Android setup](#android-setup)
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [Configuration reference](#configuration-reference)
|
|
22
|
+
- [Tracking behavior](#tracking-behavior)
|
|
23
|
+
- [Battery](#battery)
|
|
24
|
+
- [Geofencing](#geofencing)
|
|
25
|
+
- [Records, uploads and the queue](#records-uploads-and-the-queue) (including
|
|
26
|
+
[live location with `http.syncInterval`](#live-location-httpsyncinterval))
|
|
27
|
+
- [Events reference](#events-reference)
|
|
28
|
+
- [Companion plugins (native API)](#companion-plugins-native-api)
|
|
29
|
+
- [Error codes](#error-codes)
|
|
30
|
+
- [Web stub](#web-stub)
|
|
31
|
+
- [Server side](#server-side)
|
|
32
|
+
- [Example app](#example-app)
|
|
33
|
+
- [Field-force example](#field-force-example)
|
|
34
|
+
- [End-to-end tests](#end-to-end-tests)
|
|
35
|
+
- [Publishing to npm](#publishing-to-npm)
|
|
36
|
+
- [API](#api)
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Motion-aware tracking.** The plugin switches between *moving* (continuous, high-accuracy updates with an elastic
|
|
41
|
+
distance filter) and *stationary* (GPS off; a stationary geofence, passive fixes and activity recognition watch for
|
|
42
|
+
movement). It reports `motionchange` and `activitychange` events. See [Battery](#battery).
|
|
43
|
+
- **Three location backends.** It supports GMS fused location, HMS fused location and the Android `LocationManager`.
|
|
44
|
+
You choose which SDKs go into the APK at build time (`locationTracking.providers`) and which one to use at runtime
|
|
45
|
+
(`locationProvider: 'auto' | 'gms' | 'hms' | 'android'`).
|
|
46
|
+
- **Heartbeat audit.** If no record has been created for `heartbeat.minInterval` seconds (default 180), the plugin
|
|
47
|
+
creates a `heartbeat` record with the last known location. It should exist before `maxInterval` (default 300), and
|
|
48
|
+
it is uploaded immediately. Its `heartbeat` object tells the server when the next one is due. See
|
|
49
|
+
[docs/heartbeat.md](docs/heartbeat.md).
|
|
50
|
+
- **Audit records.** `tracking_start`, `tracking_stop` (with a reason) and `providerchange` records are uploaded the
|
|
51
|
+
same way, so the server can explain gaps.
|
|
52
|
+
- **Offline-first HTTP sync.** Every record is stored in SQLite first. Uploads can send one record or a batch per
|
|
53
|
+
request, use JSON templates, merge `params` into the body, and refresh a JWT on `401`. Failed uploads stay queued
|
|
54
|
+
and are retried, and `recorded_at` / `sent_at` expose late delivery. `http.syncInterval` batches location uploads
|
|
55
|
+
while keeping the server's live location at most that many seconds old. See [docs/wire-format.md](docs/wire-format.md).
|
|
56
|
+
- **Companion native API.** Another plugin in the same app (Kotlin) receives every record and every event natively,
|
|
57
|
+
also when no WebView runs (after a reboot or an alarm), and can call the plugin. See
|
|
58
|
+
[Companion plugins](#companion-plugins-native-api).
|
|
59
|
+
- **Geofencing.** Up to 100 circular or polygon geofences, with `ENTER`, `EXIT` and `DWELL` transitions, and a
|
|
60
|
+
geofences-only tracking mode (`startGeofences()`). See [Geofencing](#geofencing).
|
|
61
|
+
- **One-off and watched positions.** `getCurrentPosition()` (best of N samples) and `watchPosition()`.
|
|
62
|
+
- **Odometer** with an accuracy gate, plus location filters: accuracy threshold, implied-speed filter, optional Kalman
|
|
63
|
+
smoothing and mock-location handling.
|
|
64
|
+
- **Foreground service** with a configurable notification and up to three action buttons.
|
|
65
|
+
- **Restart after reboot, app update and process death** (`startOnBoot`, `stopOnTerminate: false`).
|
|
66
|
+
- **Device helpers.** Permission handling (including the Android 10/11+ background-location flow), battery-optimization
|
|
67
|
+
status and settings, phone-maker power-manager screens, provider state, device info and sensors.
|
|
68
|
+
- **Persistent log.** A file log with levels and retention, which you can query, upload or email.
|
|
69
|
+
|
|
70
|
+
## Platform status
|
|
71
|
+
|
|
72
|
+
| Platform | Status |
|
|
73
|
+
|---|---|
|
|
74
|
+
| Android (API 24+, Kotlin) | ✔ Supported |
|
|
75
|
+
| iOS | Planned for a later phase. Until then every call rejects with "not implemented on ios". |
|
|
76
|
+
| 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
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm i @bricks-soft/capacitor-location-tracking
|
|
82
|
+
npx cap sync android
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Requirements: Capacitor 8, Android `minSdk` 24, `compileSdk` / `targetSdk` 36 and JDK 21. The plugin is built with
|
|
86
|
+
AGP 8.13 and Kotlin 2.2.
|
|
87
|
+
|
|
88
|
+
## Android setup
|
|
89
|
+
|
|
90
|
+
### 1. Choose the location SDKs to package
|
|
91
|
+
|
|
92
|
+
The plugin compiles against both SDKs. The Gradle property `locationTracking.providers` decides which of them are
|
|
93
|
+
**packaged** into your APK. Set it in your app's `android/gradle.properties`:
|
|
94
|
+
|
|
95
|
+
```properties
|
|
96
|
+
# gms (default) | hms | gms,hms
|
|
97
|
+
locationTracking.providers=gms,hms
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| Value | Packages | Typical use |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `gms` (default) | `com.google.android.gms:play-services-location` | Google Play builds |
|
|
103
|
+
| `hms` | `com.huawei.hms:location` | AppGallery-only builds |
|
|
104
|
+
| `gms,hms` | both | One APK for phones with Google services and Huawei phones without them |
|
|
105
|
+
|
|
106
|
+
You can also pass the property on the command line (`./gradlew assembleRelease -PlocationTracking.providers=hms`).
|
|
107
|
+
The property applies to the whole Gradle run, so every product flavor of the app gets the same SDKs. To build a Play
|
|
108
|
+
APK and an AppGallery APK from product flavors, use `none` and add the SDKs per flavor, as described in
|
|
109
|
+
[Separate Play and AppGallery APKs (product flavors)](#separate-play-and-appgallery-apks-product-flavors).
|
|
110
|
+
|
|
111
|
+
The SDK versions default to `play-services-location` 21.3.0 and `com.huawei.hms:location` 6.16.0.302. To use other
|
|
112
|
+
versions, set `playServicesLocationVersion` or `hmsLocationVersion` in the `ext` block of your app's
|
|
113
|
+
`android/variables.gradle`. If you package `hms` for Google Play, keep 6.16.0.302: it is Huawei's first release with
|
|
114
|
+
16 KB page-size support, and the latest one in Huawei's version history. Earlier versions bring native libraries with
|
|
115
|
+
4 KB alignment, which fail Play's 16 KB page-size requirement; so do the undocumented 6.17–6.19 builds in Huawei's
|
|
116
|
+
Maven repository.
|
|
117
|
+
|
|
118
|
+
At runtime, the `locationProvider` config option picks the backend. GMS or HMS counts as *packaged* when all three
|
|
119
|
+
of these are true:
|
|
120
|
+
|
|
121
|
+
1. The app allows it. By default both are allowed. An app restricts them with a `<meta-data>` entry in its
|
|
122
|
+
`<application>`, for example per product flavor:
|
|
123
|
+
|
|
124
|
+
```xml
|
|
125
|
+
<meta-data android:name="com.brickssoft.locationtracking.PROVIDERS" android:value="hms"/>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The value is a comma-separated list of `gms`, `hms` and `android` (case and spaces don't matter). `android` alone
|
|
129
|
+
means the plugin uses neither GMS nor HMS. Unknown names are ignored with a warning in the plugin log; a value with
|
|
130
|
+
no known name is ignored with an error, and both providers stay allowed.
|
|
131
|
+
2. Its SDK classes are in the APK.
|
|
132
|
+
3. The app's merged manifest declares the plugin's two receivers for it (`provider.gms.GmsActivityReceiver` and
|
|
133
|
+
`GmsGeofenceReceiver`, or `provider.hms.HmsActivityReceiver` and `HmsGeofenceReceiver`). Activity and geofence
|
|
134
|
+
results arrive through those receivers, so a backend without them cannot work.
|
|
135
|
+
|
|
136
|
+
A provider that the `<meta-data>` entry lists but that fails 2 or 3 is a setup error: the plugin logs a warning and
|
|
137
|
+
does not use it. The `locationProvider` values:
|
|
138
|
+
|
|
139
|
+
- `'auto'` (default): GMS if it is packaged and Google Play services are available; otherwise HMS if it is packaged and
|
|
140
|
+
HMS Core is available; otherwise the Android `LocationManager`.
|
|
141
|
+
- `'gms'` or `'hms'`: that backend. If it is not packaged or not available on the phone, the plugin logs a warning and
|
|
142
|
+
uses the Android `LocationManager`.
|
|
143
|
+
- `'android'`: always the Android `LocationManager`.
|
|
144
|
+
|
|
145
|
+
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
|
+
|
|
149
|
+
The `android` backend has two limitations: it has **no activity recognition**, so motion detection relies on distance
|
|
150
|
+
only, and **no native geofence dwell**, so the plugin synthesizes `DWELL` itself (see [Geofencing](#geofencing)).
|
|
151
|
+
|
|
152
|
+
**Release builds (R8 / minify).** Nothing to add: the plugin ships consumer ProGuard/R8 rules that keep its provider
|
|
153
|
+
bundles and the GMS/HMS class names it probes by reflection, so a minified release app still detects Google Play
|
|
154
|
+
services and HMS. They also include Huawei's recommended keep rules and silence warnings about the SDK that is not
|
|
155
|
+
packaged.
|
|
156
|
+
|
|
157
|
+
#### Separate Play and AppGallery APKs (product flavors)
|
|
158
|
+
|
|
159
|
+
An app that publishes a Google Play APK with only GMS and an AppGallery APK with only HMS adds the SDKs per product
|
|
160
|
+
flavor itself, instead of through `locationTracking.providers`. The plugin chooses the backend by checking at runtime
|
|
161
|
+
which SDK classes are in the APK and which of its receivers the manifest declares, so it needs no other setting.
|
|
162
|
+
|
|
163
|
+
1. In `android/gradle.properties`, stop the plugin from packaging any SDK:
|
|
164
|
+
|
|
165
|
+
```properties
|
|
166
|
+
locationTracking.providers=none
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
2. In `android/app/build.gradle`, declare the flavors and add one SDK to each:
|
|
170
|
+
|
|
171
|
+
```groovy
|
|
172
|
+
android {
|
|
173
|
+
flavorDimensions = ['store']
|
|
174
|
+
productFlavors {
|
|
175
|
+
gms { dimension 'store' }
|
|
176
|
+
hms { dimension 'store' }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
dependencies {
|
|
181
|
+
gmsImplementation 'com.google.android.gms:play-services-location:21.3.0'
|
|
182
|
+
hmsImplementation 'com.huawei.hms:location:6.16.0.302'
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The app now sets the SDK versions; `playServicesLocationVersion` and `hmsLocationVersion` no longer apply.
|
|
187
|
+
|
|
188
|
+
3. Add the Huawei Maven repository ([step 2](#2-huawei-maven-repository-needed-for-hms-and-harmless-otherwise)).
|
|
189
|
+
For AppGallery Connect ([step 3](#3-appgallery-connect-only-for-hms)), put `agconnect-services.json` in
|
|
190
|
+
`android/app/src/hms/` instead of `android/app/`.
|
|
191
|
+
|
|
192
|
+
4. In each flavor's manifest, declare the provider and remove the other provider's entries. The plugin's manifest
|
|
193
|
+
declares the receivers and the Android 9 activity-recognition permissions of both SDKs, and Android merges it into
|
|
194
|
+
every flavor. These entries do nothing in an APK without that SDK, but they show in the merged manifest and in the
|
|
195
|
+
store listing's permission list. Two flavor manifests remove them.
|
|
196
|
+
|
|
197
|
+
Each flavor manifest also declares which provider the plugin may use, with the
|
|
198
|
+
`com.brickssoft.locationtracking.PROVIDERS` `<meta-data>` entry. The plugin never selects a provider that is not
|
|
199
|
+
listed, also not with `locationProvider: 'gms'` or `'hms'` (it falls back to `android` with a warning). So the
|
|
200
|
+
AppGallery APK uses HMS even when another library brings Google's location classes into it (for example another
|
|
201
|
+
location plugin, or the app's own Google Play services availability check) and the phone has Google Play services
|
|
202
|
+
installed. The app does not need to set `locationProvider`.
|
|
203
|
+
|
|
204
|
+
`android/app/src/gms/AndroidManifest.xml` (Google Play APK: allows GMS, removes the HMS entries):
|
|
205
|
+
|
|
206
|
+
```xml
|
|
207
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
208
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
209
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
210
|
+
<uses-permission android:name="com.huawei.hms.permission.ACTIVITY_RECOGNITION" tools:node="remove"/>
|
|
211
|
+
<application>
|
|
212
|
+
<meta-data android:name="com.brickssoft.locationtracking.PROVIDERS" android:value="gms"/>
|
|
213
|
+
<receiver android:name="com.brickssoft.locationtracking.provider.hms.HmsActivityReceiver" tools:node="remove"/>
|
|
214
|
+
<receiver android:name="com.brickssoft.locationtracking.provider.hms.HmsGeofenceReceiver" tools:node="remove"/>
|
|
215
|
+
</application>
|
|
216
|
+
</manifest>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`android/app/src/hms/AndroidManifest.xml` (AppGallery APK: allows HMS, removes the GMS entries):
|
|
220
|
+
|
|
221
|
+
```xml
|
|
222
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
223
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
224
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
225
|
+
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" tools:node="remove"/>
|
|
226
|
+
<application>
|
|
227
|
+
<meta-data android:name="com.brickssoft.locationtracking.PROVIDERS" android:value="hms"/>
|
|
228
|
+
<receiver android:name="com.brickssoft.locationtracking.provider.gms.GmsActivityReceiver" tools:node="remove"/>
|
|
229
|
+
<receiver android:name="com.brickssoft.locationtracking.provider.gms.GmsGeofenceReceiver" tools:node="remove"/>
|
|
230
|
+
</application>
|
|
231
|
+
</manifest>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Keep everything else from the plugin's manifest in both flavors. The `<queries>` entries such as
|
|
235
|
+
`com.huawei.systemmanager` are phone makers' battery settings apps, not HMS, and the plugin uses them on every
|
|
236
|
+
phone ([step 6](#6-battery-optimization-and-phone-makers)).
|
|
237
|
+
|
|
238
|
+
5. Build both APKs: `./gradlew assembleGmsRelease assembleHmsRelease` (or `bundleGmsRelease` for Play).
|
|
239
|
+
|
|
240
|
+
What each APK then contains:
|
|
241
|
+
|
|
242
|
+
| | Google Play APK (`gms`) | AppGallery APK (`hms`) |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| Location SDK and its dependencies | `play-services-location` only | `com.huawei.hms:location` only |
|
|
245
|
+
| Manifest entries that the SDK adds itself | Google's only | Huawei's only |
|
|
246
|
+
| Plugin receivers and activity-recognition permission | GMS only (after step 4) | HMS only (after step 4) |
|
|
247
|
+
| Plugin classes in `provider.gms` and `provider.hms` | both | both |
|
|
248
|
+
| `getDeviceInfo().packagedProviders` | `["gms"]` | `["hms"]` |
|
|
249
|
+
|
|
250
|
+
The plugin classes of both providers stay in both APKs, because the R8 rules keep every provider bundle for the
|
|
251
|
+
runtime check. They are in the `com.brickssoft.locationtracking` package and contain no Google or Huawei code.
|
|
252
|
+
|
|
253
|
+
Other libraries in the app can still bring an SDK into the wrong flavor; for example, Firebase depends on Google
|
|
254
|
+
Play services. Check each flavor before publishing:
|
|
255
|
+
|
|
256
|
+
- `./gradlew :app:dependencies --configuration hmsReleaseRuntimeClasspath` lists the libraries in the AppGallery APK
|
|
257
|
+
(`gmsReleaseRuntimeClasspath` for the Play APK).
|
|
258
|
+
- `android/app/build/intermediates/merged_manifests/hmsRelease/` (or Android Studio's **Merged Manifest** tab) shows
|
|
259
|
+
the final manifest.
|
|
260
|
+
- On a phone, `getDeviceInfo().packagedProviders` shows which providers the installed APK can use.
|
|
261
|
+
|
|
262
|
+
### 2. Huawei Maven repository (needed for `hms`, and harmless otherwise)
|
|
263
|
+
|
|
264
|
+
The HMS SDK is served from Huawei's own Maven repository. Add it to **both** repository blocks of your app's
|
|
265
|
+
`android/build.gradle`:
|
|
266
|
+
|
|
267
|
+
```groovy
|
|
268
|
+
buildscript {
|
|
269
|
+
repositories {
|
|
270
|
+
google()
|
|
271
|
+
mavenCentral()
|
|
272
|
+
maven { url = 'https://developer.huawei.com/repo/' }
|
|
273
|
+
}
|
|
274
|
+
// ...
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
allprojects {
|
|
278
|
+
repositories {
|
|
279
|
+
google()
|
|
280
|
+
mavenCentral()
|
|
281
|
+
maven { url = 'https://developer.huawei.com/repo/' }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 3. AppGallery Connect (only for HMS)
|
|
287
|
+
|
|
288
|
+
HMS Location Kit works only for an app that is registered in AppGallery Connect (AGC) with the right signing
|
|
289
|
+
certificate:
|
|
290
|
+
|
|
291
|
+
1. In [AppGallery Connect](https://developer.huawei.com/consumer/en/service/josp/agc/index.html), create a project and
|
|
292
|
+
add an Android app whose package name matches your `applicationId`.
|
|
293
|
+
2. Under **Project settings → General information**, add the **SHA-256 certificate fingerprint** of every signing key
|
|
294
|
+
you use (debug and release, and the app-signing key if AppGallery re-signs your app). You can print the fingerprints
|
|
295
|
+
with `./gradlew signingReport` or `keytool -list -v -keystore <keystore>`.
|
|
296
|
+
3. Make sure **Location Kit** is enabled under **Project settings → Manage APIs**.
|
|
297
|
+
4. Download `agconnect-services.json` and put it in `android/app/`.
|
|
298
|
+
5. Apply the AGC Gradle plugin. In `android/build.gradle`, add
|
|
299
|
+
`classpath 'com.huawei.agconnect:agcp:1.9.6.300'` (or the latest version from the Huawei repository) to
|
|
300
|
+
`buildscript.dependencies`. Then, in `android/app/build.gradle`, add `apply plugin: 'com.huawei.agconnect'` after
|
|
301
|
+
`apply plugin: 'com.android.application'`.
|
|
302
|
+
|
|
303
|
+
The phone needs **HMS Core** (the "HMS Core" / "Huawei Mobile Services" app). If AGC is not set up correctly,
|
|
304
|
+
Location Kit calls fail with errors such as `907135xxx` or `6003`. Check the plugin log (`getLog()`) for them.
|
|
305
|
+
|
|
306
|
+
### 4. Permissions (merged automatically)
|
|
307
|
+
|
|
308
|
+
The plugin's manifest is merged into your app, so you don't have to declare anything. It declares:
|
|
309
|
+
|
|
310
|
+
| Permission | Why |
|
|
311
|
+
|---|---|
|
|
312
|
+
| `INTERNET` | Uploading records, heartbeats and logs. |
|
|
313
|
+
| `ACCESS_NETWORK_STATE` | Detecting connectivity: retry when the network returns, `disableAutoSyncOnCellular`, the `connectivitychange` event. |
|
|
314
|
+
| `ACCESS_COARSE_LOCATION`, `ACCESS_FINE_LOCATION` | Foreground ("while in use") location. Runtime permission, alias `location`. |
|
|
315
|
+
| `ACCESS_BACKGROUND_LOCATION` | "Allow all the time" (Android 10+). Runtime permission, alias `backgroundLocation`. Needed for geofencing and for restarting tracking from the background (boot, app update, process restore). |
|
|
316
|
+
| `ACTIVITY_RECOGNITION` | Motion activity (still, walking, in vehicle, ...) on Android 10+. Runtime permission, alias `activityRecognition`. |
|
|
317
|
+
| `com.google.android.gms.permission.ACTIVITY_RECOGNITION` (maxSdk 28) | GMS activity recognition on Android 9 and older. |
|
|
318
|
+
| `com.huawei.hms.permission.ACTIVITY_RECOGNITION` (maxSdk 28) | HMS activity identification on Android 9 and older. **Not requested at runtime** by `requestPermissions()`: if HMS Core requires it on such a phone, request it yourself, otherwise activity updates fail (logged) and motion detection uses distance only. |
|
|
319
|
+
| `FOREGROUND_SERVICE` | Running the tracking foreground service. |
|
|
320
|
+
| `FOREGROUND_SERVICE_LOCATION` | The `location` foreground-service type (required on Android 14+). |
|
|
321
|
+
| `POST_NOTIFICATIONS` | Showing the foreground-service notification on Android 13+. Runtime permission, alias `notifications`. |
|
|
322
|
+
| `RECEIVE_BOOT_COMPLETED` | Resuming tracking after a reboot or an app update (`app.startOnBoot`). |
|
|
323
|
+
| `WAKE_LOCK` | Keeping the CPU awake briefly while a heartbeat is created and uploaded. |
|
|
324
|
+
|
|
325
|
+
These are **intentionally not declared**:
|
|
326
|
+
|
|
327
|
+
| Permission | Why not |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `SCHEDULE_EXACT_ALARM` | Denied by default on Android 14+ for new installs, and it needs a policy justification. Apps exempt from battery optimization may use exact alarms without it. |
|
|
330
|
+
| `USE_EXACT_ALARM` | Google Play allows it only for alarm-clock and calendar apps. |
|
|
331
|
+
| `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` | Google Play restricts the direct "allow this app to ignore battery optimizations?" prompt. The plugin opens the system list instead (`openBatteryOptimizationSettings()`), where the user exempts the app. |
|
|
332
|
+
|
|
333
|
+
The manifest also adds the tracking service (`foregroundServiceType="location"`, `stopWithTask="false"`), the boot,
|
|
334
|
+
alarm, notification-action, geofence and activity receivers, and a `FileProvider` for sharing log files (authority
|
|
335
|
+
`${applicationId}.locationtracking.logs`). It also adds `<queries>` entries, so that the phone makers'
|
|
336
|
+
power-manager apps and `mailto:` handlers are visible on Android 11+.
|
|
337
|
+
|
|
338
|
+
If you don't want a permission, remove it in your app's manifest and turn off the matching feature. For example:
|
|
339
|
+
|
|
340
|
+
```xml
|
|
341
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
342
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
343
|
+
<!-- no activity recognition: also set activity.disableMotionActivityUpdates = true -->
|
|
344
|
+
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" tools:node="remove"/>
|
|
345
|
+
</manifest>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 5. Runtime permission flow
|
|
349
|
+
|
|
350
|
+
`requestPermissions()` asks, by default, for all four permissions in this order: `location`, `notifications`,
|
|
351
|
+
`activityRecognition`, then `backgroundLocation`. Pass `{ permissions: [...] }` to ask for only some of them. It asks
|
|
352
|
+
for one permission at a time, skips those that are already granted or don't exist on the phone's Android version, and
|
|
353
|
+
resolves with the resulting `PermissionStatus`. Overlapping calls run one after another.
|
|
354
|
+
|
|
355
|
+
- **Status values.** A permission that is not granted is `'prompt'` if the plugin never asked for it (in this
|
|
356
|
+
install), `'prompt-with-rationale'` if Android recommends explaining it first, and `'denied'` otherwise (the user
|
|
357
|
+
refused, and Android won't show the dialog again). Coarse-only location counts as `location: 'granted'`.
|
|
358
|
+
- **`start()` needs only foreground location.** The foreground service keeps "while in use" access while it runs. Call
|
|
359
|
+
`start()` while your app is visible, because Android 12+ restricts starting foreground services from the
|
|
360
|
+
background.
|
|
361
|
+
- **Android 10 (API 29).** The system dialog offers "Allow all the time" directly.
|
|
362
|
+
- **Android 11+ (API 30+).** Background location is requested only *after* foreground location has been granted (if
|
|
363
|
+
it isn't, the background step is skipped), and the "request" opens the app's location-permission page in Settings,
|
|
364
|
+
where the user picks **Allow all the time**. The plugin first shows an explanation dialog, which you can reword with
|
|
365
|
+
`backgroundPermissionRationale`; if the user declines it, or no Activity is visible, the step is skipped. Google
|
|
366
|
+
Play requires your own in-app disclosure before this, plus a declaration in the Play Console.
|
|
367
|
+
- **Android 12+.** The user may grant only *approximate* location. `getProviderState().accuracy` then reports
|
|
368
|
+
`'approximate'`.
|
|
369
|
+
- **Android 13+.** `POST_NOTIFICATIONS` is a runtime permission. If it is denied, tracking still works, but the
|
|
370
|
+
foreground-service notification is hidden.
|
|
371
|
+
- **Below Android 10**, there is no separate background permission (it is reported the same as `location`), and
|
|
372
|
+
activity recognition needs no runtime prompt (it is reported as `'granted'`). Notifications below Android 13 are
|
|
373
|
+
also reported as `'granted'`.
|
|
374
|
+
- A permission whose Android permission you removed from the app's manifest (`tools:node="remove"`) is not requested.
|
|
375
|
+
- 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`.
|
|
378
|
+
- Revoking a location permission in Settings kills the app's process. While tracking is on, the plugin records a
|
|
379
|
+
`providerchange` the next time it runs; if location permission is gone completely, it also stops tracking with a
|
|
380
|
+
`tracking_stop` whose reason is `permission_denied`.
|
|
381
|
+
|
|
382
|
+
For apps targeting Android 14+, Google Play also asks you to declare the `location` foreground-service type in the
|
|
383
|
+
Play Console.
|
|
384
|
+
|
|
385
|
+
### 6. Battery optimization and phone makers
|
|
386
|
+
|
|
387
|
+
For reliable heartbeats on idle phones, ask the user to exempt the app from battery optimization, and on some phones
|
|
388
|
+
to allow it in the phone maker's power manager. See [docs/heartbeat.md](docs/heartbeat.md#android-reliability) for the
|
|
389
|
+
details and the expected intervals.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const status = await LocationTracking.getBatteryOptimizationStatus();
|
|
393
|
+
if (!status.isIgnoringBatteryOptimizations) {
|
|
394
|
+
// Explain why first; the user picks the app and chooses "Don't optimize" / "Unrestricted".
|
|
395
|
+
await LocationTracking.openBatteryOptimizationSettings();
|
|
396
|
+
}
|
|
397
|
+
const pm = await LocationTracking.getPowerManagerInfo();
|
|
398
|
+
if (pm.available) {
|
|
399
|
+
// e.g. Huawei: Battery → App launch → your app → Manage manually (all switches on)
|
|
400
|
+
await LocationTracking.openPowerManagerSettings();
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Quick start
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { LocationTracking, onHeartbeat, onLocation } from '@bricks-soft/capacitor-location-tracking';
|
|
408
|
+
|
|
409
|
+
// 1. Listen first, so no event is missed.
|
|
410
|
+
await onLocation((location) => console.log('[location]', location.event, location.coords));
|
|
411
|
+
await onHeartbeat(({ location }) => console.log('[heartbeat]', location.recorded_at, location.coords));
|
|
412
|
+
await LocationTracking.addListener('providerchange', (state) => console.log('[providerchange]', state));
|
|
413
|
+
await LocationTracking.addListener('http', (e) => console.log('[http]', e.status, e.uuids.length));
|
|
414
|
+
|
|
415
|
+
// 2. Call ready() on every app launch, before any other method.
|
|
416
|
+
const state = await LocationTracking.ready({
|
|
417
|
+
config: {
|
|
418
|
+
locationProvider: 'auto',
|
|
419
|
+
geolocation: { desiredAccuracy: 'high', distanceFilter: 10 },
|
|
420
|
+
heartbeat: { enabled: true, minInterval: 180, maxInterval: 300 },
|
|
421
|
+
http: {
|
|
422
|
+
url: 'https://api.example.com/locations',
|
|
423
|
+
headers: { 'X-Api-Key': 'my-key' },
|
|
424
|
+
params: { device_id: 'device-123' },
|
|
425
|
+
},
|
|
426
|
+
app: { stopOnTerminate: false, startOnBoot: true },
|
|
427
|
+
notification: { title: 'Trip recording', text: 'Your location is being shared' },
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// 3. Ask for foreground permissions, then start while the app is visible.
|
|
432
|
+
const permissions = await LocationTracking.requestPermissions({
|
|
433
|
+
permissions: ['location', 'notifications', 'activityRecognition'],
|
|
434
|
+
});
|
|
435
|
+
if (permissions.location === 'granted' && !state.enabled) {
|
|
436
|
+
await LocationTracking.start();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 4. Later, after explaining why, ask for "Allow all the time".
|
|
440
|
+
await LocationTracking.requestPermissions({ permissions: ['backgroundLocation'] });
|
|
441
|
+
|
|
442
|
+
// Check the heartbeat at any time:
|
|
443
|
+
const hb = await LocationTracking.getHeartbeatStatus();
|
|
444
|
+
console.log(hb.strategy, hb.nextHeartbeatAt, hb.isIgnoringBatteryOptimizations);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**The `NOT_READY` rule.** Until `ready()` has resolved in the current process (once per process, not per WebView),
|
|
448
|
+
every method rejects with `NOT_READY`, except `ready`, `getState`, `checkPermissions`, `requestPermissions`,
|
|
449
|
+
`getDeviceInfo`, `getSensors`, `log`, `getLog`, `getProviderState` and the `open*Settings` methods
|
|
450
|
+
(`openBatteryOptimizationSettings`, `openPowerManagerSettings`, `openLocationSettings`, `openAppSettings`).
|
|
451
|
+
|
|
452
|
+
An app that calls `getCurrentPosition()` (or `watchPosition()`) for users who never configure tracking calls
|
|
453
|
+
`ready({ reset: false })` without a config first; that is the intended pattern. It loads the persisted config (the
|
|
454
|
+
defaults on the first run) and changes nothing, and calling `ready()` again later in the process is fine. Two side
|
|
455
|
+
effects:
|
|
456
|
+
- If tracking is enabled but not running in this process, this `ready()` resumes it (reason `restore`), with the
|
|
457
|
+
persisted config.
|
|
458
|
+
- It counts as the first `ready()` after install, so a later `ready({ config, reset: false })` keeps the persisted
|
|
459
|
+
config and ignores the one passed. Use `reset: true` or `setConfig()` to apply a config after that.
|
|
460
|
+
|
|
461
|
+
**`ready({ reset })`.** With `reset: true` (the default), the config is the defaults plus the config you pass, on every
|
|
462
|
+
launch. With `reset: false`, the config you pass is applied only on the very first `ready()` after install, and
|
|
463
|
+
afterwards the persisted config wins (change it with `setConfig()`). Note that with `reset: true`, JWT tokens the
|
|
464
|
+
plugin refreshed are replaced by the ones in the config you pass (see
|
|
465
|
+
[JWT refresh](docs/wire-format.md#jwt-refresh)). If tracking was on but is not running in this process (for example
|
|
466
|
+
after the app was force-stopped or its process was killed), `ready()` resumes it and records a `tracking_start` with
|
|
467
|
+
reason `restore`.
|
|
468
|
+
|
|
469
|
+
**Force stop.** After the user force-stops the app (Settings → Force stop), Android cancels its alarms and does not
|
|
470
|
+
restart its service, so tracking stays off until the app is opened again; `ready()` then resumes it. On Android 11
|
|
471
|
+
and newer the plugin also ignores a background event that reaches the stopped app anyway (an activity update or a
|
|
472
|
+
stationary-region exit that was already on its way): it does not restart tracking from it, and it releases the leftover
|
|
473
|
+
activity, location and geofence registrations so they stop waking the app. The server sees a gap without a
|
|
474
|
+
`tracking_stop` record, followed by `tracking_start` with reason `restore` when the app is opened.
|
|
475
|
+
|
|
476
|
+
**`setConfig()`** deep-merges: arrays and map-like objects (`headers`, `params`, `extras`, ...) are replaced as a
|
|
477
|
+
whole, and `null` resets a key or a group to its default. Unknown keys are ignored with a warning in the log. Enum
|
|
478
|
+
values are case-insensitive, and numeric or boolean strings (`"15"`, `"false"`) are accepted. A wrong type or an
|
|
479
|
+
unknown enum value rejects the call with `INVALID_ARGUMENT`, and nothing is applied. Out-of-range values are clamped
|
|
480
|
+
(see the [Configuration reference](#configuration-reference)). `reset({ config })` starts from the defaults
|
|
481
|
+
instead.
|
|
482
|
+
|
|
483
|
+
**Event helpers.** Besides `LocationTracking.addListener(name, cb)`, the package exports typed helpers (`onLocation`,
|
|
484
|
+
`onMotionChange`, `onActivityChange`, `onProviderChange`, `onHeartbeat`, `onGeofence`, `onGeofencesChange`, `onHttp`,
|
|
485
|
+
`onConnectivityChange`, `onPowerSaveChange`, `onEnabledChange`, `onNotificationAction`, `onAuthorization`), a generic
|
|
486
|
+
`on(name, cb)` and an `Events` map of event names. Each returns a `PluginListenerHandle`. Call `handle.remove()` or
|
|
487
|
+
`LocationTracking.removeAllListeners()` to unsubscribe.
|
|
488
|
+
|
|
489
|
+
## Configuration reference
|
|
490
|
+
|
|
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.
|
|
494
|
+
|
|
495
|
+
### `config` (top level)
|
|
496
|
+
|
|
497
|
+
| Option | Type | Default | Description |
|
|
498
|
+
|---|---|---|---|
|
|
499
|
+
| `locationProvider` | `'auto' \| 'gms' \| 'hms' \| 'android'` | `'auto'` | Location backend (Android only). See [Android setup](#1-choose-the-location-sdks-to-package). |
|
|
500
|
+
|
|
501
|
+
### `config.geolocation`
|
|
502
|
+
|
|
503
|
+
| Option | Type | Default | Description |
|
|
504
|
+
|---|---|---|---|
|
|
505
|
+
| `desiredAccuracy` | `'high' \| 'balanced' \| 'low' \| 'passive'` | `'high'` | Accuracy of the location request while moving, and of the first fix after `start()`. While stationary the plugin requests only passive fixes (or, when the stationary geofence cannot be registered, one low-power fix at most every 3 minutes), whatever this value is (see [Battery](#battery)). |
|
|
506
|
+
| `distanceFilter` | number (m, ≥ 0) | `10` | Minimum distance between recorded locations while moving. The plugin applies it itself (the OS request has no distance filter). |
|
|
507
|
+
| `locationUpdateInterval` | number (ms, ≥ 0) | `1000` | Desired update interval while moving. |
|
|
508
|
+
| `fastestLocationUpdateInterval` | number (ms, ≥ 0) | `500` | Fastest accepted update interval. |
|
|
509
|
+
| `disableElasticity` | boolean | `false` | Turns off the speed-based scaling of `distanceFilter`. |
|
|
510
|
+
| `elasticityMultiplier` | number (≥ 0) | `1` | Elastic filter: `distanceFilter × max(1, round(speed / 5) × elasticityMultiplier)`. |
|
|
511
|
+
| `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
|
+
| `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. |
|
|
514
|
+
| `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
|
+
| `locationTimeout` | number (ms, > 0) | `30000` | Default timeout of `getCurrentPosition()`, and of the first fix after `start()`. |
|
|
516
|
+
| `filter` | object | see below | Location filters. |
|
|
517
|
+
|
|
518
|
+
### `config.geolocation.filter`
|
|
519
|
+
|
|
520
|
+
| Option | Type | Default | Description |
|
|
521
|
+
|---|---|---|---|
|
|
522
|
+
| `useKalman` | boolean | `false` | Smooths latitude and longitude with a Kalman filter. |
|
|
523
|
+
| `trackingAccuracyThreshold` | number (m, ≥ 0) | `100` | Rejects fixes whose accuracy is worse than this. |
|
|
524
|
+
| `maxImpliedSpeed` | number (m/s, ≥ 0) | `80` | Rejects fixes that imply a higher speed than this (GPS jumps). `0` turns the check off. |
|
|
525
|
+
| `odometerAccuracyThreshold` | number (m, ≥ 0) | `20` | The odometer ignores fixes whose accuracy is worse than this. |
|
|
526
|
+
| `allowIdenticalLocations` | boolean | `false` | Records fixes that are identical to the previous one. |
|
|
527
|
+
| `rejectMockLocations` | boolean | `false` | Drops mock fixes. Otherwise they are kept, with `mock: true`. |
|
|
528
|
+
|
|
529
|
+
### `config.activity`
|
|
530
|
+
|
|
531
|
+
| Option | Type | Default | Description |
|
|
532
|
+
|---|---|---|---|
|
|
533
|
+
| `disableMotionActivityUpdates` | boolean | `false` | Doesn't use activity recognition. Motion detection then relies on distance only. |
|
|
534
|
+
| `activityRecognitionInterval` | number (ms, ≥ 0) | `10000` | Requested interval of activity updates. |
|
|
535
|
+
| `minimumActivityRecognitionConfidence` | number (0–100) | `75` | Minimum confidence for an activity to count (for motion detection and the `activitychange` event). |
|
|
536
|
+
| `motionTriggerDelay` | number (ms, ≥ 0) | `0` | How long a moving activity must last before switching to moving. |
|
|
537
|
+
| `disableStopDetection` | boolean | `false` | Never switch to stationary automatically; only `changePace()` does. |
|
|
538
|
+
|
|
539
|
+
### `config.heartbeat`
|
|
540
|
+
|
|
541
|
+
| Option | Type | Default | Description |
|
|
542
|
+
|---|---|---|---|
|
|
543
|
+
| `enabled` | boolean | `true` | Creates `heartbeat` records while tracking is on. |
|
|
544
|
+
| `minInterval` | number (s, ≥ 60) | `180` | A heartbeat is due when no record has been created for this long. |
|
|
545
|
+
| `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. |
|
|
546
|
+
|
|
547
|
+
### `config.http`
|
|
548
|
+
|
|
549
|
+
| Option | Type | Default | Description |
|
|
550
|
+
|---|---|---|---|
|
|
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. |
|
|
552
|
+
| `method` | `'POST' \| 'PUT' \| 'PATCH'` | `'POST'` | HTTP method. |
|
|
553
|
+
| `headers` | object | `{}` | Extra request headers. They can override `Content-Type`; an `Authorization` header turns the JWT handling off. |
|
|
554
|
+
| `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: '.'`. |
|
|
555
|
+
| `autoSync` | boolean | `true` | Uploads normal records automatically. Priority records (heartbeat and audit records) are always uploaded immediately, and take queued normal records along. |
|
|
556
|
+
| `autoSyncThreshold` | number (≥ 0) | `0` | Uploads normal records when the queue holds at least this many records. With `syncInterval` `0`, the value `0` uploads every record. With `syncInterval` above `0`, it is a size limit: the queue is uploaded early when it reaches this many records, and `0` means no size limit. |
|
|
557
|
+
| `syncInterval` | number (s, ≥ 0) | `0` | Uploads normal records once the oldest queued one is this old, so the server's live location is at most about this stale (the field-force setup uses `300`). Needs `autoSync: true`. `0` turns it off; priority records are not affected. See [Live location](#live-location-httpsyncinterval). |
|
|
558
|
+
| `batchSync` | boolean | `false` | Sends several records per request, as an array, oldest first. |
|
|
559
|
+
| `maxBatchSize` | number (≥ 1) | `100` | Maximum records per batch request. |
|
|
560
|
+
| `disableAutoSyncOnCellular` | boolean | `false` | On cellular, uploads only priority records. `sync()` ignores it. |
|
|
561
|
+
| `rootProperty` | string | `'location'` | Key that wraps the record or records. `'.'` (or an empty string) means no wrapping. |
|
|
562
|
+
| `locationTemplate` | string \| null | `null` | JSON text with `<%= name %>` placeholders that replaces the default record shape. |
|
|
563
|
+
| `geofenceTemplate` | string \| null | `null` | Template for `geofence` records. Falls back to `locationTemplate`. |
|
|
564
|
+
| `timeout` | number (ms, > 0) | `60000` | Request timeout (call, read and write). |
|
|
565
|
+
| `authorization` | object \| null | `null` | JWT settings, see below. |
|
|
566
|
+
|
|
567
|
+
The upload rules (priority records, retries, templates, body shapes) are described in
|
|
568
|
+
[docs/wire-format.md](docs/wire-format.md).
|
|
569
|
+
|
|
570
|
+
### `config.http.authorization`
|
|
571
|
+
|
|
572
|
+
| Option | Type | Default | Description |
|
|
573
|
+
|---|---|---|---|
|
|
574
|
+
| `strategy` | `'JWT'` | `'JWT'` | Only JWT is supported. |
|
|
575
|
+
| `accessToken` | string | none | Sent as `Authorization: Bearer <accessToken>`, unless `headers` already contain `Authorization`. If it is missing and `refreshUrl` is set, the plugin refreshes it before the first upload. |
|
|
576
|
+
| `refreshToken` | string | none | Substituted for `{refreshToken}` in `refreshPayload`. |
|
|
577
|
+
| `refreshUrl` | string | none | Token-refresh endpoint (POST). |
|
|
578
|
+
| `refreshPayload` | object | `{}` | Refresh request body. String values may contain `{refreshToken}`. |
|
|
579
|
+
| `refreshHeaders` | object | none | Headers of the refresh request. |
|
|
580
|
+
| `refreshPayloadEncoding` | `'json' \| 'form'` | `'json'` | Body encoding of the refresh request. |
|
|
581
|
+
| `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
|
+
|
|
583
|
+
### `config.persistence`
|
|
584
|
+
|
|
585
|
+
| Option | Type | Default | Description |
|
|
586
|
+
|---|---|---|---|
|
|
587
|
+
| `maxDaysToPersist` | number (≥ 1) | `7` | Queued records older than this many days are deleted, even if they were never uploaded. Applies to every record type, heartbeats and audit records included. |
|
|
588
|
+
| `maxRecordsToPersist` | number (≥ -1) | `-1` | Maximum queued records; the oldest are deleted first. `-1` (or `0`) means unlimited. Pruning runs every 50 inserts, so the queue can exceed the limit by up to 49 records. |
|
|
589
|
+
| `extras` | object | `{}` | Merged into the `extras` of every record when it is created. Extras passed to a call (for example `getCurrentPosition({ extras })`) win over these. |
|
|
590
|
+
|
|
591
|
+
### `config.app`
|
|
592
|
+
|
|
593
|
+
| Option | Type | Default | Description |
|
|
594
|
+
|---|---|---|---|
|
|
595
|
+
| `stopOnTerminate` | boolean | `true` | Stops tracking when the user swipes the app away (`tracking_stop`, reason `terminate`). With `false`, tracking continues. |
|
|
596
|
+
| `startOnBoot` | boolean | `false` | Resumes tracking after a reboot or an app update, if it was on (`tracking_start`, reason `boot` or `package_replaced`). Needs "Allow all the time" location on Android 14+. With `false`, tracking is off after a reboot or an update, and a `tracking_stop` with reason `reboot` or `package_replaced` is recorded. On Android 14+ with only "while in use" location, Android refuses it (`tracking_stop` reason `service_start_failed`); [`notification.resume`](#confignotificationresume) lets the user resume with a tap. |
|
|
597
|
+
|
|
598
|
+
### `config.notification`
|
|
599
|
+
|
|
600
|
+
| Option | Type | Default | Description |
|
|
601
|
+
|---|---|---|---|
|
|
602
|
+
| `title` | string | app label | Notification title. |
|
|
603
|
+
| `text` | string | `'Location tracking is active'` | Notification text. |
|
|
604
|
+
| `smallIcon` | string | `'drawable/lt_ic_notification'` | `'drawable/name'` or `'mipmap/name'`. Must be a monochrome icon. If it can't be found (or is an adaptive launcher icon), the plugin's icon is used. |
|
|
605
|
+
| `largeIcon` | string | none | `'drawable/name'` or `'mipmap/name'`. |
|
|
606
|
+
| `color` | string | none | Accent color, `'#RRGGBB'`. |
|
|
607
|
+
| `priority` | `'min' \| 'low' \| 'default' \| 'high' \| 'max'` | `'default'` | Mapped to the channel importance. Android fixes a channel's importance when it is created, so use a new `channelId` to change it later. |
|
|
608
|
+
| `channelId` | string | `'location_tracking'` | Notification channel id. |
|
|
609
|
+
| `channelName` | string | `'Location tracking'` | Channel name shown in the system settings. |
|
|
610
|
+
| `actions` | `{ id, label }[]` | none | Up to 3 buttons (extra ones are dropped). A tap emits `notificationaction` with the button's `id`. |
|
|
611
|
+
| `resume` | object | see below | The notification that resumes tracking after Android refused to restore it. |
|
|
612
|
+
|
|
613
|
+
### `config.notification.resume`
|
|
614
|
+
|
|
615
|
+
When tracking was on and Android refuses to restore it from the background, the plugin records a `tracking_stop` with
|
|
616
|
+
reason `service_start_failed`, and tracking stays off until the app starts it again. This is the normal case after a
|
|
617
|
+
reboot, an app update or a killed process on Android 14+ when the app has only "while in use" location (no "Allow all
|
|
618
|
+
the time"). With `resume.enabled`, the plugin then posts a notification. Tapping it starts the tracking service
|
|
619
|
+
directly: Android lets a location foreground service started from a notification use "while in use" location. The
|
|
620
|
+
session resumes with the persisted config and its original `stopAfterElapsedMinutes` deadline, and records a
|
|
621
|
+
`tracking_start` with reason `resume_notification`.
|
|
622
|
+
|
|
623
|
+
- The notification has its own channel (`location_tracking_resume`, default importance), so it shows even when the
|
|
624
|
+
tracking notification's `priority` is `min`. It uses the tracking notification's small icon and color.
|
|
625
|
+
- It is not posted when the session's `stopAfterElapsedMinutes` deadline has passed, and it disappears at that
|
|
626
|
+
deadline.
|
|
627
|
+
- It is removed when tracking starts by any path, when the app calls `stop()`, and when `resume.enabled` is turned off.
|
|
628
|
+
- Without notification permission (`POST_NOTIFICATIONS`, Android 13+) it is not posted; the log says so.
|
|
629
|
+
|
|
630
|
+
| Option | Type | Default | Description |
|
|
631
|
+
|---|---|---|---|
|
|
632
|
+
| `enabled` | boolean | `false` | Post the resume notification when Android refuses to restore tracking. |
|
|
633
|
+
| `title` | string | app label | Notification title. |
|
|
634
|
+
| `text` | string | `'Location tracking is paused. Tap to resume.'` | Notification text. The default comes from the string resource `lt_resume_notification_text`, so an app can translate it. |
|
|
635
|
+
| `channelName` | string | `'Paused location tracking'` | Channel name shown in the system settings (string resource `lt_resume_channel_name`). |
|
|
636
|
+
|
|
637
|
+
### `config.geofence`
|
|
638
|
+
|
|
639
|
+
| Option | Type | Default | Description |
|
|
640
|
+
|---|---|---|---|
|
|
641
|
+
| `initialTriggerEntry` | boolean | `true` | Fires `ENTER` right away for geofences that already contain the device when they are registered (with GMS and HMS also `DWELL` after `loiteringDelay` for dwell geofences). The `android` backend always reports `ENTER` for a circle the device is already inside, so there `false` only applies to polygons. |
|
|
642
|
+
|
|
643
|
+
### `config.logger`
|
|
644
|
+
|
|
645
|
+
| Option | Type | Default | Description |
|
|
646
|
+
|---|---|---|---|
|
|
647
|
+
| `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. |
|
|
649
|
+
|
|
650
|
+
### `config.backgroundPermissionRationale`
|
|
651
|
+
|
|
652
|
+
The dialog shown before the Android 11+ "Allow all the time" settings page.
|
|
653
|
+
|
|
654
|
+
| Option | Type | Default |
|
|
655
|
+
|---|---|---|
|
|
656
|
+
| `title` | string | `Allow background location` |
|
|
657
|
+
| `message` | string | `To keep tracking your location while the app is closed or not in use, select "Allow all the time" on the next screen.` |
|
|
658
|
+
| `positiveAction` | string | `Change to "Allow all the time"` |
|
|
659
|
+
| `negativeAction` | string | `Not now` |
|
|
660
|
+
|
|
661
|
+
## Tracking behavior
|
|
662
|
+
|
|
663
|
+
**Starting and stopping.**
|
|
664
|
+
|
|
665
|
+
- `start()` needs foreground location permission (otherwise it rejects with `PERMISSION_DENIED`). It starts the
|
|
666
|
+
foreground service, records a `tracking_start` (reason `start`), emits `enabledchange`, and resolves with the new
|
|
667
|
+
`State`. The first `motionchange` (`is_moving: false`, with a fresh fix or the last known location) follows
|
|
668
|
+
**asynchronously**, within `locationTimeout` + 5 s.
|
|
669
|
+
- If Android refuses to start the foreground service right away, `start()` records a `tracking_stop` with reason
|
|
670
|
+
`service_start_failed` and rejects with `PERMISSION_DENIED`. This happens in two cases:
|
|
671
|
+
- Android 12+ throws for a foreground-service start from the background (the app is not visible and has no
|
|
672
|
+
exemption);
|
|
673
|
+
- on Android 14+, the plugin's own check predicts that Android would refuse the `location` service type, and the
|
|
674
|
+
plugin does not try. The check refuses only when all of these are true: the tracking service is not in the
|
|
675
|
+
foreground yet; "Allow all the time" location is not granted; Android rates the app's process below "visible" (a
|
|
676
|
+
process that runs another foreground service counts as visible); the app's current activity is not started; and
|
|
677
|
+
no activity of the app was started, stopped or destroyed in the last 15 s.
|
|
678
|
+
|
|
679
|
+
If the service fails a moment later (after `start()` resolved), tracking stops with reason `service_start_failed`;
|
|
680
|
+
listen for `enabledchange`.
|
|
681
|
+
- `startGeofences()` works the same way in geofences-only mode (reason `start_geofences`). Calling `start()` while
|
|
682
|
+
`startGeofences()` runs (or the reverse) switches the mode and records another `tracking_start`. Calling the same
|
|
683
|
+
one twice does nothing.
|
|
684
|
+
- `stop()` removes the location and activity updates, records a `tracking_stop` (reason `stop`) while the foreground
|
|
685
|
+
service is still running, then stops the service, the heartbeat and the OS geofences, and emits `enabledchange`.
|
|
686
|
+
- Tracking also stops by itself: `stopOnStationary`, `stopAfterElapsedMinutes`, `stopOnTerminate` (app swiped away),
|
|
687
|
+
a revoked location permission (`permission_denied`), or Android refusing the foreground service
|
|
688
|
+
(`service_start_failed`). Each case records a `tracking_stop` with that reason and emits `enabledchange`. When
|
|
689
|
+
Android restarts the killed service by itself and the restart fails, the reason is `permission_denied` if location
|
|
690
|
+
permission is no longer granted, and `service_start_failed` otherwise.
|
|
691
|
+
- Tracking resumes by itself after the process was killed (reason `restore`), after a reboot (`boot`) and after an
|
|
692
|
+
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
|
|
694
|
+
refuses such a restore, [`notification.resume`](#confignotificationresume) can offer a tap that resumes it
|
|
695
|
+
(`resume_notification`).
|
|
696
|
+
- A boot broadcast restores tracking once per boot. The plugin reads the phone's boot counter
|
|
697
|
+
(`Settings.Global.BOOT_COUNT`) and ignores a boot broadcast when the counter is the same as for the last boot
|
|
698
|
+
broadcast it handled, or the same as when it last started the tracking service. So a repeated or fake
|
|
699
|
+
`QUICKBOOT_POWERON` broadcast without a real reboot creates no second `tracking_start`. On a phone that does not
|
|
700
|
+
report the counter, this check is skipped; as before, only the first boot broadcast in a process is handled.
|
|
701
|
+
|
|
702
|
+
**Motion states** (`start()` mode only).
|
|
703
|
+
|
|
704
|
+
- **Moving:** continuous updates with the configured `desiredAccuracy` and intervals. A `location` record is created
|
|
705
|
+
when a fix is at least the (elastic) `distanceFilter` away from the last recorded one.
|
|
706
|
+
- **Stationary:** GPS off. The plugin removes its location request and asks only for **passive** fixes (fixes that
|
|
707
|
+
other apps requested), and registers one OS geofence, the *stationary region*, around the stop point (the
|
|
708
|
+
*anchor* fix). The foreground service keeps running and heartbeats continue; they carry the anchor fix. Fixes that
|
|
709
|
+
still arrive only feed motion detection and polygon geofences; **no `location` records are created and the
|
|
710
|
+
odometer doesn't grow**, so position jitter while parked is neither uploaded nor counted. Details in
|
|
711
|
+
[Battery](#battery).
|
|
712
|
+
- Stationary → moving, whichever comes first: the OS reports that the device left the stationary region; a fix is
|
|
713
|
+
certainly outside (its distance from the anchor minus its accuracy is more than `stationaryRadius`, and its accuracy
|
|
714
|
+
is no worse than `filter.trackingAccuracyThreshold`); a moving activity (walking, running, on foot, on bicycle, in
|
|
715
|
+
vehicle) at or above `minimumActivityRecognitionConfidence` lasts for `motionTriggerDelay`; or
|
|
716
|
+
`changePace({ isMoving: true })`. Then the configured request (GPS for `'high'`) starts again and a `motionchange`
|
|
717
|
+
(`is_moving: true`) is recorded with the fix that showed the movement (the region's exit fix, or the outside fix),
|
|
718
|
+
or with the best known fix when the trigger was an activity or `changePace`.
|
|
719
|
+
- Moving → stationary (stop detection, unless `disableStopDetection`): `stopTimeout` minutes (at least 1) after the
|
|
720
|
+
last evidence of motion, even if no more fixes arrive.
|
|
721
|
+
- Each transition records a `motionchange` with the new `is_moving`. `changePace({ isMoving })` forces a transition;
|
|
722
|
+
it is ignored while tracking is off or in geofences-only mode.
|
|
723
|
+
- Without activity recognition (the `android` backend, `disableMotionActivityUpdates`, or no activity permission),
|
|
724
|
+
motion is detected from the stationary region and from distance only.
|
|
725
|
+
|
|
726
|
+
**Positions.** `getCurrentPosition()` works whether or not tracking is on. It needs location permission
|
|
727
|
+
(`PERMISSION_DENIED`) and enabled location services (`LOCATION_DISABLED`). It takes up to `samples` fixes, stops
|
|
728
|
+
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
|
|
730
|
+
right away. With `persist: true` (default) the fix is stored as a `current_position` record, which is uploaded,
|
|
731
|
+
emitted as a `location` event and restarts the heartbeat window. `watchPosition()` reports each fix (or an error,
|
|
732
|
+
such as `PERMISSION_DENIED`) to its callback until `clearWatch()`; it doesn't report disabled location services as
|
|
733
|
+
an error (fixes start once they are switched on). With `persist: true`, each fix is also stored as a
|
|
734
|
+
`watch_position` record (uploaded, and emitted as a `location` event).
|
|
735
|
+
|
|
736
|
+
## Battery
|
|
737
|
+
|
|
738
|
+
Tracking for 12 hours or more a day must cost little battery without losing the audit trail. The plugin follows the
|
|
739
|
+
same idea as Transistor Software's
|
|
740
|
+
["philosophy of operation"](https://docs.transistorsoft.com/help/philosophy/): **GPS runs only while the device
|
|
741
|
+
moves.** Heartbeats are a separate, fixed rule: they are always created and uploaded while tracking is on, and they
|
|
742
|
+
never turn GPS on.
|
|
743
|
+
|
|
744
|
+
**What runs in each state** (`start()` mode):
|
|
745
|
+
|
|
746
|
+
| State | Runs | Does not run |
|
|
747
|
+
|---|---|---|
|
|
748
|
+
| Moving | The location request with `desiredAccuracy` (`'high'` = GPS) every `locationUpdateInterval` (1 s by default); activity recognition; uploads; the heartbeat schedule (heartbeats are rarely due, because records keep coming). | – |
|
|
749
|
+
| Stationary | The foreground service and its notification; a **passive** location request (it receives only fixes that other apps requested, and costs nothing by itself); the **stationary region**: one OS geofence of `max(stationaryRadius, 150)` m around the anchor fix (the stop point), with an exit trigger only; activity recognition; a heartbeat every `heartbeat.minInterval` seconds. | GPS, and any network-location request of the plugin (except the low-power fallback below). |
|
|
750
|
+
| Tracking off | Nothing. | Everything. |
|
|
751
|
+
|
|
752
|
+
**How the plugin notices that the device moves again** (whichever comes first):
|
|
753
|
+
|
|
754
|
+
1. the OS reports that the device left the stationary region (typically after 150–250 m). An exit report whose own
|
|
755
|
+
fix is certainly inside the region (distance from the centre plus accuracy ≤ radius) is ignored: it is a late
|
|
756
|
+
report about an earlier region;
|
|
757
|
+
2. a fix that arrives while stationary (a passive fix, or a fix of the low-power fallback) is certainly outside: its
|
|
758
|
+
distance from the anchor minus its accuracy is more than `stationaryRadius`, and its accuracy is no worse than
|
|
759
|
+
`filter.trackingAccuracyThreshold` (a coarse fix alone never wakes GPS);
|
|
760
|
+
3. activity recognition reports walking, running, on foot, on bicycle or in vehicle, with at least
|
|
761
|
+
`minimumActivityRecognitionConfidence`, for `motionTriggerDelay`;
|
|
762
|
+
4. the app calls `changePace({ isMoving: true })`.
|
|
763
|
+
|
|
764
|
+
Then GPS starts again, and a `motionchange` (`is_moving: true`) is recorded: with the fix of the region's exit report
|
|
765
|
+
(case 1, when that fix passes the location filters) or the outside fix (case 2), otherwise with the best known fix.
|
|
766
|
+
|
|
767
|
+
**The anchor.** The anchor is the fix where the device became stationary: the fix of the `motionchange` with
|
|
768
|
+
`is_moving: false` (after `start()`, a fresh fix). While stationary, the anchor changes only in these cases:
|
|
769
|
+
|
|
770
|
+
- no fix was known when the device became stationary: the first accepted fix becomes the anchor;
|
|
771
|
+
- the anchor was more than 10 minutes old when it became the anchor, and a fix arrives that is at most 10 minutes
|
|
772
|
+
old and has an accuracy no worse than `filter.trackingAccuracyThreshold`: that fix replaces the anchor;
|
|
773
|
+
- a fix that is not certainly outside has a better (smaller) accuracy than the anchor: that fix replaces the anchor.
|
|
774
|
+
|
|
775
|
+
The stationary region is registered only around an anchor that was at most 10 minutes old when it became the anchor
|
|
776
|
+
(the OS never reports an exit for a device that is already outside a new region). The region is moved only when the
|
|
777
|
+
anchor is more than 50 m from the region's centre.
|
|
778
|
+
|
|
779
|
+
**Low-power fallback.** The plugin uses a low-power request (Wi-Fi and cell towers, no GPS; at most one fix per
|
|
780
|
+
3 minutes) instead of the passive one when:
|
|
781
|
+
|
|
782
|
+
- the region cannot be registered: no "Allow all the time" location, a backend error, or no answer from the OS within
|
|
783
|
+
10 s;
|
|
784
|
+
- no anchor is known, or the anchor was more than 10 minutes old when it became the anchor (see above);
|
|
785
|
+
- the app has 99 or 100 geofences of its own. GMS accepts at most 100 geofences per app, and the plugin gives the last
|
|
786
|
+
slot to the app's geofences: when a geofence change brings the app to 99 geofences, the plugin removes the
|
|
787
|
+
stationary region (see [Geofencing](#geofencing)).
|
|
788
|
+
|
|
789
|
+
The plugin logs each case. With `desiredAccuracy: 'passive'` the fallback request stays passive. After a failed
|
|
790
|
+
registration the plugin tries again when the device leaves the stationary state, when the location backend changes,
|
|
791
|
+
and on every location provider change while stationary (for example after "Allow all the time" is granted); it does
|
|
792
|
+
not try again on every fix. A polygon geofence still forces the moving request while the device is inside the
|
|
793
|
+
polygon's enclosing circle (see [Geofencing](#geofencing)).
|
|
794
|
+
|
|
795
|
+
**Heartbeats while stationary.** A heartbeat carries the anchor fix. Its `recorded_at` is the time the heartbeat was
|
|
796
|
+
created (now); `timestamp` (`location.timestamp` in JavaScript) is the time the anchor fix was **acquired**, which can
|
|
797
|
+
be hours earlier. A passive or low-power fix that arrives while stationary changes what the heartbeat carries only
|
|
798
|
+
when it becomes the anchor. A record with its own fix does change it: a transition of one of the app's geofences, or
|
|
799
|
+
`getCurrentPosition()` / `watchPosition()` with `persist: true`; later heartbeats carry that record's fix until the
|
|
800
|
+
anchor changes again. So the server can show "the device is on (last heartbeat 11:59), last position from 10:26".
|
|
801
|
+
Details in [docs/heartbeat.md](docs/heartbeat.md#stationary-gps-off-heartbeats-continue).
|
|
802
|
+
|
|
803
|
+
**What costs battery, and the keys that change it** (largest cost first):
|
|
804
|
+
|
|
805
|
+
| Cost | When | Config keys |
|
|
806
|
+
|---|---|---|
|
|
807
|
+
| GPS | While moving, and for `stopTimeout` minutes after the device stops (stop detection waits that long). | `geolocation.desiredAccuracy` (`'balanced'` uses Wi-Fi and cell towers, no GPS); `locationUpdateInterval` and `fastestLocationUpdateInterval`; `stopTimeout` (5 min by default); `disableStopDetection: true` keeps GPS on all the time: avoid it. |
|
|
808
|
+
| Mobile radio | Every upload request wakes the radio for several seconds. | `http.syncInterval` (the field-force setup uses 300 s: one upload about every 5 minutes while moving instead of one per record), `batchSync`, `maxBatchSize`, `autoSyncThreshold`, `disableAutoSyncOnCellular`. Heartbeats and audit records are always uploaded at once. |
|
|
809
|
+
| CPU wake-ups | Every heartbeat alarm; every fix while moving. | `heartbeat.minInterval` / `maxInterval` (180/300 s). With the battery-optimization exemption, heartbeats stay exact in Doze (about 20 wake-ups per hour at 180 s); without it, Android spaces them about 9 minutes apart. |
|
|
810
|
+
| Polygon geofences | Continuous location while the device is inside a polygon's enclosing circle. | Prefer circles. |
|
|
811
|
+
| Activity recognition | While tracking in `start()` mode. It uses the motion sensors and costs little. | `activity.activityRecognitionInterval`, `activity.disableMotionActivityUpdates` (then only the stationary region and passive fixes detect movement). |
|
|
812
|
+
| Logging | Every log line is written to a file. | `logger.logLevel` (`'info'` by default; `'debug'` writes much more). |
|
|
813
|
+
|
|
814
|
+
How to measure the cost on a real phone: procedure M-04 in the
|
|
815
|
+
[e2e runbook](docs/e2e-runbook.md#m-04-12-hour-battery-measurement).
|
|
816
|
+
|
|
817
|
+
## Geofencing
|
|
818
|
+
|
|
819
|
+
- Up to **100** geofences (`TOO_MANY_GEOFENCES` beyond that). An identifier is 1–100 characters; adding a geofence
|
|
820
|
+
with an existing identifier replaces it. `loiteringDelay` must be ≥ 0. The identifier `__lt_stationary__` is
|
|
821
|
+
reserved for the plugin's stationary region: `addGeofence` rejects it with `INVALID_ARGUMENT`.
|
|
822
|
+
- The stationary region (see [Battery](#battery)) does not count against the 100. GMS accepts at most 100 geofences
|
|
823
|
+
per app, stationary region included. While the app has 99 or 100 geofences, the plugin does not register the
|
|
824
|
+
stationary region (stationary tracking then uses the low-power fallback). When an add brings the app to 99
|
|
825
|
+
geofences, the plugin removes a registered region after that add has finished; the removal waits for the tracking
|
|
826
|
+
engine and for the OS. So an `addGeofence` of the 100th geofence right after the 99th, or one `addGeofences` call
|
|
827
|
+
that goes from 98 to 100 geofences, can still fail with `TOO_MANY_GEOFENCES` from the OS while the region is
|
|
828
|
+
registered (an open item in [DECISIONS.md](docs/DECISIONS.md#r25-open-requests-and-known-limitations)).
|
|
829
|
+
- **Circle:** `latitude`, `longitude` and a `radius` > 0.
|
|
830
|
+
- **Polygon:** `vertices`, at least 3 distinct points with a non-zero area, not crossing the 180° meridian. The plugin
|
|
831
|
+
registers the polygon's smallest enclosing circle, padded by 10 % (at least 50 m), with the OS; `getGeofences()`
|
|
832
|
+
returns it as `latitude` / `longitude` / `radius`. While the device is inside that circle, the plugin requests
|
|
833
|
+
continuous fixes and tests them against the polygon. To avoid flapping at the edge, the state changes only when a
|
|
834
|
+
fix's accuracy circle is entirely on one side of the edge, or when two fixes in a row agree. Leaving the circle
|
|
835
|
+
also means leaving the polygon. Polygon `DWELL` is always computed by the plugin.
|
|
836
|
+
- Geofences are stored in the plugin's database, but registered with the OS and reported **only while tracking is
|
|
837
|
+
on** (`start()` or `startGeofences()`); `stop()` unregisters them. GMS and HMS drop all geofences when location
|
|
838
|
+
services are switched off; the plugin registers them again when location comes back or the location permission
|
|
839
|
+
level changes. If the backend answers that geofencing is not available yet (GMS `GEOFENCE_NOT_AVAILABLE`), the
|
|
840
|
+
plugin tries again after 10, 30, 60, 120 and 300 s.
|
|
841
|
+
- **Backends.** GMS and HMS report `ENTER`, `EXIT` and `DWELL` natively; with `initialTriggerEntry` their initial
|
|
842
|
+
trigger is `ENTER | DWELL`. The `android` backend (proximity alerts) has no dwell: the plugin synthesizes `DWELL`
|
|
843
|
+
when the device stays inside for `loiteringDelay` after `ENTER`. It always reports `ENTER` for a circle the device
|
|
844
|
+
is already inside.
|
|
845
|
+
- Every transition creates a `geofence` record (uploaded like a location) and a `geofence` event. `addGeofence(s)`
|
|
846
|
+
and `removeGeofence(s)` emit `geofenceschange`.
|
|
847
|
+
- `removeGeofences()` without `identifiers` (or with `identifiers: null`) removes **all** geofences;
|
|
848
|
+
`removeGeofences({ identifiers: [] })` removes **nothing**, so an empty computed list never wipes everything.
|
|
849
|
+
Unknown identifiers are ignored.
|
|
850
|
+
- Background location permission ("Allow all the time") is needed for geofences to fire while the app is in the
|
|
851
|
+
background.
|
|
852
|
+
|
|
853
|
+
## Records, uploads and the queue
|
|
854
|
+
|
|
855
|
+
- Every record is written to SQLite first and uploaded to `http.url` according to the rules in
|
|
856
|
+
[docs/wire-format.md](docs/wire-format.md#when-uploads-happen). Priority records (`heartbeat`, `tracking_start`,
|
|
857
|
+
`tracking_stop`, `providerchange`) are uploaded right away and take the older queued records along.
|
|
858
|
+
- 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).
|
|
865
|
+
- `getLocations({ limit })` returns queued records, oldest first; `getCount()` counts them; `destroyLocations()` and
|
|
866
|
+
`destroyLocation({ uuid })` delete them without uploading.
|
|
867
|
+
- `sync()` uploads the whole queue now, ignoring `autoSync`, `autoSyncThreshold` and `disableAutoSyncOnCellular`, and
|
|
868
|
+
resolves with the uploaded records. It rejects with `NO_URL` (no valid `http.url`), or with `HTTP_ERROR` /
|
|
869
|
+
`NETWORK_ERROR` at the first failed upload, after its tries (records uploaded before the failure are already
|
|
870
|
+
deleted).
|
|
871
|
+
- `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.
|
|
874
|
+
- `persistence.maxDaysToPersist` and `maxRecordsToPersist` prune the queue, including heartbeats and audit records.
|
|
875
|
+
|
|
876
|
+
### Live location: `http.syncInterval`
|
|
877
|
+
|
|
878
|
+
A back office that shows where a worker is now needs a recent position, but one upload per location record keeps the
|
|
879
|
+
mobile radio busy. `http.syncInterval` sets the maximum age of the newest position on the server:
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
http: {
|
|
883
|
+
url: 'https://api.example.com/locations',
|
|
884
|
+
autoSync: true, // required: syncInterval works only with autoSync
|
|
885
|
+
syncInterval: 300, // seconds: live location at most about 5 minutes old
|
|
886
|
+
batchSync: true, // one request carries many records
|
|
887
|
+
maxBatchSize: 100,
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
- **Normal records** (`location`, `motionchange`, `current_position`, `watch_position`, `geofence`) wait in the
|
|
892
|
+
queue until the **oldest** of them is `syncInterval` seconds old. "Oldest" is the queued normal record with the
|
|
893
|
+
smallest `recorded_at`; its age is now − `recorded_at`, and a negative age (the wall clock was set back) counts as
|
|
894
|
+
due. Then the whole queue is uploaded, in batches of `maxBatchSize` when `batchSync` is on. While the device moves,
|
|
895
|
+
that is one upload about every `syncInterval` seconds.
|
|
896
|
+
- **Audit records** (`heartbeat`, `tracking_start`, `tracking_stop`, `providerchange`) are still uploaded at once, and
|
|
897
|
+
they take the queued normal records with them.
|
|
898
|
+
- While stationary, no location records are created; the heartbeat (every `minInterval`) is uploaded at once and
|
|
899
|
+
carries the last position with its acquisition time.
|
|
900
|
+
- `autoSyncThreshold` above 0 is a size limit: the queue is uploaded earlier when it reaches that many records.
|
|
901
|
+
- The check runs on every insert, when the network comes back, when the uploader starts, and on a timer in the
|
|
902
|
+
tracking process (at `oldest.recorded_at + syncInterval`). `disableAutoSyncOnCellular` still holds normal records
|
|
903
|
+
back on cellular, and `sync()` still uploads everything at once.
|
|
904
|
+
- **The timer holds no wake lock.** It counts only the time the CPU is awake, so in deep sleep or Doze it fires late.
|
|
905
|
+
Two other triggers limit the delay: while the device moves, every new location record runs the check against the
|
|
906
|
+
wall clock; while it is stationary, each heartbeat is uploaded at once and takes the queue with it. With the
|
|
907
|
+
heartbeat disabled and no new record, held records wait until the CPU has been awake long enough.
|
|
908
|
+
- **While tracking is off**, normal records are uploaded as with `syncInterval: 0` (by `autoSyncThreshold`), because
|
|
909
|
+
there is no timer then. For example, with the default `autoSyncThreshold: 0`, a `getCurrentPosition()` record after
|
|
910
|
+
the 02:00 stop is uploaded at once. When tracking stops, records that were held are handled by this rule at once.
|
|
911
|
+
- **After a failed automatic upload** (its last try failed), normal records are retried once per `syncInterval`,
|
|
912
|
+
counted from that last try (measured on the elapsed-time clock), by the timer, not on every insert. These still upload at once: the network coming back, a
|
|
913
|
+
queued audit record, and `sync()`.
|
|
914
|
+
- [Native listeners](#companion-plugins-native-api) receive every record when it is queued, not when it is uploaded,
|
|
915
|
+
so `syncInterval` does not delay them.
|
|
916
|
+
- `syncInterval: 0` (the default) keeps the behavior without it: normal records follow `autoSync` and
|
|
917
|
+
`autoSyncThreshold`.
|
|
918
|
+
- Held records count against `persistence.maxRecordsToPersist` (unlimited by default). A limit smaller than the number
|
|
919
|
+
of records created in `syncInterval` lets pruning delete held records before they are uploaded.
|
|
920
|
+
|
|
921
|
+
Rules and a timeline example: [docs/wire-format.md](docs/wire-format.md#live-location-with-syncinterval).
|
|
922
|
+
|
|
923
|
+
## Events reference
|
|
924
|
+
|
|
925
|
+
Subscribe with `LocationTracking.addListener(name, callback)` or the typed helpers. Events reach JavaScript only
|
|
926
|
+
while the app's WebView is alive; [native listeners](#companion-plugins-native-api) receive the same events in every
|
|
927
|
+
process, also without a WebView. Records are persisted and uploaded whether or not anyone is listening.
|
|
928
|
+
`connectivitychange` and `powersavechange`, and the `providerchange` events caused by system broadcasts, start once
|
|
929
|
+
tracking has been started (or resumed) in the app's process.
|
|
930
|
+
|
|
931
|
+
| Event | Payload | Emitted when |
|
|
932
|
+
|---|---|---|
|
|
933
|
+
| `location` | `Location` | A location record was stored (`event` is `location`, `motionchange`, `current_position` or `watch_position`). Not for `insertLocation()` records, nor for audit, heartbeat or geofence records (they have their own events or none). |
|
|
934
|
+
| `motionchange` | `{ isMoving, location }` | The motion state switched between moving and stationary (also emitted as a `location` event). |
|
|
935
|
+
| `activitychange` | `{ activity, confidence }` | Activity recognition reported a different activity type, with at least `minimumActivityRecognitionConfidence`. The `android` backend has none. |
|
|
936
|
+
| `providerchange` | `ProviderState` | Location services, permission level, accuracy or backend changed. Also emitted while tracking is off (then without a record); not for the very first state observed after install. |
|
|
937
|
+
| `heartbeat` | `{ location }` | A heartbeat record was created. `location` is the heartbeat record, with the last known coords. |
|
|
938
|
+
| `geofence` | `{ identifier, action, location, extras? }` | A geofence was entered, exited or dwelled in (only while tracking is on). |
|
|
939
|
+
| `geofenceschange` | `{ on, off }` | The set of geofences changed (`addGeofence(s)`, `removeGeofence(s)`). `on` holds geofences, `off` holds identifiers. |
|
|
940
|
+
| `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
|
+
| `connectivitychange` | `{ connected, type }` | The network connection changed. A network blocked for the app by Doze or Data Saver counts as disconnected. |
|
|
942
|
+
| `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. |
|
|
944
|
+
| `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. |
|
|
946
|
+
|
|
947
|
+
## Companion plugins (native API)
|
|
948
|
+
|
|
949
|
+
Another Capacitor plugin in the same app, or the app's own Android code, can work with this plugin directly in Kotlin,
|
|
950
|
+
without JavaScript. This is for a companion that must see **every** record and event, including those created while
|
|
951
|
+
no WebView exists: after a reboot, when a heartbeat alarm wakes the app at night, or after Android restarted a killed
|
|
952
|
+
process. The example is the fake PremiseMonitor plugin of the [field-force example](#field-force-example), which
|
|
953
|
+
audits a premise with its own foreground service.
|
|
954
|
+
|
|
955
|
+
Package `com.brickssoft.locationtracking.api`:
|
|
956
|
+
|
|
957
|
+
- `LocationTrackingListener` with two methods, both optional:
|
|
958
|
+
- `onRecord(context, record: JSONObject)`: every record that is queued for upload (all `event` types, including
|
|
959
|
+
`heartbeat`, `tracking_start`, `tracking_stop`, `providerchange`, `geofence` and records from `insertLocation()`),
|
|
960
|
+
in the [wire format](docs/wire-format.md) without `sent_at`. It is called when the record is queued, before any
|
|
961
|
+
upload, so `syncInterval` and network problems do not delay it.
|
|
962
|
+
- `onEvent(context, name: String, payload: JSONObject)`: every event, with the same name and payload as the
|
|
963
|
+
JavaScript `addListener` API.
|
|
964
|
+
- `LocationTrackingNative`: `addListener`, and calls that do what the JavaScript methods do (`ready`, `setConfig`,
|
|
965
|
+
`start`, `startGeofences`, `stop`, `changePace`, `getState`, `getHeartbeatStatus`, `sync`, `insertLocation`,
|
|
966
|
+
`addGeofence`, `removeGeofence`, `getGeofences`). Results arrive in a `NativeCallback` as a Kotlin `Result`; a
|
|
967
|
+
failure carries the same error `code` as in JavaScript.
|
|
968
|
+
|
|
969
|
+
**Register a listener in the manifest** (recommended: it never misses a record). The plugin creates the class once per
|
|
970
|
+
process, before it can emit anything, in every process that runs the plugin (app launch, the tracking service, boot,
|
|
971
|
+
alarms):
|
|
972
|
+
|
|
973
|
+
```xml
|
|
974
|
+
<application>
|
|
975
|
+
<meta-data android:name="com.brickssoft.locationtracking.LISTENER"
|
|
976
|
+
android:value="com.example.audit.AuditListener"/>
|
|
977
|
+
</application>
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
```kotlin
|
|
981
|
+
package com.example.audit
|
|
982
|
+
|
|
983
|
+
import android.content.Context
|
|
984
|
+
import com.brickssoft.locationtracking.api.LocationTrackingListener
|
|
985
|
+
import org.json.JSONObject
|
|
986
|
+
|
|
987
|
+
class AuditListener : LocationTrackingListener { // public no-argument constructor
|
|
988
|
+
override fun onRecord(context: Context, record: JSONObject) {
|
|
989
|
+
// e.g. record.getString("event") == "heartbeat"; hand real work (I/O, uploads) to your own executor
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
override fun onEvent(context: Context, name: String, payload: JSONObject) {
|
|
993
|
+
// e.g. name == "providerchange"
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
A second listener in the same app needs its own meta-data name with a suffix, for example
|
|
999
|
+
`com.brickssoft.locationtracking.LISTENER.analytics`: Android's manifest merger rejects two libraries that declare the
|
|
1000
|
+
same meta-data name with different values.
|
|
1001
|
+
|
|
1002
|
+
**Or subscribe from code.** Such a listener receives only what is emitted after the call:
|
|
1003
|
+
|
|
1004
|
+
```kotlin
|
|
1005
|
+
val subscription = LocationTrackingNative.addListener(context, AuditListener())
|
|
1006
|
+
LocationTrackingNative.getState(context) { result ->
|
|
1007
|
+
result.onSuccess { state -> Log.i("Audit", "tracking enabled: ${state.getBoolean("enabled")}") }
|
|
1008
|
+
.onFailure { error -> Log.w("Audit", "getState failed", error) }
|
|
1009
|
+
}
|
|
1010
|
+
subscription.remove() // idempotent
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
Listener methods and callbacks run on one background thread named `LT-native`. Listener methods run in the order the
|
|
1014
|
+
records and events were created (a record's `onRecord` comes before the events that carry it). A callback runs when
|
|
1015
|
+
its call completes, so callbacks come in completion order, not in call order. Exceptions thrown by a listener or a
|
|
1016
|
+
callback are caught and logged. Keep the methods short; the listener's constructor must not block. Native calls do not
|
|
1017
|
+
need the JavaScript `ready()`, and are not subject to the `NOT_READY` rule; call `LocationTrackingNative.ready` first
|
|
1018
|
+
when you need a config. The consumer R8 rules keep the API and the listeners' constructors in minified builds.
|
|
1019
|
+
|
|
1020
|
+
The full guide for companion-plugin authors: [docs/native-api.md](docs/native-api.md).
|
|
1021
|
+
|
|
1022
|
+
## Error codes
|
|
1023
|
+
|
|
1024
|
+
Every rejected promise has a `code` (see the `ErrorCode` type) and a `message`.
|
|
1025
|
+
|
|
1026
|
+
| Code | Meaning |
|
|
1027
|
+
|---|---|
|
|
1028
|
+
| `NOT_READY` | The method was called before `ready()` resolved in this process (see the [NOT_READY rule](#quick-start)). |
|
|
1029
|
+
| `PERMISSION_DENIED` | A required permission is missing, for example `start()` or `getCurrentPosition()` without location permission. Also `start()` / `startGeofences()` when Android refused to start the foreground service. |
|
|
1030
|
+
| `LOCATION_DISABLED` | Location services are turned off on the device (`getCurrentPosition()`; some HMS geofence errors). |
|
|
1031
|
+
| `TIMEOUT` | The operation timed out, for example `getCurrentPosition()` got no fix in time. |
|
|
1032
|
+
| `UNAVAILABLE` | A required system service, backend or feature is not available right now, for example no app can send the log email. |
|
|
1033
|
+
| `INVALID_ARGUMENT` | Invalid options or config, for example a wrong config type, a geofence without a radius, a polygon with fewer than 3 vertices, or the reserved geofence identifier `__lt_stationary__`. |
|
|
1034
|
+
| `NOT_FOUND` | Reserved; not currently used. Lookups of unknown items resolve instead (`getGeofence()` with `null`, `destroyLocation()` with `deleted: false`, `clearWatch()` does nothing). |
|
|
1035
|
+
| `NO_URL` | `sync()` was called without a valid `http.url`. |
|
|
1036
|
+
| `HTTP_ERROR` | `sync()` got a non-2xx response. |
|
|
1037
|
+
| `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). |
|
|
1039
|
+
| `NO_ACTIVITY` | `emailLog()` was called while no Activity is attached. (The `open*Settings` methods work without one.) |
|
|
1040
|
+
| `IO_ERROR` | Storage or file error (database, log files). |
|
|
1041
|
+
| `UNIMPLEMENTED` | Not available on this platform (web stub, iOS). |
|
|
1042
|
+
| `INTERNAL` | Unexpected internal error. The plugin log has details. |
|
|
1043
|
+
|
|
1044
|
+
```ts
|
|
1045
|
+
try {
|
|
1046
|
+
await LocationTracking.start();
|
|
1047
|
+
} catch (e: any) {
|
|
1048
|
+
if (e.code === 'PERMISSION_DENIED') {
|
|
1049
|
+
/* ask again or explain */
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
## Web stub
|
|
1055
|
+
|
|
1056
|
+
The web implementation exists so that an app can run in a browser during development. It keeps no queue, uploads
|
|
1057
|
+
nothing and cannot track.
|
|
1058
|
+
|
|
1059
|
+
- **Works:** `ready`, `setConfig`, `reset` and `getState` (an in-memory config; `getState()` reports `enabled: false`,
|
|
1060
|
+
`backend: 'web'` and `lastRecordAt: null`), `getCurrentPosition`, `watchPosition` and `clearWatch` (through
|
|
1061
|
+
`navigator.geolocation`), `checkPermissions`, `requestPermissions` and `getDeviceInfo` (`platform: 'web'`, values
|
|
1062
|
+
parsed from the user agent, `sdkInt: -1`).
|
|
1063
|
+
- **`NOT_READY`:** `setConfig`, `reset`, `getCurrentPosition`, `watchPosition` and `clearWatch` reject until
|
|
1064
|
+
`ready()` has resolved, like on Android.
|
|
1065
|
+
- **Positions:** `getCurrentPosition` (unless `persist: false`) and `watchPosition` with `persist: true` also emit
|
|
1066
|
+
`location` events, but nothing is stored or uploaded. `WatchPositionOptions.interval` is ignored (the browser
|
|
1067
|
+
decides how often fixes arrive). `getCurrentPosition` has its own deadline, because browsers don't count the time
|
|
1068
|
+
the permission prompt is open.
|
|
1069
|
+
- **Permissions:** `location` comes from the Permissions API (`'prompt'` when it can't tell); the other three are
|
|
1070
|
+
always `'granted'`. `requestPermissions` asks for a fix so the browser shows its prompt, and gives up after 60 s
|
|
1071
|
+
(the status stays `'prompt'`).
|
|
1072
|
+
- **Everything else** rejects with `UNIMPLEMENTED`.
|
|
1073
|
+
|
|
1074
|
+
## Server side
|
|
1075
|
+
|
|
1076
|
+
- [docs/wire-format.md](docs/wire-format.md) describes every record variant, batching, templates, upload and retry
|
|
1077
|
+
rules, and JWT refresh.
|
|
1078
|
+
- [docs/migrating-from-transistor-v8.md](docs/migrating-from-transistor-v8.md) lists what a server built for
|
|
1079
|
+
Transistor v8 uploads has to change, together with the app-side migration.
|
|
1080
|
+
- [docs/heartbeat.md](docs/heartbeat.md) covers heartbeat semantics, what reliability to expect on Android, and how
|
|
1081
|
+
to audit tracking gaps on the server.
|
|
1082
|
+
- [docs/device-test-checklist.md](docs/device-test-checklist.md) is a manual test plan for real GMS, HMS and
|
|
1083
|
+
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.
|
|
1085
|
+
|
|
1086
|
+
In short, store records idempotently by `uuid` and answer `2xx` quickly (also for records you will never accept, or
|
|
1087
|
+
they are retried until pruned). While tracking is on, expect some record at least every `maxInterval` seconds (about
|
|
1088
|
+
every 9 minutes in deep idle when the app is not exempt from battery optimization), and flag longer gaps. Each
|
|
1089
|
+
heartbeat's `heartbeat` object (`strategy`, `next_at`, `battery_exempt`, `device_idle`) tells you which gap to expect
|
|
1090
|
+
next, so a normal 9-minute gap in Doze is not flagged as a failure (see
|
|
1091
|
+
[docs/heartbeat.md](docs/heartbeat.md#heartbeat-metadata)). A back office can show tracking as **online** while the
|
|
1092
|
+
latest record is not older than that expected gap. A `tracking_stop` with reason `service_start_failed` means Android
|
|
1093
|
+
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.
|
|
1095
|
+
|
|
1096
|
+
## Example app
|
|
1097
|
+
|
|
1098
|
+
[`example/`](example/) is a plain HTML/JS Capacitor app, with a button for every method and a live log of all 13
|
|
1099
|
+
events:
|
|
1100
|
+
|
|
1101
|
+
```bash
|
|
1102
|
+
npm ci && npm run build # in the repository root
|
|
1103
|
+
cd example && npm ci && npm run sync
|
|
1104
|
+
cd android && ./gradlew assembleDebug -PlocationTracking.providers=gms,hms
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
Its **debug** build also contains the test hooks of the [end-to-end tests](#end-to-end-tests): a broadcast receiver
|
|
1108
|
+
that runs plugin calls without the web page, and an "E2E mode" in which the page never changes the plugin's state by
|
|
1109
|
+
itself. The receiver accepts only senders that hold `android.permission.DUMP` (the adb shell and root hold it; other
|
|
1110
|
+
apps on the phone do not). Release builds contain neither.
|
|
1111
|
+
|
|
1112
|
+
## Field-force example
|
|
1113
|
+
|
|
1114
|
+
[`examples/field-force/`](examples/field-force/) is the setup this plugin was built for: a field-force app whose back
|
|
1115
|
+
office audits a moving worker. It is plain HTML/JS (no bundler), Android only, and packages GMS only (like a Google
|
|
1116
|
+
Play build).
|
|
1117
|
+
|
|
1118
|
+
- **Auto start.** Every time the app's page loads, it calls `ready()` with the preset below and then `start()` if
|
|
1119
|
+
tracking is not on yet. It runs the same startup again when the app comes back to the foreground while tracking is
|
|
1120
|
+
off, for example the morning after the 02:00 stop when the app was never closed. It detects the foreground with the
|
|
1121
|
+
document event `resume` (Capacitor 8 fires it on every activity resume after the first pause) and, as a fallback,
|
|
1122
|
+
`visibilitychange` to visible; both lead to one check. The check does nothing while a startup runs, while tracking
|
|
1123
|
+
is on, or when the test overrides set `autoStart: false`.
|
|
1124
|
+
- **Stop at 02:00.** At start, the app computes the minutes until the next 02:00 local time and passes them as
|
|
1125
|
+
`geolocation.stopAfterElapsedMinutes`. The plugin then records `tracking_stop` with reason `stop_after_elapsed` at
|
|
1126
|
+
about 02:00. This is app code, not a plugin schedule feature: any app can compute its own stop time the same way.
|
|
1127
|
+
When tracking is already on at launch, the app keeps the running session's value, because the plugin measures it
|
|
1128
|
+
from the session start. When tracking is off, the app computes the minutes again right before `start()` (permission
|
|
1129
|
+
dialogs can take minutes) and applies them with `setConfig` if they changed. The daily stop ends each session at
|
|
1130
|
+
night, so the next morning starts a fresh session. (The owner's reason: an app that is never closed must not hit a
|
|
1131
|
+
`ForegroundServiceDidNotStartInTimeException` on a cold start the next morning.)
|
|
1132
|
+
- **When the 02:00 stop happens.** The plugin's stop timer counts only the time the CPU is awake. The plugin therefore
|
|
1133
|
+
also checks the stop time whenever a fix, an activity update, a stationary-region exit or a heartbeat arrives.
|
|
1134
|
+
While the phone moves, fixes arrive every second, so the stop is on time. While it is stationary (GPS off) and
|
|
1135
|
+
asleep, the stop happens at the first heartbeat after the stop time: at most `maxInterval` (300 s) late when the app
|
|
1136
|
+
is exempt from battery optimization (heartbeats about every 180 s), and about 9–11 minutes late in deep Doze without
|
|
1137
|
+
the exemption. The check runs in a separate step right after the heartbeat is queued, and the heartbeat's wake lock
|
|
1138
|
+
may already be released then; if the phone falls asleep in that short moment, the stop comes with a later
|
|
1139
|
+
heartbeat.
|
|
1140
|
+
- **Live location at most about 5 minutes old:** `http.syncInterval: 300` with batches.
|
|
1141
|
+
- **Device details and battery.** `http.params.device` carries the manufacturer, model, brand, OS version, SDK level,
|
|
1142
|
+
plugin version, backend and GMS/HMS availability in every request; every record carries `battery`.
|
|
1143
|
+
- **Online status, route, travel time and distance** come from the records: heartbeats while stationary, `location`
|
|
1144
|
+
records while moving, `motionchange` for the moving periods, `odometer` for the distance.
|
|
1145
|
+
- **Companion plugin.** A fake PremiseMonitor plugin (`examples/field-force/plugins/premise-monitor/`) listens to all
|
|
1146
|
+
records and events through the [native API](#companion-plugins-native-api), monitors one circular premise with a
|
|
1147
|
+
geofence, runs its own foreground location service while the worker is inside, and uploads its own audit entries.
|
|
1148
|
+
|
|
1149
|
+
The preset (production values):
|
|
1150
|
+
|
|
1151
|
+
| Group | Values |
|
|
1152
|
+
|---|---|
|
|
1153
|
+
| `geolocation` | `desiredAccuracy: 'high'`, `distanceFilter: 20`, `stationaryRadius: 50`, `stopTimeout: 5`, `stopAfterElapsedMinutes`: minutes to the next 02:00, `filter.trackingAccuracyThreshold: 50` |
|
|
1154
|
+
| `heartbeat` | `enabled: true`, `minInterval: 180`, `maxInterval: 300` |
|
|
1155
|
+
| `http` | `url: <backend>/locations`, `autoSync: true`, `syncInterval: 300`, `batchSync: true`, `maxBatchSize: 100`, `params: { worker_id, device: {…} }`, JWT `authorization` with `refreshUrl: <backend>/auth/refresh` |
|
|
1156
|
+
| `app` | `stopOnTerminate: false`, `startOnBoot: true` |
|
|
1157
|
+
| other | `notification: { title: 'Field Force', text: 'Shift tracking is on' }`, `logger.logLevel: 'debug'`, `locationProvider: 'auto'` |
|
|
1158
|
+
|
|
1159
|
+
The startup logic is in `examples/field-force/www/ff-core.js`. The page exposes its progress as `window.FF_APP`:
|
|
1160
|
+
|
|
1161
|
+
- `status`: `'running'`, `'done'` or `'failed'` (the latest startup run); `step`: the step that runs now;
|
|
1162
|
+
- `result`: the state, the computed `stopAfterElapsedMinutes`, the config, the device info and `warnings` about
|
|
1163
|
+
ignored test overrides; `error`: `{code, message, step}` of a failed run;
|
|
1164
|
+
- `startup`: the promise of the latest startup run (page load or resume);
|
|
1165
|
+
- `startupCount`: the number of startup runs in this page; `lastStartupReason`: `'load'` or `'resume'`;
|
|
1166
|
+
- `lastResume`: the latest foreground check, `{at, trigger, outcome}`, where `trigger` is `'resume'`,
|
|
1167
|
+
`'visibilitychange'` or `'manual'` and `outcome` is `'started'`, `'enabled'`, `'busy'`, `'autostart_off'` or
|
|
1168
|
+
`'error'`;
|
|
1169
|
+
- `checkResume()`: runs the foreground check by hand and resolves with its outcome.
|
|
1170
|
+
|
|
1171
|
+
The startup asks for every permission that is not granted and waits until the permission dialog is answered.
|
|
1172
|
+
|
|
1173
|
+
Build it (after `npm ci && npm run build` in the repository root), and run the Node tests of its startup logic:
|
|
1174
|
+
|
|
1175
|
+
```bash
|
|
1176
|
+
cd examples/field-force && npm ci && npm test # Node tests of www/ff-core.js and www/app.js
|
|
1177
|
+
npm run sync # FF_BACKEND_URL=<url> sets the back office (default http://10.0.2.2:8787)
|
|
1178
|
+
cd android && ./gradlew assembleDebug
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
## End-to-end tests
|
|
1182
|
+
|
|
1183
|
+
The plugin is tested end to end on an Android emulator (AVD), in GitHub Actions and on a developer's machine:
|
|
1184
|
+
|
|
1185
|
+
| Part | Where | What |
|
|
1186
|
+
|---|---|---|
|
|
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. |
|
|
1188
|
+
| 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
|
+
| 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
|
+
| 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. |
|
|
1191
|
+
|
|
1192
|
+
Run them on a machine with KVM and a booted emulator (details, triage and the manual procedures are in the
|
|
1193
|
+
[runbook](docs/e2e-runbook.md)):
|
|
1194
|
+
|
|
1195
|
+
```bash
|
|
1196
|
+
cd e2e/plugin && npm ci
|
|
1197
|
+
export E2E_APK=../../example/android/app/build/outputs/apk/debug/app-debug.apk
|
|
1198
|
+
npm run test:e2e # the whole plugin suite
|
|
1199
|
+
NODE_OPTIONS='--test-name-pattern=^P-L01' npm run test:e2e # one scenario
|
|
1200
|
+
npm run dry-run # list the scenarios, no device
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
(`npm run test:e2e -- --test-name-pattern=...` does not filter: npm puts the option after the file pattern, where
|
|
1204
|
+
Node ignores it.)
|
|
1205
|
+
|
|
1206
|
+
CI runs the emulator suites through `.github/scripts/run-e2e.sh` in `.github/workflows/e2e-android.yml`, only when
|
|
1207
|
+
started by hand (Actions tab or `gh workflow run`; pushes and pull requests start no run, to save Actions minutes):
|
|
1208
|
+
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). The build workflow
|
|
1210
|
+
`.github/workflows/ci.yml`, also started by hand only, type-checks the kit and runs its unit tests,
|
|
1211
|
+
type-checks and dry-runs both suites, runs the field-force Node tests, and checks that the Google Play build of the
|
|
1212
|
+
field-force app supports 16 KB memory pages (`.github/scripts/check-16kb.py`).
|
|
1213
|
+
|
|
1214
|
+
The field-force suite, together with the field-force app and the kit, is written so that it can move into the Bricks
|
|
1215
|
+
app as its integration test. The contract behind the tests (scenario ids, debug commands, mock back office) is
|
|
1216
|
+
[docs/e2e/architecture.md](docs/e2e/architecture.md).
|
|
1217
|
+
|
|
1218
|
+
## Publishing to npm
|
|
1219
|
+
|
|
1220
|
+
The package is published as
|
|
1221
|
+
[`@bricks-soft/capacitor-location-tracking`](https://www.npmjs.com/package/@bricks-soft/capacitor-location-tracking),
|
|
1222
|
+
with public access, by the workflow [`.github/workflows/npm-publish.yml`](.github/workflows/npm-publish.yml). The
|
|
1223
|
+
setup is the same as in `bricks-soft/cap-downloader`: npm trusted publishing (OpenID Connect, no npm token) and npm
|
|
1224
|
+
staged publishing.
|
|
1225
|
+
|
|
1226
|
+
One-time setup on npmjs.com (by an owner of the `@bricks-soft` npm scope), after the first version exists on npm
|
|
1227
|
+
(trusted publishers are configured per package):
|
|
1228
|
+
|
|
1229
|
+
1. Open the package's **Settings** → **Trusted publishing** → **GitHub Actions**.
|
|
1230
|
+
2. Enter organization `bricks-soft`, repository `location-tracking`, workflow file `npm-publish.yml`, no environment.
|
|
1231
|
+
3. Give it the same permission as the `cap-downloader` publisher: stage only (`npm stage publish`).
|
|
1232
|
+
|
|
1233
|
+
The first version cannot use the trusted publisher, because the package does not exist yet. Publish it once by hand
|
|
1234
|
+
from a clean checkout, logged in with `npm login` as a member of the `@bricks-soft` scope:
|
|
1235
|
+
|
|
1236
|
+
```bash
|
|
1237
|
+
npm ci && npm run build && npm test
|
|
1238
|
+
npm publish # prepare builds again; publishConfig.access makes it public
|
|
1239
|
+
```
|
|
1240
|
+
|
|
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
|
|
1249
|
+
`npm stage approve <stage-id>`) with 2FA. Only then can apps install it.
|
|
1250
|
+
|
|
1251
|
+
Versions published by the workflow carry an npm provenance statement (the repository is public), which links each
|
|
1252
|
+
version to the commit and workflow run that built it. The first version, published by hand, has none.
|
|
1253
|
+
|
|
1254
|
+
## API
|
|
1255
|
+
|
|
1256
|
+
<docgen-index>
|
|
1257
|
+
|
|
1258
|
+
* [`ready(...)`](#ready)
|
|
1259
|
+
* [`setConfig(...)`](#setconfig)
|
|
1260
|
+
* [`reset(...)`](#reset)
|
|
1261
|
+
* [`getState()`](#getstate)
|
|
1262
|
+
* [`start()`](#start)
|
|
1263
|
+
* [`startGeofences()`](#startgeofences)
|
|
1264
|
+
* [`stop()`](#stop)
|
|
1265
|
+
* [`changePace(...)`](#changepace)
|
|
1266
|
+
* [`getCurrentPosition(...)`](#getcurrentposition)
|
|
1267
|
+
* [`watchPosition(...)`](#watchposition)
|
|
1268
|
+
* [`clearWatch(...)`](#clearwatch)
|
|
1269
|
+
* [`getOdometer()`](#getodometer)
|
|
1270
|
+
* [`setOdometer(...)`](#setodometer)
|
|
1271
|
+
* [`resetOdometer()`](#resetodometer)
|
|
1272
|
+
* [`getLocations(...)`](#getlocations)
|
|
1273
|
+
* [`getCount()`](#getcount)
|
|
1274
|
+
* [`insertLocation(...)`](#insertlocation)
|
|
1275
|
+
* [`destroyLocations()`](#destroylocations)
|
|
1276
|
+
* [`destroyLocation(...)`](#destroylocation)
|
|
1277
|
+
* [`sync()`](#sync)
|
|
1278
|
+
* [`addGeofence(...)`](#addgeofence)
|
|
1279
|
+
* [`addGeofences(...)`](#addgeofences)
|
|
1280
|
+
* [`removeGeofence(...)`](#removegeofence)
|
|
1281
|
+
* [`removeGeofences(...)`](#removegeofences)
|
|
1282
|
+
* [`getGeofences()`](#getgeofences)
|
|
1283
|
+
* [`getGeofence(...)`](#getgeofence)
|
|
1284
|
+
* [`geofenceExists(...)`](#geofenceexists)
|
|
1285
|
+
* [`getHeartbeatStatus()`](#getheartbeatstatus)
|
|
1286
|
+
* [`getProviderState()`](#getproviderstate)
|
|
1287
|
+
* [`isPowerSaveMode()`](#ispowersavemode)
|
|
1288
|
+
* [`getBatteryOptimizationStatus()`](#getbatteryoptimizationstatus)
|
|
1289
|
+
* [`openBatteryOptimizationSettings()`](#openbatteryoptimizationsettings)
|
|
1290
|
+
* [`getPowerManagerInfo()`](#getpowermanagerinfo)
|
|
1291
|
+
* [`openPowerManagerSettings()`](#openpowermanagersettings)
|
|
1292
|
+
* [`openLocationSettings()`](#openlocationsettings)
|
|
1293
|
+
* [`openAppSettings()`](#openappsettings)
|
|
1294
|
+
* [`getDeviceInfo()`](#getdeviceinfo)
|
|
1295
|
+
* [`getSensors()`](#getsensors)
|
|
1296
|
+
* [`checkPermissions()`](#checkpermissions)
|
|
1297
|
+
* [`requestPermissions(...)`](#requestpermissions)
|
|
1298
|
+
* [`log(...)`](#log)
|
|
1299
|
+
* [`getLog(...)`](#getlog)
|
|
1300
|
+
* [`destroyLog()`](#destroylog)
|
|
1301
|
+
* [`uploadLog(...)`](#uploadlog)
|
|
1302
|
+
* [`emailLog(...)`](#emaillog)
|
|
1303
|
+
* [`addListener('location', ...)`](#addlistenerlocation-)
|
|
1304
|
+
* [`addListener('motionchange', ...)`](#addlistenermotionchange-)
|
|
1305
|
+
* [`addListener('activitychange', ...)`](#addlisteneractivitychange-)
|
|
1306
|
+
* [`addListener('providerchange', ...)`](#addlistenerproviderchange-)
|
|
1307
|
+
* [`addListener('heartbeat', ...)`](#addlistenerheartbeat-)
|
|
1308
|
+
* [`addListener('geofence', ...)`](#addlistenergeofence-)
|
|
1309
|
+
* [`addListener('geofenceschange', ...)`](#addlistenergeofenceschange-)
|
|
1310
|
+
* [`addListener('http', ...)`](#addlistenerhttp-)
|
|
1311
|
+
* [`addListener('connectivitychange', ...)`](#addlistenerconnectivitychange-)
|
|
1312
|
+
* [`addListener('powersavechange', ...)`](#addlistenerpowersavechange-)
|
|
1313
|
+
* [`addListener('enabledchange', ...)`](#addlistenerenabledchange-)
|
|
1314
|
+
* [`addListener('notificationaction', ...)`](#addlistenernotificationaction-)
|
|
1315
|
+
* [`addListener('authorization', ...)`](#addlistenerauthorization-)
|
|
1316
|
+
* [`removeAllListeners()`](#removealllisteners)
|
|
1317
|
+
* [Interfaces](#interfaces)
|
|
1318
|
+
* [Type Aliases](#type-aliases)
|
|
1319
|
+
|
|
1320
|
+
</docgen-index>
|
|
1321
|
+
|
|
1322
|
+
<docgen-api>
|
|
1323
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
1324
|
+
|
|
1325
|
+
### ready(...)
|
|
1326
|
+
|
|
1327
|
+
```typescript
|
|
1328
|
+
ready(options?: ReadyOptions | undefined) => Promise<State>
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
Loads persisted config and state. Must resolve before most other methods (see NOT_READY rule).
|
|
1332
|
+
|
|
1333
|
+
| Param | Type |
|
|
1334
|
+
| ------------- | ----------------------------------------------------- |
|
|
1335
|
+
| **`options`** | <code><a href="#readyoptions">ReadyOptions</a></code> |
|
|
1336
|
+
|
|
1337
|
+
**Returns:** <code>Promise<<a href="#state">State</a>></code>
|
|
1338
|
+
|
|
1339
|
+
--------------------
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
### setConfig(...)
|
|
1343
|
+
|
|
1344
|
+
```typescript
|
|
1345
|
+
setConfig(options: { config: Config; }) => Promise<State>
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
deep merge; arrays replaced; null resets key to default
|
|
1349
|
+
|
|
1350
|
+
| Param | Type |
|
|
1351
|
+
| ------------- | ------------------------------------------------------ |
|
|
1352
|
+
| **`options`** | <code>{ config: <a href="#config">Config</a>; }</code> |
|
|
1353
|
+
|
|
1354
|
+
**Returns:** <code>Promise<<a href="#state">State</a>></code>
|
|
1355
|
+
|
|
1356
|
+
--------------------
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
### reset(...)
|
|
1360
|
+
|
|
1361
|
+
```typescript
|
|
1362
|
+
reset(options?: { config?: Config | undefined; } | undefined) => Promise<State>
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
Resets the config to defaults, then applies the given config.
|
|
1366
|
+
|
|
1367
|
+
| Param | Type |
|
|
1368
|
+
| ------------- | ------------------------------------------------------- |
|
|
1369
|
+
| **`options`** | <code>{ config?: <a href="#config">Config</a>; }</code> |
|
|
1370
|
+
|
|
1371
|
+
**Returns:** <code>Promise<<a href="#state">State</a>></code>
|
|
1372
|
+
|
|
1373
|
+
--------------------
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
### getState()
|
|
1377
|
+
|
|
1378
|
+
```typescript
|
|
1379
|
+
getState() => Promise<State>
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
**Returns:** <code>Promise<<a href="#state">State</a>></code>
|
|
1383
|
+
|
|
1384
|
+
--------------------
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
### start()
|
|
1388
|
+
|
|
1389
|
+
```typescript
|
|
1390
|
+
start() => Promise<State>
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
Starts location tracking (mode 'location').
|
|
1394
|
+
|
|
1395
|
+
**Returns:** <code>Promise<<a href="#state">State</a>></code>
|
|
1396
|
+
|
|
1397
|
+
--------------------
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
### startGeofences()
|
|
1401
|
+
|
|
1402
|
+
```typescript
|
|
1403
|
+
startGeofences() => Promise<State>
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
Starts geofence-only tracking (mode 'geofences').
|
|
1407
|
+
|
|
1408
|
+
**Returns:** <code>Promise<<a href="#state">State</a>></code>
|
|
1409
|
+
|
|
1410
|
+
--------------------
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
### stop()
|
|
1414
|
+
|
|
1415
|
+
```typescript
|
|
1416
|
+
stop() => Promise<State>
|
|
1417
|
+
```
|
|
1418
|
+
|
|
1419
|
+
**Returns:** <code>Promise<<a href="#state">State</a>></code>
|
|
1420
|
+
|
|
1421
|
+
--------------------
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
### changePace(...)
|
|
1425
|
+
|
|
1426
|
+
```typescript
|
|
1427
|
+
changePace(options: { isMoving: boolean; }) => Promise<void>
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
Forces the motion state to moving or stationary.
|
|
1431
|
+
|
|
1432
|
+
| Param | Type |
|
|
1433
|
+
| ------------- | ----------------------------------- |
|
|
1434
|
+
| **`options`** | <code>{ isMoving: boolean; }</code> |
|
|
1435
|
+
|
|
1436
|
+
--------------------
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
### getCurrentPosition(...)
|
|
1440
|
+
|
|
1441
|
+
```typescript
|
|
1442
|
+
getCurrentPosition(options?: CurrentPositionOptions | undefined) => Promise<Location>
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
| Param | Type |
|
|
1446
|
+
| ------------- | ------------------------------------------------------------------------- |
|
|
1447
|
+
| **`options`** | <code><a href="#currentpositionoptions">CurrentPositionOptions</a></code> |
|
|
1448
|
+
|
|
1449
|
+
**Returns:** <code>Promise<<a href="#location">Location</a>></code>
|
|
1450
|
+
|
|
1451
|
+
--------------------
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
### watchPosition(...)
|
|
1455
|
+
|
|
1456
|
+
```typescript
|
|
1457
|
+
watchPosition(options: WatchPositionOptions, callback: WatchPositionCallback) => Promise<CallbackID>
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
| Param | Type |
|
|
1461
|
+
| -------------- | ----------------------------------------------------------------------- |
|
|
1462
|
+
| **`options`** | <code><a href="#watchpositionoptions">WatchPositionOptions</a></code> |
|
|
1463
|
+
| **`callback`** | <code><a href="#watchpositioncallback">WatchPositionCallback</a></code> |
|
|
1464
|
+
|
|
1465
|
+
**Returns:** <code>Promise<string></code>
|
|
1466
|
+
|
|
1467
|
+
--------------------
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
### clearWatch(...)
|
|
1471
|
+
|
|
1472
|
+
```typescript
|
|
1473
|
+
clearWatch(options: { id: CallbackID; }) => Promise<void>
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
| Param | Type |
|
|
1477
|
+
| ------------- | ---------------------------- |
|
|
1478
|
+
| **`options`** | <code>{ id: string; }</code> |
|
|
1479
|
+
|
|
1480
|
+
--------------------
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
### getOdometer()
|
|
1484
|
+
|
|
1485
|
+
```typescript
|
|
1486
|
+
getOdometer() => Promise<{ odometer: number; }>
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
**Returns:** <code>Promise<{ odometer: number; }></code>
|
|
1490
|
+
|
|
1491
|
+
--------------------
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
### setOdometer(...)
|
|
1495
|
+
|
|
1496
|
+
```typescript
|
|
1497
|
+
setOdometer(options: { odometer: number; }) => Promise<{ odometer: number; }>
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
| Param | Type |
|
|
1501
|
+
| ------------- | ---------------------------------- |
|
|
1502
|
+
| **`options`** | <code>{ odometer: number; }</code> |
|
|
1503
|
+
|
|
1504
|
+
**Returns:** <code>Promise<{ odometer: number; }></code>
|
|
1505
|
+
|
|
1506
|
+
--------------------
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
### resetOdometer()
|
|
1510
|
+
|
|
1511
|
+
```typescript
|
|
1512
|
+
resetOdometer() => Promise<{ odometer: number; }>
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
**Returns:** <code>Promise<{ odometer: number; }></code>
|
|
1516
|
+
|
|
1517
|
+
--------------------
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
### getLocations(...)
|
|
1521
|
+
|
|
1522
|
+
```typescript
|
|
1523
|
+
getLocations(options?: { limit?: number | undefined; } | undefined) => Promise<{ locations: Location[]; }>
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
| Param | Type |
|
|
1527
|
+
| ------------- | -------------------------------- |
|
|
1528
|
+
| **`options`** | <code>{ limit?: number; }</code> |
|
|
1529
|
+
|
|
1530
|
+
**Returns:** <code>Promise<{ locations: Location[]; }></code>
|
|
1531
|
+
|
|
1532
|
+
--------------------
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
### getCount()
|
|
1536
|
+
|
|
1537
|
+
```typescript
|
|
1538
|
+
getCount() => Promise<{ count: number; }>
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
**Returns:** <code>Promise<{ count: number; }></code>
|
|
1542
|
+
|
|
1543
|
+
--------------------
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
### insertLocation(...)
|
|
1547
|
+
|
|
1548
|
+
```typescript
|
|
1549
|
+
insertLocation(options: { location: InsertLocationInput; }) => Promise<{ uuid: string; }>
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
| Param | Type |
|
|
1553
|
+
| ------------- | ---------------------------------------------------------------------------------- |
|
|
1554
|
+
| **`options`** | <code>{ location: <a href="#insertlocationinput">InsertLocationInput</a>; }</code> |
|
|
1555
|
+
|
|
1556
|
+
**Returns:** <code>Promise<{ uuid: string; }></code>
|
|
1557
|
+
|
|
1558
|
+
--------------------
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
### destroyLocations()
|
|
1562
|
+
|
|
1563
|
+
```typescript
|
|
1564
|
+
destroyLocations() => Promise<{ count: number; }>
|
|
1565
|
+
```
|
|
1566
|
+
|
|
1567
|
+
**Returns:** <code>Promise<{ count: number; }></code>
|
|
1568
|
+
|
|
1569
|
+
--------------------
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
### destroyLocation(...)
|
|
1573
|
+
|
|
1574
|
+
```typescript
|
|
1575
|
+
destroyLocation(options: { uuid: string; }) => Promise<{ deleted: boolean; }>
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
| Param | Type |
|
|
1579
|
+
| ------------- | ------------------------------ |
|
|
1580
|
+
| **`options`** | <code>{ uuid: string; }</code> |
|
|
1581
|
+
|
|
1582
|
+
**Returns:** <code>Promise<{ deleted: boolean; }></code>
|
|
1583
|
+
|
|
1584
|
+
--------------------
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
### sync()
|
|
1588
|
+
|
|
1589
|
+
```typescript
|
|
1590
|
+
sync() => Promise<{ locations: Location[]; }>
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
uploads whole queue now; resolves with uploaded records
|
|
1594
|
+
|
|
1595
|
+
**Returns:** <code>Promise<{ locations: Location[]; }></code>
|
|
1596
|
+
|
|
1597
|
+
--------------------
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
### addGeofence(...)
|
|
1601
|
+
|
|
1602
|
+
```typescript
|
|
1603
|
+
addGeofence(options: { geofence: Geofence; }) => Promise<void>
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
| Param | Type |
|
|
1607
|
+
| ------------- | ------------------------------------------------------------ |
|
|
1608
|
+
| **`options`** | <code>{ geofence: <a href="#geofence">Geofence</a>; }</code> |
|
|
1609
|
+
|
|
1610
|
+
--------------------
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
### addGeofences(...)
|
|
1614
|
+
|
|
1615
|
+
```typescript
|
|
1616
|
+
addGeofences(options: { geofences: Geofence[]; }) => Promise<void>
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
| Param | Type |
|
|
1620
|
+
| ------------- | --------------------------------------- |
|
|
1621
|
+
| **`options`** | <code>{ geofences: Geofence[]; }</code> |
|
|
1622
|
+
|
|
1623
|
+
--------------------
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
### removeGeofence(...)
|
|
1627
|
+
|
|
1628
|
+
```typescript
|
|
1629
|
+
removeGeofence(options: { identifier: string; }) => Promise<void>
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
| Param | Type |
|
|
1633
|
+
| ------------- | ------------------------------------ |
|
|
1634
|
+
| **`options`** | <code>{ identifier: string; }</code> |
|
|
1635
|
+
|
|
1636
|
+
--------------------
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
### removeGeofences(...)
|
|
1640
|
+
|
|
1641
|
+
```typescript
|
|
1642
|
+
removeGeofences(options?: { identifiers?: string[] | undefined; } | undefined) => Promise<void>
|
|
1643
|
+
```
|
|
1644
|
+
|
|
1645
|
+
identifiers omitted (or null) = remove all; an empty array removes nothing
|
|
1646
|
+
|
|
1647
|
+
| Param | Type |
|
|
1648
|
+
| ------------- | ---------------------------------------- |
|
|
1649
|
+
| **`options`** | <code>{ identifiers?: string[]; }</code> |
|
|
1650
|
+
|
|
1651
|
+
--------------------
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
### getGeofences()
|
|
1655
|
+
|
|
1656
|
+
```typescript
|
|
1657
|
+
getGeofences() => Promise<{ geofences: Geofence[]; }>
|
|
1658
|
+
```
|
|
1659
|
+
|
|
1660
|
+
**Returns:** <code>Promise<{ geofences: Geofence[]; }></code>
|
|
1661
|
+
|
|
1662
|
+
--------------------
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
### getGeofence(...)
|
|
1666
|
+
|
|
1667
|
+
```typescript
|
|
1668
|
+
getGeofence(options: { identifier: string; }) => Promise<{ geofence: Geofence | null; }>
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
| Param | Type |
|
|
1672
|
+
| ------------- | ------------------------------------ |
|
|
1673
|
+
| **`options`** | <code>{ identifier: string; }</code> |
|
|
1674
|
+
|
|
1675
|
+
**Returns:** <code>Promise<{ geofence: <a href="#geofence">Geofence</a> | null; }></code>
|
|
1676
|
+
|
|
1677
|
+
--------------------
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
### geofenceExists(...)
|
|
1681
|
+
|
|
1682
|
+
```typescript
|
|
1683
|
+
geofenceExists(options: { identifier: string; }) => Promise<{ exists: boolean; }>
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
| Param | Type |
|
|
1687
|
+
| ------------- | ------------------------------------ |
|
|
1688
|
+
| **`options`** | <code>{ identifier: string; }</code> |
|
|
1689
|
+
|
|
1690
|
+
**Returns:** <code>Promise<{ exists: boolean; }></code>
|
|
1691
|
+
|
|
1692
|
+
--------------------
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
### getHeartbeatStatus()
|
|
1696
|
+
|
|
1697
|
+
```typescript
|
|
1698
|
+
getHeartbeatStatus() => Promise<HeartbeatStatus>
|
|
1699
|
+
```
|
|
1700
|
+
|
|
1701
|
+
**Returns:** <code>Promise<<a href="#heartbeatstatus">HeartbeatStatus</a>></code>
|
|
1702
|
+
|
|
1703
|
+
--------------------
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
### getProviderState()
|
|
1707
|
+
|
|
1708
|
+
```typescript
|
|
1709
|
+
getProviderState() => Promise<ProviderState>
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
**Returns:** <code>Promise<<a href="#providerstate">ProviderState</a>></code>
|
|
1713
|
+
|
|
1714
|
+
--------------------
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
### isPowerSaveMode()
|
|
1718
|
+
|
|
1719
|
+
```typescript
|
|
1720
|
+
isPowerSaveMode() => Promise<{ isPowerSaveMode: boolean; }>
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
**Returns:** <code>Promise<{ isPowerSaveMode: boolean; }></code>
|
|
1724
|
+
|
|
1725
|
+
--------------------
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
### getBatteryOptimizationStatus()
|
|
1729
|
+
|
|
1730
|
+
```typescript
|
|
1731
|
+
getBatteryOptimizationStatus() => Promise<BatteryOptimizationStatus>
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
**Returns:** <code>Promise<<a href="#batteryoptimizationstatus">BatteryOptimizationStatus</a>></code>
|
|
1735
|
+
|
|
1736
|
+
--------------------
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
### openBatteryOptimizationSettings()
|
|
1740
|
+
|
|
1741
|
+
```typescript
|
|
1742
|
+
openBatteryOptimizationSettings() => Promise<{ opened: boolean; }>
|
|
1743
|
+
```
|
|
1744
|
+
|
|
1745
|
+
**Returns:** <code>Promise<{ opened: boolean; }></code>
|
|
1746
|
+
|
|
1747
|
+
--------------------
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
### getPowerManagerInfo()
|
|
1751
|
+
|
|
1752
|
+
```typescript
|
|
1753
|
+
getPowerManagerInfo() => Promise<PowerManagerInfo>
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
**Returns:** <code>Promise<<a href="#powermanagerinfo">PowerManagerInfo</a>></code>
|
|
1757
|
+
|
|
1758
|
+
--------------------
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
### openPowerManagerSettings()
|
|
1762
|
+
|
|
1763
|
+
```typescript
|
|
1764
|
+
openPowerManagerSettings() => Promise<{ opened: boolean; }>
|
|
1765
|
+
```
|
|
1766
|
+
|
|
1767
|
+
**Returns:** <code>Promise<{ opened: boolean; }></code>
|
|
1768
|
+
|
|
1769
|
+
--------------------
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
### openLocationSettings()
|
|
1773
|
+
|
|
1774
|
+
```typescript
|
|
1775
|
+
openLocationSettings() => Promise<{ opened: boolean; }>
|
|
1776
|
+
```
|
|
1777
|
+
|
|
1778
|
+
**Returns:** <code>Promise<{ opened: boolean; }></code>
|
|
1779
|
+
|
|
1780
|
+
--------------------
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
### openAppSettings()
|
|
1784
|
+
|
|
1785
|
+
```typescript
|
|
1786
|
+
openAppSettings() => Promise<{ opened: boolean; }>
|
|
1787
|
+
```
|
|
1788
|
+
|
|
1789
|
+
**Returns:** <code>Promise<{ opened: boolean; }></code>
|
|
1790
|
+
|
|
1791
|
+
--------------------
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
### getDeviceInfo()
|
|
1795
|
+
|
|
1796
|
+
```typescript
|
|
1797
|
+
getDeviceInfo() => Promise<DeviceInfo>
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
**Returns:** <code>Promise<<a href="#deviceinfo">DeviceInfo</a>></code>
|
|
1801
|
+
|
|
1802
|
+
--------------------
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
### getSensors()
|
|
1806
|
+
|
|
1807
|
+
```typescript
|
|
1808
|
+
getSensors() => Promise<Sensors>
|
|
1809
|
+
```
|
|
1810
|
+
|
|
1811
|
+
**Returns:** <code>Promise<<a href="#sensors">Sensors</a>></code>
|
|
1812
|
+
|
|
1813
|
+
--------------------
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
### checkPermissions()
|
|
1817
|
+
|
|
1818
|
+
```typescript
|
|
1819
|
+
checkPermissions() => Promise<PermissionStatus>
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1822
|
+
**Returns:** <code>Promise<<a href="#permissionstatus">PermissionStatus</a>></code>
|
|
1823
|
+
|
|
1824
|
+
--------------------
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
### requestPermissions(...)
|
|
1828
|
+
|
|
1829
|
+
```typescript
|
|
1830
|
+
requestPermissions(options?: { permissions?: PermissionType[] | undefined; } | undefined) => Promise<PermissionStatus>
|
|
1831
|
+
```
|
|
1832
|
+
|
|
1833
|
+
default all four, order: location, notifications, activityRecognition, backgroundLocation
|
|
1834
|
+
|
|
1835
|
+
| Param | Type |
|
|
1836
|
+
| ------------- | ------------------------------------------------ |
|
|
1837
|
+
| **`options`** | <code>{ permissions?: PermissionType[]; }</code> |
|
|
1838
|
+
|
|
1839
|
+
**Returns:** <code>Promise<<a href="#permissionstatus">PermissionStatus</a>></code>
|
|
1840
|
+
|
|
1841
|
+
--------------------
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
### log(...)
|
|
1845
|
+
|
|
1846
|
+
```typescript
|
|
1847
|
+
log(options: { level: Exclude<LogLevel, 'off'>; message: string; }) => Promise<void>
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
| Param | Type |
|
|
1851
|
+
| ------------- | --------------------------------------------------------------------------------------------- |
|
|
1852
|
+
| **`options`** | <code>{ level: 'error' \| 'warn' \| 'info' \| 'debug' \| 'verbose'; message: string; }</code> |
|
|
1853
|
+
|
|
1854
|
+
--------------------
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
### getLog(...)
|
|
1858
|
+
|
|
1859
|
+
```typescript
|
|
1860
|
+
getLog(options?: LogQuery | undefined) => Promise<{ log: string; }>
|
|
1861
|
+
```
|
|
1862
|
+
|
|
1863
|
+
| Param | Type |
|
|
1864
|
+
| ------------- | --------------------------------------------- |
|
|
1865
|
+
| **`options`** | <code><a href="#logquery">LogQuery</a></code> |
|
|
1866
|
+
|
|
1867
|
+
**Returns:** <code>Promise<{ log: string; }></code>
|
|
1868
|
+
|
|
1869
|
+
--------------------
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
### destroyLog()
|
|
1873
|
+
|
|
1874
|
+
```typescript
|
|
1875
|
+
destroyLog() => Promise<void>
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
--------------------
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
### uploadLog(...)
|
|
1882
|
+
|
|
1883
|
+
```typescript
|
|
1884
|
+
uploadLog(options: { url: string; headers?: Record<string, string>; params?: Record<string, unknown>; }) => Promise<{ success: boolean; status: number; }>
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
| Param | Type |
|
|
1888
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1889
|
+
| **`options`** | <code>{ url: string; headers?: <a href="#record">Record</a><string, string>; params?: <a href="#record">Record</a><string, unknown>; }</code> |
|
|
1890
|
+
|
|
1891
|
+
**Returns:** <code>Promise<{ success: boolean; status: number; }></code>
|
|
1892
|
+
|
|
1893
|
+
--------------------
|
|
1894
|
+
|
|
1895
|
+
|
|
1896
|
+
### emailLog(...)
|
|
1897
|
+
|
|
1898
|
+
```typescript
|
|
1899
|
+
emailLog(options: { email: string; subject?: string; }) => Promise<void>
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
| Param | Type |
|
|
1903
|
+
| ------------- | ------------------------------------------------- |
|
|
1904
|
+
| **`options`** | <code>{ email: string; subject?: string; }</code> |
|
|
1905
|
+
|
|
1906
|
+
--------------------
|
|
1907
|
+
|
|
1908
|
+
|
|
1909
|
+
### addListener('location', ...)
|
|
1910
|
+
|
|
1911
|
+
```typescript
|
|
1912
|
+
addListener(eventName: 'location', listenerFunc: (e: Location) => void) => Promise<PluginListenerHandle>
|
|
1913
|
+
```
|
|
1914
|
+
|
|
1915
|
+
| Param | Type |
|
|
1916
|
+
| ------------------ | ------------------------------------------------------------- |
|
|
1917
|
+
| **`eventName`** | <code>'location'</code> |
|
|
1918
|
+
| **`listenerFunc`** | <code>(e: <a href="#location">Location</a>) => void</code> |
|
|
1919
|
+
|
|
1920
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
1921
|
+
|
|
1922
|
+
--------------------
|
|
1923
|
+
|
|
1924
|
+
|
|
1925
|
+
### addListener('motionchange', ...)
|
|
1926
|
+
|
|
1927
|
+
```typescript
|
|
1928
|
+
addListener(eventName: 'motionchange', listenerFunc: (e: MotionChangeEvent) => void) => Promise<PluginListenerHandle>
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
| Param | Type |
|
|
1932
|
+
| ------------------ | ------------------------------------------------------------------------------- |
|
|
1933
|
+
| **`eventName`** | <code>'motionchange'</code> |
|
|
1934
|
+
| **`listenerFunc`** | <code>(e: <a href="#motionchangeevent">MotionChangeEvent</a>) => void</code> |
|
|
1935
|
+
|
|
1936
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
1937
|
+
|
|
1938
|
+
--------------------
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
### addListener('activitychange', ...)
|
|
1942
|
+
|
|
1943
|
+
```typescript
|
|
1944
|
+
addListener(eventName: 'activitychange', listenerFunc: (e: ActivityChangeEvent) => void) => Promise<PluginListenerHandle>
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
| Param | Type |
|
|
1948
|
+
| ------------------ | ----------------------------------------------------------------------------------- |
|
|
1949
|
+
| **`eventName`** | <code>'activitychange'</code> |
|
|
1950
|
+
| **`listenerFunc`** | <code>(e: <a href="#activitychangeevent">ActivityChangeEvent</a>) => void</code> |
|
|
1951
|
+
|
|
1952
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
1953
|
+
|
|
1954
|
+
--------------------
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
### addListener('providerchange', ...)
|
|
1958
|
+
|
|
1959
|
+
```typescript
|
|
1960
|
+
addListener(eventName: 'providerchange', listenerFunc: (e: ProviderState) => void) => Promise<PluginListenerHandle>
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
| Param | Type |
|
|
1964
|
+
| ------------------ | ----------------------------------------------------------------------- |
|
|
1965
|
+
| **`eventName`** | <code>'providerchange'</code> |
|
|
1966
|
+
| **`listenerFunc`** | <code>(e: <a href="#providerstate">ProviderState</a>) => void</code> |
|
|
1967
|
+
|
|
1968
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
1969
|
+
|
|
1970
|
+
--------------------
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
### addListener('heartbeat', ...)
|
|
1974
|
+
|
|
1975
|
+
```typescript
|
|
1976
|
+
addListener(eventName: 'heartbeat', listenerFunc: (e: HeartbeatEvent) => void) => Promise<PluginListenerHandle>
|
|
1977
|
+
```
|
|
1978
|
+
|
|
1979
|
+
| Param | Type |
|
|
1980
|
+
| ------------------ | ------------------------------------------------------------------------- |
|
|
1981
|
+
| **`eventName`** | <code>'heartbeat'</code> |
|
|
1982
|
+
| **`listenerFunc`** | <code>(e: <a href="#heartbeatevent">HeartbeatEvent</a>) => void</code> |
|
|
1983
|
+
|
|
1984
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
1985
|
+
|
|
1986
|
+
--------------------
|
|
1987
|
+
|
|
1988
|
+
|
|
1989
|
+
### addListener('geofence', ...)
|
|
1990
|
+
|
|
1991
|
+
```typescript
|
|
1992
|
+
addListener(eventName: 'geofence', listenerFunc: (e: GeofenceEvent) => void) => Promise<PluginListenerHandle>
|
|
1993
|
+
```
|
|
1994
|
+
|
|
1995
|
+
| Param | Type |
|
|
1996
|
+
| ------------------ | ----------------------------------------------------------------------- |
|
|
1997
|
+
| **`eventName`** | <code>'geofence'</code> |
|
|
1998
|
+
| **`listenerFunc`** | <code>(e: <a href="#geofenceevent">GeofenceEvent</a>) => void</code> |
|
|
1999
|
+
|
|
2000
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2001
|
+
|
|
2002
|
+
--------------------
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
### addListener('geofenceschange', ...)
|
|
2006
|
+
|
|
2007
|
+
```typescript
|
|
2008
|
+
addListener(eventName: 'geofenceschange', listenerFunc: (e: GeofencesChangeEvent) => void) => Promise<PluginListenerHandle>
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
| Param | Type |
|
|
2012
|
+
| ------------------ | ------------------------------------------------------------------------------------- |
|
|
2013
|
+
| **`eventName`** | <code>'geofenceschange'</code> |
|
|
2014
|
+
| **`listenerFunc`** | <code>(e: <a href="#geofenceschangeevent">GeofencesChangeEvent</a>) => void</code> |
|
|
2015
|
+
|
|
2016
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2017
|
+
|
|
2018
|
+
--------------------
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
### addListener('http', ...)
|
|
2022
|
+
|
|
2023
|
+
```typescript
|
|
2024
|
+
addListener(eventName: 'http', listenerFunc: (e: HttpEvent) => void) => Promise<PluginListenerHandle>
|
|
2025
|
+
```
|
|
2026
|
+
|
|
2027
|
+
| Param | Type |
|
|
2028
|
+
| ------------------ | --------------------------------------------------------------- |
|
|
2029
|
+
| **`eventName`** | <code>'http'</code> |
|
|
2030
|
+
| **`listenerFunc`** | <code>(e: <a href="#httpevent">HttpEvent</a>) => void</code> |
|
|
2031
|
+
|
|
2032
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2033
|
+
|
|
2034
|
+
--------------------
|
|
2035
|
+
|
|
2036
|
+
|
|
2037
|
+
### addListener('connectivitychange', ...)
|
|
2038
|
+
|
|
2039
|
+
```typescript
|
|
2040
|
+
addListener(eventName: 'connectivitychange', listenerFunc: (e: ConnectivityChangeEvent) => void) => Promise<PluginListenerHandle>
|
|
2041
|
+
```
|
|
2042
|
+
|
|
2043
|
+
| Param | Type |
|
|
2044
|
+
| ------------------ | ------------------------------------------------------------------------------------------- |
|
|
2045
|
+
| **`eventName`** | <code>'connectivitychange'</code> |
|
|
2046
|
+
| **`listenerFunc`** | <code>(e: <a href="#connectivitychangeevent">ConnectivityChangeEvent</a>) => void</code> |
|
|
2047
|
+
|
|
2048
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2049
|
+
|
|
2050
|
+
--------------------
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
### addListener('powersavechange', ...)
|
|
2054
|
+
|
|
2055
|
+
```typescript
|
|
2056
|
+
addListener(eventName: 'powersavechange', listenerFunc: (e: PowerSaveChangeEvent) => void) => Promise<PluginListenerHandle>
|
|
2057
|
+
```
|
|
2058
|
+
|
|
2059
|
+
| Param | Type |
|
|
2060
|
+
| ------------------ | ------------------------------------------------------------------------------------- |
|
|
2061
|
+
| **`eventName`** | <code>'powersavechange'</code> |
|
|
2062
|
+
| **`listenerFunc`** | <code>(e: <a href="#powersavechangeevent">PowerSaveChangeEvent</a>) => void</code> |
|
|
2063
|
+
|
|
2064
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2065
|
+
|
|
2066
|
+
--------------------
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
### addListener('enabledchange', ...)
|
|
2070
|
+
|
|
2071
|
+
```typescript
|
|
2072
|
+
addListener(eventName: 'enabledchange', listenerFunc: (e: EnabledChangeEvent) => void) => Promise<PluginListenerHandle>
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
| Param | Type |
|
|
2076
|
+
| ------------------ | --------------------------------------------------------------------------------- |
|
|
2077
|
+
| **`eventName`** | <code>'enabledchange'</code> |
|
|
2078
|
+
| **`listenerFunc`** | <code>(e: <a href="#enabledchangeevent">EnabledChangeEvent</a>) => void</code> |
|
|
2079
|
+
|
|
2080
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2081
|
+
|
|
2082
|
+
--------------------
|
|
2083
|
+
|
|
2084
|
+
|
|
2085
|
+
### addListener('notificationaction', ...)
|
|
2086
|
+
|
|
2087
|
+
```typescript
|
|
2088
|
+
addListener(eventName: 'notificationaction', listenerFunc: (e: NotificationActionEvent) => void) => Promise<PluginListenerHandle>
|
|
2089
|
+
```
|
|
2090
|
+
|
|
2091
|
+
| Param | Type |
|
|
2092
|
+
| ------------------ | ------------------------------------------------------------------------------------------- |
|
|
2093
|
+
| **`eventName`** | <code>'notificationaction'</code> |
|
|
2094
|
+
| **`listenerFunc`** | <code>(e: <a href="#notificationactionevent">NotificationActionEvent</a>) => void</code> |
|
|
2095
|
+
|
|
2096
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2097
|
+
|
|
2098
|
+
--------------------
|
|
2099
|
+
|
|
2100
|
+
|
|
2101
|
+
### addListener('authorization', ...)
|
|
2102
|
+
|
|
2103
|
+
```typescript
|
|
2104
|
+
addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) => void) => Promise<PluginListenerHandle>
|
|
2105
|
+
```
|
|
2106
|
+
|
|
2107
|
+
| Param | Type |
|
|
2108
|
+
| ------------------ | --------------------------------------------------------------------------------- |
|
|
2109
|
+
| **`eventName`** | <code>'authorization'</code> |
|
|
2110
|
+
| **`listenerFunc`** | <code>(e: <a href="#authorizationevent">AuthorizationEvent</a>) => void</code> |
|
|
2111
|
+
|
|
2112
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
2113
|
+
|
|
2114
|
+
--------------------
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
### removeAllListeners()
|
|
2118
|
+
|
|
2119
|
+
```typescript
|
|
2120
|
+
removeAllListeners() => Promise<void>
|
|
2121
|
+
```
|
|
2122
|
+
|
|
2123
|
+
--------------------
|
|
2124
|
+
|
|
2125
|
+
|
|
2126
|
+
### Interfaces
|
|
2127
|
+
|
|
2128
|
+
|
|
2129
|
+
#### State
|
|
2130
|
+
|
|
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 |
|
|
2140
|
+
|
|
2141
|
+
|
|
2142
|
+
#### Config
|
|
2143
|
+
|
|
2144
|
+
| Prop | Type | Description | Default |
|
|
2145
|
+
| ----------------------------------- | --------------------------------------------------------------------------------------- | ------------ | ------------------- |
|
|
2146
|
+
| **`geolocation`** | <code><a href="#geolocationconfig">GeolocationConfig</a></code> | | |
|
|
2147
|
+
| **`activity`** | <code><a href="#activityconfig">ActivityConfig</a></code> | | |
|
|
2148
|
+
| **`heartbeat`** | <code><a href="#heartbeatconfig">HeartbeatConfig</a></code> | | |
|
|
2149
|
+
| **`http`** | <code><a href="#httpconfig">HttpConfig</a></code> | | |
|
|
2150
|
+
| **`persistence`** | <code><a href="#persistenceconfig">PersistenceConfig</a></code> | | |
|
|
2151
|
+
| **`app`** | <code><a href="#appconfig">AppConfig</a></code> | | |
|
|
2152
|
+
| **`notification`** | <code><a href="#notificationconfig">NotificationConfig</a></code> | | |
|
|
2153
|
+
| **`geofence`** | <code><a href="#geofenceconfig">GeofenceConfig</a></code> | | |
|
|
2154
|
+
| **`logger`** | <code><a href="#loggerconfig">LoggerConfig</a></code> | | |
|
|
2155
|
+
| **`backgroundPermissionRationale`** | <code><a href="#backgroundpermissionrationale">BackgroundPermissionRationale</a></code> | | |
|
|
2156
|
+
| **`locationProvider`** | <code><a href="#locationprovidersetting">LocationProviderSetting</a></code> | Android only | <code>'auto'</code> |
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
#### GeolocationConfig
|
|
2160
|
+
|
|
2161
|
+
| Prop | Type | Description | Default |
|
|
2162
|
+
| ----------------------------------- | --------------------------------------------------------------------- | -------------------------------------- | ------------------- |
|
|
2163
|
+
| **`desiredAccuracy`** | <code><a href="#desiredaccuracy">DesiredAccuracy</a></code> | | <code>'high'</code> |
|
|
2164
|
+
| **`distanceFilter`** | <code>number</code> | meters | <code>10</code> |
|
|
2165
|
+
| **`locationUpdateInterval`** | <code>number</code> | ms | <code>1000</code> |
|
|
2166
|
+
| **`fastestLocationUpdateInterval`** | <code>number</code> | ms | <code>500</code> |
|
|
2167
|
+
| **`disableElasticity`** | <code>boolean</code> | | <code>false</code> |
|
|
2168
|
+
| **`elasticityMultiplier`** | <code>number</code> | | <code>1</code> |
|
|
2169
|
+
| **`stationaryRadius`** | <code>number</code> | meters | <code>25</code> |
|
|
2170
|
+
| **`stopTimeout`** | <code>number</code> | minutes | <code>5</code> |
|
|
2171
|
+
| **`stopAfterElapsedMinutes`** | <code>number</code> | minutes, 0 = off | <code>0</code> |
|
|
2172
|
+
| **`stopOnStationary`** | <code>boolean</code> | | <code>false</code> |
|
|
2173
|
+
| **`locationTimeout`** | <code>number</code> | default getCurrentPosition timeout, ms | <code>30000</code> |
|
|
2174
|
+
| **`filter`** | <code><a href="#locationfilterconfig">LocationFilterConfig</a></code> | | |
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
#### LocationFilterConfig
|
|
2178
|
+
|
|
2179
|
+
| Prop | Type | Description | Default |
|
|
2180
|
+
| ------------------------------- | -------------------- | ------------------------------------------------------------------ | ------------------ |
|
|
2181
|
+
| **`useKalman`** | <code>boolean</code> | | <code>false</code> |
|
|
2182
|
+
| **`trackingAccuracyThreshold`** | <code>number</code> | Reject fixes with accuracy worse than this (m). | <code>100</code> |
|
|
2183
|
+
| **`maxImpliedSpeed`** | <code>number</code> | Reject fixes implying speed above this (m/s); 0 = off. | <code>80</code> |
|
|
2184
|
+
| **`odometerAccuracyThreshold`** | <code>number</code> | Odometer ignores fixes worse than this (m). | <code>20</code> |
|
|
2185
|
+
| **`allowIdenticalLocations`** | <code>boolean</code> | | <code>false</code> |
|
|
2186
|
+
| **`rejectMockLocations`** | <code>boolean</code> | Drop mock fixes entirely (otherwise they are kept with mock:true). | <code>false</code> |
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
#### ActivityConfig
|
|
2190
|
+
|
|
2191
|
+
| Prop | Type | Description | Default |
|
|
2192
|
+
| ------------------------------------------ | -------------------- | ----------- | ------------------ |
|
|
2193
|
+
| **`disableMotionActivityUpdates`** | <code>boolean</code> | | <code>false</code> |
|
|
2194
|
+
| **`activityRecognitionInterval`** | <code>number</code> | ms | <code>10000</code> |
|
|
2195
|
+
| **`minimumActivityRecognitionConfidence`** | <code>number</code> | 0-100 | <code>75</code> |
|
|
2196
|
+
| **`motionTriggerDelay`** | <code>number</code> | ms | <code>0</code> |
|
|
2197
|
+
| **`disableStopDetection`** | <code>boolean</code> | | <code>false</code> |
|
|
2198
|
+
|
|
2199
|
+
|
|
2200
|
+
#### HeartbeatConfig
|
|
2201
|
+
|
|
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, >= minInterval | <code>300</code> |
|
|
2207
|
+
|
|
2208
|
+
|
|
2209
|
+
#### HttpConfig
|
|
2210
|
+
|
|
2211
|
+
| Prop | Type | Description | Default |
|
|
2212
|
+
| ------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------- |
|
|
2213
|
+
| **`url`** | <code>string \| null</code> | No url => 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><string, string></code> | | <code>{}</code> |
|
|
2216
|
+
| **`params`** | <code><a href="#record">Record</a><string, unknown></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 >= 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 `<%= name %>` 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> | | |
|
|
2228
|
+
|
|
2229
|
+
|
|
2230
|
+
#### AuthorizationConfig
|
|
2231
|
+
|
|
2232
|
+
| Prop | Type | Description | Default |
|
|
2233
|
+
| ---------------------------- | --------------------------------------------------------------- | ------------------------------------------- | ------------------- |
|
|
2234
|
+
| **`strategy`** | <code>'JWT'</code> | | <code>'JWT'</code> |
|
|
2235
|
+
| **`accessToken`** | <code>string</code> | | |
|
|
2236
|
+
| **`refreshToken`** | <code>string</code> | | |
|
|
2237
|
+
| **`refreshUrl`** | <code>string</code> | | |
|
|
2238
|
+
| **`refreshPayload`** | <code><a href="#record">Record</a><string, string></code> | String values may contain `{refreshToken}`. | <code>{}</code> |
|
|
2239
|
+
| **`refreshHeaders`** | <code><a href="#record">Record</a><string, string></code> | | |
|
|
2240
|
+
| **`refreshPayloadEncoding`** | <code>'json' \| 'form'</code> | | <code>'json'</code> |
|
|
2241
|
+
| **`expires`** | <code>number</code> | access-token expiry, epoch ms; -1 = unknown | <code>-1</code> |
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
#### PersistenceConfig
|
|
2245
|
+
|
|
2246
|
+
| Prop | Type | Description | Default |
|
|
2247
|
+
| ------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | --------------- |
|
|
2248
|
+
| **`maxDaysToPersist`** | <code>number</code> | | <code>7</code> |
|
|
2249
|
+
| **`maxRecordsToPersist`** | <code>number</code> | -1 = unlimited | <code>-1</code> |
|
|
2250
|
+
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code> | merged into `extras` of every record at creation time | <code>{}</code> |
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
#### AppConfig
|
|
2254
|
+
|
|
2255
|
+
| Prop | Type | Default |
|
|
2256
|
+
| --------------------- | -------------------- | ------------------ |
|
|
2257
|
+
| **`stopOnTerminate`** | <code>boolean</code> | <code>true</code> |
|
|
2258
|
+
| **`startOnBoot`** | <code>boolean</code> | <code>false</code> |
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
#### NotificationConfig
|
|
2262
|
+
|
|
2263
|
+
| Prop | Type | Description | Default |
|
|
2264
|
+
| ----------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
2265
|
+
| **`title`** | <code>string</code> | | <code>app label</code> |
|
|
2266
|
+
| **`text`** | <code>string</code> | | <code>'Location tracking is active'</code> |
|
|
2267
|
+
| **`smallIcon`** | <code>string</code> | 'drawable/name' \| 'mipmap/name' | <code>plugin icon 'drawable/lt_ic_notification'</code> |
|
|
2268
|
+
| **`largeIcon`** | <code>string</code> | | |
|
|
2269
|
+
| **`color`** | <code>string</code> | '#RRGGBB' | |
|
|
2270
|
+
| **`priority`** | <code><a href="#notificationpriority">NotificationPriority</a></code> | | <code>'default'</code> |
|
|
2271
|
+
| **`channelId`** | <code>string</code> | | <code>'location_tracking'</code> |
|
|
2272
|
+
| **`channelName`** | <code>string</code> | | <code>'Location tracking'</code> |
|
|
2273
|
+
| **`actions`** | <code>NotificationActionButton[]</code> | max 3; tap => 'notificationaction' event | |
|
|
2274
|
+
| **`resume`** | <code><a href="#resumenotificationconfig">ResumeNotificationConfig</a></code> | notification that resumes tracking when Android refuses to restore it from the background (Android 14+) | |
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
#### NotificationActionButton
|
|
2278
|
+
|
|
2279
|
+
| Prop | Type |
|
|
2280
|
+
| ----------- | ------------------- |
|
|
2281
|
+
| **`id`** | <code>string</code> |
|
|
2282
|
+
| **`label`** | <code>string</code> |
|
|
2283
|
+
|
|
2284
|
+
|
|
2285
|
+
#### ResumeNotificationConfig
|
|
2286
|
+
|
|
2287
|
+
Posted when Android refuses to restore tracking from the background (after a reboot, an app update or a process
|
|
2288
|
+
restart), typically on Android 14+ without "Allow all the time". A tap resumes the session
|
|
2289
|
+
(`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.
|
|
2290
|
+
|
|
2291
|
+
| Prop | Type | Default |
|
|
2292
|
+
| ----------------- | -------------------- | ---------------------------------------------------------- |
|
|
2293
|
+
| **`enabled`** | <code>boolean</code> | <code>false</code> |
|
|
2294
|
+
| **`title`** | <code>string</code> | <code>app label</code> |
|
|
2295
|
+
| **`text`** | <code>string</code> | <code>'Location tracking is paused. Tap to resume.'</code> |
|
|
2296
|
+
| **`channelName`** | <code>string</code> | <code>'Paused location tracking'</code> |
|
|
2297
|
+
|
|
2298
|
+
|
|
2299
|
+
#### GeofenceConfig
|
|
2300
|
+
|
|
2301
|
+
| Prop | Type | Default |
|
|
2302
|
+
| ------------------------- | -------------------- | ----------------- |
|
|
2303
|
+
| **`initialTriggerEntry`** | <code>boolean</code> | <code>true</code> |
|
|
2304
|
+
|
|
2305
|
+
|
|
2306
|
+
#### LoggerConfig
|
|
2307
|
+
|
|
2308
|
+
| Prop | Type | Default |
|
|
2309
|
+
| ---------------- | --------------------------------------------- | ------------------- |
|
|
2310
|
+
| **`logLevel`** | <code><a href="#loglevel">LogLevel</a></code> | <code>'info'</code> |
|
|
2311
|
+
| **`logMaxDays`** | <code>number</code> | <code>3</code> |
|
|
2312
|
+
|
|
2313
|
+
|
|
2314
|
+
#### BackgroundPermissionRationale
|
|
2315
|
+
|
|
2316
|
+
| Prop | Type | Description |
|
|
2317
|
+
| -------------------- | ------------------- | ------------------------------------- |
|
|
2318
|
+
| **`title`** | <code>string</code> | defaults from plugin string resources |
|
|
2319
|
+
| **`message`** | <code>string</code> | |
|
|
2320
|
+
| **`positiveAction`** | <code>string</code> | |
|
|
2321
|
+
| **`negativeAction`** | <code>string</code> | |
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
#### ReadyOptions
|
|
2325
|
+
|
|
2326
|
+
| Prop | Type | Description | Default |
|
|
2327
|
+
| ------------ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
|
2328
|
+
| **`config`** | <code><a href="#config">Config</a></code> | | |
|
|
2329
|
+
| **`reset`** | <code>boolean</code> | true: config = defaults + given config (every launch). false: given config applied only on the very first ready(); afterwards persisted config wins. | <code>true</code> |
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
#### Location
|
|
2333
|
+
|
|
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><string, unknown></code> | |
|
|
2351
|
+
| **`geofence`** | <code>{ identifier: string; action: <a href="#geofenceaction">GeofenceAction</a>; extras?: <a href="#record">Record</a><string, unknown>; }</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 |
|
|
2355
|
+
|
|
2356
|
+
|
|
2357
|
+
#### Coords
|
|
2358
|
+
|
|
2359
|
+
| Prop | Type |
|
|
2360
|
+
| ----------------------- | --------------------------- |
|
|
2361
|
+
| **`latitude`** | <code>number</code> |
|
|
2362
|
+
| **`longitude`** | <code>number</code> |
|
|
2363
|
+
| **`accuracy`** | <code>number</code> |
|
|
2364
|
+
| **`altitude`** | <code>number \| null</code> |
|
|
2365
|
+
| **`altitude_accuracy`** | <code>number \| null</code> |
|
|
2366
|
+
| **`speed`** | <code>number \| null</code> |
|
|
2367
|
+
| **`speed_accuracy`** | <code>number \| null</code> |
|
|
2368
|
+
| **`heading`** | <code>number \| null</code> |
|
|
2369
|
+
| **`heading_accuracy`** | <code>number \| null</code> |
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
#### ProviderState
|
|
2373
|
+
|
|
2374
|
+
| Prop | Type |
|
|
2375
|
+
| ---------------- | ----------------------------------------------------------- |
|
|
2376
|
+
| **`enabled`** | <code>boolean</code> |
|
|
2377
|
+
| **`gps`** | <code>boolean</code> |
|
|
2378
|
+
| **`network`** | <code>boolean</code> |
|
|
2379
|
+
| **`permission`** | <code>'always' \| 'when_in_use' \| 'denied'</code> |
|
|
2380
|
+
| **`accuracy`** | <code>'precise' \| 'approximate' \| 'none'</code> |
|
|
2381
|
+
| **`backend`** | <code><a href="#locationbackend">LocationBackend</a></code> |
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
#### HeartbeatMeta
|
|
2385
|
+
|
|
2386
|
+
Scheduling metadata of a `heartbeat` record (snake_case = wire format).
|
|
2387
|
+
|
|
2388
|
+
| Prop | Type | Description |
|
|
2389
|
+
| -------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
2390
|
+
| **`strategy`** | <code>'exact' \| 'listener_with_backup' \| 'idle_paced'</code> | |
|
|
2391
|
+
| **`min_interval`** | <code>number</code> | heartbeat.minInterval (s) when the heartbeat was created |
|
|
2392
|
+
| **`max_interval`** | <code>number</code> | heartbeat.maxInterval (s) when the heartbeat was created |
|
|
2393
|
+
| **`next_at`** | <code>string \| null</code> | when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown |
|
|
2394
|
+
| **`battery_exempt`** | <code>boolean</code> | the app is exempt from battery optimization |
|
|
2395
|
+
| **`device_idle`** | <code>boolean</code> | the device was in deep Doze |
|
|
2396
|
+
|
|
2397
|
+
|
|
2398
|
+
#### CurrentPositionOptions
|
|
2399
|
+
|
|
2400
|
+
| Prop | Type | Description | Default |
|
|
2401
|
+
| --------------------- | ---------------------------------------------------------------- | ----------- | ---------------------------------------- |
|
|
2402
|
+
| **`samples`** | <code>number</code> | | <code>3</code> |
|
|
2403
|
+
| **`timeout`** | <code>number</code> | ms | <code>geolocation.locationTimeout</code> |
|
|
2404
|
+
| **`maximumAge`** | <code>number</code> | ms | <code>0</code> |
|
|
2405
|
+
| **`desiredAccuracy`** | <code><a href="#desiredaccuracy">DesiredAccuracy</a></code> | | <code>'high'</code> |
|
|
2406
|
+
| **`persist`** | <code>boolean</code> | | <code>true</code> |
|
|
2407
|
+
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code> | | |
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
#### WatchPositionOptions
|
|
2411
|
+
|
|
2412
|
+
| Prop | Type | Description | Default |
|
|
2413
|
+
| --------------------- | ---------------------------------------------------------------- | ----------- | ------------------- |
|
|
2414
|
+
| **`interval`** | <code>number</code> | ms | <code>1000</code> |
|
|
2415
|
+
| **`desiredAccuracy`** | <code><a href="#desiredaccuracy">DesiredAccuracy</a></code> | | <code>'high'</code> |
|
|
2416
|
+
| **`persist`** | <code>boolean</code> | | <code>false</code> |
|
|
2417
|
+
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code> | | |
|
|
2418
|
+
|
|
2419
|
+
|
|
2420
|
+
#### InsertLocationInput
|
|
2421
|
+
|
|
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> |
|
|
2429
|
+
|
|
2430
|
+
|
|
2431
|
+
#### Geofence
|
|
2432
|
+
|
|
2433
|
+
Either (latitude, longitude, radius) = circle, or vertices = polygon. For polygons, getGeofences() also returns
|
|
2434
|
+
the computed enclosing circle.
|
|
2435
|
+
|
|
2436
|
+
| Prop | Type | Description | Default |
|
|
2437
|
+
| -------------------- | ---------------------------------------------------------------- | ------------------------------- | ------------------ |
|
|
2438
|
+
| **`identifier`** | <code>string</code> | | |
|
|
2439
|
+
| **`latitude`** | <code>number</code> | | |
|
|
2440
|
+
| **`longitude`** | <code>number</code> | | |
|
|
2441
|
+
| **`radius`** | <code>number</code> | m | |
|
|
2442
|
+
| **`vertices`** | <code>[number, number][]</code> | [[lat,lng], ...] >= 3 points | |
|
|
2443
|
+
| **`notifyOnEntry`** | <code>boolean</code> | | <code>true</code> |
|
|
2444
|
+
| **`notifyOnExit`** | <code>boolean</code> | | <code>true</code> |
|
|
2445
|
+
| **`notifyOnDwell`** | <code>boolean</code> | | <code>false</code> |
|
|
2446
|
+
| **`loiteringDelay`** | <code>number</code> | ms | <code>30000</code> |
|
|
2447
|
+
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code> | | |
|
|
2448
|
+
|
|
2449
|
+
|
|
2450
|
+
#### HeartbeatStatus
|
|
2451
|
+
|
|
2452
|
+
| Prop | Type |
|
|
2453
|
+
| ------------------------------------ | --------------------------------------------------------------- |
|
|
2454
|
+
| **`enabled`** | <code>boolean</code> |
|
|
2455
|
+
| **`minInterval`** | <code>number</code> |
|
|
2456
|
+
| **`maxInterval`** | <code>number</code> |
|
|
2457
|
+
| **`lastRecordAt`** | <code>string \| null</code> |
|
|
2458
|
+
| **`lastHeartbeatAt`** | <code>string \| null</code> |
|
|
2459
|
+
| **`nextHeartbeatAt`** | <code>string \| null</code> |
|
|
2460
|
+
| **`strategy`** | <code><a href="#heartbeatstrategy">HeartbeatStrategy</a></code> |
|
|
2461
|
+
| **`canScheduleExactAlarms`** | <code>boolean</code> |
|
|
2462
|
+
| **`isIgnoringBatteryOptimizations`** | <code>boolean</code> |
|
|
2463
|
+
| **`isDeviceIdleMode`** | <code>boolean</code> |
|
|
2464
|
+
| **`isPowerSaveMode`** | <code>boolean</code> |
|
|
2465
|
+
| **`pendingHeartbeats`** | <code>number</code> |
|
|
2466
|
+
|
|
2467
|
+
|
|
2468
|
+
#### BatteryOptimizationStatus
|
|
2469
|
+
|
|
2470
|
+
| Prop | Type |
|
|
2471
|
+
| ------------------------------------ | -------------------- |
|
|
2472
|
+
| **`isIgnoringBatteryOptimizations`** | <code>boolean</code> |
|
|
2473
|
+
| **`canScheduleExactAlarms`** | <code>boolean</code> |
|
|
2474
|
+
| **`isDeviceIdleMode`** | <code>boolean</code> |
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
#### PowerManagerInfo
|
|
2478
|
+
|
|
2479
|
+
| Prop | Type |
|
|
2480
|
+
| ------------------ | -------------------- |
|
|
2481
|
+
| **`manufacturer`** | <code>string</code> |
|
|
2482
|
+
| **`available`** | <code>boolean</code> |
|
|
2483
|
+
|
|
2484
|
+
|
|
2485
|
+
#### DeviceInfo
|
|
2486
|
+
|
|
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> |
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
#### Sensors
|
|
2503
|
+
|
|
2504
|
+
| Prop | Type |
|
|
2505
|
+
| ----------------------- | -------------------- |
|
|
2506
|
+
| **`accelerometer`** | <code>boolean</code> |
|
|
2507
|
+
| **`gyroscope`** | <code>boolean</code> |
|
|
2508
|
+
| **`magnetometer`** | <code>boolean</code> |
|
|
2509
|
+
| **`significantMotion`** | <code>boolean</code> |
|
|
2510
|
+
| **`stepCounter`** | <code>boolean</code> |
|
|
2511
|
+
| **`stepDetector`** | <code>boolean</code> |
|
|
2512
|
+
| **`barometer`** | <code>boolean</code> |
|
|
2513
|
+
|
|
2514
|
+
|
|
2515
|
+
#### PermissionStatus
|
|
2516
|
+
|
|
2517
|
+
| Prop | Type |
|
|
2518
|
+
| ------------------------- | ----------------------------------------------------------- |
|
|
2519
|
+
| **`location`** | <code><a href="#permissionstate">PermissionState</a></code> |
|
|
2520
|
+
| **`backgroundLocation`** | <code><a href="#permissionstate">PermissionState</a></code> |
|
|
2521
|
+
| **`activityRecognition`** | <code><a href="#permissionstate">PermissionState</a></code> |
|
|
2522
|
+
| **`notifications`** | <code><a href="#permissionstate">PermissionState</a></code> |
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
#### LogQuery
|
|
2526
|
+
|
|
2527
|
+
| Prop | Type |
|
|
2528
|
+
| ----------- | --------------------------------------------- |
|
|
2529
|
+
| **`start`** | <code>number</code> |
|
|
2530
|
+
| **`end`** | <code>number</code> |
|
|
2531
|
+
| **`level`** | <code><a href="#loglevel">LogLevel</a></code> |
|
|
2532
|
+
| **`limit`** | <code>number</code> |
|
|
2533
|
+
| **`order`** | <code>'asc' \| 'desc'</code> |
|
|
2534
|
+
|
|
2535
|
+
|
|
2536
|
+
#### PluginListenerHandle
|
|
2537
|
+
|
|
2538
|
+
| Prop | Type |
|
|
2539
|
+
| ------------ | ----------------------------------------- |
|
|
2540
|
+
| **`remove`** | <code>() => Promise<void></code> |
|
|
2541
|
+
|
|
2542
|
+
|
|
2543
|
+
#### MotionChangeEvent
|
|
2544
|
+
|
|
2545
|
+
| Prop | Type |
|
|
2546
|
+
| -------------- | --------------------------------------------- |
|
|
2547
|
+
| **`isMoving`** | <code>boolean</code> |
|
|
2548
|
+
| **`location`** | <code><a href="#location">Location</a></code> |
|
|
2549
|
+
|
|
2550
|
+
|
|
2551
|
+
#### ActivityChangeEvent
|
|
2552
|
+
|
|
2553
|
+
| Prop | Type |
|
|
2554
|
+
| ---------------- | ----------------------------------------------------- |
|
|
2555
|
+
| **`activity`** | <code><a href="#activitytype">ActivityType</a></code> |
|
|
2556
|
+
| **`confidence`** | <code>number</code> |
|
|
2557
|
+
|
|
2558
|
+
|
|
2559
|
+
#### HeartbeatEvent
|
|
2560
|
+
|
|
2561
|
+
| Prop | Type |
|
|
2562
|
+
| -------------- | --------------------------------------------- |
|
|
2563
|
+
| **`location`** | <code><a href="#location">Location</a></code> |
|
|
2564
|
+
|
|
2565
|
+
|
|
2566
|
+
#### GeofenceEvent
|
|
2567
|
+
|
|
2568
|
+
| Prop | Type |
|
|
2569
|
+
| ---------------- | ---------------------------------------------------------------- |
|
|
2570
|
+
| **`identifier`** | <code>string</code> |
|
|
2571
|
+
| **`action`** | <code><a href="#geofenceaction">GeofenceAction</a></code> |
|
|
2572
|
+
| **`location`** | <code><a href="#location">Location</a></code> |
|
|
2573
|
+
| **`extras`** | <code><a href="#record">Record</a><string, unknown></code> |
|
|
2574
|
+
|
|
2575
|
+
|
|
2576
|
+
#### GeofencesChangeEvent
|
|
2577
|
+
|
|
2578
|
+
| Prop | Type |
|
|
2579
|
+
| --------- | ----------------------- |
|
|
2580
|
+
| **`on`** | <code>Geofence[]</code> |
|
|
2581
|
+
| **`off`** | <code>string[]</code> |
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
#### HttpEvent
|
|
2585
|
+
|
|
2586
|
+
| Prop | Type |
|
|
2587
|
+
| ------------------ | --------------------- |
|
|
2588
|
+
| **`success`** | <code>boolean</code> |
|
|
2589
|
+
| **`status`** | <code>number</code> |
|
|
2590
|
+
| **`responseText`** | <code>string</code> |
|
|
2591
|
+
| **`uuids`** | <code>string[]</code> |
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
#### ConnectivityChangeEvent
|
|
2595
|
+
|
|
2596
|
+
| Prop | Type |
|
|
2597
|
+
| --------------- | ------------------------------------------------------------- |
|
|
2598
|
+
| **`connected`** | <code>boolean</code> |
|
|
2599
|
+
| **`type`** | <code><a href="#connectivitytype">ConnectivityType</a></code> |
|
|
2600
|
+
|
|
2601
|
+
|
|
2602
|
+
#### PowerSaveChangeEvent
|
|
2603
|
+
|
|
2604
|
+
| Prop | Type |
|
|
2605
|
+
| --------------------- | -------------------- |
|
|
2606
|
+
| **`isPowerSaveMode`** | <code>boolean</code> |
|
|
2607
|
+
|
|
2608
|
+
|
|
2609
|
+
#### EnabledChangeEvent
|
|
2610
|
+
|
|
2611
|
+
| Prop | Type |
|
|
2612
|
+
| ------------- | -------------------- |
|
|
2613
|
+
| **`enabled`** | <code>boolean</code> |
|
|
2614
|
+
|
|
2615
|
+
|
|
2616
|
+
#### NotificationActionEvent
|
|
2617
|
+
|
|
2618
|
+
| Prop | Type |
|
|
2619
|
+
| -------- | ------------------- |
|
|
2620
|
+
| **`id`** | <code>string</code> |
|
|
2621
|
+
|
|
2622
|
+
|
|
2623
|
+
#### AuthorizationEvent
|
|
2624
|
+
|
|
2625
|
+
| Prop | Type |
|
|
2626
|
+
| -------------- | ---------------------------------------------------------------- |
|
|
2627
|
+
| **`success`** | <code>boolean</code> |
|
|
2628
|
+
| **`status`** | <code>number</code> |
|
|
2629
|
+
| **`error`** | <code>string</code> |
|
|
2630
|
+
| **`response`** | <code><a href="#record">Record</a><string, unknown></code> |
|
|
2631
|
+
|
|
2632
|
+
|
|
2633
|
+
### Type Aliases
|
|
2634
|
+
|
|
2635
|
+
|
|
2636
|
+
#### TrackingMode
|
|
2637
|
+
|
|
2638
|
+
<code>'location' | 'geofences'</code>
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
#### LocationBackend
|
|
2642
|
+
|
|
2643
|
+
<code>'gms' | 'hms' | 'android' | 'web'</code>
|
|
2644
|
+
|
|
2645
|
+
|
|
2646
|
+
#### DesiredAccuracy
|
|
2647
|
+
|
|
2648
|
+
<code>'high' | 'balanced' | 'low' | 'passive'</code>
|
|
2649
|
+
|
|
2650
|
+
|
|
2651
|
+
#### HttpMethod
|
|
2652
|
+
|
|
2653
|
+
<code>'POST' | 'PUT' | 'PATCH'</code>
|
|
2654
|
+
|
|
2655
|
+
|
|
2656
|
+
#### Record
|
|
2657
|
+
|
|
2658
|
+
Construct a type with a set of properties K of type T
|
|
2659
|
+
|
|
2660
|
+
<code>{
|
|
2661
|
+
[P in K]: T;
|
|
2662
|
+
}</code>
|
|
2663
|
+
|
|
2664
|
+
|
|
2665
|
+
#### NotificationPriority
|
|
2666
|
+
|
|
2667
|
+
<code>'min' | 'low' | 'default' | 'high' | 'max'</code>
|
|
2668
|
+
|
|
2669
|
+
|
|
2670
|
+
#### LogLevel
|
|
2671
|
+
|
|
2672
|
+
<code>'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose'</code>
|
|
2673
|
+
|
|
2674
|
+
|
|
2675
|
+
#### LocationProviderSetting
|
|
2676
|
+
|
|
2677
|
+
<code>'auto' | 'gms' | 'hms' | 'android'</code>
|
|
2678
|
+
|
|
2679
|
+
|
|
2680
|
+
#### RecordEvent
|
|
2681
|
+
|
|
2682
|
+
<code>'location' | 'motionchange' | 'current_position' | 'watch_position' | 'heartbeat' | 'geofence' | 'tracking_start' | 'tracking_stop' | 'providerchange'</code>
|
|
2683
|
+
|
|
2684
|
+
|
|
2685
|
+
#### ActivityType
|
|
2686
|
+
|
|
2687
|
+
<code>'still' | 'on_foot' | 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'unknown'</code>
|
|
2688
|
+
|
|
2689
|
+
|
|
2690
|
+
#### GeofenceAction
|
|
2691
|
+
|
|
2692
|
+
<code>'ENTER' | 'EXIT' | 'DWELL'</code>
|
|
2693
|
+
|
|
2694
|
+
|
|
2695
|
+
#### WatchPositionCallback
|
|
2696
|
+
|
|
2697
|
+
<code>(location: <a href="#location">Location</a> | null, error?: { code: <a href="#errorcode">ErrorCode</a>; message: string; }): void</code>
|
|
2698
|
+
|
|
2699
|
+
|
|
2700
|
+
#### ErrorCode
|
|
2701
|
+
|
|
2702
|
+
Rejection `code` of every failed promise.
|
|
2703
|
+
|
|
2704
|
+
<code>'NOT_READY' | 'PERMISSION_DENIED' | 'LOCATION_DISABLED' | 'TIMEOUT' | 'UNAVAILABLE' | 'INVALID_ARGUMENT' | 'NOT_FOUND' | 'NO_URL' | 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TOO_MANY_GEOFENCES' | 'NO_ACTIVITY' | 'IO_ERROR' | 'UNIMPLEMENTED' | 'INTERNAL'</code>
|
|
2705
|
+
|
|
2706
|
+
|
|
2707
|
+
#### CallbackID
|
|
2708
|
+
|
|
2709
|
+
<code>string</code>
|
|
2710
|
+
|
|
2711
|
+
|
|
2712
|
+
#### Pick
|
|
2713
|
+
|
|
2714
|
+
From T, pick a set of properties whose keys are in the union K
|
|
2715
|
+
|
|
2716
|
+
<code>{
|
|
2717
|
+
[P in K]: T[P];
|
|
2718
|
+
}</code>
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
#### Partial
|
|
2722
|
+
|
|
2723
|
+
Make all properties in T optional
|
|
2724
|
+
|
|
2725
|
+
<code>{
|
|
2726
|
+
[P in keyof T]?: T[P];
|
|
2727
|
+
}</code>
|
|
2728
|
+
|
|
2729
|
+
|
|
2730
|
+
#### HeartbeatStrategy
|
|
2731
|
+
|
|
2732
|
+
<code>'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled'</code>
|
|
2733
|
+
|
|
2734
|
+
|
|
2735
|
+
#### PermissionState
|
|
2736
|
+
|
|
2737
|
+
<code>'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'</code>
|
|
2738
|
+
|
|
2739
|
+
|
|
2740
|
+
#### PermissionType
|
|
2741
|
+
|
|
2742
|
+
<code>'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications'</code>
|
|
2743
|
+
|
|
2744
|
+
|
|
2745
|
+
#### Exclude
|
|
2746
|
+
|
|
2747
|
+
<a href="#exclude">Exclude</a> from T those types that are assignable to U
|
|
2748
|
+
|
|
2749
|
+
<code>T extends U ? never : T</code>
|
|
2750
|
+
|
|
2751
|
+
|
|
2752
|
+
#### ConnectivityType
|
|
2753
|
+
|
|
2754
|
+
<code>'wifi' | 'cellular' | 'ethernet' | 'other' | 'none'</code>
|
|
2755
|
+
|
|
2756
|
+
</docgen-api>
|
|
2757
|
+
|
|
2758
|
+
## License
|
|
2759
|
+
|
|
2760
|
+
MIT, © 2026 Bricks Soft. See [LICENSE](LICENSE).
|