@tokamakdev/plugin-location 0.1.0-beta.107 → 0.1.0-beta.112

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.
@@ -23,19 +23,6 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
23
23
  name: "NotAllowedError",
24
24
  message: "Location permission was denied"
25
25
  )
26
- private static let timedOut = TokamakPluginError(
27
- name: "TimeoutError",
28
- message: "Location was not found in time"
29
- )
30
-
31
- /// A current position request, timed from when locating starts.
32
- private struct Request {
33
- let reply: TokamakPluginReply
34
- let maximumAge: TimeInterval
35
- let timeout: TimeInterval
36
- var timer: DispatchWorkItem?
37
- }
38
-
39
26
  /// A `requestPermission` waiting for the user to answer a prompt.
40
27
  private struct Prompt {
41
28
  /// The purpose key of the temporary precise location to ask for next.
@@ -45,14 +32,10 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
45
32
 
46
33
  private let host: TokamakHost
47
34
  private let defaults = UserDefaults.standard
48
- /// Locates for watchers, and asks for permission.
49
- private let watching = CLLocationManager()
50
- /// Takes single fixes for current position requests, apart from watching.
51
- private let fixes = CLLocationManager()
35
+ /// Reports the app's authorization, and asks for permission.
36
+ private let authorization = CLLocationManager()
52
37
  /// Delivers the significant changes and visits `setup` names.
53
38
  private let monitoring = CLLocationManager()
54
- private var current: [UUID: Request] = [:]
55
- private var watchers: [UUID: TokamakPluginReply] = [:]
56
39
  private var prompts: [UUID: Prompt] = [:]
57
40
  /// The prompts waiting when the app resigned active, as a prompt showing
58
41
  /// makes it.
@@ -73,10 +56,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
73
56
  updates = TokamakLocationUpdates(events: events)
74
57
  regions = TokamakLocationRegions(events: events)
75
58
  super.init()
76
- for manager in [watching, fixes] {
77
- manager.delegate = self
78
- manager.desiredAccuracy = kCLLocationAccuracyBest
79
- }
59
+ authorization.delegate = self
80
60
  monitoring.delegate = self
81
61
  if let stored = defaults.data(forKey: setupKey),
82
62
  let config = try? JSONSerialization.jsonObject(with: stored) as? [String: Any]
@@ -98,7 +78,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
98
78
  method: String,
99
79
  arguments: Any,
100
80
  reply: @escaping TokamakPluginReply
101
- ) {
81
+ ) -> (() -> Void)? {
102
82
  let options = arguments as? [String: Any] ?? [:]
103
83
  do throws(TokamakPluginError) {
104
84
  switch method {
@@ -111,13 +91,6 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
111
91
  apply(options)
112
92
  defaults.set(try? JSONSerialization.data(withJSONObject: options), forKey: setupKey)
113
93
  reply(.success(nil))
114
- case "getCurrentPosition":
115
- current[UUID()] = Request(
116
- reply: reply,
117
- maximumAge: (options["maximumAge"] as? Double ?? 0) / 1000,
118
- timeout: (options["timeout"] as? Double ?? Double(UInt32.max)) / 1000
119
- )
120
- start()
121
94
  case "permission":
122
95
  reply(.success(permission()))
123
96
  case "requestPermission":
@@ -142,7 +115,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
142
115
  reply(.success(updates.options))
143
116
  case "addRegion":
144
117
  try requirePermission()
145
- guard watching.accuracyAuthorization == .fullAccuracy else {
118
+ guard authorization.accuracyAuthorization == .fullAccuracy else {
146
119
  throw TokamakPluginError(
147
120
  name: "NotAllowedError", message: "Regions need precise location")
148
121
  }
@@ -170,6 +143,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
170
143
  } catch {
171
144
  reply(.failure(error))
172
145
  }
146
+ return nil
173
147
  }
174
148
 
175
149
  func subscribe(
@@ -177,66 +151,41 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
177
151
  arguments: Any,
178
152
  reply: @escaping TokamakPluginReply
179
153
  ) throws(TokamakPluginError) -> (() -> Void) {
180
- if let remove = events.listen(method, reply) {
181
- return remove
182
- }
183
- guard method == "watchPosition" else {
154
+ guard let remove = events.listen(method, reply) else {
184
155
  throw .notSupported("\(id).\(method) is not supported")
185
156
  }
186
- let subscription = UUID()
187
- watchers[subscription] = reply
188
- start()
189
- return { [weak self] in
190
- self?.watchers.removeValue(forKey: subscription)
191
- if self?.watchers.isEmpty == true {
192
- self?.watching.stopUpdatingLocation()
193
- }
194
- }
157
+ return remove
195
158
  }
196
159
 
197
- /// Also called when each manager is created, before anything asks for a
198
- /// position. Every manager reports the app's one authorization, which
199
- /// `watching`'s calls handle.
160
+ /// Also called when each manager is created. Every manager reports the
161
+ /// app's one authorization, which `authorization`'s calls handle.
200
162
  func locationManagerDidChangeAuthorization(
201
163
  _ manager: CLLocationManager
202
164
  ) {
203
- guard manager === watching else { return }
165
+ guard manager === authorization else { return }
204
166
  holdAlwaysSession()
205
167
  guard manager.authorizationStatus != .notDetermined else { return }
206
168
  answer(Array(prompts.keys))
207
- if !current.isEmpty || !watchers.isEmpty {
208
- serve(manager.authorizationStatus)
209
- }
210
169
  }
211
170
 
212
171
  func locationManager(
213
172
  _ manager: CLLocationManager,
214
173
  didUpdateLocations locations: [CLLocation]
215
174
  ) {
216
- if manager === monitoring {
217
- for location in locations {
218
- events.update(location)
219
- }
220
- return
175
+ guard manager === monitoring else { return }
176
+ for location in locations {
177
+ events.update(location)
221
178
  }
222
- guard let location = locations.last else { return }
223
- deliver(.success(TokamakLocationEvents.position(location)), from: manager)
224
179
  }
225
180
 
226
- /// A fix that is not yet available is taken again; updates continue.
181
+ /// Monitoring continues while the position is not yet known.
227
182
  func locationManager(
228
183
  _ manager: CLLocationManager,
229
184
  didFailWithError error: Error
230
185
  ) {
231
- if (error as NSError).code == CLError.Code.locationUnknown.rawValue {
232
- if manager === fixes && !current.isEmpty {
233
- fixes.requestLocation()
234
- }
235
- } else if manager === monitoring {
236
- events.fail(locationError(error))
237
- } else {
238
- deliver(.failure(locationError(error)), from: manager)
239
- }
186
+ guard manager === monitoring, (error as NSError).code != CLError.Code.locationUnknown.rawValue
187
+ else { return }
188
+ events.fail(locationError(error))
240
189
  }
241
190
 
242
191
  /// Core Location delivers each visit to every manager's delegate.
@@ -262,102 +211,14 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
262
211
  }
263
212
  }
264
213
 
265
- /// Checks location services off the main thread, which the check can block.
266
- private func start() {
267
- DispatchQueue.global(qos: .userInitiated).async {
268
- let enabled = CLLocationManager.locationServicesEnabled()
269
- DispatchQueue.main.async { self.start(servicesEnabled: enabled) }
270
- }
271
- }
272
-
273
- private func start(servicesEnabled: Bool) {
274
- guard servicesEnabled else {
275
- fail(.locationUnavailable("Location services are disabled"))
276
- return
277
- }
278
- let status = watching.authorizationStatus
279
- guard status == .notDetermined else {
280
- serve(status)
281
- return
282
- }
283
- do throws(TokamakPluginError) {
284
- try host.requireUI()
285
- watching.requestWhenInUseAuthorization()
286
- } catch {
287
- fail(error)
288
- }
289
- }
290
-
291
- /// Locates for the waiting requests and watchers when `status` allows it,
292
- /// and fails them when it forbids it.
293
- private func serve(_ status: CLAuthorizationStatus) {
294
- switch status {
295
- case .authorizedAlways, .authorizedWhenInUse:
296
- if !watchers.isEmpty {
297
- watching.startUpdatingLocation()
298
- }
299
- locate()
300
- case .denied, .restricted:
301
- fail(Self.permissionDenied)
302
- case .notDetermined:
303
- break
304
- @unknown default:
305
- fail(.notSupported("Location authorization is not supported"))
306
- }
307
- }
308
-
309
- /// Answers each new request with a fix no older than its `maximumAge`, and
310
- /// takes a fix for the rest, each failing after its `timeout`.
311
- private func locate() {
312
- for (key, request) in current where request.timer == nil {
313
- if let fix = fixes.location, -fix.timestamp.timeIntervalSinceNow <= request.maximumAge {
314
- finish(key, .success(TokamakLocationEvents.position(fix)))
315
- continue
316
- }
317
- let timer = DispatchWorkItem { [weak self] in self?.finish(key, .failure(Self.timedOut)) }
318
- current[key]?.timer = timer
319
- DispatchQueue.main.asyncAfter(deadline: .now() + request.timeout, execute: timer)
320
- }
321
- if !current.isEmpty {
322
- fixes.requestLocation()
323
- }
324
- }
325
-
326
- /// Answers the waiting requests with a fix, or the watchers with an update.
327
- private func deliver(_ result: Result<Any?, TokamakPluginError>, from manager: CLLocationManager)
328
- {
329
- guard manager === fixes else {
330
- for reply in watchers.values {
331
- reply(result)
332
- }
333
- return
334
- }
335
- for key in current.keys {
336
- finish(key, result)
337
- }
338
- }
339
-
340
- private func fail(_ error: TokamakPluginError) {
341
- deliver(.failure(error), from: fixes)
342
- deliver(.failure(error), from: watching)
343
- }
344
-
345
- private func finish(_ key: UUID, _ result: Result<Any?, TokamakPluginError>) {
346
- guard let request = current.removeValue(forKey: key) else { return }
347
- request.timer?.cancel()
348
- request.reply(result)
349
- if current.isEmpty {
350
- fixes.stopUpdatingLocation()
351
- }
352
- }
353
-
354
214
  /// The app's in-use and background permission, and the precision it gets.
355
215
  private func permission() -> [String: Any] {
356
- let status = watching.authorizationStatus
216
+ let status = authorization.authorizationStatus
357
217
  return [
358
218
  "inUse": Self.state(status),
359
219
  "background": backgroundPermission(status),
360
- "precision": watching.accuracyAuthorization == .reducedAccuracy ? "approximate" : "precise",
220
+ "precision": authorization.accuracyAuthorization == .reducedAccuracy
221
+ ? "approximate" : "precise",
361
222
  ]
362
223
  }
363
224
 
@@ -377,7 +238,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
377
238
  }
378
239
 
379
240
  private var hasPermission: Bool {
380
- ![.notDetermined, .denied, .restricted].contains(watching.authorizationStatus)
241
+ ![.notDetermined, .denied, .restricted].contains(authorization.authorizationStatus)
381
242
  }
382
243
 
383
244
  /// Throws `NotAllowedError` unless the app has location permission.
@@ -391,7 +252,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
391
252
  private func holdAlwaysSession() {
392
253
  #if os(iOS)
393
254
  guard #available(iOS 18, *) else { return }
394
- let always = watching.authorizationStatus == .authorizedAlways
255
+ let always = authorization.authorizationStatus == .authorizedAlways
395
256
  if always && alwaysSession == nil {
396
257
  alwaysSession = CLServiceSession(authorization: .always)
397
258
  } else if !always {
@@ -430,7 +291,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
430
291
  "Background location needs the location plugin's background build setting")
431
292
  }
432
293
  let prompt = Prompt(purpose: try Self.purpose(options), reply: reply)
433
- let status = watching.authorizationStatus
294
+ let status = authorization.authorizationStatus
434
295
  let upgrades = always && canUpgradeToAlways(status)
435
296
  guard status == .notDetermined || upgrades else {
436
297
  respond(to: prompt)
@@ -440,9 +301,9 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
440
301
  prompts[UUID()] = prompt
441
302
  if always {
442
303
  defaults.set(true, forKey: askedForAlwaysKey)
443
- watching.requestAlwaysAuthorization()
304
+ authorization.requestAlwaysAuthorization()
444
305
  } else {
445
- watching.requestWhenInUseAuthorization()
306
+ authorization.requestWhenInUseAuthorization()
446
307
  }
447
308
  }
448
309
 
@@ -490,7 +351,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
490
351
  /// precise location for its purpose when the app has approximate location.
491
352
  private func respond(to prompt: Prompt) {
492
353
  guard let purpose = prompt.purpose, hasPermission,
493
- watching.accuracyAuthorization == .reducedAccuracy
354
+ authorization.accuracyAuthorization == .reducedAccuracy
494
355
  else {
495
356
  prompt.reply(.success(permission()))
496
357
  return
@@ -501,7 +362,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
501
362
  prompt.reply(.failure(error))
502
363
  return
503
364
  }
504
- watching.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purpose) { _ in
365
+ authorization.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purpose) { _ in
505
366
  DispatchQueue.main.async { prompt.reply(.success(self.permission())) }
506
367
  }
507
368
  }
@@ -60,7 +60,8 @@ final class TokamakLocationRegions {
60
60
 
61
61
  /// Monitors the region `region` describes, replacing one with the same id.
62
62
  /// It starts outside, so a device already inside reports an entry, as on
63
- /// Android.
63
+ /// Android. `CLMonitor` keeps the state of a condition added again under its
64
+ /// identifier, so the old condition is removed first.
64
65
  func add(_ region: [String: Any]) async throws(TokamakPluginError) {
65
66
  guard let id = region["id"] as? String, let radius = region["radius"] as? Double,
66
67
  let latitude = region["latitude"] as? Double, let longitude = region["longitude"] as? Double
@@ -79,6 +80,7 @@ final class TokamakLocationRegions {
79
80
  let condition = CLMonitor.CircularGeographicCondition(center: center, radius: radius)
80
81
  regions[id] = region
81
82
  inside.remove(id)
83
+ await monitor.remove(id)
82
84
  await monitor.add(condition, identifier: id, assuming: .unsatisfied)
83
85
  }
84
86