@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/events/index.ts DELETED
@@ -1,18 +0,0 @@
1
- import { defineEvent } from "@tokamakdev/plugin/events";
2
-
3
- import "../src/types.js";
4
-
5
- /**
6
- * Receives the positions of continuous updates and significant changes in native builds: on
7
- * screen, off screen, or after the system launches the app for them.
8
- */
9
- export const onLocationUpdate = defineEvent("location.update");
10
-
11
- /** Receives each region entry or exit, including one that launches the app. */
12
- export const onRegionTransition = defineEvent("location.region");
13
-
14
- /** Apple only: receives each arrival at or departure from a place, including one that launches the app. */
15
- export const onVisit = defineEvent("location.visit");
16
-
17
- /** Receives failures of background location services, such as a region the system cannot monitor. */
18
- export const onLocationError = defineEvent("location.error");
package/src/android.ts DELETED
@@ -1,150 +0,0 @@
1
- import type { TokamakContext } from "@tokamakdev/plugin";
2
- import { applicationContext, release } from "@tokamakdev/plugin/bindings";
3
-
4
- import {
5
- CancellationSignal,
6
- Consumer,
7
- Context,
8
- Location,
9
- LocationListener,
10
- LocationManager,
11
- LocationRequest,
12
- LocationRequest$Builder,
13
- SystemClock,
14
- type Executor,
15
- } from "../android/bindings.js";
16
- import { permissionDenied, unavailable, within } from "./errors.js";
17
- import type { Position, PositionOptions } from "./types.js";
18
-
19
- /**
20
- * The device's position: the provider's last known when no older than `maximumAge`, or a new
21
- * high-accuracy fix within `timeout`. Null when Android finds none.
22
- */
23
- export async function currentPosition(ctx: TokamakContext, { maximumAge = 0, timeout }: PositionOptions): Promise<Position | null> {
24
- const { context, manager } = await locationManager(ctx);
25
- const name = await provider(manager);
26
- const recent = await manager.getLastKnownLocation(name);
27
- if (recent && await age(ctx, recent) <= maximumAge) return await position(recent);
28
- const [request, signal, executor] = await Promise.all([highAccuracy(ctx, 0), CancellationSignal.new(ctx), mainExecutor(context)]);
29
- let consumer: Consumer | undefined;
30
- let finished = false;
31
- const found = new Promise<Position | null>((resolve, reject) => {
32
- Consumer.implement(ctx, {
33
- accept(location) { resolve(location instanceof Location ? position(location) : null); },
34
- }).then(async (created) => {
35
- consumer = created;
36
- if (finished) {
37
- release(created);
38
- return;
39
- }
40
- await manager.getCurrentLocation(name, request, signal, executor, created);
41
- }).catch((failure: unknown) => { reject(locationError(failure as Error)); });
42
- });
43
- return within(found, timeout, () => {
44
- finished = true;
45
- void signal.cancel();
46
- if (consumer) release(consumer);
47
- });
48
- }
49
-
50
- /** Passes each position the provider reports to `next`, and its failures to `error`, until the returned function is called. */
51
- export async function watchPositions(
52
- ctx: TokamakContext,
53
- next: (position: Position) => void,
54
- error: (error: unknown) => void,
55
- ): Promise<() => void> {
56
- const { context, manager } = await locationManager(ctx);
57
- const [name, executor, request] = await Promise.all([provider(manager), mainExecutor(context), highAccuracy(ctx, 1000)]);
58
- const listener = await LocationListener.implement(ctx, {
59
- async onLocationChanged(location) {
60
- next(await position(location));
61
- release(location);
62
- },
63
- onProviderDisabled() { error(unavailable("Location provider is unavailable")); },
64
- });
65
- try {
66
- await manager.requestLocationUpdates(name, request, executor, listener);
67
- } catch (failure) {
68
- release(listener);
69
- throw locationError(failure as Error);
70
- }
71
- return () => {
72
- void manager.removeUpdates(listener).finally(() => { release(listener); });
73
- };
74
- }
75
-
76
- async function locationManager(ctx: TokamakContext): Promise<{ context: Context; manager: LocationManager }> {
77
- const context = new Context(await applicationContext(ctx));
78
- const manager = await context.getSystemService(Context.LOCATION_SERVICE);
79
- if (!(manager instanceof LocationManager)) throw unavailable("The device has no location service");
80
- return { context, manager };
81
- }
82
-
83
- async function mainExecutor(context: Context): Promise<Executor> {
84
- const executor = await context.getMainExecutor();
85
- if (!executor) throw unavailable("The app has no main executor");
86
- return executor;
87
- }
88
-
89
- /**
90
- * The first enabled of the fused provider, which combines GPS with Wi-Fi and cell signals, GPS and
91
- * the network, or any other enabled provider.
92
- */
93
- async function provider(manager: LocationManager): Promise<string> {
94
- for (const name of [LocationManager.FUSED_PROVIDER, LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER]) {
95
- if (await manager.isProviderEnabled(name)) return name;
96
- }
97
- const [other] = await manager.getProviders(true);
98
- if (!other) throw unavailable("No location provider is available");
99
- return other;
100
- }
101
-
102
- /** A request for the most accurate fixes the provider can give, every `interval` milliseconds. */
103
- async function highAccuracy(ctx: TokamakContext, interval: number): Promise<LocationRequest> {
104
- const builder = await LocationRequest$Builder.new(ctx, interval);
105
- await builder.setQuality(LocationRequest.QUALITY_HIGH_ACCURACY);
106
- return builder.build();
107
- }
108
-
109
- /** The milliseconds since Android took `location`. */
110
- async function age(ctx: TokamakContext, location: Location): Promise<number> {
111
- const [now, taken] = await Promise.all([SystemClock.elapsedRealtimeNanos(ctx), location.getElapsedRealtimeNanos()]);
112
- return (now - taken) / 1_000_000;
113
- }
114
-
115
- async function position(location: Location): Promise<Position> {
116
- const [latitude, longitude, accuracy, hasAltitude, altitude, hasVertical, vertical, hasBearing, bearing, hasSpeed, speed, time] =
117
- await Promise.all([
118
- location.getLatitude(),
119
- location.getLongitude(),
120
- location.getAccuracy(),
121
- location.hasAltitude(),
122
- location.getAltitude(),
123
- location.hasVerticalAccuracy(),
124
- location.getVerticalAccuracyMeters(),
125
- location.hasBearing(),
126
- location.getBearing(),
127
- location.hasSpeed(),
128
- location.getSpeed(),
129
- location.getTime(),
130
- ]);
131
- return {
132
- coords: {
133
- latitude,
134
- longitude,
135
- accuracy,
136
- altitude: hasAltitude ? altitude : null,
137
- altitudeAccuracy: hasVertical ? vertical : null,
138
- heading: hasBearing ? bearing : null,
139
- speed: hasSpeed ? speed : null,
140
- },
141
- timestamp: time,
142
- };
143
- }
144
-
145
- /** The error a failed Android call rejected with, as location operations name it. */
146
- function locationError(failure: Error): Error {
147
- if (!(failure instanceof DOMException)) return failure;
148
- const exception = (failure.cause as { exception?: string } | undefined)?.exception;
149
- return exception === "java.lang.SecurityException" ? permissionDenied() : unavailable(failure.message);
150
- }
package/src/apple.ts DELETED
@@ -1,117 +0,0 @@
1
- import type { TokamakContext } from "@tokamakdev/plugin";
2
- import { release } from "@tokamakdev/plugin/bindings";
3
-
4
- import { CLLocationManager, CLLocationManagerDelegate, type CLLocation } from "../apple/bindings.js";
5
- import { permissionDenied, unavailable, within } from "./errors.js";
6
- import type { Position, PositionOptions } from "./types.js";
7
-
8
- /** The Core Location error codes the plugin tells apart. */
9
- const locationUnknown = 0;
10
- const denied = 1;
11
-
12
- /** The device's position: Core Location's latest when no older than `maximumAge`, or a new fix within `timeout`. */
13
- export async function currentPosition(ctx: TokamakContext, { maximumAge = 0, timeout }: PositionOptions): Promise<Position> {
14
- const manager = await CLLocationManager.init(ctx);
15
- try {
16
- const recent = await manager.location();
17
- if (recent && Date.now() - (await recent.timestamp()).getTime() <= maximumAge) return await position(recent);
18
- return await nextFix(ctx, manager, timeout);
19
- } finally {
20
- release(manager);
21
- }
22
- }
23
-
24
- /** The next fix `manager` takes, asking again while Core Location does not know the position yet. */
25
- function nextFix(ctx: TokamakContext, manager: CLLocationManager, timeout: number | undefined): Promise<Position> {
26
- let delegate: CLLocationManagerDelegate | undefined;
27
- let finished = false;
28
- const found = new Promise<Position>((resolve, reject) => {
29
- CLLocationManagerDelegate.implement(ctx, {
30
- locationManagerDidUpdateLocations(_, locations) {
31
- const latest = locations.at(-1);
32
- if (latest) resolve(position(latest));
33
- },
34
- locationManagerDidFailWithError(_, error) {
35
- if (code(error) === locationUnknown) manager.requestLocation().catch(reject);
36
- else reject(locationError(error));
37
- },
38
- }).then(async (created) => {
39
- delegate = created;
40
- if (finished) {
41
- release(created);
42
- return;
43
- }
44
- await manager.setDelegate(created);
45
- await manager.requestLocation();
46
- }).catch(reject);
47
- });
48
- return within(found, timeout, () => {
49
- finished = true;
50
- if (delegate) release(delegate);
51
- });
52
- }
53
-
54
- /** Passes each position Core Location reports to `next`, and its failures to `error`, until the returned function is called. */
55
- export async function watchPositions(
56
- ctx: TokamakContext,
57
- next: (position: Position) => void,
58
- error: (error: unknown) => void,
59
- ): Promise<() => void> {
60
- const manager = await CLLocationManager.init(ctx);
61
- const delegate = await CLLocationManagerDelegate.implement(ctx, {
62
- async locationManagerDidUpdateLocations(updated, locations) {
63
- release(updated);
64
- for (const location of locations) {
65
- next(await position(location));
66
- release(location);
67
- }
68
- },
69
- locationManagerDidFailWithError(updated, failure) {
70
- release(updated);
71
- if (code(failure) !== locationUnknown) error(locationError(failure));
72
- },
73
- });
74
- await manager.setDelegate(delegate);
75
- await manager.startUpdatingLocation();
76
- return () => {
77
- void manager.stopUpdatingLocation().finally(() => {
78
- release(delegate);
79
- release(manager);
80
- });
81
- };
82
- }
83
-
84
- async function position(location: CLLocation): Promise<Position> {
85
- const [coordinate, accuracy, altitude, verticalAccuracy, course, speed, timestamp] = await Promise.all([
86
- location.coordinate(),
87
- location.horizontalAccuracy(),
88
- location.altitude(),
89
- location.verticalAccuracy(),
90
- location.course(),
91
- location.speed(),
92
- location.timestamp(),
93
- ]);
94
- // Core Location reports a negative accuracy, course or speed when it has none.
95
- return {
96
- coords: {
97
- latitude: coordinate.latitude,
98
- longitude: coordinate.longitude,
99
- accuracy,
100
- altitude: verticalAccuracy >= 0 ? altitude : null,
101
- altitudeAccuracy: verticalAccuracy >= 0 ? verticalAccuracy : null,
102
- heading: course >= 0 ? course : null,
103
- speed: speed >= 0 ? speed : null,
104
- },
105
- timestamp: timestamp.getTime(),
106
- };
107
- }
108
-
109
- /** The code of a Core Location error. */
110
- function code(error: DOMException): number | undefined {
111
- const cause = error.cause as { domain?: string; code?: number } | undefined;
112
- return cause?.domain === "kCLErrorDomain" ? cause.code : undefined;
113
- }
114
-
115
- function locationError(error: DOMException): DOMException {
116
- return code(error) === denied ? permissionDenied() : unavailable(error.message);
117
- }