@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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +107 -0
  4. package/android/consumer-rules.pro +42 -0
  5. package/android/src/main/AndroidManifest.xml +52 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
  7. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
  12. package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
  15. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
  17. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
  20. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
  23. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
  30. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
  34. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
  37. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
  39. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
  40. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
  42. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
  43. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
  46. package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
  51. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
  53. package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
  54. package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
  55. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
  56. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
  57. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
  58. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
  59. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
  60. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
  61. package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
  62. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
  63. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
  64. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
  65. package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
  66. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
  67. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
  68. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
  69. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
  70. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
  71. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
  72. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
  73. package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
  74. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
  75. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
  76. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
  77. package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
  78. package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
  79. package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
  80. package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
  81. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
  82. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
  83. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
  84. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
  85. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
  86. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
  87. package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
  88. package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
  89. package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
  90. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
  91. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
  92. package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
  93. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
  94. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
  95. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
  96. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
  97. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
  98. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
  99. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
  100. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
  101. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
  102. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
  103. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
  104. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
  105. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
  106. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
  107. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
  108. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
  109. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
  110. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
  111. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
  112. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
  113. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
  114. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
  115. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
  116. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
  117. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
  118. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
  119. package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
  120. package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
  121. package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
  122. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
  123. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
  124. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
  125. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
  126. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
  127. package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
  128. package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
  129. package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
  130. package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
  131. package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
  132. package/android/src/main/res/values/lt_strings.xml +11 -0
  133. package/android/src/main/res/xml/lt_file_paths.xml +5 -0
  134. package/dist/esm/definitions.d.ts +719 -0
  135. package/dist/esm/definitions.js +2 -0
  136. package/dist/esm/definitions.js.map +1 -0
  137. package/dist/esm/events.d.ts +50 -0
  138. package/dist/esm/events.js +52 -0
  139. package/dist/esm/events.js.map +1 -0
  140. package/dist/esm/index.d.ts +3 -0
  141. package/dist/esm/index.js +5 -0
  142. package/dist/esm/index.js.map +1 -0
  143. package/dist/esm/plugin.d.ts +3 -0
  144. package/dist/esm/plugin.js +10 -0
  145. package/dist/esm/plugin.js.map +1 -0
  146. package/dist/esm/web/config.d.ts +19 -0
  147. package/dist/esm/web/config.js +243 -0
  148. package/dist/esm/web/config.js.map +1 -0
  149. package/dist/esm/web/device.d.ts +9 -0
  150. package/dist/esm/web/device.js +90 -0
  151. package/dist/esm/web/device.js.map +1 -0
  152. package/dist/esm/web/errors.d.ts +14 -0
  153. package/dist/esm/web/errors.js +19 -0
  154. package/dist/esm/web/errors.js.map +1 -0
  155. package/dist/esm/web/geolocation.d.ts +26 -0
  156. package/dist/esm/web/geolocation.js +117 -0
  157. package/dist/esm/web/geolocation.js.map +1 -0
  158. package/dist/esm/web/location.d.ts +7 -0
  159. package/dist/esm/web/location.js +70 -0
  160. package/dist/esm/web/location.js.map +1 -0
  161. package/dist/esm/web.d.ts +164 -0
  162. package/dist/esm/web.js +328 -0
  163. package/dist/esm/web.js.map +1 -0
  164. package/dist/plugin.cjs.js +945 -0
  165. package/dist/plugin.cjs.js.map +1 -0
  166. package/dist/plugin.js +948 -0
  167. package/dist/plugin.js.map +1 -0
  168. package/package.json +60 -4
package/README.md CHANGED
@@ -1,3 +1,2760 @@
1
- # Temporary Holding Version
1
+ # @bricks-soft/capacitor-location-tracking
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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&lt;<a href="#state">State</a>&gt;</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&lt;<a href="#state">State</a>&gt;</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&lt;<a href="#state">State</a>&gt;</code>
1372
+
1373
+ --------------------
1374
+
1375
+
1376
+ ### getState()
1377
+
1378
+ ```typescript
1379
+ getState() => Promise<State>
1380
+ ```
1381
+
1382
+ **Returns:** <code>Promise&lt;<a href="#state">State</a>&gt;</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&lt;<a href="#state">State</a>&gt;</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&lt;<a href="#state">State</a>&gt;</code>
1409
+
1410
+ --------------------
1411
+
1412
+
1413
+ ### stop()
1414
+
1415
+ ```typescript
1416
+ stop() => Promise<State>
1417
+ ```
1418
+
1419
+ **Returns:** <code>Promise&lt;<a href="#state">State</a>&gt;</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&lt;<a href="#location">Location</a>&gt;</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&lt;string&gt;</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&lt;{ odometer: number; }&gt;</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&lt;{ odometer: number; }&gt;</code>
1505
+
1506
+ --------------------
1507
+
1508
+
1509
+ ### resetOdometer()
1510
+
1511
+ ```typescript
1512
+ resetOdometer() => Promise<{ odometer: number; }>
1513
+ ```
1514
+
1515
+ **Returns:** <code>Promise&lt;{ odometer: number; }&gt;</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&lt;{ locations: Location[]; }&gt;</code>
1531
+
1532
+ --------------------
1533
+
1534
+
1535
+ ### getCount()
1536
+
1537
+ ```typescript
1538
+ getCount() => Promise<{ count: number; }>
1539
+ ```
1540
+
1541
+ **Returns:** <code>Promise&lt;{ count: number; }&gt;</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&lt;{ uuid: string; }&gt;</code>
1557
+
1558
+ --------------------
1559
+
1560
+
1561
+ ### destroyLocations()
1562
+
1563
+ ```typescript
1564
+ destroyLocations() => Promise<{ count: number; }>
1565
+ ```
1566
+
1567
+ **Returns:** <code>Promise&lt;{ count: number; }&gt;</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&lt;{ deleted: boolean; }&gt;</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&lt;{ locations: Location[]; }&gt;</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&lt;{ geofences: Geofence[]; }&gt;</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&lt;{ geofence: <a href="#geofence">Geofence</a> | null; }&gt;</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&lt;{ exists: boolean; }&gt;</code>
1691
+
1692
+ --------------------
1693
+
1694
+
1695
+ ### getHeartbeatStatus()
1696
+
1697
+ ```typescript
1698
+ getHeartbeatStatus() => Promise<HeartbeatStatus>
1699
+ ```
1700
+
1701
+ **Returns:** <code>Promise&lt;<a href="#heartbeatstatus">HeartbeatStatus</a>&gt;</code>
1702
+
1703
+ --------------------
1704
+
1705
+
1706
+ ### getProviderState()
1707
+
1708
+ ```typescript
1709
+ getProviderState() => Promise<ProviderState>
1710
+ ```
1711
+
1712
+ **Returns:** <code>Promise&lt;<a href="#providerstate">ProviderState</a>&gt;</code>
1713
+
1714
+ --------------------
1715
+
1716
+
1717
+ ### isPowerSaveMode()
1718
+
1719
+ ```typescript
1720
+ isPowerSaveMode() => Promise<{ isPowerSaveMode: boolean; }>
1721
+ ```
1722
+
1723
+ **Returns:** <code>Promise&lt;{ isPowerSaveMode: boolean; }&gt;</code>
1724
+
1725
+ --------------------
1726
+
1727
+
1728
+ ### getBatteryOptimizationStatus()
1729
+
1730
+ ```typescript
1731
+ getBatteryOptimizationStatus() => Promise<BatteryOptimizationStatus>
1732
+ ```
1733
+
1734
+ **Returns:** <code>Promise&lt;<a href="#batteryoptimizationstatus">BatteryOptimizationStatus</a>&gt;</code>
1735
+
1736
+ --------------------
1737
+
1738
+
1739
+ ### openBatteryOptimizationSettings()
1740
+
1741
+ ```typescript
1742
+ openBatteryOptimizationSettings() => Promise<{ opened: boolean; }>
1743
+ ```
1744
+
1745
+ **Returns:** <code>Promise&lt;{ opened: boolean; }&gt;</code>
1746
+
1747
+ --------------------
1748
+
1749
+
1750
+ ### getPowerManagerInfo()
1751
+
1752
+ ```typescript
1753
+ getPowerManagerInfo() => Promise<PowerManagerInfo>
1754
+ ```
1755
+
1756
+ **Returns:** <code>Promise&lt;<a href="#powermanagerinfo">PowerManagerInfo</a>&gt;</code>
1757
+
1758
+ --------------------
1759
+
1760
+
1761
+ ### openPowerManagerSettings()
1762
+
1763
+ ```typescript
1764
+ openPowerManagerSettings() => Promise<{ opened: boolean; }>
1765
+ ```
1766
+
1767
+ **Returns:** <code>Promise&lt;{ opened: boolean; }&gt;</code>
1768
+
1769
+ --------------------
1770
+
1771
+
1772
+ ### openLocationSettings()
1773
+
1774
+ ```typescript
1775
+ openLocationSettings() => Promise<{ opened: boolean; }>
1776
+ ```
1777
+
1778
+ **Returns:** <code>Promise&lt;{ opened: boolean; }&gt;</code>
1779
+
1780
+ --------------------
1781
+
1782
+
1783
+ ### openAppSettings()
1784
+
1785
+ ```typescript
1786
+ openAppSettings() => Promise<{ opened: boolean; }>
1787
+ ```
1788
+
1789
+ **Returns:** <code>Promise&lt;{ opened: boolean; }&gt;</code>
1790
+
1791
+ --------------------
1792
+
1793
+
1794
+ ### getDeviceInfo()
1795
+
1796
+ ```typescript
1797
+ getDeviceInfo() => Promise<DeviceInfo>
1798
+ ```
1799
+
1800
+ **Returns:** <code>Promise&lt;<a href="#deviceinfo">DeviceInfo</a>&gt;</code>
1801
+
1802
+ --------------------
1803
+
1804
+
1805
+ ### getSensors()
1806
+
1807
+ ```typescript
1808
+ getSensors() => Promise<Sensors>
1809
+ ```
1810
+
1811
+ **Returns:** <code>Promise&lt;<a href="#sensors">Sensors</a>&gt;</code>
1812
+
1813
+ --------------------
1814
+
1815
+
1816
+ ### checkPermissions()
1817
+
1818
+ ```typescript
1819
+ checkPermissions() => Promise<PermissionStatus>
1820
+ ```
1821
+
1822
+ **Returns:** <code>Promise&lt;<a href="#permissionstatus">PermissionStatus</a>&gt;</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&lt;<a href="#permissionstatus">PermissionStatus</a>&gt;</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&lt;{ log: string; }&gt;</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>&lt;string, string&gt;; params?: <a href="#record">Record</a>&lt;string, unknown&gt;; }</code> |
1890
+
1891
+ **Returns:** <code>Promise&lt;{ success: boolean; status: number; }&gt;</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>) =&gt; void</code> |
1919
+
1920
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
1935
+
1936
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
1951
+
1952
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
1967
+
1968
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
1983
+
1984
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
1999
+
2000
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
2015
+
2016
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
2031
+
2032
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
2047
+
2048
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
2063
+
2064
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
2079
+
2080
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
2095
+
2096
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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>) =&gt; void</code> |
2111
+
2112
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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, &gt;= minInterval | <code>300</code> |
2207
+
2208
+
2209
+ #### HttpConfig
2210
+
2211
+ | Prop | Type | Description | Default |
2212
+ | ------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------- |
2213
+ | **`url`** | <code>string \| null</code> | No url =&gt; nothing is uploaded (records stay queued). | |
2214
+ | **`method`** | <code><a href="#httpmethod">HttpMethod</a></code> | | <code>'POST'</code> |
2215
+ | **`headers`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | | <code>{}</code> |
2216
+ | **`params`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> | merged into the ROOT of every request body | <code>{}</code> |
2217
+ | **`autoSync`** | <code>boolean</code> | | <code>true</code> |
2218
+ | **`autoSyncThreshold`** | <code>number</code> | upload when queue &gt;= threshold; 0 = every record | <code>0</code> |
2219
+ | **`syncInterval`** | <code>number</code> | s; upload normal records once the oldest queued one is this old; 0 = off | <code>0</code> |
2220
+ | **`batchSync`** | <code>boolean</code> | | <code>false</code> |
2221
+ | **`maxBatchSize`** | <code>number</code> | | <code>100</code> |
2222
+ | **`disableAutoSyncOnCellular`** | <code>boolean</code> | | <code>false</code> |
2223
+ | **`rootProperty`** | <code>string</code> | '.' = no wrapping | <code>'location'</code> |
2224
+ | **`locationTemplate`** | <code>string \| null</code> | JSON text with `&lt;%= name %&gt;` placeholders | |
2225
+ | **`geofenceTemplate`** | <code>string \| null</code> | used for event 'geofence'; falls back to locationTemplate | |
2226
+ | **`timeout`** | <code>number</code> | ms | <code>60000</code> |
2227
+ | **`authorization`** | <code><a href="#authorizationconfig">AuthorizationConfig</a> \| null</code> | | |
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>&lt;string, string&gt;</code> | String values may contain `{refreshToken}`. | <code>{}</code> |
2239
+ | **`refreshHeaders`** | <code><a href="#record">Record</a>&lt;string, string&gt;</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>&lt;string, unknown&gt;</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 =&gt; '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>&lt;string, unknown&gt;</code> | |
2351
+ | **`geofence`** | <code>{ identifier: string; action: <a href="#geofenceaction">GeofenceAction</a>; extras?: <a href="#record">Record</a>&lt;string, unknown&gt;; }</code> | event 'geofence' only |
2352
+ | **`provider`** | <code><a href="#providerstate">ProviderState</a></code> | provider state when the record was created; the new state for event 'providerchange' |
2353
+ | **`reason`** | <code>string</code> | tracking_start: start\|start_geofences\|boot\|restore\|package_replaced\|resume_notification; tracking_stop: stop\|stop_on_stationary\|stop_after_elapsed\|terminate\|permission_denied\|service_start_failed\|reboot\|package_replaced |
2354
+ | **`heartbeat`** | <code><a href="#heartbeatmeta">HeartbeatMeta</a></code> | event 'heartbeat' only, optional: how the heartbeat is scheduled |
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>&lt;string, unknown&gt;</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>&lt;string, unknown&gt;</code> | | |
2418
+
2419
+
2420
+ #### InsertLocationInput
2421
+
2422
+ | Prop | Type |
2423
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2424
+ | **`coords`** | <code><a href="#pick">Pick</a>&lt;<a href="#coords">Coords</a>, 'latitude' \| 'longitude'&gt; & <a href="#partial">Partial</a>&lt;<a href="#coords">Coords</a>&gt;</code> |
2425
+ | **`timestamp`** | <code>string</code> |
2426
+ | **`event`** | <code><a href="#recordevent">RecordEvent</a></code> |
2427
+ | **`is_moving`** | <code>boolean</code> |
2428
+ | **`extras`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> |
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], ...] &gt;= 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>&lt;string, unknown&gt;</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>() =&gt; Promise&lt;void&gt;</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>&lt;string, unknown&gt;</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>&lt;string, unknown&gt;</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).