@tokamakdev/plugin-location 0.1.0-beta.128 → 0.1.0-beta.130
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 -2
- package/android/Exports.generated.kt +264 -0
- package/android/Exports.kt +30 -0
- package/android/LocationEvents.kt +19 -33
- package/android/LocationPlugin.kt +102 -160
- package/android/LocationPositions.kt +30 -15
- 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 +36 -68
- package/apple/LocationPlugin.swift +172 -186
- package/apple/LocationPositions.swift +34 -24
- package/apple/LocationRegions.swift +29 -34
- package/apple/LocationTypes.swift +158 -0
- package/apple/LocationUpdates.swift +20 -28
- package/package.json +5 -4
- package/src/android.generated.ts +170 -0
- package/src/apple.generated.ts +183 -0
- package/src/events.ts +31 -0
- package/src/index.ts +114 -139
- package/src/types.ts +9 -20
- package/tokamak-plugin.json +3 -26
- package/web/index.ts +11 -10
- package/events/index.ts +0 -18
|
@@ -18,15 +18,15 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
18
18
|
CLLocationManagerDelegate
|
|
19
19
|
{
|
|
20
20
|
let id = "location"
|
|
21
|
+
let host: TokamakHost
|
|
21
22
|
|
|
22
23
|
/// A `requestPermission` waiting for the user to answer a prompt.
|
|
23
24
|
private struct Prompt {
|
|
24
25
|
/// The purpose key of the temporary precise location to ask for next.
|
|
25
26
|
let purpose: String?
|
|
26
|
-
let
|
|
27
|
+
let answer: CheckedContinuation<LocationPermission, Error>
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
private let host: TokamakHost
|
|
30
30
|
private let defaults = UserDefaults.standard
|
|
31
31
|
/// Reports the app's authorization, and asks for permission.
|
|
32
32
|
private let authorization = CLLocationManager()
|
|
@@ -55,7 +55,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
55
55
|
authorization.delegate = self
|
|
56
56
|
monitoring.delegate = self
|
|
57
57
|
if let stored = defaults.data(forKey: setupKey),
|
|
58
|
-
let config = try?
|
|
58
|
+
let config = try? JSONDecoder().decode(SetupConfig.self, from: stored)
|
|
59
59
|
{
|
|
60
60
|
apply(config)
|
|
61
61
|
}
|
|
@@ -70,100 +70,148 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
70
70
|
updates.resume()
|
|
71
71
|
}
|
|
72
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
|
-
Task { @MainActor in
|
|
140
|
-
await regions.remove(id)
|
|
141
|
-
reply(.success(nil))
|
|
142
|
-
}
|
|
143
|
-
default:
|
|
144
|
-
throw .notSupported("\(id).\(method) is not supported")
|
|
73
|
+
/// Applies long-term location awareness, which the plugin applies again at each start.
|
|
74
|
+
func setup(_ config: SetupConfig) throws(TokamakPluginError) {
|
|
75
|
+
if config.significantChanges == true
|
|
76
|
+
&& !CLLocationManager.significantLocationChangeMonitoringAvailable()
|
|
77
|
+
{
|
|
78
|
+
throw .notSupported("The device cannot monitor significant changes of location")
|
|
79
|
+
}
|
|
80
|
+
apply(config)
|
|
81
|
+
defaults.set(try? JSONEncoder().encode(config), forKey: setupKey)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Core Location's latest position when it is no older than `maximumAge`
|
|
85
|
+
/// milliseconds, or nil.
|
|
86
|
+
func recentPosition(maximumAge: Double) -> Position? {
|
|
87
|
+
guard let recent = authorization.location,
|
|
88
|
+
-recent.timestamp.timeIntervalSinceNow <= maximumAge / 1000
|
|
89
|
+
else { return nil }
|
|
90
|
+
return Position(recent)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// A new fix, from a location manager that the call owns until it ends.
|
|
94
|
+
@MainActor
|
|
95
|
+
func currentPosition() async throws -> Position {
|
|
96
|
+
for try await position in TokamakLocationPositions.stream(watching: false) {
|
|
97
|
+
return position
|
|
98
|
+
}
|
|
99
|
+
throw CancellationError()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func watchPosition() -> AsyncThrowingStream<Position, Error> {
|
|
103
|
+
TokamakLocationPositions.stream(watching: true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func permission() -> LocationPermission {
|
|
107
|
+
let status = authorization.authorizationStatus
|
|
108
|
+
return LocationPermission(
|
|
109
|
+
inUse: Self.state(status),
|
|
110
|
+
background: backgroundPermission(status),
|
|
111
|
+
precision: authorization.accuracyAuthorization == .reducedAccuracy ? .approximate : .precise
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Asks for in-use permission while the user has not answered, or for
|
|
116
|
+
/// Always permission once from When In Use, then for temporary precise
|
|
117
|
+
/// location when `request` names a purpose.
|
|
118
|
+
@MainActor
|
|
119
|
+
func requestPermission(_ request: PermissionRequest?) async throws -> LocationPermission {
|
|
120
|
+
let always = request?.background == true
|
|
121
|
+
if always && !Self.declaresAlways {
|
|
122
|
+
throw TokamakPluginError.notSupported(
|
|
123
|
+
"Background location needs the location plugin's background build setting")
|
|
124
|
+
}
|
|
125
|
+
let purpose = try Self.purpose(request)
|
|
126
|
+
let status = authorization.authorizationStatus
|
|
127
|
+
guard status == .notDetermined || always && canUpgradeToAlways(status) else {
|
|
128
|
+
return try await respond(purpose: purpose)
|
|
129
|
+
}
|
|
130
|
+
try host.requireUI()
|
|
131
|
+
return try await withCheckedThrowingContinuation { answer in
|
|
132
|
+
prompts[UUID()] = Prompt(purpose: purpose, answer: answer)
|
|
133
|
+
if always {
|
|
134
|
+
defaults.set(true, forKey: askedForAlwaysKey)
|
|
135
|
+
authorization.requestAlwaysAuthorization()
|
|
136
|
+
} else {
|
|
137
|
+
authorization.requestWhenInUseAuthorization()
|
|
145
138
|
}
|
|
146
|
-
} catch {
|
|
147
|
-
reply(.failure(error))
|
|
148
139
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Whether location services are on, which Core Location reports off the
|
|
143
|
+
/// main thread.
|
|
144
|
+
func servicesEnabled() async -> Bool {
|
|
145
|
+
CLLocationManager.locationServicesEnabled()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Opens the app's page of the system settings on iOS, and Location Services on macOS.
|
|
149
|
+
@MainActor
|
|
150
|
+
func openSettings() async throws {
|
|
151
|
+
try host.requireUI()
|
|
152
|
+
#if os(iOS)
|
|
153
|
+
let opened = await UIApplication.shared.open(
|
|
154
|
+
URL(string: UIApplication.openSettingsURLString)!)
|
|
155
|
+
#else
|
|
156
|
+
let page = "com.apple.preference.security?Privacy_LocationServices"
|
|
157
|
+
let opened =
|
|
158
|
+
URL(string: "x-apple.systempreferences:\(page)").map(NSWorkspace.shared.open) == true
|
|
159
|
+
#endif
|
|
160
|
+
guard opened else {
|
|
161
|
+
throw TokamakPluginError(
|
|
162
|
+
name: "OperationError", message: "The location settings could not be opened")
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Starts continuous updates, which a new background activity session
|
|
167
|
+
/// allows only on screen.
|
|
168
|
+
func startUpdates(_ options: UpdateOptions?) throws(TokamakPluginError) {
|
|
169
|
+
try host.requireUI()
|
|
170
|
+
try requirePermission()
|
|
171
|
+
try updates.start(options ?? UpdateOptions(activity: nil))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func stopUpdates() {
|
|
175
|
+
updates.stop()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func getUpdates() -> UpdateOptions? {
|
|
179
|
+
updates.options
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@MainActor
|
|
183
|
+
func addRegion(_ region: Region) async throws {
|
|
184
|
+
try requirePermission()
|
|
185
|
+
guard authorization.accuracyAuthorization == .fullAccuracy else {
|
|
186
|
+
throw TokamakPluginError(name: "NotAllowedError", message: "Regions need precise location")
|
|
165
187
|
}
|
|
166
|
-
|
|
188
|
+
try await regions.add(region)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@MainActor
|
|
192
|
+
func getRegions() async -> [MonitoredRegion] {
|
|
193
|
+
await regions.all()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@MainActor
|
|
197
|
+
func removeRegion(_ id: String) async {
|
|
198
|
+
await regions.remove(id)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func onLocationUpdate() -> AsyncStream<LocationUpdate> {
|
|
202
|
+
events.updates.stream()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func onRegionTransition() -> AsyncStream<RegionTransition> {
|
|
206
|
+
events.transitions.stream()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func onVisit() -> AsyncStream<Visit> {
|
|
210
|
+
events.visits.stream()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func onLocationError() -> AsyncStream<LocationErrorEvent> {
|
|
214
|
+
events.errors.stream()
|
|
167
215
|
}
|
|
168
216
|
|
|
169
217
|
/// Also called when each manager is created. Every manager reports the
|
|
@@ -204,45 +252,26 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
204
252
|
}
|
|
205
253
|
}
|
|
206
254
|
|
|
207
|
-
/// Core Location's latest position when it is no older than `maximumAge`
|
|
208
|
-
/// seconds, or nil.
|
|
209
|
-
private func recentPosition(maximumAge: TimeInterval) -> Any? {
|
|
210
|
-
guard let recent = authorization.location, -recent.timestamp.timeIntervalSinceNow <= maximumAge
|
|
211
|
-
else { return nil }
|
|
212
|
-
return TokamakLocationEvents.position(recent)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
255
|
/// Starts the significant changes and visits `config` names, and stops the
|
|
216
256
|
/// others. After a launch for one of their events, starting them again
|
|
217
257
|
/// delivers it.
|
|
218
|
-
private func apply(_ config:
|
|
219
|
-
if config
|
|
258
|
+
private func apply(_ config: SetupConfig) {
|
|
259
|
+
if config.significantChanges == true {
|
|
220
260
|
monitoring.startMonitoringSignificantLocationChanges()
|
|
221
261
|
} else {
|
|
222
262
|
monitoring.stopMonitoringSignificantLocationChanges()
|
|
223
263
|
}
|
|
224
|
-
if config
|
|
264
|
+
if config.visits == true {
|
|
225
265
|
monitoring.startMonitoringVisits()
|
|
226
266
|
} else {
|
|
227
267
|
monitoring.stopMonitoringVisits()
|
|
228
268
|
}
|
|
229
269
|
}
|
|
230
270
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let status = authorization.authorizationStatus
|
|
234
|
-
return [
|
|
235
|
-
"inUse": Self.state(status),
|
|
236
|
-
"background": backgroundPermission(status),
|
|
237
|
-
"precision": authorization.accuracyAuthorization == .reducedAccuracy
|
|
238
|
-
? "approximate" : "precise",
|
|
239
|
-
]
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private func backgroundPermission(_ status: CLAuthorizationStatus) -> String {
|
|
243
|
-
if status == .authorizedAlways { return "granted" }
|
|
271
|
+
private func backgroundPermission(_ status: CLAuthorizationStatus) -> Permission {
|
|
272
|
+
if status == .authorizedAlways { return .granted }
|
|
244
273
|
let asks = status == .notDetermined ? Self.declaresAlways : canUpgradeToAlways(status)
|
|
245
|
-
return asks ?
|
|
274
|
+
return asks ? .prompt : .denied
|
|
246
275
|
}
|
|
247
276
|
|
|
248
277
|
/// Whether asking for Always shows a prompt: once, from When In Use, on iOS.
|
|
@@ -279,11 +308,11 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
279
308
|
#endif
|
|
280
309
|
}
|
|
281
310
|
|
|
282
|
-
private static func state(_ status: CLAuthorizationStatus) ->
|
|
311
|
+
private static func state(_ status: CLAuthorizationStatus) -> Permission {
|
|
283
312
|
switch status {
|
|
284
|
-
case .notDetermined:
|
|
285
|
-
case .denied, .restricted:
|
|
286
|
-
default:
|
|
313
|
+
case .notDetermined: .prompt
|
|
314
|
+
case .denied, .restricted: .denied
|
|
315
|
+
default: .granted
|
|
287
316
|
}
|
|
288
317
|
}
|
|
289
318
|
|
|
@@ -296,41 +325,13 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
296
325
|
defaults.bool(forKey: askedForAlwaysKey)
|
|
297
326
|
}
|
|
298
327
|
|
|
299
|
-
///
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
private func requestPermission(
|
|
303
|
-
_ options: [String: Any], _ reply: @escaping TokamakPluginReply
|
|
304
|
-
) throws(TokamakPluginError) {
|
|
305
|
-
let always = options["background"] as? Bool == true
|
|
306
|
-
if always && !Self.declaresAlways {
|
|
307
|
-
throw .notSupported(
|
|
308
|
-
"Background location needs the location plugin's background build setting")
|
|
309
|
-
}
|
|
310
|
-
let prompt = Prompt(purpose: try Self.purpose(options), reply: reply)
|
|
311
|
-
let status = authorization.authorizationStatus
|
|
312
|
-
let upgrades = always && canUpgradeToAlways(status)
|
|
313
|
-
guard status == .notDetermined || upgrades else {
|
|
314
|
-
respond(to: prompt)
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
try host.requireUI()
|
|
318
|
-
prompts[UUID()] = prompt
|
|
319
|
-
if always {
|
|
320
|
-
defaults.set(true, forKey: askedForAlwaysKey)
|
|
321
|
-
authorization.requestAlwaysAuthorization()
|
|
322
|
-
} else {
|
|
323
|
-
authorization.requestWhenInUseAuthorization()
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/// The purpose key of the temporary precise location `options` ask for.
|
|
328
|
-
private static func purpose(_ options: [String: Any]) throws(TokamakPluginError) -> String? {
|
|
329
|
-
guard options["precision"] as? String == "precise" else { return nil }
|
|
328
|
+
/// The purpose key of the temporary precise location `request` asks for.
|
|
329
|
+
private static func purpose(_ request: PermissionRequest?) throws(TokamakPluginError) -> String? {
|
|
330
|
+
guard request?.precision == .precise else { return nil }
|
|
330
331
|
let purposes =
|
|
331
332
|
Bundle.main.object(forInfoDictionaryKey: "NSLocationTemporaryUsageDescriptionDictionary")
|
|
332
333
|
as? [String: Any]
|
|
333
|
-
guard let purpose =
|
|
334
|
+
guard let purpose = request?.purpose, purposes?[purpose] != nil else {
|
|
334
335
|
throw TokamakPluginError(
|
|
335
336
|
name: "TypeError",
|
|
336
337
|
message: "precise location needs a purpose from the accuracy-purposes build setting")
|
|
@@ -358,44 +359,29 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
358
359
|
/// Answers the prompts `keys` that still wait.
|
|
359
360
|
private func answer(_ keys: [UUID]) {
|
|
360
361
|
for key in keys {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
362
|
+
guard let prompt = prompts.removeValue(forKey: key) else { continue }
|
|
363
|
+
Task { @MainActor in await answer(prompt) }
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
authorization.accuracyAuthorization == .reducedAccuracy
|
|
372
|
-
else {
|
|
373
|
-
prompt.reply(.success(permission()))
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
do throws(TokamakPluginError) {
|
|
377
|
-
try host.requireUI()
|
|
367
|
+
@MainActor
|
|
368
|
+
private func answer(_ prompt: Prompt) async {
|
|
369
|
+
do {
|
|
370
|
+
prompt.answer.resume(returning: try await respond(purpose: prompt.purpose))
|
|
378
371
|
} catch {
|
|
379
|
-
prompt.
|
|
380
|
-
return
|
|
381
|
-
}
|
|
382
|
-
authorization.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purpose) { _ in
|
|
383
|
-
DispatchQueue.main.async { prompt.reply(.success(self.permission())) }
|
|
372
|
+
prompt.answer.resume(throwing: error)
|
|
384
373
|
}
|
|
385
374
|
}
|
|
386
375
|
|
|
387
|
-
///
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
let opened = URL(string: "x-apple.systempreferences:\(page)").map(NSWorkspace.shared.open)
|
|
398
|
-
reply(opened == true ? .success(nil) : .failure(notOpened))
|
|
399
|
-
#endif
|
|
376
|
+
/// The app's permission, after asking for temporary precise location for
|
|
377
|
+
/// `purpose` when the app has approximate location.
|
|
378
|
+
@MainActor
|
|
379
|
+
private func respond(purpose: String?) async throws -> LocationPermission {
|
|
380
|
+
if let purpose, hasPermission, authorization.accuracyAuthorization == .reducedAccuracy {
|
|
381
|
+
try host.requireUI()
|
|
382
|
+
_ = try? await authorization.requestTemporaryFullAccuracyAuthorization(
|
|
383
|
+
withPurposeKey: purpose)
|
|
384
|
+
}
|
|
385
|
+
return permission()
|
|
400
386
|
}
|
|
401
387
|
}
|
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
import CoreLocation
|
|
2
2
|
import Foundation
|
|
3
3
|
|
|
4
|
-
/// Positions from a location manager of its own, for one `
|
|
5
|
-
/// or `watchPosition`. Use it on the main thread
|
|
6
|
-
|
|
4
|
+
/// Positions from a location manager of its own, for one `currentPosition`
|
|
5
|
+
/// or `watchPosition`. Use it on the main thread, where the end of its stream
|
|
6
|
+
/// stops it.
|
|
7
|
+
final class TokamakLocationPositions: NSObject, CLLocationManagerDelegate, @unchecked Sendable {
|
|
7
8
|
private let manager = CLLocationManager()
|
|
8
|
-
private let
|
|
9
|
-
private
|
|
9
|
+
private let continuation: AsyncThrowingStream<TokamakLocationPlugin.Position, Error>.Continuation
|
|
10
|
+
private let watching: Bool
|
|
10
11
|
|
|
11
|
-
///
|
|
12
|
-
///
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/// A new fix, or each position as the device moves when `watching`, until
|
|
13
|
+
/// the stream ends.
|
|
14
|
+
static func stream(watching: Bool) -> AsyncThrowingStream<TokamakLocationPlugin.Position, Error> {
|
|
15
|
+
AsyncThrowingStream { continuation in
|
|
16
|
+
let positions = TokamakLocationPositions(continuation, watching: watching)
|
|
17
|
+
// The stream owns the location manager until it ends.
|
|
18
|
+
continuation.onTermination = { _ in
|
|
19
|
+
DispatchQueue.main.async { positions.manager.stopUpdatingLocation() }
|
|
20
|
+
}
|
|
21
|
+
if watching {
|
|
22
|
+
positions.manager.startUpdatingLocation()
|
|
23
|
+
} else {
|
|
24
|
+
positions.manager.requestLocation()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private init(
|
|
30
|
+
_ continuation: AsyncThrowingStream<TokamakLocationPlugin.Position, Error>.Continuation,
|
|
31
|
+
watching: Bool
|
|
32
|
+
) {
|
|
33
|
+
self.continuation = continuation
|
|
34
|
+
self.watching = watching
|
|
15
35
|
super.init()
|
|
16
36
|
manager.delegate = self
|
|
17
37
|
manager.desiredAccuracy = kCLLocationAccuracyBest
|
|
18
38
|
}
|
|
19
39
|
|
|
20
|
-
func locate() {
|
|
21
|
-
manager.requestLocation()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
func watch() {
|
|
25
|
-
watching = true
|
|
26
|
-
manager.startUpdatingLocation()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
func stop() {
|
|
30
|
-
manager.stopUpdatingLocation()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
40
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
34
41
|
for location in watching ? locations : locations.suffix(1) {
|
|
35
|
-
|
|
42
|
+
continuation.yield(TokamakLocationPlugin.Position(location))
|
|
43
|
+
}
|
|
44
|
+
if !watching {
|
|
45
|
+
continuation.finish()
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -40,7 +50,7 @@ final class TokamakLocationPositions: NSObject, CLLocationManagerDelegate {
|
|
|
40
50
|
/// position yet.
|
|
41
51
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
42
52
|
guard (error as NSError).code == CLError.Code.locationUnknown.rawValue else {
|
|
43
|
-
return
|
|
53
|
+
return continuation.finish(throwing: TokamakPluginError.location(error))
|
|
44
54
|
}
|
|
45
55
|
if !watching {
|
|
46
56
|
manager.requestLocation()
|
|
@@ -5,7 +5,7 @@ import Foundation
|
|
|
5
5
|
import UIKit
|
|
6
6
|
#endif
|
|
7
7
|
|
|
8
|
-
/// The JSON of each monitored region,
|
|
8
|
+
/// The JSON of each monitored region, by id.
|
|
9
9
|
private let regionsKey = "tokamak.location.regions"
|
|
10
10
|
/// The ids of the regions whose last transition was an entry.
|
|
11
11
|
private let insideKey = "tokamak.location.inside"
|
|
@@ -30,26 +30,23 @@ final class TokamakLocationRegions {
|
|
|
30
30
|
return await CLMonitor(monitorName)
|
|
31
31
|
}
|
|
32
32
|
self.monitor = monitor
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self?.receive(event)
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
self?.events.fail(
|
|
40
|
-
.locationUnavailable(error.localizedDescription))
|
|
33
|
+
events.task { [weak self] in
|
|
34
|
+
for try await event in await monitor.value.events {
|
|
35
|
+
self?.receive(event)
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
/// The regions, by id.
|
|
46
|
-
private var regions: [String:
|
|
41
|
+
private var regions: [String: TokamakLocationPlugin.Region] {
|
|
47
42
|
get {
|
|
48
43
|
defaults.data(forKey: regionsKey)
|
|
49
|
-
.flatMap {
|
|
44
|
+
.flatMap {
|
|
45
|
+
try? JSONDecoder().decode([String: TokamakLocationPlugin.Region].self, from: $0)
|
|
46
|
+
} ?? [:]
|
|
50
47
|
}
|
|
51
48
|
set {
|
|
52
|
-
defaults.set(try?
|
|
49
|
+
defaults.set(try? JSONEncoder().encode(newValue), forKey: regionsKey)
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -58,17 +55,12 @@ final class TokamakLocationRegions {
|
|
|
58
55
|
set { defaults.set(Array(newValue), forKey: insideKey) }
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
/// Monitors
|
|
62
|
-
///
|
|
63
|
-
///
|
|
64
|
-
///
|
|
65
|
-
func add(_ region:
|
|
66
|
-
|
|
67
|
-
let latitude = region["latitude"] as? Double, let longitude = region["longitude"] as? Double
|
|
68
|
-
else {
|
|
69
|
-
throw TokamakPluginError(
|
|
70
|
-
name: "TypeError", message: "region must have an id, latitude, longitude and radius")
|
|
71
|
-
}
|
|
58
|
+
/// Monitors `region`, replacing one with the same id. It starts outside, so
|
|
59
|
+
/// a device already inside reports an entry, as on Android. `CLMonitor`
|
|
60
|
+
/// keeps the state of a condition added again under its identifier, so the
|
|
61
|
+
/// old condition is removed first.
|
|
62
|
+
func add(_ region: TokamakLocationPlugin.Region) async throws(TokamakPluginError) {
|
|
63
|
+
let id = region.id
|
|
72
64
|
let monitor = await monitor.value
|
|
73
65
|
let monitored = await monitor.identifiers
|
|
74
66
|
guard monitored.count < regionLimit || monitored.contains(id) else {
|
|
@@ -76,8 +68,8 @@ final class TokamakLocationRegions {
|
|
|
76
68
|
name: "QuotaExceededError",
|
|
77
69
|
message: "The app monitors \(regionLimit) regions, the most the system allows")
|
|
78
70
|
}
|
|
79
|
-
let center = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
|
80
|
-
let condition = CLMonitor.CircularGeographicCondition(center: center, radius: radius)
|
|
71
|
+
let center = CLLocationCoordinate2D(latitude: region.latitude, longitude: region.longitude)
|
|
72
|
+
let condition = CLMonitor.CircularGeographicCondition(center: center, radius: region.radius)
|
|
81
73
|
regions[id] = region
|
|
82
74
|
inside.remove(id)
|
|
83
75
|
await monitor.remove(id)
|
|
@@ -92,19 +84,22 @@ final class TokamakLocationRegions {
|
|
|
92
84
|
}
|
|
93
85
|
|
|
94
86
|
/// The monitored regions, each with whether the device is inside it.
|
|
95
|
-
func all() async -> [
|
|
87
|
+
func all() async -> [TokamakLocationPlugin.MonitoredRegion] {
|
|
96
88
|
let monitor = await monitor.value
|
|
97
89
|
let regions = regions
|
|
98
|
-
var all: [
|
|
90
|
+
var all: [TokamakLocationPlugin.MonitoredRegion] = []
|
|
99
91
|
for id in await monitor.identifiers {
|
|
100
|
-
guard
|
|
101
|
-
|
|
92
|
+
guard let region = regions[id] else { continue }
|
|
93
|
+
let state: TokamakLocationPlugin.RegionState =
|
|
102
94
|
switch await monitor.record(for: id)?.lastEvent.state {
|
|
103
|
-
case .satisfied:
|
|
104
|
-
case .unsatisfied:
|
|
105
|
-
default:
|
|
95
|
+
case .satisfied: .inside
|
|
96
|
+
case .unsatisfied: .outside
|
|
97
|
+
default: .unknown
|
|
106
98
|
}
|
|
107
|
-
all.append(
|
|
99
|
+
all.append(
|
|
100
|
+
TokamakLocationPlugin.MonitoredRegion(
|
|
101
|
+
id: id, latitude: region.latitude, longitude: region.longitude, radius: region.radius,
|
|
102
|
+
onEntry: region.onEntry ?? true, onExit: region.onExit ?? true, state: state))
|
|
108
103
|
}
|
|
109
104
|
return all
|
|
110
105
|
}
|
|
@@ -121,7 +116,7 @@ final class TokamakLocationRegions {
|
|
|
121
116
|
let entered = event.state == .satisfied
|
|
122
117
|
guard entered != inside.contains(id) else { return }
|
|
123
118
|
inside = entered ? inside.union([id]) : inside.subtracting([id])
|
|
124
|
-
if
|
|
119
|
+
if (entered ? region.onEntry : region.onExit) != false {
|
|
125
120
|
events.transition(id, entered: entered, at: event.date)
|
|
126
121
|
}
|
|
127
122
|
}
|