@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
|
@@ -18,19 +18,15 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
18
18
|
CLLocationManagerDelegate
|
|
19
19
|
{
|
|
20
20
|
let id = "location"
|
|
21
|
+
let host: TokamakHost
|
|
21
22
|
|
|
22
|
-
private static let permissionDenied = TokamakPluginError(
|
|
23
|
-
name: "NotAllowedError",
|
|
24
|
-
message: "Location permission was denied"
|
|
25
|
-
)
|
|
26
23
|
/// A `requestPermission` waiting for the user to answer a prompt.
|
|
27
24
|
private struct Prompt {
|
|
28
25
|
/// The purpose key of the temporary precise location to ask for next.
|
|
29
26
|
let purpose: String?
|
|
30
|
-
let
|
|
27
|
+
let answer: CheckedContinuation<LocationPermission, Error>
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
private let host: TokamakHost
|
|
34
30
|
private let defaults = UserDefaults.standard
|
|
35
31
|
/// Reports the app's authorization, and asks for permission.
|
|
36
32
|
private let authorization = CLLocationManager()
|
|
@@ -59,7 +55,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
59
55
|
authorization.delegate = self
|
|
60
56
|
monitoring.delegate = self
|
|
61
57
|
if let stored = defaults.data(forKey: setupKey),
|
|
62
|
-
let config = try?
|
|
58
|
+
let config = try? JSONDecoder().decode(SetupConfig.self, from: stored)
|
|
63
59
|
{
|
|
64
60
|
apply(config)
|
|
65
61
|
}
|
|
@@ -74,87 +70,148 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
74
70
|
updates.resume()
|
|
75
71
|
}
|
|
76
72
|
|
|
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
|
-
|
|
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()
|
|
142
138
|
}
|
|
143
|
-
} catch {
|
|
144
|
-
reply(.failure(error))
|
|
145
139
|
}
|
|
146
|
-
return nil
|
|
147
140
|
}
|
|
148
141
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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")
|
|
163
|
+
}
|
|
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")
|
|
156
187
|
}
|
|
157
|
-
|
|
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()
|
|
158
215
|
}
|
|
159
216
|
|
|
160
217
|
/// Also called when each manager is created. Every manager reports the
|
|
@@ -185,7 +242,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
185
242
|
) {
|
|
186
243
|
guard manager === monitoring, (error as NSError).code != CLError.Code.locationUnknown.rawValue
|
|
187
244
|
else { return }
|
|
188
|
-
events.fail(
|
|
245
|
+
events.fail(.location(error))
|
|
189
246
|
}
|
|
190
247
|
|
|
191
248
|
/// Core Location delivers each visit to every manager's delegate.
|
|
@@ -198,34 +255,23 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
198
255
|
/// Starts the significant changes and visits `config` names, and stops the
|
|
199
256
|
/// others. After a launch for one of their events, starting them again
|
|
200
257
|
/// delivers it.
|
|
201
|
-
private func apply(_ config:
|
|
202
|
-
if config
|
|
258
|
+
private func apply(_ config: SetupConfig) {
|
|
259
|
+
if config.significantChanges == true {
|
|
203
260
|
monitoring.startMonitoringSignificantLocationChanges()
|
|
204
261
|
} else {
|
|
205
262
|
monitoring.stopMonitoringSignificantLocationChanges()
|
|
206
263
|
}
|
|
207
|
-
if config
|
|
264
|
+
if config.visits == true {
|
|
208
265
|
monitoring.startMonitoringVisits()
|
|
209
266
|
} else {
|
|
210
267
|
monitoring.stopMonitoringVisits()
|
|
211
268
|
}
|
|
212
269
|
}
|
|
213
270
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let status = authorization.authorizationStatus
|
|
217
|
-
return [
|
|
218
|
-
"inUse": Self.state(status),
|
|
219
|
-
"background": backgroundPermission(status),
|
|
220
|
-
"precision": authorization.accuracyAuthorization == .reducedAccuracy
|
|
221
|
-
? "approximate" : "precise",
|
|
222
|
-
]
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private func backgroundPermission(_ status: CLAuthorizationStatus) -> String {
|
|
226
|
-
if status == .authorizedAlways { return "granted" }
|
|
271
|
+
private func backgroundPermission(_ status: CLAuthorizationStatus) -> Permission {
|
|
272
|
+
if status == .authorizedAlways { return .granted }
|
|
227
273
|
let asks = status == .notDetermined ? Self.declaresAlways : canUpgradeToAlways(status)
|
|
228
|
-
return asks ?
|
|
274
|
+
return asks ? .prompt : .denied
|
|
229
275
|
}
|
|
230
276
|
|
|
231
277
|
/// Whether asking for Always shows a prompt: once, from When In Use, on iOS.
|
|
@@ -262,11 +308,11 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
262
308
|
#endif
|
|
263
309
|
}
|
|
264
310
|
|
|
265
|
-
private static func state(_ status: CLAuthorizationStatus) ->
|
|
311
|
+
private static func state(_ status: CLAuthorizationStatus) -> Permission {
|
|
266
312
|
switch status {
|
|
267
|
-
case .notDetermined:
|
|
268
|
-
case .denied, .restricted:
|
|
269
|
-
default:
|
|
313
|
+
case .notDetermined: .prompt
|
|
314
|
+
case .denied, .restricted: .denied
|
|
315
|
+
default: .granted
|
|
270
316
|
}
|
|
271
317
|
}
|
|
272
318
|
|
|
@@ -279,41 +325,13 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
279
325
|
defaults.bool(forKey: askedForAlwaysKey)
|
|
280
326
|
}
|
|
281
327
|
|
|
282
|
-
///
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
private func requestPermission(
|
|
286
|
-
_ options: [String: Any], _ reply: @escaping TokamakPluginReply
|
|
287
|
-
) throws(TokamakPluginError) {
|
|
288
|
-
let always = options["background"] as? Bool == true
|
|
289
|
-
if always && !Self.declaresAlways {
|
|
290
|
-
throw .notSupported(
|
|
291
|
-
"Background location needs the location plugin's background build setting")
|
|
292
|
-
}
|
|
293
|
-
let prompt = Prompt(purpose: try Self.purpose(options), reply: reply)
|
|
294
|
-
let status = authorization.authorizationStatus
|
|
295
|
-
let upgrades = always && canUpgradeToAlways(status)
|
|
296
|
-
guard status == .notDetermined || upgrades else {
|
|
297
|
-
respond(to: prompt)
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
try host.requireUI()
|
|
301
|
-
prompts[UUID()] = prompt
|
|
302
|
-
if always {
|
|
303
|
-
defaults.set(true, forKey: askedForAlwaysKey)
|
|
304
|
-
authorization.requestAlwaysAuthorization()
|
|
305
|
-
} else {
|
|
306
|
-
authorization.requestWhenInUseAuthorization()
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/// The purpose key of the temporary precise location `options` ask for.
|
|
311
|
-
private static func purpose(_ options: [String: Any]) throws(TokamakPluginError) -> String? {
|
|
312
|
-
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 }
|
|
313
331
|
let purposes =
|
|
314
332
|
Bundle.main.object(forInfoDictionaryKey: "NSLocationTemporaryUsageDescriptionDictionary")
|
|
315
333
|
as? [String: Any]
|
|
316
|
-
guard let purpose =
|
|
334
|
+
guard let purpose = request?.purpose, purposes?[purpose] != nil else {
|
|
317
335
|
throw TokamakPluginError(
|
|
318
336
|
name: "TypeError",
|
|
319
337
|
message: "precise location needs a purpose from the accuracy-purposes build setting")
|
|
@@ -341,51 +359,29 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
341
359
|
/// Answers the prompts `keys` that still wait.
|
|
342
360
|
private func answer(_ keys: [UUID]) {
|
|
343
361
|
for key in keys {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
362
|
+
guard let prompt = prompts.removeValue(forKey: key) else { continue }
|
|
363
|
+
Task { @MainActor in await answer(prompt) }
|
|
347
364
|
}
|
|
348
365
|
}
|
|
349
366
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
authorization.accuracyAuthorization == .reducedAccuracy
|
|
355
|
-
else {
|
|
356
|
-
prompt.reply(.success(permission()))
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
do throws(TokamakPluginError) {
|
|
360
|
-
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))
|
|
361
371
|
} catch {
|
|
362
|
-
prompt.
|
|
363
|
-
return
|
|
372
|
+
prompt.answer.resume(throwing: error)
|
|
364
373
|
}
|
|
365
|
-
authorization.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purpose) { _ in
|
|
366
|
-
DispatchQueue.main.async { prompt.reply(.success(self.permission())) }
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/// Opens the app's page of the system settings on iOS, and Location Services on macOS.
|
|
371
|
-
private func openSettings(_ reply: @escaping TokamakPluginReply) {
|
|
372
|
-
let notOpened = TokamakPluginError(
|
|
373
|
-
name: "OperationError", message: "The location settings could not be opened")
|
|
374
|
-
#if os(iOS)
|
|
375
|
-
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) { opened in
|
|
376
|
-
reply(opened ? .success(nil) : .failure(notOpened))
|
|
377
|
-
}
|
|
378
|
-
#else
|
|
379
|
-
let page = "com.apple.preference.security?Privacy_LocationServices"
|
|
380
|
-
let opened = URL(string: "x-apple.systempreferences:\(page)").map(NSWorkspace.shared.open)
|
|
381
|
-
reply(opened == true ? .success(nil) : .failure(notOpened))
|
|
382
|
-
#endif
|
|
383
374
|
}
|
|
384
375
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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)
|
|
388
384
|
}
|
|
389
|
-
return
|
|
385
|
+
return permission()
|
|
390
386
|
}
|
|
391
387
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
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 {
|
|
8
|
+
private let manager = CLLocationManager()
|
|
9
|
+
private let continuation: AsyncThrowingStream<TokamakLocationPlugin.Position, Error>.Continuation
|
|
10
|
+
private let watching: Bool
|
|
11
|
+
|
|
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
|
|
35
|
+
super.init()
|
|
36
|
+
manager.delegate = self
|
|
37
|
+
manager.desiredAccuracy = kCLLocationAccuracyBest
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
41
|
+
for location in watching ? locations : locations.suffix(1) {
|
|
42
|
+
continuation.yield(TokamakLocationPlugin.Position(location))
|
|
43
|
+
}
|
|
44
|
+
if !watching {
|
|
45
|
+
continuation.finish()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Asks again, or keeps watching, while Core Location does not know the
|
|
50
|
+
/// position yet.
|
|
51
|
+
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
52
|
+
guard (error as NSError).code == CLError.Code.locationUnknown.rawValue else {
|
|
53
|
+
return continuation.finish(throwing: TokamakPluginError.location(error))
|
|
54
|
+
}
|
|
55
|
+
if !watching {
|
|
56
|
+
manager.requestLocation()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -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
|
}
|