@tokamakdev/plugin-location 0.1.0-beta.127 → 0.1.0-beta.129
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/README.md +2 -10
- package/android/Exports.generated.kt +264 -0
- package/android/Exports.kt +30 -0
- package/android/LocationEvents.kt +19 -33
- package/android/LocationPlugin.kt +103 -140
- package/android/LocationPositions.kt +67 -0
- package/android/LocationRegions.kt +8 -16
- package/android/LocationService.kt +10 -11
- package/android/LocationTypes.kt +132 -0
- package/android/LocationUpdates.kt +16 -17
- package/apple/Exports.generated.swift +134 -0
- package/apple/Exports.swift +48 -0
- package/apple/LocationEvents.swift +45 -68
- package/apple/LocationPlugin.swift +171 -175
- package/apple/LocationPositions.swift +59 -0
- package/apple/LocationRegions.swift +29 -34
- package/apple/LocationTypes.swift +158 -0
- package/apple/LocationUpdates.swift +20 -28
- package/package.json +5 -6
- package/src/android.generated.ts +170 -0
- package/src/apple.generated.ts +183 -0
- package/src/errors.ts +0 -17
- package/src/events.ts +31 -0
- package/src/index.ts +115 -153
- package/src/types.ts +9 -20
- package/tokamak-plugin.json +3 -43
- package/web/index.ts +11 -10
- package/android/Bindings.kt +0 -997
- package/android/bindings.ts +0 -4150
- package/apple/Bindings.swift +0 -1059
- package/apple/bindings.ts +0 -1314
- package/events/index.ts +0 -18
- package/src/android.ts +0 -150
- package/src/apple.ts +0 -117
package/src/index.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { valueStream } from "@tokamakdev/plugin/streams";
|
|
1
|
+
import { capabilitiesOf, lifecycleStage, platformApi, type JsonStream, type TokamakContext } from "@tokamakdev/plugin";
|
|
3
2
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { backgroundDenied, permissionDenied, unavailable } from "./errors.js";
|
|
3
|
+
import { android } from "./android.generated.js";
|
|
4
|
+
import { apple } from "./apple.generated.js";
|
|
5
|
+
import { backgroundDenied, permissionDenied, timedOut, unavailable } from "./errors.js";
|
|
7
6
|
import type {
|
|
8
7
|
LocationErrorEvent,
|
|
9
8
|
LocationPermission,
|
|
@@ -21,157 +20,120 @@ import type {
|
|
|
21
20
|
|
|
22
21
|
export type * from "./types.js";
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
super(ctx, "location");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Applies long-term location awareness, replacing the previous configuration, which the plugin keeps
|
|
31
|
-
* and applies again at each start. Rejects on Apple platforms with `NotSupportedError` where the
|
|
32
|
-
* device cannot monitor significant changes.
|
|
33
|
-
*/
|
|
34
|
-
setup(config: SetupConfig): Promise<void> {
|
|
35
|
-
return this.call("setup", config);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The device's position, asking for in-use permission where the user has not answered. Rejects
|
|
40
|
-
* with `NotAllowedError` without it, `TimeoutError` after `timeout`, and `NotReadableError`
|
|
41
|
-
* when location is unavailable. Asking off screen rejects with `NeedsUIError`, as
|
|
42
|
-
* `requestPermission` does.
|
|
43
|
-
*/
|
|
44
|
-
async getCurrentPosition(options: PositionOptions = {}): Promise<Position> {
|
|
45
|
-
await this.#authorize();
|
|
46
|
-
const position = await this.#platform().currentPosition(this.ctx, options);
|
|
47
|
-
if (position) return position;
|
|
48
|
-
// Android finds no position in the background without background permission.
|
|
49
|
-
const background = await lifecycleStage(this.ctx) === "background";
|
|
50
|
-
throw background && (await this.permission()).background !== "granted" ? backgroundDenied() : unavailable("Location is unavailable");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Positions as the device moves, after `getCurrentPosition`'s permission checks, which can reject
|
|
55
|
-
* with `NeedsUIError` off screen. A slow reader receives the newest.
|
|
56
|
-
*/
|
|
57
|
-
watchPosition(): JsonStream<Position> {
|
|
58
|
-
return valueStream<Position>(async (next, error) => {
|
|
59
|
-
await this.#authorize();
|
|
60
|
-
return this.#platform().watchPositions(this.ctx, next, error);
|
|
61
|
-
}, "dropOldest");
|
|
62
|
-
}
|
|
23
|
+
export function location(ctx: TokamakContext) {
|
|
24
|
+
const native = platformApi(ctx, { apple, android });
|
|
63
25
|
|
|
64
26
|
/** Requires location services and in-use permission, asking for it where the user has not answered. */
|
|
65
|
-
async
|
|
66
|
-
if (!await
|
|
67
|
-
let { inUse } = await
|
|
68
|
-
if (inUse === "prompt") ({ inUse } = await
|
|
27
|
+
async function authorize(): Promise<void> {
|
|
28
|
+
if (!await native.servicesEnabled()) throw unavailable("Location services are disabled");
|
|
29
|
+
let { inUse } = await native.permission();
|
|
30
|
+
if (inUse === "prompt") ({ inUse } = await native.requestPermission());
|
|
69
31
|
if (inUse !== "granted") throw permissionDenied();
|
|
70
32
|
}
|
|
71
33
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
34
|
+
return {
|
|
35
|
+
/** Whether each operation is implemented where the app runs, without calling it. Permission is not reported. */
|
|
36
|
+
get capabilities(): Readonly<Record<string, boolean>> {
|
|
37
|
+
return capabilitiesOf(ctx, "location");
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Applies long-term location awareness, replacing the previous configuration, which the plugin
|
|
42
|
+
* keeps and applies again at each start. Rejects on Apple platforms with `NotSupportedError` where
|
|
43
|
+
* the device cannot monitor significant changes.
|
|
44
|
+
*/
|
|
45
|
+
setup: (config: SetupConfig): Promise<void> => native.setup(config),
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The device's position, asking for in-use permission where the user has not answered. Rejects
|
|
49
|
+
* with `NotAllowedError` without it, `TimeoutError` after `timeout`, and `NotReadableError`
|
|
50
|
+
* when location is unavailable. Asking off screen rejects with `NeedsUIError`, as
|
|
51
|
+
* `requestPermission` does.
|
|
52
|
+
*/
|
|
53
|
+
async getCurrentPosition({ maximumAge = 0, timeout }: PositionOptions = {}): Promise<Position> {
|
|
54
|
+
await authorize();
|
|
55
|
+
// JSON has no Infinity, which accepts any recent position.
|
|
56
|
+
const recent = await native.recentPosition(Math.min(maximumAge, Number.MAX_VALUE));
|
|
57
|
+
if (recent) return recent;
|
|
58
|
+
const located = new AbortController();
|
|
59
|
+
const timer = timeout === undefined ? undefined : setTimeout(() => { located.abort(timedOut()); }, timeout);
|
|
60
|
+
const position = await platformApi(ctx, { apple, android }, located.signal).currentPosition()
|
|
61
|
+
.finally(() => { clearTimeout(timer); });
|
|
62
|
+
if (position) return position;
|
|
63
|
+
// Android finds no position in the background without background permission.
|
|
64
|
+
const background = await lifecycleStage(ctx) === "background";
|
|
65
|
+
throw background && (await native.permission()).background !== "granted" ? backgroundDenied() : unavailable("Location is unavailable");
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Positions as the device moves, after `getCurrentPosition`'s permission checks, which can reject
|
|
70
|
+
* with `NeedsUIError` off screen. A slow reader receives the newest.
|
|
71
|
+
*/
|
|
72
|
+
async watchPosition(): Promise<JsonStream<Position>> {
|
|
73
|
+
await authorize();
|
|
74
|
+
return native.watchPosition();
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/** The app's in-use and background permission, and the precision it gets. Never asks. */
|
|
78
|
+
permission: (): Promise<LocationPermission> => native.permission(),
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Asks for in-use permission, and for what `options` names, where the user has not answered.
|
|
82
|
+
* Android asks for background permission once in-use permission is granted. Rejects with
|
|
83
|
+
* `NeedsUIError` when a prompt would show off screen.
|
|
84
|
+
*/
|
|
85
|
+
requestPermission: (options?: PermissionRequest): Promise<LocationPermission> => native.requestPermission(options),
|
|
86
|
+
|
|
87
|
+
/** Whether location services are on for the device. While they are off, nothing locates. */
|
|
88
|
+
servicesEnabled: (): Promise<boolean> => native.servicesEnabled(),
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Opens the app's page in the system settings, where the user changes its location permission.
|
|
92
|
+
* Rejects with `NeedsUIError` off screen on iOS and Android.
|
|
93
|
+
*/
|
|
94
|
+
openSettings: (): Promise<void> => native.openSettings(),
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Starts continuous tracking, replacing any running, until `stopUpdates`; `setup` gives long-term
|
|
98
|
+
* awareness instead. The updates' positions reach `onLocationUpdate`, including after the system
|
|
99
|
+
* ended the app. Rejects with `NotAllowedError` without in-use permission, and on iOS with
|
|
100
|
+
* `NeedsUIError` off screen. Android's `foregroundService` rejects with `NotSupportedError`
|
|
101
|
+
* without the `background-updates` build setting, and with `NeedsUIError` when Android does not
|
|
102
|
+
* allow the service to start off screen.
|
|
103
|
+
*/
|
|
104
|
+
startUpdates: (options?: UpdateOptions): Promise<void> => native.startUpdates(options),
|
|
105
|
+
|
|
106
|
+
stopUpdates: (): Promise<void> => native.stopUpdates(),
|
|
107
|
+
|
|
108
|
+
/** The options of the running updates, or null when none are running. */
|
|
109
|
+
getUpdates: (): Promise<UpdateOptions | null> => native.getUpdates(),
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Monitors a circular region, replacing one with the same `id`, until `removeRegion`. Its entries
|
|
113
|
+
* and exits reach `onRegionTransition`, including when the system launches the app for one. A
|
|
114
|
+
* region starts outside, so a device already inside reports an entry. Rejects with
|
|
115
|
+
* `NotAllowedError` without precise location, and with `QuotaExceededError` beyond Apple's 20.
|
|
116
|
+
*/
|
|
117
|
+
addRegion: (region: Region): Promise<void> => native.addRegion(region),
|
|
118
|
+
|
|
119
|
+
/** The monitored regions. */
|
|
120
|
+
getRegions: (): Promise<MonitoredRegion[]> => native.getRegions(),
|
|
121
|
+
|
|
122
|
+
removeRegion: (id: string): Promise<void> => native.removeRegion(id),
|
|
123
|
+
|
|
124
|
+
/** Receives the updates the Worker's `onLocationUpdate` receives, while the listening page or Worker runs. */
|
|
125
|
+
onLocationUpdate: (): JsonStream<LocationUpdate> => native.onLocationUpdate(),
|
|
126
|
+
|
|
127
|
+
/** Receives the transitions the Worker's `onRegionTransition` receives, while the listening page or Worker runs. */
|
|
128
|
+
onRegionTransition: (): JsonStream<RegionTransition> => native.onRegionTransition(),
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Receives the visits the Worker's `onVisit` receives, while the listening page or Worker runs.
|
|
132
|
+
* @platforms ios macos
|
|
133
|
+
*/
|
|
134
|
+
onVisit: (): JsonStream<Visit> => apple(ctx).onVisit(),
|
|
135
|
+
|
|
136
|
+
/** Receives the failures the Worker's `onLocationError` receives, while the listening page or Worker runs. */
|
|
137
|
+
onLocationError: (): JsonStream<LocationErrorEvent> => native.onLocationError(),
|
|
138
|
+
};
|
|
174
139
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
export function location(ctx: TokamakContext): Location { return new Location(ctx); }
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
/** A position's coordinates, without those the device does not report. */
|
|
1
2
|
export interface Coordinates {
|
|
2
3
|
readonly latitude: number;
|
|
3
4
|
readonly longitude: number;
|
|
4
5
|
readonly accuracy: number;
|
|
5
|
-
readonly altitude
|
|
6
|
-
readonly altitudeAccuracy
|
|
7
|
-
readonly heading
|
|
8
|
-
readonly speed
|
|
6
|
+
readonly altitude?: number;
|
|
7
|
+
readonly altitudeAccuracy?: number;
|
|
8
|
+
readonly heading?: number;
|
|
9
|
+
readonly speed?: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface Position {
|
|
@@ -157,10 +158,10 @@ export interface Visit {
|
|
|
157
158
|
readonly longitude: number;
|
|
158
159
|
/** In metres. */
|
|
159
160
|
readonly accuracy: number;
|
|
160
|
-
/** Milliseconds since the epoch,
|
|
161
|
-
readonly arrival
|
|
162
|
-
/** Milliseconds since the epoch,
|
|
163
|
-
readonly departure
|
|
161
|
+
/** Milliseconds since the epoch, absent when unknown. */
|
|
162
|
+
readonly arrival?: number;
|
|
163
|
+
/** Milliseconds since the epoch, absent before the user leaves. */
|
|
164
|
+
readonly departure?: number;
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
/** A failure of a background location service. */
|
|
@@ -174,15 +175,3 @@ export interface LocationErrorEvent {
|
|
|
174
175
|
|
|
175
176
|
export type Listener<T> = (event: T) => void;
|
|
176
177
|
export type ErrorListener = (error: DOMException) => void;
|
|
177
|
-
|
|
178
|
-
declare global {
|
|
179
|
-
// eslint-disable-next-line @typescript-eslint/no-namespace -- events are declared by merging into Tokamak.Events.
|
|
180
|
-
namespace Tokamak {
|
|
181
|
-
interface Events {
|
|
182
|
-
"location.update": LocationUpdate;
|
|
183
|
-
"location.region": RegionTransition;
|
|
184
|
-
"location.visit": Visit;
|
|
185
|
-
"location.error": LocationErrorEvent;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
package/tokamak-plugin.json
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"id": "location",
|
|
4
|
-
"bindings": {
|
|
5
|
-
"apple": {
|
|
6
|
-
"sdk": "26.2",
|
|
7
|
-
"modules": [
|
|
8
|
-
"CoreLocation",
|
|
9
|
-
"_LocationEssentials"
|
|
10
|
-
]
|
|
11
|
-
},
|
|
12
|
-
"android": {
|
|
13
|
-
"sdk": 35,
|
|
14
|
-
"modules": [
|
|
15
|
-
"android.location",
|
|
16
|
-
"android.content.Context",
|
|
17
|
-
"android.os.CancellationSignal",
|
|
18
|
-
"android.os.SystemClock",
|
|
19
|
-
"java.util.concurrent.Executor",
|
|
20
|
-
"java.util.function.Consumer"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
4
|
"settings": {
|
|
25
5
|
"background": {
|
|
26
6
|
"kind": "boolean",
|
|
@@ -58,33 +38,13 @@
|
|
|
58
38
|
"build": "build/index.mjs",
|
|
59
39
|
"platforms": {
|
|
60
40
|
"macos": {
|
|
61
|
-
"class": "TokamakLocationPlugin"
|
|
62
|
-
"sources": [
|
|
63
|
-
"apple/LocationEvents.swift",
|
|
64
|
-
"apple/LocationPlugin.swift",
|
|
65
|
-
"apple/LocationRegions.swift",
|
|
66
|
-
"apple/LocationUpdates.swift"
|
|
67
|
-
]
|
|
41
|
+
"class": "TokamakLocationPlugin"
|
|
68
42
|
},
|
|
69
43
|
"ios": {
|
|
70
|
-
"class": "TokamakLocationPlugin"
|
|
71
|
-
"sources": [
|
|
72
|
-
"apple/LocationEvents.swift",
|
|
73
|
-
"apple/LocationPlugin.swift",
|
|
74
|
-
"apple/LocationRegions.swift",
|
|
75
|
-
"apple/LocationUpdates.swift"
|
|
76
|
-
]
|
|
44
|
+
"class": "TokamakLocationPlugin"
|
|
77
45
|
},
|
|
78
46
|
"android": {
|
|
79
|
-
"class": "com.tokamak.plugins.location.TokamakLocationPlugin"
|
|
80
|
-
"sources": [
|
|
81
|
-
"android/LocationEvents.kt",
|
|
82
|
-
"android/LocationPlugin.kt",
|
|
83
|
-
"android/LocationReceiver.kt",
|
|
84
|
-
"android/LocationRegions.kt",
|
|
85
|
-
"android/LocationService.kt",
|
|
86
|
-
"android/LocationUpdates.kt"
|
|
87
|
-
]
|
|
47
|
+
"class": "com.tokamak.plugins.location.TokamakLocationPlugin"
|
|
88
48
|
}
|
|
89
49
|
},
|
|
90
50
|
"api": {
|
package/web/index.ts
CHANGED
|
@@ -59,19 +59,20 @@ function browserLocationApi(): Geolocation {
|
|
|
59
59
|
throw new DOMException("Location is unavailable", "NotSupportedError");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
/** `position` as native platforms give it: without the coordinates the device does not report. */
|
|
63
|
+
function copyPosition({ coords, timestamp }: GeolocationPosition): Position {
|
|
64
|
+
const { latitude, longitude, accuracy, altitude, altitudeAccuracy, heading, speed } = coords;
|
|
64
65
|
return {
|
|
65
66
|
coords: {
|
|
66
|
-
latitude
|
|
67
|
-
longitude
|
|
68
|
-
accuracy
|
|
69
|
-
altitude:
|
|
70
|
-
altitudeAccuracy:
|
|
71
|
-
heading:
|
|
72
|
-
speed:
|
|
67
|
+
latitude,
|
|
68
|
+
longitude,
|
|
69
|
+
accuracy,
|
|
70
|
+
...(altitude === null ? {} : { altitude }),
|
|
71
|
+
...(altitudeAccuracy === null ? {} : { altitudeAccuracy }),
|
|
72
|
+
...(heading === null ? {} : { heading }),
|
|
73
|
+
...(speed === null ? {} : { speed }),
|
|
73
74
|
},
|
|
74
|
-
timestamp
|
|
75
|
+
timestamp,
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
|