@tokamakdev/plugin-location 0.1.0-beta.76 → 0.1.0-beta.80
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 +6 -0
- package/android/LocationPlugin.kt +34 -4
- package/apple/LocationPlugin.swift +99 -33
- package/package.json +2 -2
- package/src/index.ts +21 -2
- package/web/index.ts +8 -2
package/README.md
CHANGED
|
@@ -19,6 +19,12 @@ const stop = location.watchPosition(
|
|
|
19
19
|
);
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
`getCurrentPosition` takes the Geolocation API's `maximumAge` and `timeout`
|
|
23
|
+
options. It resolves with a recent position no older than `maximumAge`
|
|
24
|
+
milliseconds, 0 by default, or takes a new one, and rejects with
|
|
25
|
+
`TimeoutError` when none arrives within `timeout` milliseconds, unlimited by
|
|
26
|
+
default. The timeout starts once the user allows location.
|
|
27
|
+
|
|
22
28
|
The web implementation uses `navigator.geolocation`. Native Tokamak builds use
|
|
23
29
|
Core Location on Apple platforms, `LocationManager` on Android, and WebView2's
|
|
24
30
|
location implementation on Windows. Calling the API on a platform without
|
|
@@ -8,11 +8,14 @@ import android.location.LocationListener
|
|
|
8
8
|
import android.location.LocationManager
|
|
9
9
|
import android.os.Bundle
|
|
10
10
|
import android.os.CancellationSignal
|
|
11
|
+
import android.os.Handler
|
|
11
12
|
import android.os.Looper
|
|
13
|
+
import android.os.SystemClock
|
|
12
14
|
import com.tokamak.runtime.TokamakHost
|
|
13
15
|
import com.tokamak.runtime.TokamakPlugin
|
|
14
16
|
import com.tokamak.runtime.TokamakPluginError
|
|
15
17
|
import com.tokamak.runtime.TokamakPluginReply
|
|
18
|
+
import org.json.JSONObject
|
|
16
19
|
|
|
17
20
|
class TokamakLocationPlugin(
|
|
18
21
|
private val host: TokamakHost,
|
|
@@ -22,14 +25,16 @@ class TokamakLocationPlugin(
|
|
|
22
25
|
private val context = host.context
|
|
23
26
|
private val manager =
|
|
24
27
|
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
28
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
25
29
|
|
|
26
30
|
override fun call(method: String, arguments: Any?, reply: TokamakPluginReply) {
|
|
27
31
|
if (method != "getCurrentPosition") {
|
|
28
32
|
super.call(method, arguments, reply)
|
|
29
33
|
return
|
|
30
34
|
}
|
|
35
|
+
val options = arguments as? JSONObject
|
|
31
36
|
withPermission { granted ->
|
|
32
|
-
if (granted) currentPosition(reply) else reply(permissionDenied())
|
|
37
|
+
if (granted) currentPosition(options, reply) else reply(permissionDenied())
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -63,20 +68,43 @@ class TokamakLocationPlugin(
|
|
|
63
68
|
host.requestPermissions(PERMISSIONS) { granted -> action(granted.containsValue(true)) }
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Replies with a fix no older than the `maximumAge` option, taking a new one when there is
|
|
73
|
+
* none, or fails after the `timeout` option.
|
|
74
|
+
*/
|
|
75
|
+
private fun currentPosition(options: JSONObject?, reply: TokamakPluginReply) {
|
|
67
76
|
val provider = availableProvider(reply) ?: return
|
|
77
|
+
val maximumAge = options?.optLong("maximumAge", 0) ?: 0
|
|
78
|
+
val recent = runCatching { manager.getLastKnownLocation(provider) }.getOrNull()
|
|
79
|
+
if (recent != null && ageMillis(recent) <= maximumAge) {
|
|
80
|
+
reply(Result.success(position(recent)))
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
val cancellation = CancellationSignal()
|
|
84
|
+
val timeout = Runnable {
|
|
85
|
+
cancellation.cancel()
|
|
86
|
+
reply(Result.failure(TokamakPluginError("TimeoutError", "Location was not found in time")))
|
|
87
|
+
}
|
|
88
|
+
handler.postDelayed(timeout, options?.optLong("timeout", UNLIMITED) ?: UNLIMITED)
|
|
68
89
|
runCatching {
|
|
69
90
|
manager.getCurrentLocation(
|
|
70
91
|
provider,
|
|
71
|
-
|
|
92
|
+
cancellation,
|
|
72
93
|
context.mainExecutor,
|
|
73
94
|
) { location ->
|
|
95
|
+
handler.removeCallbacks(timeout)
|
|
74
96
|
if (location == null) reply(unavailable("Location is unavailable"))
|
|
75
97
|
else reply(Result.success(position(location)))
|
|
76
98
|
}
|
|
77
|
-
}.onFailure {
|
|
99
|
+
}.onFailure {
|
|
100
|
+
handler.removeCallbacks(timeout)
|
|
101
|
+
reply(locationFailure(it))
|
|
102
|
+
}
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
private fun ageMillis(location: Location): Long =
|
|
106
|
+
(SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos) / 1_000_000
|
|
107
|
+
|
|
80
108
|
private fun watchPosition(reply: TokamakPluginReply): () -> Unit {
|
|
81
109
|
val provider = availableProvider(reply) ?: return {}
|
|
82
110
|
val listener = object : LocationListener {
|
|
@@ -145,6 +173,8 @@ class TokamakLocationPlugin(
|
|
|
145
173
|
else unavailable(error.message ?: "Location is unavailable")
|
|
146
174
|
|
|
147
175
|
private companion object {
|
|
176
|
+
/** The `timeout` that never expires, as the page sends it. */
|
|
177
|
+
const val UNLIMITED = 0xffffffffL
|
|
148
178
|
val PERMISSIONS = setOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
149
179
|
}
|
|
150
180
|
}
|
|
@@ -10,15 +10,32 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
10
10
|
name: "NotAllowedError",
|
|
11
11
|
message: "Location permission was denied"
|
|
12
12
|
)
|
|
13
|
+
private static let timedOut = TokamakPluginError(
|
|
14
|
+
name: "TimeoutError",
|
|
15
|
+
message: "Location was not found in time"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
/// A current position request, timed from when locating starts.
|
|
19
|
+
private struct Request {
|
|
20
|
+
let reply: TokamakPluginReply
|
|
21
|
+
let maximumAge: TimeInterval
|
|
22
|
+
let timeout: TimeInterval
|
|
23
|
+
var timer: DispatchWorkItem?
|
|
24
|
+
}
|
|
13
25
|
|
|
14
|
-
|
|
15
|
-
private
|
|
26
|
+
/// Updates watchers.
|
|
27
|
+
private let updates = CLLocationManager()
|
|
28
|
+
/// Takes single fixes for current position requests, apart from watching.
|
|
29
|
+
private let fixes = CLLocationManager()
|
|
30
|
+
private var current: [UUID: Request] = [:]
|
|
16
31
|
private var watchers: [UUID: TokamakPluginReply] = [:]
|
|
17
32
|
|
|
18
33
|
init(host: TokamakHost) {
|
|
19
34
|
super.init()
|
|
20
|
-
manager
|
|
21
|
-
|
|
35
|
+
for manager in [updates, fixes] {
|
|
36
|
+
manager.delegate = self
|
|
37
|
+
manager.desiredAccuracy = kCLLocationAccuracyBest
|
|
38
|
+
}
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
func call(
|
|
@@ -30,7 +47,12 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
30
47
|
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
31
48
|
return
|
|
32
49
|
}
|
|
33
|
-
|
|
50
|
+
let options = arguments as? [String: Any] ?? [:]
|
|
51
|
+
current[UUID()] = Request(
|
|
52
|
+
reply: reply,
|
|
53
|
+
maximumAge: (options["maximumAge"] as? Double ?? 0) / 1000,
|
|
54
|
+
timeout: (options["timeout"] as? Double ?? Double(UInt32.max)) / 1000
|
|
55
|
+
)
|
|
34
56
|
start()
|
|
35
57
|
}
|
|
36
58
|
|
|
@@ -48,14 +70,19 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
48
70
|
start()
|
|
49
71
|
return { [weak self] in
|
|
50
72
|
self?.watchers.removeValue(forKey: subscription)
|
|
51
|
-
self?.
|
|
73
|
+
if self?.watchers.isEmpty == true {
|
|
74
|
+
self?.updates.stopUpdatingLocation()
|
|
75
|
+
}
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
78
|
|
|
79
|
+
/// Also called when each manager is created, before anything asks for a
|
|
80
|
+
/// position. Both managers report the app's one authorization.
|
|
55
81
|
func locationManagerDidChangeAuthorization(
|
|
56
82
|
_ manager: CLLocationManager
|
|
57
83
|
) {
|
|
58
|
-
|
|
84
|
+
guard manager === updates, !current.isEmpty || !watchers.isEmpty else { return }
|
|
85
|
+
serve(manager.authorizationStatus)
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
func locationManager(
|
|
@@ -63,38 +90,53 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
63
90
|
didUpdateLocations locations: [CLLocation]
|
|
64
91
|
) {
|
|
65
92
|
guard let location = locations.last else { return }
|
|
66
|
-
deliver(.success(position(location)))
|
|
93
|
+
deliver(.success(position(location)), from: manager)
|
|
67
94
|
}
|
|
68
95
|
|
|
96
|
+
/// A fix that is not yet available is taken again; updates continue.
|
|
69
97
|
func locationManager(
|
|
70
98
|
_ manager: CLLocationManager,
|
|
71
99
|
didFailWithError error: Error
|
|
72
100
|
) {
|
|
73
|
-
|
|
101
|
+
guard (error as NSError).code == CLError.Code.locationUnknown.rawValue else {
|
|
102
|
+
deliver(.failure(locationError(error)), from: manager)
|
|
74
103
|
return
|
|
75
104
|
}
|
|
76
|
-
|
|
105
|
+
if manager === fixes && !current.isEmpty {
|
|
106
|
+
fixes.requestLocation()
|
|
107
|
+
}
|
|
77
108
|
}
|
|
78
109
|
|
|
110
|
+
/// Checks location services off the main thread, which the check can block.
|
|
79
111
|
private func start() {
|
|
80
|
-
|
|
112
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
113
|
+
let enabled = CLLocationManager.locationServicesEnabled()
|
|
114
|
+
DispatchQueue.main.async { self.start(servicesEnabled: enabled) }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func start(servicesEnabled: Bool) {
|
|
119
|
+
guard servicesEnabled else {
|
|
81
120
|
fail(unavailable("Location services are disabled"))
|
|
82
121
|
return
|
|
83
122
|
}
|
|
84
|
-
let status =
|
|
123
|
+
let status = updates.authorizationStatus
|
|
85
124
|
if status == .notDetermined {
|
|
86
|
-
|
|
125
|
+
updates.requestWhenInUseAuthorization()
|
|
87
126
|
} else {
|
|
88
|
-
|
|
127
|
+
serve(status)
|
|
89
128
|
}
|
|
90
129
|
}
|
|
91
130
|
|
|
92
|
-
///
|
|
93
|
-
/// forbids
|
|
94
|
-
private func
|
|
131
|
+
/// Locates for the waiting requests and watchers when `status` allows it,
|
|
132
|
+
/// and fails them when it forbids it.
|
|
133
|
+
private func serve(_ status: CLAuthorizationStatus) {
|
|
95
134
|
switch status {
|
|
96
135
|
case .authorizedAlways, .authorizedWhenInUse:
|
|
97
|
-
|
|
136
|
+
if !watchers.isEmpty {
|
|
137
|
+
updates.startUpdatingLocation()
|
|
138
|
+
}
|
|
139
|
+
locate()
|
|
98
140
|
case .denied, .restricted:
|
|
99
141
|
fail(Self.permissionDenied)
|
|
100
142
|
case .notDetermined:
|
|
@@ -104,19 +146,49 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
104
146
|
}
|
|
105
147
|
}
|
|
106
148
|
|
|
149
|
+
/// Answers each new request with a fix no older than its `maximumAge`, and
|
|
150
|
+
/// takes a fix for the rest, each failing after its `timeout`.
|
|
151
|
+
private func locate() {
|
|
152
|
+
for (key, request) in current where request.timer == nil {
|
|
153
|
+
if let fix = fixes.location, -fix.timestamp.timeIntervalSinceNow <= request.maximumAge {
|
|
154
|
+
finish(key, .success(position(fix)))
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
let timer = DispatchWorkItem { [weak self] in self?.finish(key, .failure(Self.timedOut)) }
|
|
158
|
+
current[key]?.timer = timer
|
|
159
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + request.timeout, execute: timer)
|
|
160
|
+
}
|
|
161
|
+
if !current.isEmpty {
|
|
162
|
+
fixes.requestLocation()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Answers the waiting requests with a fix, or the watchers with an update.
|
|
167
|
+
private func deliver(_ result: Result<Any?, TokamakPluginError>, from manager: CLLocationManager)
|
|
168
|
+
{
|
|
169
|
+
guard manager === fixes else {
|
|
170
|
+
for reply in watchers.values {
|
|
171
|
+
reply(result)
|
|
172
|
+
}
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
for key in current.keys {
|
|
176
|
+
finish(key, result)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
107
180
|
private func fail(_ error: TokamakPluginError) {
|
|
108
|
-
deliver(.failure(error))
|
|
181
|
+
deliver(.failure(error), from: fixes)
|
|
182
|
+
deliver(.failure(error), from: updates)
|
|
109
183
|
}
|
|
110
184
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
current.
|
|
116
|
-
|
|
117
|
-
reply(result)
|
|
185
|
+
private func finish(_ key: UUID, _ result: Result<Any?, TokamakPluginError>) {
|
|
186
|
+
guard let request = current.removeValue(forKey: key) else { return }
|
|
187
|
+
request.timer?.cancel()
|
|
188
|
+
request.reply(result)
|
|
189
|
+
if current.isEmpty {
|
|
190
|
+
fixes.stopUpdatingLocation()
|
|
118
191
|
}
|
|
119
|
-
stopIfIdle()
|
|
120
192
|
}
|
|
121
193
|
|
|
122
194
|
private func locationError(_ error: Error) -> TokamakPluginError {
|
|
@@ -130,12 +202,6 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
130
202
|
TokamakPluginError(name: "NotReadableError", message: message)
|
|
131
203
|
}
|
|
132
204
|
|
|
133
|
-
private func stopIfIdle() {
|
|
134
|
-
if current.isEmpty && watchers.isEmpty {
|
|
135
|
-
manager.stopUpdatingLocation()
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
205
|
private func position(_ location: CLLocation) -> [String: Any] {
|
|
140
206
|
let coordinates = location.coordinate
|
|
141
207
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokamakdev/plugin-location",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.80",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/mantty/tokamak.git",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
".": "./src/index.ts"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@tokamakdev/plugin": "0.1.0-beta.
|
|
14
|
+
"@tokamakdev/plugin": "0.1.0-beta.80"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"android",
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,14 @@ export interface Position {
|
|
|
16
16
|
readonly timestamp: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Options for `getCurrentPosition`, as the Geolocation API reads them. */
|
|
20
|
+
export interface PositionOptions {
|
|
21
|
+
/** The age in milliseconds of a recent position to accept instead of a new one; 0 by default. */
|
|
22
|
+
readonly maximumAge?: number;
|
|
23
|
+
/** Milliseconds to wait for a position before rejecting with `TimeoutError`; unlimited by default. */
|
|
24
|
+
readonly timeout?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
export type PositionCallback = (position: Position) => void;
|
|
20
28
|
export type PositionErrorCallback = (error: DOMException) => void;
|
|
21
29
|
|
|
@@ -24,8 +32,12 @@ class Location extends FrontendPlugin {
|
|
|
24
32
|
super("location");
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
getCurrentPosition(): Promise<Position> {
|
|
28
|
-
|
|
35
|
+
getCurrentPosition(options: PositionOptions = {}): Promise<Position> {
|
|
36
|
+
if (!this.hasNativeTransport) return web.getCurrentPosition(options);
|
|
37
|
+
return this.call("getCurrentPosition", {
|
|
38
|
+
maximumAge: milliseconds(options.maximumAge ?? 0),
|
|
39
|
+
timeout: milliseconds(options.timeout ?? unlimited),
|
|
40
|
+
});
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
watchPosition(next: PositionCallback, error: PositionErrorCallback): () => void {
|
|
@@ -34,4 +46,11 @@ class Location extends FrontendPlugin {
|
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
const unlimited = 0xffffffff;
|
|
50
|
+
|
|
51
|
+
/** `value` clamped to whole milliseconds, as the Geolocation API reads its options. */
|
|
52
|
+
function milliseconds(value: number): number {
|
|
53
|
+
return Number.isNaN(value) ? 0 : Math.min(Math.max(Math.round(value), 0), unlimited);
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
export const location = new Location();
|
package/web/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Position,
|
|
3
|
+
PositionCallback,
|
|
4
|
+
PositionErrorCallback,
|
|
5
|
+
PositionOptions,
|
|
6
|
+
} from "../src/index.js";
|
|
2
7
|
|
|
3
|
-
export function getCurrentPosition(): Promise<Position> {
|
|
8
|
+
export function getCurrentPosition(options: PositionOptions): Promise<Position> {
|
|
4
9
|
return new Promise((resolve, reject) => {
|
|
5
10
|
const browserLocation = browserLocationApi();
|
|
6
11
|
browserLocation.getCurrentPosition(
|
|
@@ -10,6 +15,7 @@ export function getCurrentPosition(): Promise<Position> {
|
|
|
10
15
|
(error) => {
|
|
11
16
|
reject(positionError(error));
|
|
12
17
|
},
|
|
18
|
+
options,
|
|
13
19
|
);
|
|
14
20
|
});
|
|
15
21
|
}
|