@bricks-soft/capacitor-location-tracking 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BricksSoftCapacitorLocationTracking.podspec +18 -0
- package/Package.swift +13 -0
- package/README.md +107 -29
- package/android/build.gradle +3 -1
- package/dist/esm/definitions.d.ts +10 -4
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web/device.d.ts +1 -1
- package/dist/esm/web/device.js +1 -1
- package/dist/esm/web/device.js.map +1 -1
- package/dist/plugin.cjs.js +1 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
- package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
- package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
- package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
- package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
- package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
- package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
- package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
- package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
- package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
- package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
- package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
- package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
- package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
- package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
- package/package.json +9 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
3
|
+
Pod::Spec.new do |s|
|
|
4
|
+
s.name = 'BricksSoftCapacitorLocationTracking'
|
|
5
|
+
s.version = package['version']
|
|
6
|
+
s.summary = package['description']
|
|
7
|
+
s.license = package['license']
|
|
8
|
+
s.homepage = package['homepage']
|
|
9
|
+
s.author = package['author']
|
|
10
|
+
s.source = { :git => 'https://github.com/bricks-soft/location-tracking.git', :tag => s.version.to_s }
|
|
11
|
+
s.source_files = 'ios/Sources/LocationTrackingPlugin/**/*.swift'
|
|
12
|
+
s.resource_bundles = { 'LocationTrackingPrivacy' => ['ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy'] }
|
|
13
|
+
s.ios.deployment_target = '15.0'
|
|
14
|
+
s.module_name = 'LocationTrackingPlugin'
|
|
15
|
+
s.swift_version = '5.9'
|
|
16
|
+
s.dependency 'Capacitor', '>= 8.0.0', '< 9.0'
|
|
17
|
+
s.libraries = 'sqlite3'
|
|
18
|
+
end
|
package/Package.swift
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "BricksSoftCapacitorLocationTracking",
|
|
6
|
+
platforms: [.iOS(.v15)],
|
|
7
|
+
products: [.library(name: "BricksSoftCapacitorLocationTracking", targets: ["LocationTrackingPlugin"])],
|
|
8
|
+
dependencies: [.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")],
|
|
9
|
+
targets: [
|
|
10
|
+
.target(name: "LocationTrackingPlugin", dependencies: [.product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm")], path: "ios/Sources/LocationTrackingPlugin", resources: [.copy("PrivacyInfo.xcprivacy")], linkerSettings: [.linkedLibrary("sqlite3")]),
|
|
11
|
+
.testTarget(name: "LocationTrackingPluginTests", dependencies: ["LocationTrackingPlugin"], path: "ios/Tests/LocationTrackingPluginTests")
|
|
12
|
+
]
|
|
13
|
+
)
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ whether the user really kept tracking on.
|
|
|
74
74
|
| Platform | Status |
|
|
75
75
|
|---|---|
|
|
76
76
|
| Android (API 24+, Kotlin) | ✔ Supported |
|
|
77
|
-
| iOS |
|
|
77
|
+
| iOS 15+ | Native route tracking, heartbeat, restore, positions and HTTP sync. Geofencing on iOS 17+, up to 20 geofences. |
|
|
78
78
|
| Web | Stub for development only: positions through `navigator.geolocation`, an in-memory config, permissions and device info. Nothing is stored or uploaded, and tracking can't be started. Everything else rejects with `UNIMPLEMENTED`. See [Web stub](#web-stub). |
|
|
79
79
|
|
|
80
80
|
## Installation
|
|
@@ -914,7 +914,7 @@ How to measure the cost on a real phone: procedure M-04 in the
|
|
|
914
914
|
|
|
915
915
|
## Geofencing
|
|
916
916
|
|
|
917
|
-
- Up to **100** geofences (`TOO_MANY_GEOFENCES` beyond that). An identifier is 1–100 characters; adding a geofence
|
|
917
|
+
- Up to **100** geofences on Android and **20** on iOS (`TOO_MANY_GEOFENCES` beyond that). An identifier is 1–100 characters; adding a geofence
|
|
918
918
|
with an existing identifier replaces it. `loiteringDelay` must be ≥ 0. The identifier `__lt_stationary__` is
|
|
919
919
|
reserved for the plugin's stationary region: `addGeofence` rejects it with `INVALID_ARGUMENT`.
|
|
920
920
|
- The stationary region (see [Battery](#battery)) does not count against the 100. GMS accepts at most 100 geofences
|
|
@@ -1150,7 +1150,7 @@ Every rejected promise has a `code` (see the `ErrorCode` type) and a `message`.
|
|
|
1150
1150
|
| `NO_URL` | `sync()` was called without a valid `http.url`. |
|
|
1151
1151
|
| `HTTP_ERROR` | `sync()` got a non-2xx response. |
|
|
1152
1152
|
| `NETWORK_ERROR` | `sync()` failed with a network or I/O error. |
|
|
1153
|
-
| `TOO_MANY_GEOFENCES` | Adding the geofences would exceed 100 (or the OS geofence limit reported by GMS/HMS). |
|
|
1153
|
+
| `TOO_MANY_GEOFENCES` | Adding the geofences would exceed 100 on Android, 20 on iOS (or the OS geofence limit reported by GMS/HMS). |
|
|
1154
1154
|
| `NO_ACTIVITY` | `emailLog()` was called while no Activity is attached. (The `open*Settings` methods work without one.) |
|
|
1155
1155
|
| `IO_ERROR` | Storage or file error (database, log files). |
|
|
1156
1156
|
| `UNIMPLEMENTED` | Not available on this platform (web stub, iOS). |
|
|
@@ -1359,10 +1359,13 @@ Node ignores it.)
|
|
|
1359
1359
|
CI runs the emulator suites through `.github/scripts/run-e2e.sh` in `.github/workflows/e2e-android.yml`, only when
|
|
1360
1360
|
started by hand (Actions tab or `gh workflow run`; pushes and pull requests start no run, to save Actions minutes):
|
|
1361
1361
|
the plugin suite on API 34 (with smaller runs on API 29 and 35), P-P08 on an image without Google Play services, the
|
|
1362
|
-
field-force suite on API 34, and the long scenarios (with the `include-long` input).
|
|
1363
|
-
`.github/workflows/
|
|
1364
|
-
|
|
1365
|
-
|
|
1362
|
+
field-force suite on API 34, and the long scenarios (with the `include-long` input).
|
|
1363
|
+
`.github/workflows/typescript.yml` runs on every push: it type-checks the kit and runs its unit tests, type-checks and
|
|
1364
|
+
dry-runs both suites, and runs the field-force Node tests. `.github/workflows/android.yml`, also started by hand only,
|
|
1365
|
+
runs the Android unit tests, builds the example apps and checks that the Google Play build of the field-force app
|
|
1366
|
+
supports 16 KB memory pages (`.github/scripts/check-16kb.py`). `.github/workflows/ios.yml`, started by hand only, runs
|
|
1367
|
+
XCTest, the UI tests and the shared scenarios P-S01 to P-S07 on iOS simulators; on a Mac the same steps run with
|
|
1368
|
+
`.github/scripts/ios/check.sh all` ([docs/e2e-runbook.md](docs/e2e-runbook.md), 6.11).
|
|
1366
1369
|
|
|
1367
1370
|
The field-force suite, together with the field-force app and the kit, is written so that it can move into the Bricks
|
|
1368
1371
|
app as its integration test. The contract behind the tests (scenario ids, debug commands, mock back office) is
|
|
@@ -1396,13 +1399,15 @@ Each later release, with `scripts/release.sh` (bash; run it from the repository
|
|
|
1396
1399
|
1. While you work, write the changes under `## [Unreleased]` in `CHANGELOG.md`.
|
|
1397
1400
|
2. `scripts/release.sh check <version>` runs every check and changes nothing: `master` is clean and equal to
|
|
1398
1401
|
`origin/master`; the version is new and greater than the one on npm, and its major version is Capacitor's; the
|
|
1399
|
-
versions in `package.json`, `package-lock.json
|
|
1402
|
+
versions in `package.json`, `package-lock.json`, `src/web/device.ts` and
|
|
1403
|
+
`ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift` agree; `[Unreleased]` is not empty; git
|
|
1400
1404
|
can sign; `npm run docgen` leaves the README unchanged; the changes since the last tag contain none of the
|
|
1401
|
-
private-name patterns (a file outside the repository, see the script); the steps of `
|
|
1402
|
-
computer
|
|
1405
|
+
private-name patterns (a file outside the repository, see the script); the steps of `typescript.yml` and
|
|
1406
|
+
`android.yml` pass on this computer, and on macOS those of `ios.yml` (elsewhere it warns that the iOS checks were
|
|
1407
|
+
skipped); and `npm pack` contains only the files listed in `package.json`.
|
|
1403
1408
|
3. `scripts/release.sh run <version>` runs the checks, renames `[Unreleased]` to `[<version>] - <date>`, runs
|
|
1404
|
-
`npm version <version> --no-git-tag-version` (it sets `package.json`, `package-lock.json
|
|
1405
|
-
`src/web/device.ts
|
|
1409
|
+
`npm version <version> --no-git-tag-version` (it sets `package.json`, `package-lock.json`, `PLUGIN_VERSION` in
|
|
1410
|
+
`src/web/device.ts` and `PluginInfo.version` in Swift; `android/build.gradle` reads `package.json` itself), and makes a signed commit and a signed tag
|
|
1406
1411
|
`v<version>`. After you type `yes`, it pushes both and creates the GitHub release with the CHANGELOG section as
|
|
1407
1412
|
notes. The release starts the workflow (it can also be started by hand from the Actions tab), which builds, runs
|
|
1408
1413
|
`npm test` and runs `npm stage publish`; the script waits for it.
|
|
@@ -2489,12 +2494,13 @@ Posted when Android refuses to restore tracking from the background (after a reb
|
|
|
2489
2494
|
restart), typically on Android 14+ without "Allow all the time". A tap resumes the session
|
|
2490
2495
|
(`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.
|
|
2491
2496
|
|
|
2492
|
-
| Prop | Type | Default |
|
|
2493
|
-
| ----------------- | -------------------- | ---------------------------------------------------------- |
|
|
2494
|
-
| **`
|
|
2495
|
-
| **`
|
|
2496
|
-
| **`
|
|
2497
|
-
| **`
|
|
2497
|
+
| Prop | Type | Description | Default |
|
|
2498
|
+
| ----------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
2499
|
+
| **`delay`** | <code>number</code> | iOS only (Android warns about an unknown key): seconds from the last activity to the check-in notification; raised to `heartbeat.maxInterval + 60` when lower. | <code>600</code> |
|
|
2500
|
+
| **`enabled`** | <code>boolean</code> | | <code>false</code> |
|
|
2501
|
+
| **`title`** | <code>string</code> | | <code>app label</code> |
|
|
2502
|
+
| **`text`** | <code>string</code> | | <code>'Location tracking is paused. Tap to resume.'</code> |
|
|
2503
|
+
| **`channelName`** | <code>string</code> | | <code>'Paused location tracking'</code> |
|
|
2498
2504
|
|
|
2499
2505
|
|
|
2500
2506
|
#### GeofenceConfig
|
|
@@ -2589,14 +2595,14 @@ restart), typically on Android 14+ without "Allow all the time". A tap resumes t
|
|
|
2589
2595
|
|
|
2590
2596
|
Scheduling metadata of a `heartbeat` record (snake_case = wire format).
|
|
2591
2597
|
|
|
2592
|
-
| Prop | Type
|
|
2593
|
-
| -------------------- |
|
|
2594
|
-
| **`strategy`** | <code>'exact' \| 'listener_with_backup' \| 'idle_paced'</code> | |
|
|
2595
|
-
| **`min_interval`** | <code>number</code>
|
|
2596
|
-
| **`max_interval`** | <code>number</code>
|
|
2597
|
-
| **`next_at`** | <code>string \| null</code>
|
|
2598
|
-
| **`battery_exempt`** | <code>boolean</code>
|
|
2599
|
-
| **`device_idle`** | <code>boolean</code>
|
|
2598
|
+
| Prop | Type | Description |
|
|
2599
|
+
| -------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
2600
|
+
| **`strategy`** | <code>'exact' \| 'listener_with_backup' \| 'idle_paced' \| 'timer'</code> | |
|
|
2601
|
+
| **`min_interval`** | <code>number</code> | heartbeat.minInterval (s) when the heartbeat was created |
|
|
2602
|
+
| **`max_interval`** | <code>number</code> | heartbeat.maxInterval (s) when the heartbeat was created |
|
|
2603
|
+
| **`next_at`** | <code>string \| null</code> | when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown |
|
|
2604
|
+
| **`battery_exempt`** | <code>boolean</code> | the app is exempt from battery optimization |
|
|
2605
|
+
| **`device_idle`** | <code>boolean</code> | the device was in deep Doze |
|
|
2600
2606
|
|
|
2601
2607
|
|
|
2602
2608
|
#### DeviceReadings
|
|
@@ -2712,7 +2718,7 @@ the computed enclosing circle.
|
|
|
2712
2718
|
|
|
2713
2719
|
| Prop | Type | Description |
|
|
2714
2720
|
| --------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
2715
|
-
| **`platform`** | <code>'android' \| 'web'</code>
|
|
2721
|
+
| **`platform`** | <code>'android' \| 'web' \| 'ios'</code> | |
|
|
2716
2722
|
| **`manufacturer`** | <code>string</code> | |
|
|
2717
2723
|
| **`model`** | <code>string</code> | |
|
|
2718
2724
|
| **`brand`** | <code>string</code> | |
|
|
@@ -2897,7 +2903,7 @@ A record could not be stored because the device storage is full.
|
|
|
2897
2903
|
|
|
2898
2904
|
#### LocationBackend
|
|
2899
2905
|
|
|
2900
|
-
<code>'gms' | 'hms' | 'android' | 'web'</code>
|
|
2906
|
+
<code>'gms' | 'hms' | 'android' | 'web' | 'ios'</code>
|
|
2901
2907
|
|
|
2902
2908
|
|
|
2903
2909
|
#### DesiredAccuracy
|
|
@@ -2985,7 +2991,7 @@ Make all properties in T optional
|
|
|
2985
2991
|
|
|
2986
2992
|
#### HeartbeatStrategy
|
|
2987
2993
|
|
|
2988
|
-
<code>'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled'</code>
|
|
2994
|
+
<code>'exact' | 'listener_with_backup' | 'idle_paced' | 'timer' | 'disabled'</code>
|
|
2989
2995
|
|
|
2990
2996
|
|
|
2991
2997
|
#### PermissionState
|
|
@@ -3009,3 +3015,75 @@ Make all properties in T optional
|
|
|
3009
3015
|
## License
|
|
3010
3016
|
|
|
3011
3017
|
MIT, © 2026 Bricks Soft. See [LICENSE](LICENSE).
|
|
3018
|
+
|
|
3019
|
+
## iOS setup
|
|
3020
|
+
|
|
3021
|
+
The iOS implementation supports native route tracking, motion detection, heartbeat audit records, restore, check-in
|
|
3022
|
+
notifications, configuration, SQLite queueing, HTTP sync, permissions, current positions and watches. Geofencing needs
|
|
3023
|
+
iOS 17; below it, `startGeofences` and the geofence methods reject with `UNAVAILABLE`.
|
|
3024
|
+
|
|
3025
|
+
The deployment target is iOS 15. Install `@capacitor/ios` 8 and run `npx cap sync ios`. Both SPM and CocoaPods are supported.
|
|
3026
|
+
In the host app's Info.plist, add:
|
|
3027
|
+
|
|
3028
|
+
```xml
|
|
3029
|
+
<key>UIBackgroundModes</key><array><string>location</string><string>fetch</string></array>
|
|
3030
|
+
<key>BGTaskSchedulerPermittedIdentifiers</key><array><string>com.brickssoft.locationtracking.refresh</string></array>
|
|
3031
|
+
<key>NSLocationWhenInUseUsageDescription</key><string>Explain your foreground tracking purpose.</string>
|
|
3032
|
+
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key><string>Explain your optional background tracking purpose.</string>
|
|
3033
|
+
<key>NSMotionUsageDescription</key><string>Explain how motion selects the tracking profile.</string>
|
|
3034
|
+
```
|
|
3035
|
+
|
|
3036
|
+
Call the native bootstrap before `application(_:didFinishLaunchingWithOptions:)` returns, so the background refresh
|
|
3037
|
+
handler is registered during launch:
|
|
3038
|
+
|
|
3039
|
+
```swift
|
|
3040
|
+
import LocationTrackingPlugin
|
|
3041
|
+
|
|
3042
|
+
LocationTrackingNative.bootstrap()
|
|
3043
|
+
```
|
|
3044
|
+
|
|
3045
|
+
For apps that track only while they are in use, request `{ permissions: ['location'] }`. Always is optional, and the
|
|
3046
|
+
Always purpose string is still required because the library references that API. When the user grants approximate
|
|
3047
|
+
location, `ProviderState.accuracy` is `'approximate'` and a `providerchange` event follows; stored geofences stay inactive
|
|
3048
|
+
and a `PERMISSION_DENIED` `locationerror` says why. Explain it in the app and call `openAppSettings()` so the user can
|
|
3049
|
+
turn on Precise Location, which stays on.
|
|
3050
|
+
|
|
3051
|
+
Queued records and configuration use Application Support with backup exclusion and protection until first unlock.
|
|
3052
|
+
Authorization is held in Keychain with the same unlock requirement. HTTP uses an ephemeral session, refuses redirects,
|
|
3053
|
+
and treats every 2xx as success; keep your upload endpoint on HTTPS. A refresh task is best effort and does not keep
|
|
3054
|
+
the app continuously running. `stop()` leaves position watches active; clear each watch explicitly.
|
|
3055
|
+
|
|
3056
|
+
While moving, the route uses the configured accuracy. While stationary, it keeps continuous 100 m updates so that
|
|
3057
|
+
movement can resume tracking without Motion permission. Core Motion confidence is 33, 66 or 100; the default confidence
|
|
3058
|
+
threshold accepts only high-confidence samples. Location update intervals are controlled by iOS.
|
|
3059
|
+
|
|
3060
|
+
Heartbeats use an in-process timer and are not backfilled after a suspension or kill. When In Use tracking must start
|
|
3061
|
+
in the foreground; enabled intent restores when the app next becomes active. Always authorization adds significant-change
|
|
3062
|
+
monitoring as a relaunch trigger. iOS does not relaunch a force-quit app for location events. If Location Services are
|
|
3063
|
+
switched off for the whole device, enabled tracking keeps its intent and original deadline and resumes when they are
|
|
3064
|
+
switched back on; a revoked app permission still stops it with `permission_denied`.
|
|
3065
|
+
|
|
3066
|
+
Enable `notification.resume` and request `notifications` permission for a pending check-in after tracking goes quiet.
|
|
3067
|
+
Its iOS `delay` defaults to 600 seconds and is raised to `heartbeat.maxInterval + 60` when lower. Fixes and records move
|
|
3068
|
+
one pending notification, at most once a minute. Stop and session expiry cancel it. A tap opens the app and restores the
|
|
3069
|
+
original session deadline. The plugin leaves Capacitor's notification-center delegate in place.
|
|
3070
|
+
|
|
3071
|
+
Geofences use one [`CLMonitor`](https://developer.apple.com/documentation/corelocation/clmonitor), opened again under the
|
|
3072
|
+
same name at every launch, and are registered while tracking runs. A polygon registers its enclosing circle and is
|
|
3073
|
+
hit-tested against fixes while the device is inside that circle. DWELL is synthesized from the time inside, so it fires
|
|
3074
|
+
only while the app runs. With Always authorization on iOS 18 and later, the plugin holds a
|
|
3075
|
+
[`CLServiceSession`](https://developer.apple.com/documentation/corelocation/clservicesession) so events also arrive when
|
|
3076
|
+
the app is not in use; with When In Use they arrive only while it runs. While location accuracy is approximate, stored
|
|
3077
|
+
geofences stay inactive and a `locationerror` with `PERMISSION_DENIED` says so.
|
|
3078
|
+
|
|
3079
|
+
`stopOnTerminate` handles the best-effort termination callback. An OS kill without that callback preserves enabled intent,
|
|
3080
|
+
even when `stopOnTerminate` is true. `startOnBoot` is ignored. Deadlines use the continuous elapsed clock within the process.
|
|
3081
|
+
On restore, matching saved wall/elapsed checkpoints preserve elapsed timing; a detected reboot or clock discontinuity uses
|
|
3082
|
+
the original wall-clock start. A wall-clock change while the process is absent cannot be distinguished reliably from a
|
|
3083
|
+
reboot on current iOS, which [no longer exposes a boot-session identifier](https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-18-release-notes).
|
|
3084
|
+
Fixes are ordered by the elapsed clock at delivery, so a wall-clock correction does not make new fixes look stale.
|
|
3085
|
+
|
|
3086
|
+
`example/ios` is the example host. Run `npm run copy-vendor` and `npx cap sync ios` from `example/` before building.
|
|
3087
|
+
The iOS workflow installs the npm tarball into a fresh app through each package manager and archives both unsigned and ad-hoc signed Release
|
|
3088
|
+
IPAs. The ad-hoc identity is `-`, with no provisioning profile; it does not provide Apple device authorization. Installing it on an iPhone still requires an Apple signing certificate/private key, a matching provisioning profile
|
|
3089
|
+
and entitlements for its bundle identifier, or a device-cloud service that re-signs uploaded apps.
|
package/android/build.gradle
CHANGED
|
@@ -64,7 +64,9 @@ android {
|
|
|
64
64
|
all {
|
|
65
65
|
maxHeapSize = '1024m'
|
|
66
66
|
maxParallelForks = 1
|
|
67
|
-
|
|
67
|
+
// IPv4 only: where `localhost` is also ::1 (GitHub's Ubuntu runners), OkHttp tries ::1 after a failed try
|
|
68
|
+
// to MockWebServer (bound to 127.0.0.1), and the client does not fall back to the next address.
|
|
69
|
+
jvmArgs '-XX:+UseSerialGC', '-XX:+EnableDynamicAgentLoading', '-Djava.net.preferIPv4Stack=true'
|
|
68
70
|
testLogging {
|
|
69
71
|
events 'failed'
|
|
70
72
|
exceptionFormat 'full'
|
|
@@ -2,7 +2,7 @@ import type { PermissionState, PluginListenerHandle } from '@capacitor/core';
|
|
|
2
2
|
export type CallbackID = string;
|
|
3
3
|
export type DesiredAccuracy = 'high' | 'balanced' | 'low' | 'passive';
|
|
4
4
|
export type LocationProviderSetting = 'auto' | 'gms' | 'hms' | 'android';
|
|
5
|
-
export type LocationBackend = 'gms' | 'hms' | 'android' | 'web';
|
|
5
|
+
export type LocationBackend = 'gms' | 'hms' | 'android' | 'web' | 'ios';
|
|
6
6
|
export type TrackingMode = 'location' | 'geofences';
|
|
7
7
|
export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';
|
|
8
8
|
export type ActivityType = 'still' | 'on_foot' | 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'unknown';
|
|
@@ -12,7 +12,7 @@ export type HttpMethod = 'POST' | 'PUT' | 'PATCH';
|
|
|
12
12
|
export type NotificationPriority = 'min' | 'low' | 'default' | 'high' | 'max';
|
|
13
13
|
export type PermissionType = 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications';
|
|
14
14
|
export type ConnectivityType = 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none';
|
|
15
|
-
export type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled';
|
|
15
|
+
export type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer' | 'disabled';
|
|
16
16
|
/** Rejection `code` of every failed promise. */
|
|
17
17
|
export type ErrorCode = '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';
|
|
18
18
|
export interface LocationFilterConfig {
|
|
@@ -253,6 +253,12 @@ export interface NotificationConfig {
|
|
|
253
253
|
* (`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.
|
|
254
254
|
*/
|
|
255
255
|
export interface ResumeNotificationConfig {
|
|
256
|
+
/**
|
|
257
|
+
* iOS only (Android warns about an unknown key): seconds from the last activity to the check-in notification;
|
|
258
|
+
* raised to `heartbeat.maxInterval + 60` when lower.
|
|
259
|
+
* @default 600
|
|
260
|
+
*/
|
|
261
|
+
delay?: number;
|
|
256
262
|
/** @default false */
|
|
257
263
|
enabled?: boolean;
|
|
258
264
|
/** @default app label */
|
|
@@ -433,7 +439,7 @@ export interface DeviceReadings {
|
|
|
433
439
|
}
|
|
434
440
|
/** Scheduling metadata of a `heartbeat` record (snake_case = wire format). */
|
|
435
441
|
export interface HeartbeatMeta {
|
|
436
|
-
strategy: 'exact' | 'listener_with_backup' | 'idle_paced';
|
|
442
|
+
strategy: 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer';
|
|
437
443
|
/** heartbeat.minInterval (s) when the heartbeat was created */
|
|
438
444
|
min_interval: number;
|
|
439
445
|
/** heartbeat.maxInterval (s) when the heartbeat was created */
|
|
@@ -621,7 +627,7 @@ export interface PowerManagerInfo {
|
|
|
621
627
|
available: boolean;
|
|
622
628
|
}
|
|
623
629
|
export interface DeviceInfo {
|
|
624
|
-
platform: 'android' | 'web';
|
|
630
|
+
platform: 'android' | 'web' | 'ios';
|
|
625
631
|
manufacturer: string;
|
|
626
632
|
model: string;
|
|
627
633
|
brand: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CallbackID = string;\nexport type DesiredAccuracy = 'high' | 'balanced' | 'low' | 'passive';\nexport type LocationProviderSetting = 'auto' | 'gms' | 'hms' | 'android';\nexport type LocationBackend = 'gms' | 'hms' | 'android' | 'web';\nexport type TrackingMode = 'location' | 'geofences';\nexport type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';\nexport type ActivityType = 'still' | 'on_foot' | 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'unknown';\nexport type RecordEvent =\n | 'location'\n | 'motionchange'\n | 'current_position'\n | 'watch_position'\n | 'heartbeat'\n | 'geofence'\n | 'tracking_start'\n | 'tracking_stop'\n | 'providerchange';\nexport type GeofenceAction = 'ENTER' | 'EXIT' | 'DWELL';\nexport type HttpMethod = 'POST' | 'PUT' | 'PATCH';\nexport type NotificationPriority = 'min' | 'low' | 'default' | 'high' | 'max';\nexport type PermissionType = 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications';\nexport type ConnectivityType = 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none';\nexport type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled';\n/** Rejection `code` of every failed promise. */\nexport type ErrorCode =\n | 'NOT_READY'\n | 'PERMISSION_DENIED'\n | 'LOCATION_DISABLED'\n | 'TIMEOUT'\n | 'UNAVAILABLE'\n | 'INVALID_ARGUMENT'\n | 'NOT_FOUND'\n | 'NO_URL'\n | 'HTTP_ERROR'\n | 'NETWORK_ERROR'\n | 'TOO_MANY_GEOFENCES'\n | 'NO_ACTIVITY'\n | 'IO_ERROR'\n | 'UNIMPLEMENTED'\n | 'INTERNAL';\n\n/* ================= CONFIG (all optional; defaults shown) ================= */\n\nexport interface LocationFilterConfig {\n /** @default false */\n useKalman?: boolean;\n /**\n * Reject fixes with accuracy worse than this (m).\n * @default 100\n */\n trackingAccuracyThreshold?: number;\n /**\n * Reject fixes implying speed above this (m/s); 0 = off.\n * @default 80\n */\n maxImpliedSpeed?: number;\n /**\n * Odometer ignores fixes worse than this (m).\n * @default 20\n */\n odometerAccuracyThreshold?: number;\n /** @default false */\n allowIdenticalLocations?: boolean;\n /**\n * Drop mock fixes entirely (otherwise they are kept with mock:true).\n * @default false\n */\n rejectMockLocations?: boolean;\n}\n\nexport interface GeolocationConfig {\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /**\n * meters\n * @default 10\n */\n distanceFilter?: number;\n /**\n * ms\n * @default 1000\n */\n locationUpdateInterval?: number;\n /**\n * ms\n * @default 500\n */\n fastestLocationUpdateInterval?: number;\n /** @default false */\n disableElasticity?: boolean;\n /** @default 1 */\n elasticityMultiplier?: number;\n /**\n * meters\n * @default 25\n */\n stationaryRadius?: number;\n /**\n * minutes\n * @default 5\n */\n stopTimeout?: number;\n /**\n * minutes, 0 = off\n * @default 0\n */\n stopAfterElapsedMinutes?: number;\n /** @default false */\n stopOnStationary?: boolean;\n /**\n * default getCurrentPosition timeout, ms\n * @default 30000\n */\n locationTimeout?: number;\n filter?: LocationFilterConfig;\n}\n\nexport interface ActivityConfig {\n /** @default false */\n disableMotionActivityUpdates?: boolean;\n /**\n * ms\n * @default 10000\n */\n activityRecognitionInterval?: number;\n /**\n * 0-100\n * @default 75\n */\n minimumActivityRecognitionConfidence?: number;\n /**\n * ms\n * @default 0\n */\n motionTriggerDelay?: number;\n /** @default false */\n disableStopDetection?: boolean;\n}\n\nexport interface HeartbeatConfig {\n /** @default true */\n enabled?: boolean;\n /**\n * seconds, min 60\n * @default 180\n */\n minInterval?: number;\n /**\n * seconds, >= minInterval\n * @default 300\n */\n maxInterval?: number;\n /**\n * seconds, 0..60. After a heartbeat is queued, keep the phone awake until its upload has finished, or at most this\n * long, so it reaches the server on time while tracking is on. 0 = do not keep the phone awake.\n * @default 10\n */\n uploadKeepAwake?: number;\n}\n\nexport interface AuthorizationConfig {\n /** @default 'JWT' */\n strategy?: 'JWT';\n accessToken?: string;\n refreshToken?: string;\n refreshUrl?: string;\n /**\n * String values may contain `{refreshToken}`.\n * @default {}\n */\n refreshPayload?: Record<string, string>;\n refreshHeaders?: Record<string, string>;\n /** @default 'json' */\n refreshPayloadEncoding?: 'json' | 'form';\n /**\n * access-token expiry, epoch ms; -1 = unknown\n * @default -1\n */\n expires?: number;\n}\n\nexport interface HttpConfig {\n /** No url => nothing is uploaded (records stay queued). */\n url?: string | null;\n /** @default 'POST' */\n method?: HttpMethod;\n /** @default {} */\n headers?: Record<string, string>;\n /**\n * merged into the ROOT of every request body\n * @default {}\n */\n params?: Record<string, unknown>;\n /** @default true */\n autoSync?: boolean;\n /**\n * upload when queue >= threshold; 0 = every record\n * @default 0\n */\n autoSyncThreshold?: number;\n /**\n * s; upload normal records once the oldest queued one is this old; 0 = off\n * @default 0\n */\n syncInterval?: number;\n /** @default false */\n batchSync?: boolean;\n /** @default 100 */\n maxBatchSize?: number;\n /** @default false */\n disableAutoSyncOnCellular?: boolean;\n /**\n * '.' = no wrapping\n * @default 'location'\n */\n rootProperty?: string;\n /** JSON text with `<%= name %>` placeholders */\n locationTemplate?: string | null;\n /** used for event 'geofence'; falls back to locationTemplate */\n geofenceTemplate?: string | null;\n /**\n * ms\n * @default 60000\n */\n timeout?: number;\n /**\n * seconds, >= 0. The longest wait that a `Retry-After` header of a 429 or 503 answer can impose on automatic uploads.\n * @default 3600\n */\n maxRetryAfter?: number;\n authorization?: AuthorizationConfig | null;\n}\n\nexport interface PersistenceConfig {\n /** @default 7 */\n maxDaysToPersist?: number;\n /**\n * -1 = unlimited\n * @default -1\n */\n maxRecordsToPersist?: number;\n /**\n * merged into `extras` of every record at creation time\n * @default {}\n */\n extras?: Record<string, unknown>;\n}\n\nexport interface AppConfig {\n /** @default true */\n stopOnTerminate?: boolean;\n /** @default false */\n startOnBoot?: boolean;\n}\n\nexport interface NotificationActionButton {\n id: string;\n label: string;\n}\n\nexport interface NotificationConfig {\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is active' */\n text?: string;\n /**\n * 'drawable/name' | 'mipmap/name'\n * @default plugin icon 'drawable/lt_ic_notification'\n */\n smallIcon?: string;\n largeIcon?: string;\n /** '#RRGGBB' */\n color?: string;\n /** @default 'default' */\n priority?: NotificationPriority;\n /** @default 'location_tracking' */\n channelId?: string;\n /** @default 'Location tracking' */\n channelName?: string;\n /** max 3; tap => 'notificationaction' event */\n actions?: NotificationActionButton[];\n /** notification that resumes tracking when Android refuses to restore it from the background (Android 14+) */\n resume?: ResumeNotificationConfig;\n}\n\n/**\n * Posted when Android refuses to restore tracking from the background (after a reboot, an app update or a process\n * restart), typically on Android 14+ without \"Allow all the time\". A tap resumes the session\n * (`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.\n */\nexport interface ResumeNotificationConfig {\n /** @default false */\n enabled?: boolean;\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is paused. Tap to resume.' */\n text?: string;\n /** @default 'Paused location tracking' */\n channelName?: string;\n}\n\nexport interface GeofenceConfig {\n /** @default true */\n initialTriggerEntry?: boolean;\n}\n\nexport interface LoggerConfig {\n /** @default 'info' */\n logLevel?: LogLevel;\n /** @default 3 */\n logMaxDays?: number;\n}\n\nexport interface BackgroundPermissionRationale {\n /** defaults from plugin string resources */\n title?: string;\n message?: string;\n positiveAction?: string;\n negativeAction?: string;\n}\n\nexport interface Config {\n geolocation?: GeolocationConfig;\n activity?: ActivityConfig;\n heartbeat?: HeartbeatConfig;\n http?: HttpConfig;\n persistence?: PersistenceConfig;\n app?: AppConfig;\n notification?: NotificationConfig;\n geofence?: GeofenceConfig;\n logger?: LoggerConfig;\n backgroundPermissionRationale?: BackgroundPermissionRationale;\n /**\n * Android only\n * @default 'auto'\n */\n locationProvider?: LocationProviderSetting;\n}\n\nexport interface ReadyOptions {\n config?: Config;\n /**\n * true: config = defaults + given config (every launch). false: given config applied only on the very first\n * ready(); afterwards persisted config wins.\n * @default true\n */\n reset?: boolean;\n}\n\nexport interface State {\n enabled: boolean;\n trackingMode: TrackingMode;\n isMoving: boolean;\n odometer: number;\n backend: LocationBackend;\n lastRecordAt: string | null;\n /** reason of the last `tracking_stop` record (the `tracking_stop` reasons of `Location.reason`); null if none */\n lastStopReason: string | null;\n /** when tracking stopped for `lastStopReason` (ISO-8601 UTC ms); null if none */\n lastStopAt: string | null;\n /**\n * fully populated with defaults. On Android the `http.authorization` secrets (`accessToken`, `refreshToken`, the\n * values of `refreshPayload` and `refreshHeaders`) are masked: `***`, or `***` plus the last 4 characters for values\n * longer than 8 characters; a `refreshPayload` value that contains `{refreshToken}` is shown. Passing a masked value\n * back to `setConfig` keeps the stored one. The web stub returns them as given.\n */\n config: Config;\n /**\n * `ready()`, `setConfig()` and `reset()` only: what the given config contains that was accepted but is probably\n * wrong (for example an `http://` upload URL).\n */\n warnings?: string[];\n}\n\n/* ================= RECORDS (snake_case = identical to wire format) ================= */\n\nexport interface Coords {\n latitude: number;\n longitude: number;\n accuracy: number;\n altitude: number | null;\n altitude_accuracy: number | null;\n speed: number | null;\n speed_accuracy: number | null;\n heading: number | null;\n heading_accuracy: number | null;\n}\n\nexport interface ProviderState {\n enabled: boolean;\n gps: boolean;\n network: boolean;\n permission: 'always' | 'when_in_use' | 'denied';\n accuracy: 'precise' | 'approximate' | 'none';\n backend: LocationBackend;\n}\n\nexport interface Location {\n uuid: string;\n event: RecordEvent;\n /** fix time (ISO-8601 UTC ms); null if no location ever known */\n timestamp: string | null;\n /** record creation time */\n recorded_at: string;\n /** only present in HTTP bodies */\n sent_at?: string;\n elapsed_realtime_ms: number;\n /** -1 if unavailable */\n boot_count: number;\n is_moving: boolean;\n odometer: number;\n mock: boolean;\n coords: Coords | null;\n activity: { type: ActivityType; confidence: number };\n /**\n * level 0..1, -1 unknown. Heartbeat records also carry `temperature` (°C), `health` and `plugged` when the device\n * reports them.\n */\n battery: {\n level: number;\n is_charging: boolean;\n temperature?: number;\n health?: 'good' | 'overheat' | 'dead' | 'over_voltage' | 'cold' | 'failure' | 'unknown';\n plugged?: 'ac' | 'usb' | 'wireless' | 'dock' | 'none' | 'other';\n };\n backend: LocationBackend | null;\n extras?: Record<string, unknown>;\n /** event 'geofence' only */\n geofence?: { identifier: string; action: GeofenceAction; extras?: Record<string, unknown> };\n /** provider state when the record was created; the new state for event 'providerchange' */\n provider?: ProviderState;\n /**\n * tracking_start: start|start_geofences|boot|restore|package_replaced|resume_notification;\n * tracking_stop: stop|stop_on_stationary|stop_after_elapsed|terminate|permission_denied|service_start_failed|reboot|package_replaced|user_stopped\n */\n reason?: string;\n /** event 'tracking_stop' with reason 'user_stopped' only: when the user stopped the app (ISO-8601 UTC ms) */\n stopped_at?: string;\n /** event 'heartbeat' only, optional: how the heartbeat is scheduled */\n heartbeat?: HeartbeatMeta;\n /** connection type when the record was created */\n network?: ConnectivityType;\n /** the device id on every record; more readings on 'heartbeat' and 'tracking_start' records (see `DeviceReadings`) */\n device?: DeviceReadings;\n}\n\n/**\n * The `device` object of records; a reading is present only when the device provides it.\n * Every record: `device_id`.\n * Heartbeat records: `background_restricted`, `standby_bucket`, `disk_free_bytes`, `thermal_status`.\n * `tracking_start` records: `manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes`.\n */\nexport interface DeviceReadings {\n /**\n * the device id: on Android, `Settings.Secure.ANDROID_ID`; other platforms will fill it with their own id.\n * Absent in records queued by an older version. Declare \"Device or other IDs\" in Google Play's Data safety form.\n */\n device_id?: string;\n /** the user set the app's battery use to \"Restricted\" (Android 9+) */\n background_restricted?: boolean;\n /** App Standby bucket (Android 9+) */\n standby_bucket?: 'exempted' | 'active' | 'working_set' | 'frequent' | 'rare' | 'restricted';\n /** free space on the storage that holds the app's files */\n disk_free_bytes?: number;\n /** Android 10+ */\n thermal_status?: 'none' | 'light' | 'moderate' | 'severe' | 'critical' | 'emergency' | 'shutdown' | 'unknown';\n manufacturer?: string;\n model?: string;\n /** Android version, e.g. \"14\" */\n os_version?: string;\n sdk_int?: number;\n low_ram_device?: boolean;\n ram_total_bytes?: number;\n}\n\n/** Scheduling metadata of a `heartbeat` record (snake_case = wire format). */\nexport interface HeartbeatMeta {\n strategy: 'exact' | 'listener_with_backup' | 'idle_paced';\n /** heartbeat.minInterval (s) when the heartbeat was created */\n min_interval: number;\n /** heartbeat.maxInterval (s) when the heartbeat was created */\n max_interval: number;\n /** when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown */\n next_at: string | null;\n /** the app is exempt from battery optimization */\n battery_exempt: boolean;\n /** the device was in deep Doze */\n device_idle: boolean;\n}\n\nexport type LocationRecord = Location;\n\nexport interface InsertLocationInput {\n /** `accuracy` (m) is required and must be greater than 0; otherwise the call rejects with `INVALID_ARGUMENT`. */\n coords: Pick<Coords, 'latitude' | 'longitude' | 'accuracy'> & Partial<Coords>;\n timestamp?: string;\n event?: RecordEvent;\n is_moving?: boolean;\n extras?: Record<string, unknown>;\n}\n\n/* ================= GEOFENCES ================= */\n\n/**\n * Either (latitude, longitude, radius) = circle, or vertices = polygon. For polygons, getGeofences() also returns\n * the computed enclosing circle.\n */\nexport interface Geofence {\n identifier: string;\n latitude?: number;\n longitude?: number;\n /** m */\n radius?: number;\n /** [[lat,lng], ...] >= 3 points */\n vertices?: [number, number][];\n /** @default true */\n notifyOnEntry?: boolean;\n /** @default true */\n notifyOnExit?: boolean;\n /** @default false */\n notifyOnDwell?: boolean;\n /**\n * ms\n * @default 30000\n */\n loiteringDelay?: number;\n extras?: Record<string, unknown>;\n}\n\n/* ================= EVENTS ================= */\n\nexport interface MotionChangeEvent {\n isMoving: boolean;\n location: Location;\n}\nexport interface ActivityChangeEvent {\n activity: ActivityType;\n confidence: number;\n}\nexport interface HeartbeatEvent {\n location: Location;\n}\nexport interface GeofenceEvent {\n identifier: string;\n action: GeofenceAction;\n location: Location;\n extras?: Record<string, unknown>;\n}\nexport interface GeofencesChangeEvent {\n on: Geofence[];\n off: string[];\n}\nexport interface HttpEvent {\n success: boolean;\n status: number;\n responseText: string;\n uuids: string[];\n}\nexport interface ConnectivityChangeEvent {\n connected: boolean;\n type: ConnectivityType;\n}\nexport interface PowerSaveChangeEvent {\n isPowerSaveMode: boolean;\n}\nexport interface EnabledChangeEvent {\n enabled: boolean;\n /**\n * why: a `tracking_start` reason when tracking was switched on (start, start_geofences, resume_notification), a\n * `tracking_stop` reason when it was switched off; the same values as `Location.reason`\n */\n reason: string | null;\n}\n/** The location request failed (no permission, Play services or HMS Core refused it, no usable provider). */\nexport interface LocationErrorEvent {\n code: ErrorCode;\n message: string;\n backend: LocationBackend;\n /** the plugin requests location updates again after this many ms; null if it does not */\n retryInMs: number | null;\n}\n/** A record could not be stored because the device storage is full. */\nexport interface StorageFullEvent {\n uuid: string;\n event: RecordEvent;\n diskFreeBytes: number | null;\n}\nexport interface NotificationActionEvent {\n id: string;\n}\nexport interface AuthorizationEvent {\n success: boolean;\n status: number;\n error?: string;\n response?: Record<string, unknown>;\n}\nexport interface LocationTrackingEventMap {\n location: Location;\n motionchange: MotionChangeEvent;\n activitychange: ActivityChangeEvent;\n providerchange: ProviderState;\n heartbeat: HeartbeatEvent;\n geofence: GeofenceEvent;\n geofenceschange: GeofencesChangeEvent;\n http: HttpEvent;\n connectivitychange: ConnectivityChangeEvent;\n powersavechange: PowerSaveChangeEvent;\n enabledchange: EnabledChangeEvent;\n notificationaction: NotificationActionEvent;\n authorization: AuthorizationEvent;\n locationerror: LocationErrorEvent;\n storagefull: StorageFullEvent;\n}\nexport type LocationTrackingEventName = keyof LocationTrackingEventMap;\n\n/* ================= MISC ================= */\n\nexport interface CurrentPositionOptions {\n /** @default 3 */\n samples?: number;\n /**\n * ms\n * @default geolocation.locationTimeout\n */\n timeout?: number;\n /**\n * ms\n * @default 0\n */\n maximumAge?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default true */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport interface WatchPositionOptions {\n /**\n * ms\n * @default 1000\n */\n interval?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default false */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport type WatchPositionCallback = (location: Location | null, error?: { code: ErrorCode; message: string }) => void;\n\nexport interface HeartbeatStatus {\n enabled: boolean;\n minInterval: number;\n maxInterval: number;\n lastRecordAt: string | null;\n lastHeartbeatAt: string | null;\n nextHeartbeatAt: string | null;\n strategy: HeartbeatStrategy;\n canScheduleExactAlarms: boolean;\n isIgnoringBatteryOptimizations: boolean;\n isDeviceIdleMode: boolean;\n isPowerSaveMode: boolean;\n pendingHeartbeats: number;\n}\n\nexport interface BatteryOptimizationStatus {\n isIgnoringBatteryOptimizations: boolean;\n canScheduleExactAlarms: boolean;\n isDeviceIdleMode: boolean;\n}\n\nexport interface PowerManagerInfo {\n manufacturer: string;\n available: boolean;\n}\n\nexport interface DeviceInfo {\n platform: 'android' | 'web';\n manufacturer: string;\n model: string;\n brand: string;\n osVersion: string;\n sdkInt: number;\n pluginVersion: string;\n gmsAvailable: boolean;\n hmsAvailable: boolean;\n backend: LocationBackend;\n packagedProviders: string[];\n /** why a better location backend is not used (for example \"gms: unavailable (status 2)\"); null when none is better */\n backendFallbackReason: string | null;\n /** raw GoogleApiAvailability result (0 = success); null if GMS is not packaged or not checked yet */\n gmsStatus: number | null;\n /** raw HuaweiApiAvailability result (0 = success); null if HMS is not packaged or not checked yet */\n hmsStatus: number | null;\n /** true when HMS Core is older than 6.3.0.301; null if HMS is not packaged */\n hmsCoreOutdated: boolean | null;\n /** last LocationAvailability reported by GMS or HMS; only a hint (it can be wrong both ways); null if none */\n locationAvailableHint: boolean | null;\n /** number of CPU cores */\n cpuCores: number;\n /** CPU types (Build.SUPPORTED_ABIS), e.g. [\"arm64-v8a\"] */\n supportedAbis: string[];\n}\n\nexport interface Sensors {\n accelerometer: boolean;\n gyroscope: boolean;\n magnetometer: boolean;\n significantMotion: boolean;\n stepCounter: boolean;\n stepDetector: boolean;\n barometer: boolean;\n}\n\nexport interface PermissionStatus {\n location: PermissionState;\n backgroundLocation: PermissionState;\n activityRecognition: PermissionState;\n notifications: PermissionState;\n}\n\nexport interface LogQuery {\n start?: number;\n end?: number;\n level?: LogLevel;\n limit?: number;\n order?: 'asc' | 'desc';\n}\n\nexport interface LocationTrackingPlugin {\n /** Loads persisted config and state. Must resolve before most other methods (see NOT_READY rule). */\n ready(options?: ReadyOptions): Promise<State>;\n /** deep merge; arrays replaced; null resets key to default */\n setConfig(options: { config: Config }): Promise<State>;\n /** Resets the config to defaults, then applies the given config. */\n reset(options?: { config?: Config }): Promise<State>;\n getState(): Promise<State>;\n /** Starts location tracking (mode 'location'). */\n start(): Promise<State>;\n /** Starts geofence-only tracking (mode 'geofences'). */\n startGeofences(): Promise<State>;\n stop(): Promise<State>;\n /** Forces the motion state to moving or stationary. */\n changePace(options: { isMoving: boolean }): Promise<void>;\n\n getCurrentPosition(options?: CurrentPositionOptions): Promise<Location>;\n watchPosition(options: WatchPositionOptions, callback: WatchPositionCallback): Promise<CallbackID>;\n clearWatch(options: { id: CallbackID }): Promise<void>;\n\n getOdometer(): Promise<{ odometer: number }>;\n setOdometer(options: { odometer: number }): Promise<{ odometer: number }>;\n resetOdometer(): Promise<{ odometer: number }>;\n\n getLocations(options?: { limit?: number }): Promise<{ locations: Location[] }>;\n getCount(): Promise<{ count: number }>;\n insertLocation(options: { location: InsertLocationInput }): Promise<{ uuid: string }>;\n destroyLocations(): Promise<{ count: number }>;\n destroyLocation(options: { uuid: string }): Promise<{ deleted: boolean }>;\n /** uploads whole queue now; resolves with uploaded records */\n sync(): Promise<{ locations: Location[] }>;\n\n addGeofence(options: { geofence: Geofence }): Promise<void>;\n addGeofences(options: { geofences: Geofence[] }): Promise<void>;\n removeGeofence(options: { identifier: string }): Promise<void>;\n /** identifiers omitted (or null) = remove all; an empty array removes nothing */\n removeGeofences(options?: { identifiers?: string[] }): Promise<void>;\n getGeofences(): Promise<{ geofences: Geofence[] }>;\n getGeofence(options: { identifier: string }): Promise<{ geofence: Geofence | null }>;\n geofenceExists(options: { identifier: string }): Promise<{ exists: boolean }>;\n\n getHeartbeatStatus(): Promise<HeartbeatStatus>;\n\n getProviderState(): Promise<ProviderState>;\n isPowerSaveMode(): Promise<{ isPowerSaveMode: boolean }>;\n getBatteryOptimizationStatus(): Promise<BatteryOptimizationStatus>;\n openBatteryOptimizationSettings(): Promise<{ opened: boolean }>;\n getPowerManagerInfo(): Promise<PowerManagerInfo>;\n openPowerManagerSettings(): Promise<{ opened: boolean }>;\n openLocationSettings(): Promise<{ opened: boolean }>;\n openAppSettings(): Promise<{ opened: boolean }>;\n getDeviceInfo(): Promise<DeviceInfo>;\n getSensors(): Promise<Sensors>;\n\n checkPermissions(): Promise<PermissionStatus>;\n /** default all four, order: location, notifications, activityRecognition, backgroundLocation */\n requestPermissions(options?: { permissions?: PermissionType[] }): Promise<PermissionStatus>;\n\n log(options: { level: Exclude<LogLevel, 'off'>; message: string }): Promise<void>;\n getLog(options?: LogQuery): Promise<{ log: string }>;\n destroyLog(): Promise<void>;\n uploadLog(options: {\n url: string;\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n }): Promise<{ success: boolean; status: number }>;\n emailLog(options: { email: string; subject?: string }): Promise<void>;\n\n addListener(eventName: 'location', listenerFunc: (e: Location) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'motionchange', listenerFunc: (e: MotionChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'activitychange',\n listenerFunc: (e: ActivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'providerchange', listenerFunc: (e: ProviderState) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'heartbeat', listenerFunc: (e: HeartbeatEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'geofence', listenerFunc: (e: GeofenceEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'geofenceschange',\n listenerFunc: (e: GeofencesChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'http', listenerFunc: (e: HttpEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'connectivitychange',\n listenerFunc: (e: ConnectivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'powersavechange',\n listenerFunc: (e: PowerSaveChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'enabledchange', listenerFunc: (e: EnabledChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'notificationaction',\n listenerFunc: (e: NotificationActionEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'locationerror', listenerFunc: (e: LocationErrorEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'storagefull', listenerFunc: (e: StorageFullEvent) => void): Promise<PluginListenerHandle>;\n removeAllListeners(): Promise<void>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CallbackID = string;\nexport type DesiredAccuracy = 'high' | 'balanced' | 'low' | 'passive';\nexport type LocationProviderSetting = 'auto' | 'gms' | 'hms' | 'android';\nexport type LocationBackend = 'gms' | 'hms' | 'android' | 'web' | 'ios';\nexport type TrackingMode = 'location' | 'geofences';\nexport type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';\nexport type ActivityType = 'still' | 'on_foot' | 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'unknown';\nexport type RecordEvent =\n | 'location'\n | 'motionchange'\n | 'current_position'\n | 'watch_position'\n | 'heartbeat'\n | 'geofence'\n | 'tracking_start'\n | 'tracking_stop'\n | 'providerchange';\nexport type GeofenceAction = 'ENTER' | 'EXIT' | 'DWELL';\nexport type HttpMethod = 'POST' | 'PUT' | 'PATCH';\nexport type NotificationPriority = 'min' | 'low' | 'default' | 'high' | 'max';\nexport type PermissionType = 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications';\nexport type ConnectivityType = 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none';\nexport type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer' | 'disabled';\n/** Rejection `code` of every failed promise. */\nexport type ErrorCode =\n | 'NOT_READY'\n | 'PERMISSION_DENIED'\n | 'LOCATION_DISABLED'\n | 'TIMEOUT'\n | 'UNAVAILABLE'\n | 'INVALID_ARGUMENT'\n | 'NOT_FOUND'\n | 'NO_URL'\n | 'HTTP_ERROR'\n | 'NETWORK_ERROR'\n | 'TOO_MANY_GEOFENCES'\n | 'NO_ACTIVITY'\n | 'IO_ERROR'\n | 'UNIMPLEMENTED'\n | 'INTERNAL';\n\n/* ================= CONFIG (all optional; defaults shown) ================= */\n\nexport interface LocationFilterConfig {\n /** @default false */\n useKalman?: boolean;\n /**\n * Reject fixes with accuracy worse than this (m).\n * @default 100\n */\n trackingAccuracyThreshold?: number;\n /**\n * Reject fixes implying speed above this (m/s); 0 = off.\n * @default 80\n */\n maxImpliedSpeed?: number;\n /**\n * Odometer ignores fixes worse than this (m).\n * @default 20\n */\n odometerAccuracyThreshold?: number;\n /** @default false */\n allowIdenticalLocations?: boolean;\n /**\n * Drop mock fixes entirely (otherwise they are kept with mock:true).\n * @default false\n */\n rejectMockLocations?: boolean;\n}\n\nexport interface GeolocationConfig {\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /**\n * meters\n * @default 10\n */\n distanceFilter?: number;\n /**\n * ms\n * @default 1000\n */\n locationUpdateInterval?: number;\n /**\n * ms\n * @default 500\n */\n fastestLocationUpdateInterval?: number;\n /** @default false */\n disableElasticity?: boolean;\n /** @default 1 */\n elasticityMultiplier?: number;\n /**\n * meters\n * @default 25\n */\n stationaryRadius?: number;\n /**\n * minutes\n * @default 5\n */\n stopTimeout?: number;\n /**\n * minutes, 0 = off\n * @default 0\n */\n stopAfterElapsedMinutes?: number;\n /** @default false */\n stopOnStationary?: boolean;\n /**\n * default getCurrentPosition timeout, ms\n * @default 30000\n */\n locationTimeout?: number;\n filter?: LocationFilterConfig;\n}\n\nexport interface ActivityConfig {\n /** @default false */\n disableMotionActivityUpdates?: boolean;\n /**\n * ms\n * @default 10000\n */\n activityRecognitionInterval?: number;\n /**\n * 0-100\n * @default 75\n */\n minimumActivityRecognitionConfidence?: number;\n /**\n * ms\n * @default 0\n */\n motionTriggerDelay?: number;\n /** @default false */\n disableStopDetection?: boolean;\n}\n\nexport interface HeartbeatConfig {\n /** @default true */\n enabled?: boolean;\n /**\n * seconds, min 60\n * @default 180\n */\n minInterval?: number;\n /**\n * seconds, >= minInterval\n * @default 300\n */\n maxInterval?: number;\n /**\n * seconds, 0..60. After a heartbeat is queued, keep the phone awake until its upload has finished, or at most this\n * long, so it reaches the server on time while tracking is on. 0 = do not keep the phone awake.\n * @default 10\n */\n uploadKeepAwake?: number;\n}\n\nexport interface AuthorizationConfig {\n /** @default 'JWT' */\n strategy?: 'JWT';\n accessToken?: string;\n refreshToken?: string;\n refreshUrl?: string;\n /**\n * String values may contain `{refreshToken}`.\n * @default {}\n */\n refreshPayload?: Record<string, string>;\n refreshHeaders?: Record<string, string>;\n /** @default 'json' */\n refreshPayloadEncoding?: 'json' | 'form';\n /**\n * access-token expiry, epoch ms; -1 = unknown\n * @default -1\n */\n expires?: number;\n}\n\nexport interface HttpConfig {\n /** No url => nothing is uploaded (records stay queued). */\n url?: string | null;\n /** @default 'POST' */\n method?: HttpMethod;\n /** @default {} */\n headers?: Record<string, string>;\n /**\n * merged into the ROOT of every request body\n * @default {}\n */\n params?: Record<string, unknown>;\n /** @default true */\n autoSync?: boolean;\n /**\n * upload when queue >= threshold; 0 = every record\n * @default 0\n */\n autoSyncThreshold?: number;\n /**\n * s; upload normal records once the oldest queued one is this old; 0 = off\n * @default 0\n */\n syncInterval?: number;\n /** @default false */\n batchSync?: boolean;\n /** @default 100 */\n maxBatchSize?: number;\n /** @default false */\n disableAutoSyncOnCellular?: boolean;\n /**\n * '.' = no wrapping\n * @default 'location'\n */\n rootProperty?: string;\n /** JSON text with `<%= name %>` placeholders */\n locationTemplate?: string | null;\n /** used for event 'geofence'; falls back to locationTemplate */\n geofenceTemplate?: string | null;\n /**\n * ms\n * @default 60000\n */\n timeout?: number;\n /**\n * seconds, >= 0. The longest wait that a `Retry-After` header of a 429 or 503 answer can impose on automatic uploads.\n * @default 3600\n */\n maxRetryAfter?: number;\n authorization?: AuthorizationConfig | null;\n}\n\nexport interface PersistenceConfig {\n /** @default 7 */\n maxDaysToPersist?: number;\n /**\n * -1 = unlimited\n * @default -1\n */\n maxRecordsToPersist?: number;\n /**\n * merged into `extras` of every record at creation time\n * @default {}\n */\n extras?: Record<string, unknown>;\n}\n\nexport interface AppConfig {\n /** @default true */\n stopOnTerminate?: boolean;\n /** @default false */\n startOnBoot?: boolean;\n}\n\nexport interface NotificationActionButton {\n id: string;\n label: string;\n}\n\nexport interface NotificationConfig {\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is active' */\n text?: string;\n /**\n * 'drawable/name' | 'mipmap/name'\n * @default plugin icon 'drawable/lt_ic_notification'\n */\n smallIcon?: string;\n largeIcon?: string;\n /** '#RRGGBB' */\n color?: string;\n /** @default 'default' */\n priority?: NotificationPriority;\n /** @default 'location_tracking' */\n channelId?: string;\n /** @default 'Location tracking' */\n channelName?: string;\n /** max 3; tap => 'notificationaction' event */\n actions?: NotificationActionButton[];\n /** notification that resumes tracking when Android refuses to restore it from the background (Android 14+) */\n resume?: ResumeNotificationConfig;\n}\n\n/**\n * Posted when Android refuses to restore tracking from the background (after a reboot, an app update or a process\n * restart), typically on Android 14+ without \"Allow all the time\". A tap resumes the session\n * (`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.\n */\nexport interface ResumeNotificationConfig {\n /**\n * iOS only (Android warns about an unknown key): seconds from the last activity to the check-in notification;\n * raised to `heartbeat.maxInterval + 60` when lower.\n * @default 600\n */\n delay?: number;\n /** @default false */\n enabled?: boolean;\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is paused. Tap to resume.' */\n text?: string;\n /** @default 'Paused location tracking' */\n channelName?: string;\n}\n\nexport interface GeofenceConfig {\n /** @default true */\n initialTriggerEntry?: boolean;\n}\n\nexport interface LoggerConfig {\n /** @default 'info' */\n logLevel?: LogLevel;\n /** @default 3 */\n logMaxDays?: number;\n}\n\nexport interface BackgroundPermissionRationale {\n /** defaults from plugin string resources */\n title?: string;\n message?: string;\n positiveAction?: string;\n negativeAction?: string;\n}\n\nexport interface Config {\n geolocation?: GeolocationConfig;\n activity?: ActivityConfig;\n heartbeat?: HeartbeatConfig;\n http?: HttpConfig;\n persistence?: PersistenceConfig;\n app?: AppConfig;\n notification?: NotificationConfig;\n geofence?: GeofenceConfig;\n logger?: LoggerConfig;\n backgroundPermissionRationale?: BackgroundPermissionRationale;\n /**\n * Android only\n * @default 'auto'\n */\n locationProvider?: LocationProviderSetting;\n}\n\nexport interface ReadyOptions {\n config?: Config;\n /**\n * true: config = defaults + given config (every launch). false: given config applied only on the very first\n * ready(); afterwards persisted config wins.\n * @default true\n */\n reset?: boolean;\n}\n\nexport interface State {\n enabled: boolean;\n trackingMode: TrackingMode;\n isMoving: boolean;\n odometer: number;\n backend: LocationBackend;\n lastRecordAt: string | null;\n /** reason of the last `tracking_stop` record (the `tracking_stop` reasons of `Location.reason`); null if none */\n lastStopReason: string | null;\n /** when tracking stopped for `lastStopReason` (ISO-8601 UTC ms); null if none */\n lastStopAt: string | null;\n /**\n * fully populated with defaults. On Android the `http.authorization` secrets (`accessToken`, `refreshToken`, the\n * values of `refreshPayload` and `refreshHeaders`) are masked: `***`, or `***` plus the last 4 characters for values\n * longer than 8 characters; a `refreshPayload` value that contains `{refreshToken}` is shown. Passing a masked value\n * back to `setConfig` keeps the stored one. The web stub returns them as given.\n */\n config: Config;\n /**\n * `ready()`, `setConfig()` and `reset()` only: what the given config contains that was accepted but is probably\n * wrong (for example an `http://` upload URL).\n */\n warnings?: string[];\n}\n\n/* ================= RECORDS (snake_case = identical to wire format) ================= */\n\nexport interface Coords {\n latitude: number;\n longitude: number;\n accuracy: number;\n altitude: number | null;\n altitude_accuracy: number | null;\n speed: number | null;\n speed_accuracy: number | null;\n heading: number | null;\n heading_accuracy: number | null;\n}\n\nexport interface ProviderState {\n enabled: boolean;\n gps: boolean;\n network: boolean;\n permission: 'always' | 'when_in_use' | 'denied';\n accuracy: 'precise' | 'approximate' | 'none';\n backend: LocationBackend;\n}\n\nexport interface Location {\n uuid: string;\n event: RecordEvent;\n /** fix time (ISO-8601 UTC ms); null if no location ever known */\n timestamp: string | null;\n /** record creation time */\n recorded_at: string;\n /** only present in HTTP bodies */\n sent_at?: string;\n elapsed_realtime_ms: number;\n /** -1 if unavailable */\n boot_count: number;\n is_moving: boolean;\n odometer: number;\n mock: boolean;\n coords: Coords | null;\n activity: { type: ActivityType; confidence: number };\n /**\n * level 0..1, -1 unknown. Heartbeat records also carry `temperature` (°C), `health` and `plugged` when the device\n * reports them.\n */\n battery: {\n level: number;\n is_charging: boolean;\n temperature?: number;\n health?: 'good' | 'overheat' | 'dead' | 'over_voltage' | 'cold' | 'failure' | 'unknown';\n plugged?: 'ac' | 'usb' | 'wireless' | 'dock' | 'none' | 'other';\n };\n backend: LocationBackend | null;\n extras?: Record<string, unknown>;\n /** event 'geofence' only */\n geofence?: { identifier: string; action: GeofenceAction; extras?: Record<string, unknown> };\n /** provider state when the record was created; the new state for event 'providerchange' */\n provider?: ProviderState;\n /**\n * tracking_start: start|start_geofences|boot|restore|package_replaced|resume_notification;\n * tracking_stop: stop|stop_on_stationary|stop_after_elapsed|terminate|permission_denied|service_start_failed|reboot|package_replaced|user_stopped\n */\n reason?: string;\n /** event 'tracking_stop' with reason 'user_stopped' only: when the user stopped the app (ISO-8601 UTC ms) */\n stopped_at?: string;\n /** event 'heartbeat' only, optional: how the heartbeat is scheduled */\n heartbeat?: HeartbeatMeta;\n /** connection type when the record was created */\n network?: ConnectivityType;\n /** the device id on every record; more readings on 'heartbeat' and 'tracking_start' records (see `DeviceReadings`) */\n device?: DeviceReadings;\n}\n\n/**\n * The `device` object of records; a reading is present only when the device provides it.\n * Every record: `device_id`.\n * Heartbeat records: `background_restricted`, `standby_bucket`, `disk_free_bytes`, `thermal_status`.\n * `tracking_start` records: `manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes`.\n */\nexport interface DeviceReadings {\n /**\n * the device id: on Android, `Settings.Secure.ANDROID_ID`; other platforms will fill it with their own id.\n * Absent in records queued by an older version. Declare \"Device or other IDs\" in Google Play's Data safety form.\n */\n device_id?: string;\n /** the user set the app's battery use to \"Restricted\" (Android 9+) */\n background_restricted?: boolean;\n /** App Standby bucket (Android 9+) */\n standby_bucket?: 'exempted' | 'active' | 'working_set' | 'frequent' | 'rare' | 'restricted';\n /** free space on the storage that holds the app's files */\n disk_free_bytes?: number;\n /** Android 10+ */\n thermal_status?: 'none' | 'light' | 'moderate' | 'severe' | 'critical' | 'emergency' | 'shutdown' | 'unknown';\n manufacturer?: string;\n model?: string;\n /** Android version, e.g. \"14\" */\n os_version?: string;\n sdk_int?: number;\n low_ram_device?: boolean;\n ram_total_bytes?: number;\n}\n\n/** Scheduling metadata of a `heartbeat` record (snake_case = wire format). */\nexport interface HeartbeatMeta {\n strategy: 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer';\n /** heartbeat.minInterval (s) when the heartbeat was created */\n min_interval: number;\n /** heartbeat.maxInterval (s) when the heartbeat was created */\n max_interval: number;\n /** when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown */\n next_at: string | null;\n /** the app is exempt from battery optimization */\n battery_exempt: boolean;\n /** the device was in deep Doze */\n device_idle: boolean;\n}\n\nexport type LocationRecord = Location;\n\nexport interface InsertLocationInput {\n /** `accuracy` (m) is required and must be greater than 0; otherwise the call rejects with `INVALID_ARGUMENT`. */\n coords: Pick<Coords, 'latitude' | 'longitude' | 'accuracy'> & Partial<Coords>;\n timestamp?: string;\n event?: RecordEvent;\n is_moving?: boolean;\n extras?: Record<string, unknown>;\n}\n\n/* ================= GEOFENCES ================= */\n\n/**\n * Either (latitude, longitude, radius) = circle, or vertices = polygon. For polygons, getGeofences() also returns\n * the computed enclosing circle.\n */\nexport interface Geofence {\n identifier: string;\n latitude?: number;\n longitude?: number;\n /** m */\n radius?: number;\n /** [[lat,lng], ...] >= 3 points */\n vertices?: [number, number][];\n /** @default true */\n notifyOnEntry?: boolean;\n /** @default true */\n notifyOnExit?: boolean;\n /** @default false */\n notifyOnDwell?: boolean;\n /**\n * ms\n * @default 30000\n */\n loiteringDelay?: number;\n extras?: Record<string, unknown>;\n}\n\n/* ================= EVENTS ================= */\n\nexport interface MotionChangeEvent {\n isMoving: boolean;\n location: Location;\n}\nexport interface ActivityChangeEvent {\n activity: ActivityType;\n confidence: number;\n}\nexport interface HeartbeatEvent {\n location: Location;\n}\nexport interface GeofenceEvent {\n identifier: string;\n action: GeofenceAction;\n location: Location;\n extras?: Record<string, unknown>;\n}\nexport interface GeofencesChangeEvent {\n on: Geofence[];\n off: string[];\n}\nexport interface HttpEvent {\n success: boolean;\n status: number;\n responseText: string;\n uuids: string[];\n}\nexport interface ConnectivityChangeEvent {\n connected: boolean;\n type: ConnectivityType;\n}\nexport interface PowerSaveChangeEvent {\n isPowerSaveMode: boolean;\n}\nexport interface EnabledChangeEvent {\n enabled: boolean;\n /**\n * why: a `tracking_start` reason when tracking was switched on (start, start_geofences, resume_notification), a\n * `tracking_stop` reason when it was switched off; the same values as `Location.reason`\n */\n reason: string | null;\n}\n/** The location request failed (no permission, Play services or HMS Core refused it, no usable provider). */\nexport interface LocationErrorEvent {\n code: ErrorCode;\n message: string;\n backend: LocationBackend;\n /** the plugin requests location updates again after this many ms; null if it does not */\n retryInMs: number | null;\n}\n/** A record could not be stored because the device storage is full. */\nexport interface StorageFullEvent {\n uuid: string;\n event: RecordEvent;\n diskFreeBytes: number | null;\n}\nexport interface NotificationActionEvent {\n id: string;\n}\nexport interface AuthorizationEvent {\n success: boolean;\n status: number;\n error?: string;\n response?: Record<string, unknown>;\n}\nexport interface LocationTrackingEventMap {\n location: Location;\n motionchange: MotionChangeEvent;\n activitychange: ActivityChangeEvent;\n providerchange: ProviderState;\n heartbeat: HeartbeatEvent;\n geofence: GeofenceEvent;\n geofenceschange: GeofencesChangeEvent;\n http: HttpEvent;\n connectivitychange: ConnectivityChangeEvent;\n powersavechange: PowerSaveChangeEvent;\n enabledchange: EnabledChangeEvent;\n notificationaction: NotificationActionEvent;\n authorization: AuthorizationEvent;\n locationerror: LocationErrorEvent;\n storagefull: StorageFullEvent;\n}\nexport type LocationTrackingEventName = keyof LocationTrackingEventMap;\n\n/* ================= MISC ================= */\n\nexport interface CurrentPositionOptions {\n /** @default 3 */\n samples?: number;\n /**\n * ms\n * @default geolocation.locationTimeout\n */\n timeout?: number;\n /**\n * ms\n * @default 0\n */\n maximumAge?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default true */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport interface WatchPositionOptions {\n /**\n * ms\n * @default 1000\n */\n interval?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default false */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport type WatchPositionCallback = (location: Location | null, error?: { code: ErrorCode; message: string }) => void;\n\nexport interface HeartbeatStatus {\n enabled: boolean;\n minInterval: number;\n maxInterval: number;\n lastRecordAt: string | null;\n lastHeartbeatAt: string | null;\n nextHeartbeatAt: string | null;\n strategy: HeartbeatStrategy;\n canScheduleExactAlarms: boolean;\n isIgnoringBatteryOptimizations: boolean;\n isDeviceIdleMode: boolean;\n isPowerSaveMode: boolean;\n pendingHeartbeats: number;\n}\n\nexport interface BatteryOptimizationStatus {\n isIgnoringBatteryOptimizations: boolean;\n canScheduleExactAlarms: boolean;\n isDeviceIdleMode: boolean;\n}\n\nexport interface PowerManagerInfo {\n manufacturer: string;\n available: boolean;\n}\n\nexport interface DeviceInfo {\n platform: 'android' | 'web' | 'ios';\n manufacturer: string;\n model: string;\n brand: string;\n osVersion: string;\n sdkInt: number;\n pluginVersion: string;\n gmsAvailable: boolean;\n hmsAvailable: boolean;\n backend: LocationBackend;\n packagedProviders: string[];\n /** why a better location backend is not used (for example \"gms: unavailable (status 2)\"); null when none is better */\n backendFallbackReason: string | null;\n /** raw GoogleApiAvailability result (0 = success); null if GMS is not packaged or not checked yet */\n gmsStatus: number | null;\n /** raw HuaweiApiAvailability result (0 = success); null if HMS is not packaged or not checked yet */\n hmsStatus: number | null;\n /** true when HMS Core is older than 6.3.0.301; null if HMS is not packaged */\n hmsCoreOutdated: boolean | null;\n /** last LocationAvailability reported by GMS or HMS; only a hint (it can be wrong both ways); null if none */\n locationAvailableHint: boolean | null;\n /** number of CPU cores */\n cpuCores: number;\n /** CPU types (Build.SUPPORTED_ABIS), e.g. [\"arm64-v8a\"] */\n supportedAbis: string[];\n}\n\nexport interface Sensors {\n accelerometer: boolean;\n gyroscope: boolean;\n magnetometer: boolean;\n significantMotion: boolean;\n stepCounter: boolean;\n stepDetector: boolean;\n barometer: boolean;\n}\n\nexport interface PermissionStatus {\n location: PermissionState;\n backgroundLocation: PermissionState;\n activityRecognition: PermissionState;\n notifications: PermissionState;\n}\n\nexport interface LogQuery {\n start?: number;\n end?: number;\n level?: LogLevel;\n limit?: number;\n order?: 'asc' | 'desc';\n}\n\nexport interface LocationTrackingPlugin {\n /** Loads persisted config and state. Must resolve before most other methods (see NOT_READY rule). */\n ready(options?: ReadyOptions): Promise<State>;\n /** deep merge; arrays replaced; null resets key to default */\n setConfig(options: { config: Config }): Promise<State>;\n /** Resets the config to defaults, then applies the given config. */\n reset(options?: { config?: Config }): Promise<State>;\n getState(): Promise<State>;\n /** Starts location tracking (mode 'location'). */\n start(): Promise<State>;\n /** Starts geofence-only tracking (mode 'geofences'). */\n startGeofences(): Promise<State>;\n stop(): Promise<State>;\n /** Forces the motion state to moving or stationary. */\n changePace(options: { isMoving: boolean }): Promise<void>;\n\n getCurrentPosition(options?: CurrentPositionOptions): Promise<Location>;\n watchPosition(options: WatchPositionOptions, callback: WatchPositionCallback): Promise<CallbackID>;\n clearWatch(options: { id: CallbackID }): Promise<void>;\n\n getOdometer(): Promise<{ odometer: number }>;\n setOdometer(options: { odometer: number }): Promise<{ odometer: number }>;\n resetOdometer(): Promise<{ odometer: number }>;\n\n getLocations(options?: { limit?: number }): Promise<{ locations: Location[] }>;\n getCount(): Promise<{ count: number }>;\n insertLocation(options: { location: InsertLocationInput }): Promise<{ uuid: string }>;\n destroyLocations(): Promise<{ count: number }>;\n destroyLocation(options: { uuid: string }): Promise<{ deleted: boolean }>;\n /** uploads whole queue now; resolves with uploaded records */\n sync(): Promise<{ locations: Location[] }>;\n\n addGeofence(options: { geofence: Geofence }): Promise<void>;\n addGeofences(options: { geofences: Geofence[] }): Promise<void>;\n removeGeofence(options: { identifier: string }): Promise<void>;\n /** identifiers omitted (or null) = remove all; an empty array removes nothing */\n removeGeofences(options?: { identifiers?: string[] }): Promise<void>;\n getGeofences(): Promise<{ geofences: Geofence[] }>;\n getGeofence(options: { identifier: string }): Promise<{ geofence: Geofence | null }>;\n geofenceExists(options: { identifier: string }): Promise<{ exists: boolean }>;\n\n getHeartbeatStatus(): Promise<HeartbeatStatus>;\n\n getProviderState(): Promise<ProviderState>;\n isPowerSaveMode(): Promise<{ isPowerSaveMode: boolean }>;\n getBatteryOptimizationStatus(): Promise<BatteryOptimizationStatus>;\n openBatteryOptimizationSettings(): Promise<{ opened: boolean }>;\n getPowerManagerInfo(): Promise<PowerManagerInfo>;\n openPowerManagerSettings(): Promise<{ opened: boolean }>;\n openLocationSettings(): Promise<{ opened: boolean }>;\n openAppSettings(): Promise<{ opened: boolean }>;\n getDeviceInfo(): Promise<DeviceInfo>;\n getSensors(): Promise<Sensors>;\n\n checkPermissions(): Promise<PermissionStatus>;\n /** default all four, order: location, notifications, activityRecognition, backgroundLocation */\n requestPermissions(options?: { permissions?: PermissionType[] }): Promise<PermissionStatus>;\n\n log(options: { level: Exclude<LogLevel, 'off'>; message: string }): Promise<void>;\n getLog(options?: LogQuery): Promise<{ log: string }>;\n destroyLog(): Promise<void>;\n uploadLog(options: {\n url: string;\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n }): Promise<{ success: boolean; status: number }>;\n emailLog(options: { email: string; subject?: string }): Promise<void>;\n\n addListener(eventName: 'location', listenerFunc: (e: Location) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'motionchange', listenerFunc: (e: MotionChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'activitychange',\n listenerFunc: (e: ActivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'providerchange', listenerFunc: (e: ProviderState) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'heartbeat', listenerFunc: (e: HeartbeatEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'geofence', listenerFunc: (e: GeofenceEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'geofenceschange',\n listenerFunc: (e: GeofencesChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'http', listenerFunc: (e: HttpEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'connectivitychange',\n listenerFunc: (e: ConnectivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'powersavechange',\n listenerFunc: (e: PowerSaveChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'enabledchange', listenerFunc: (e: EnabledChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'notificationaction',\n listenerFunc: (e: NotificationActionEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'locationerror', listenerFunc: (e: LocationErrorEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'storagefull', listenerFunc: (e: StorageFullEvent) => void): Promise<PluginListenerHandle>;\n removeAllListeners(): Promise<void>;\n}\n"]}
|
package/dist/esm/web/device.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DeviceInfo } from '../definitions';
|
|
2
2
|
/** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */
|
|
3
|
-
export declare const PLUGIN_VERSION = "8.
|
|
3
|
+
export declare const PLUGIN_VERSION = "8.2.0";
|
|
4
4
|
/**
|
|
5
5
|
* Best-effort `DeviceInfo` for a browser, parsed from `navigator.userAgent`.
|
|
6
6
|
* `brand` is the browser, `model` the device when the user agent names one (else `navigator.platform`),
|
package/dist/esm/web/device.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */
|
|
2
|
-
export const PLUGIN_VERSION = '8.
|
|
2
|
+
export const PLUGIN_VERSION = '8.2.0';
|
|
3
3
|
/** Tokens after `Android x` that do not name a device ('K' is Chrome's reduced user agent). */
|
|
4
4
|
const NOT_A_MODEL = ['K', 'U', 'wv', 'Mobile', 'Tablet', 'HarmonyOS'];
|
|
5
5
|
/** The device model among the `;`-separated tokens that follow `Android x` in the user agent's comment. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.js","sourceRoot":"","sources":["../../../src/web/device.ts"],"names":[],"mappings":"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","sourcesContent":["// Owned by Unit 1 (TS wrapper + web).\nimport type { DeviceInfo } from '../definitions';\n\n/** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */\nexport const PLUGIN_VERSION = '8.
|
|
1
|
+
{"version":3,"file":"device.js","sourceRoot":"","sources":["../../../src/web/device.ts"],"names":[],"mappings":"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","sourcesContent":["// Owned by Unit 1 (TS wrapper + web).\nimport type { DeviceInfo } from '../definitions';\n\n/** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */\nexport const PLUGIN_VERSION = '8.2.0';\n\n/** Tokens after `Android x` that do not name a device ('K' is Chrome's reduced user agent). */\nconst NOT_A_MODEL = ['K', 'U', 'wv', 'Mobile', 'Tablet', 'HarmonyOS'];\n\ninterface OsInfo {\n osVersion: string;\n manufacturer: string;\n model: string | undefined;\n}\n\n/** The device model among the `;`-separated tokens that follow `Android x` in the user agent's comment. */\nfunction androidModel(tokens: string[]): string | undefined {\n for (const token of tokens) {\n const model = token.replace(/\\s*Build\\/.*$/, '').trim();\n const skip =\n !model ||\n NOT_A_MODEL.indexOf(model) >= 0 ||\n /^[a-z]{2}(?:[-_][a-z]{2})?$/i.test(model) || // locale, e.g. en-us\n /^(?:rv:|HMSCore)/.test(model);\n if (!skip) {\n return model;\n }\n }\n return undefined;\n}\n\nfunction parseOs(ua: string): OsInfo {\n let match = /\\(([^)]*?)Android\\s*([\\d.]*)([^)]*)\\)/.exec(ua);\n if (match) {\n return {\n osVersion: match[2] ? `Android ${match[2]}` : 'Android',\n manufacturer: 'unknown',\n model: androidModel(match[3].split(';')),\n };\n }\n match = /(iPhone|iPad|iPod).*?OS (\\d+(?:_\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `iOS ${match[2].replace(/_/g, '.')}`, manufacturer: 'Apple', model: match[1] };\n }\n match = /Mac OS X (\\d+(?:[_.]\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `macOS ${match[1].replace(/_/g, '.')}`, manufacturer: 'Apple', model: 'Macintosh' };\n }\n match = /Windows NT (\\d+(?:\\.\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `Windows ${match[1]}`, manufacturer: 'unknown', model: undefined };\n }\n match = /CrOS \\S+ (\\d+(?:\\.\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `ChromeOS ${match[1]}`, manufacturer: 'unknown', model: undefined };\n }\n if (/Linux/.test(ua)) {\n return { osVersion: 'Linux', manufacturer: 'unknown', model: undefined };\n }\n return { osVersion: 'unknown', manufacturer: 'unknown', model: undefined };\n}\n\n/** Browser name and major version, e.g. `Chrome 120`. Order matters: many browsers also claim Chrome or Safari. */\nfunction parseBrowser(ua: string): string {\n const browsers: [string, RegExp][] = [\n ['Edge', /Edg(?:e|A|iOS)?\\/(\\d+)/],\n ['Opera', /(?:OPR|Opera)\\/(\\d+)/],\n ['Samsung Internet', /SamsungBrowser\\/(\\d+)/],\n ['Huawei Browser', /HuaweiBrowser\\/(\\d+)/],\n ['Firefox', /(?:Firefox|FxiOS)\\/(\\d+)/],\n ['Chrome', /(?:Chrome|CriOS)\\/(\\d+)/],\n ['Safari', /Version\\/(\\d+)[\\d.]*.*Safari\\//],\n ['Node.js', /Node\\.js\\/(\\d+)/],\n ];\n for (const [name, pattern] of browsers) {\n const match = pattern.exec(ua);\n if (match) {\n return `${name} ${match[1]}`;\n }\n }\n return 'unknown';\n}\n\n/**\n * Best-effort `DeviceInfo` for a browser, parsed from `navigator.userAgent`.\n * `brand` is the browser, `model` the device when the user agent names one (else `navigator.platform`),\n * `osVersion` includes the OS name, and `sdkInt` is -1 because there is no Android API level.\n */\nexport function webDeviceInfo(userAgent: string, platform?: string): DeviceInfo {\n const os = parseOs(userAgent);\n return {\n platform: 'web',\n manufacturer: os.manufacturer,\n model: os.model || platform || 'unknown',\n brand: parseBrowser(userAgent),\n osVersion: os.osVersion,\n sdkInt: -1,\n pluginVersion: PLUGIN_VERSION,\n gmsAvailable: false,\n hmsAvailable: false,\n backend: 'web',\n packagedProviders: [],\n backendFallbackReason: null,\n gmsStatus: null,\n hmsStatus: null,\n hmsCoreOutdated: null,\n locationAvailableHint: null,\n cpuCores: typeof navigator !== 'undefined' && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 0,\n supportedAbis: [],\n };\n}\n"]}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -322,7 +322,7 @@ function webState(config) {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
/** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */
|
|
325
|
-
const PLUGIN_VERSION = '8.
|
|
325
|
+
const PLUGIN_VERSION = '8.2.0';
|
|
326
326
|
/** Tokens after `Android x` that do not name a device ('K' is Chrome's reduced user agent). */
|
|
327
327
|
const NOT_A_MODEL = ['K', 'U', 'wv', 'Mobile', 'Tablet', 'HarmonyOS'];
|
|
328
328
|
/** The device model among the `;`-separated tokens that follow `Android x` in the user agent's comment. */
|