@tokamakdev/plugin-location 0.1.0-beta.128 → 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.
@@ -7,7 +7,6 @@ import android.content.Context
7
7
  import android.content.Intent
8
8
  import android.location.LocationManager
9
9
  import android.location.LocationRequest
10
- import org.json.JSONObject
11
10
 
12
11
  /** The JSON of each kind of updates' options, by name. */
13
12
  private const val PREFERENCES = "tokamak.location"
@@ -15,9 +14,9 @@ private const val DEFAULT_INTERVAL_MILLIS = 10_000L
15
14
 
16
15
  private val QUALITIES =
17
16
  mapOf(
18
- "high" to LocationRequest.QUALITY_HIGH_ACCURACY,
19
- "balanced" to LocationRequest.QUALITY_BALANCED_POWER_ACCURACY,
20
- "low" to LocationRequest.QUALITY_LOW_POWER,
17
+ Accuracy.high to LocationRequest.QUALITY_HIGH_ACCURACY,
18
+ Accuracy.balanced to LocationRequest.QUALITY_BALANCED_POWER_ACCURACY,
19
+ Accuracy.low to LocationRequest.QUALITY_LOW_POWER,
21
20
  )
22
21
 
23
22
  /**
@@ -31,20 +30,20 @@ internal class LocationUpdates(private val context: Context, private val name: S
31
30
  private val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
32
31
 
33
32
  /** The options of the updates, or null. */
34
- val options: JSONObject?
35
- get() = preferences.getString(name, null)?.let(::JSONObject)
33
+ val options: UpdateOptions?
34
+ get() = preferences.getString(name, null)?.let(UpdateOptions::from)
36
35
 
37
36
  /** Keeps [options], which [restore] requests. */
38
- fun keep(options: JSONObject) {
39
- preferences.edit().putString(name, options.toString()).apply()
37
+ fun keep(options: UpdateOptions) {
38
+ preferences.edit().putString(name, options.json().toString()).apply()
40
39
  }
41
40
 
42
41
  /**
43
42
  * Keeps [options] and requests updates with them, replacing others; the same options running
44
43
  * change nothing. Throws `SecurityException` without permission.
45
44
  */
46
- fun start(options: JSONObject) {
47
- if (options.toString() == preferences.getString(name, null) && isRequested()) return
45
+ fun start(options: UpdateOptions) {
46
+ if (options == this.options && isRequested()) return
48
47
  keep(options)
49
48
  request(options)
50
49
  }
@@ -76,12 +75,12 @@ internal class LocationUpdates(private val context: Context, private val name: S
76
75
  .firstOrNull()
77
76
  ?.reason == ApplicationExitInfo.REASON_USER_REQUESTED
78
77
 
79
- private fun request(options: JSONObject) {
80
- val accuracy = options.optString("accuracy", "high")
78
+ private fun request(options: UpdateOptions) {
79
+ val accuracy = options.accuracy ?: Accuracy.high
81
80
  val request =
82
- LocationRequest.Builder(options.optLong("interval", DEFAULT_INTERVAL_MILLIS))
83
- .setQuality(QUALITIES[accuracy] ?: LocationRequest.QUALITY_HIGH_ACCURACY)
84
- .setMinUpdateDistanceMeters(options.optDouble("distance", 0.0).toFloat())
81
+ LocationRequest.Builder(options.interval ?: DEFAULT_INTERVAL_MILLIS)
82
+ .setQuality(QUALITIES.getValue(accuracy))
83
+ .setMinUpdateDistanceMeters((options.distance ?: 0.0).toFloat())
85
84
  .build()
86
85
  val updates = checkNotNull(pendingIntent(PendingIntent.FLAG_UPDATE_CURRENT))
87
86
  try {
@@ -94,10 +93,10 @@ internal class LocationUpdates(private val context: Context, private val name: S
94
93
  }
95
94
 
96
95
  /** The fused provider when the device has one, and otherwise GPS for high accuracy and the network for less. */
97
- private fun provider(accuracy: String): String =
96
+ private fun provider(accuracy: Accuracy): String =
98
97
  when {
99
98
  manager.hasProvider(LocationManager.FUSED_PROVIDER) -> LocationManager.FUSED_PROVIDER
100
- accuracy == "high" -> LocationManager.GPS_PROVIDER
99
+ accuracy == Accuracy.high -> LocationManager.GPS_PROVIDER
101
100
  else -> LocationManager.NETWORK_PROVIDER
102
101
  }
103
102
 
@@ -0,0 +1,134 @@
1
+ // Generated by `tok plugin generate apple`. Do not edit.
2
+
3
+ import Foundation
4
+
5
+ extension TokamakLocationPlugin {
6
+ func callBytes(method: String, payload: Data, reply: @escaping TokamakPluginBytesReply)
7
+ throws(TokamakPluginError) -> (() -> Void)?
8
+ {
9
+ switch method {
10
+ case "setup":
11
+ struct Arguments: Decodable {
12
+ let config: SetupConfig
13
+ }
14
+ let arguments = try tokamakDecode(Arguments.self, from: payload)
15
+ return tokamakCall(reply) {
16
+ try self.setup(arguments.config)
17
+ }
18
+ case "recentPosition":
19
+ struct Arguments: Decodable {
20
+ let maximumAge: Double
21
+ }
22
+ let arguments = try tokamakDecode(Arguments.self, from: payload)
23
+ return tokamakCall(reply) {
24
+ self.recentPosition(maximumAge: arguments.maximumAge)
25
+ }
26
+ case "currentPosition":
27
+ return tokamakCall(reply) {
28
+ try await self.currentPosition()
29
+ }
30
+ case "permission":
31
+ return tokamakCall(reply) {
32
+ self.permission()
33
+ }
34
+ case "requestPermission":
35
+ struct Arguments: Decodable {
36
+ let request: PermissionRequest?
37
+ }
38
+ let arguments = try tokamakDecode(Arguments.self, from: payload)
39
+ return tokamakCall(reply) {
40
+ try await self.requestPermission(arguments.request)
41
+ }
42
+ case "servicesEnabled":
43
+ return tokamakCall(reply) {
44
+ await self.servicesEnabled()
45
+ }
46
+ case "openSettings":
47
+ return tokamakCall(reply) {
48
+ try await self.openSettings()
49
+ }
50
+ case "startUpdates":
51
+ struct Arguments: Decodable {
52
+ let options: UpdateOptions?
53
+ }
54
+ let arguments = try tokamakDecode(Arguments.self, from: payload)
55
+ return tokamakCall(reply) {
56
+ try self.startUpdates(arguments.options)
57
+ }
58
+ case "stopUpdates":
59
+ return tokamakCall(reply) {
60
+ self.stopUpdates()
61
+ }
62
+ case "getUpdates":
63
+ return tokamakCall(reply) {
64
+ self.getUpdates()
65
+ }
66
+ case "addRegion":
67
+ struct Arguments: Decodable {
68
+ let region: Region
69
+ }
70
+ let arguments = try tokamakDecode(Arguments.self, from: payload)
71
+ return tokamakCall(reply) {
72
+ try await self.addRegion(arguments.region)
73
+ }
74
+ case "getRegions":
75
+ return tokamakCall(reply) {
76
+ await self.getRegions()
77
+ }
78
+ case "removeRegion":
79
+ struct Arguments: Decodable {
80
+ let id: String
81
+ }
82
+ let arguments = try tokamakDecode(Arguments.self, from: payload)
83
+ return tokamakCall(reply) {
84
+ await self.removeRegion(arguments.id)
85
+ }
86
+ default:
87
+ throw .notSupported("\(id).\(method) is not supported")
88
+ }
89
+ }
90
+
91
+ func subscribeBytes(method: String, payload: Data, reply: @escaping TokamakPluginBytesReply)
92
+ throws(TokamakPluginError) -> (() -> Void)
93
+ {
94
+ switch method {
95
+ case "watchPosition":
96
+ return try tokamakStream(reply) {
97
+ self.watchPosition()
98
+ }
99
+ case "onLocationUpdate":
100
+ return try tokamakStream(reply) {
101
+ self.onLocationUpdate()
102
+ }
103
+ case "onRegionTransition":
104
+ return try tokamakStream(reply) {
105
+ self.onRegionTransition()
106
+ }
107
+ case "onVisit":
108
+ return try tokamakStream(reply) {
109
+ self.onVisit()
110
+ }
111
+ case "onLocationError":
112
+ return try tokamakStream(reply) {
113
+ self.onLocationError()
114
+ }
115
+ default:
116
+ throw .notSupported("\(id).\(method) is not supported")
117
+ }
118
+ }
119
+
120
+ func eventPayload(_ name: String, _ event: Any) throws(TokamakPluginError) -> Data {
121
+ switch name {
122
+ case "update":
123
+ return try tokamakEvent(name, event, as: LocationUpdate.self)
124
+ case "region":
125
+ return try tokamakEvent(name, event, as: RegionTransition.self)
126
+ case "visit":
127
+ return try tokamakEvent(name, event, as: Visit.self)
128
+ case "error":
129
+ return try tokamakEvent(name, event, as: LocationErrorEvent.self)
130
+ default:
131
+ throw .unlistedEvent(name)
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,48 @@
1
+ extension TokamakLocationPlugin: TokamakExports {
2
+ static func exports(_ e: inout TokamakExporter<TokamakLocationPlugin>) {
3
+ e.call(
4
+ setup, doc: "Applies long-term location awareness, replacing the previous configuration.")
5
+ e.call(
6
+ recentPosition, doc: "Core Location's latest position, when it is no older than `maximumAge`."
7
+ )
8
+ e.call(currentPosition, doc: "A new fix. Cancelling the call stops locating.")
9
+ e.stream(watchPosition, overflow: .dropOldest, doc: "Positions as the device moves.")
10
+ e.call(
11
+ permission, doc: "The app's in-use and background permission, and the precision it gets.")
12
+ e.call(
13
+ requestPermission,
14
+ doc: "Asks for the permission `request` names where the user has not answered.")
15
+ e.call(servicesEnabled, doc: "Whether location services are on for the device.")
16
+ e.call(openSettings, doc: "Opens the app's page in the system settings.")
17
+ e.call(
18
+ startUpdates, doc: "Starts continuous updates, replacing any running, until `stopUpdates`.")
19
+ e.call(stopUpdates)
20
+ e.call(getUpdates, doc: "The options of the running updates, or null when none are running.")
21
+ e.call(addRegion, doc: "Monitors a circular region, replacing one with the same `id`.")
22
+ e.call(getRegions, doc: "The monitored regions.")
23
+ e.call(removeRegion)
24
+ e.stream(
25
+ onLocationUpdate, overflow: .dropOldest, doc: "The updates the Worker's listeners receive.")
26
+ e.stream(onRegionTransition, doc: "The transitions the Worker's listeners receive.")
27
+ e.stream(onVisit, doc: "The visits the Worker's listeners receive.")
28
+ e.stream(onLocationError, doc: "The failures the Worker's listeners receive.")
29
+ e.event(
30
+ "update", LocationUpdate.self,
31
+ doc:
32
+ "Receives continuous updates and significant changes, including those that launch the app."
33
+ )
34
+ e.event(
35
+ "region", RegionTransition.self,
36
+ doc: "Receives each region entry or exit, including one that launches the app.")
37
+ e.event(
38
+ "visit", Visit.self,
39
+ doc:
40
+ "Receives each arrival at or departure from a place, including one that launches the app."
41
+ )
42
+ e.event(
43
+ "error", LocationErrorEvent.self,
44
+ doc:
45
+ "Receives failures of background services, such as a region Core Location cannot monitor."
46
+ )
47
+ }
48
+ }
@@ -5,18 +5,17 @@ import Foundation
5
5
  /// the system gives an app it launches for one.
6
6
  private let eventDeadline: TimeInterval = 8
7
7
 
8
- /// The listener methods, which receive the Worker's events.
9
- private let listenerMethods: Set = [
10
- "onLocationUpdate", "onRegionTransition", "onVisit", "onLocationError",
11
- ]
12
-
13
8
  /// Delivers the plugin's events to the Worker's listeners and the page's. Use
14
9
  /// it on the main thread.
15
10
  final class TokamakLocationEvents {
11
+ let updates = TokamakListeners<TokamakLocationPlugin.LocationUpdate>()
12
+ let transitions = TokamakListeners<TokamakLocationPlugin.RegionTransition>()
13
+ let visits = TokamakListeners<TokamakLocationPlugin.Visit>()
14
+ let errors = TokamakListeners<TokamakLocationPlugin.LocationErrorEvent>()
15
+
16
16
  private let host: TokamakHost
17
- private var listeners: [String: [UUID: TokamakPluginReply]] = [:]
18
17
  /// Positions that arrived while the Worker handled the previous update.
19
- private var positions: [[String: Any]] = []
18
+ private var positions: [TokamakLocationPlugin.Position] = []
20
19
  private var stationary: Bool?
21
20
  private var updating = false
22
21
  /// The time of the newest position delivered, as services that run together
@@ -30,23 +29,13 @@ final class TokamakLocationEvents {
30
29
  self.host = host
31
30
  }
32
31
 
33
- /// Adds a page's or Worker's listener of `method`, returning its removal.
34
- func listen(_ method: String, _ reply: @escaping TokamakPluginReply) -> (() -> Void)? {
35
- guard listenerMethods.contains(method) else { return nil }
36
- let key = UUID()
37
- listeners[method, default: [:]][key] = reply
38
- return { [weak self] in
39
- self?.listeners[method]?.removeValue(forKey: key)
40
- }
41
- }
42
-
43
32
  /// Delivers `location` when it is newer than the positions delivered, with
44
33
  /// Apple's `stationary` flag when the system reports it. Positions that
45
34
  /// arrive while the Worker handles an update reach it together as the next.
46
35
  func update(_ location: CLLocation?, stationary: Bool? = nil) {
47
36
  if let location, location.timestamp > latest {
48
37
  latest = location.timestamp
49
- positions.append(Self.position(location))
38
+ positions.append(TokamakLocationPlugin.Position(location))
50
39
  }
51
40
  if let stationary {
52
41
  self.stationary = stationary
@@ -61,32 +50,21 @@ final class TokamakLocationEvents {
61
50
  updating = false
62
51
  return
63
52
  }
64
- var event: [String: Any] = ["positions": positions]
65
- event["stationary"] = stationary
53
+ let update = TokamakLocationPlugin.LocationUpdate(positions: positions, stationary: stationary)
66
54
  positions = []
67
55
  stationary = nil
68
56
  updating = true
69
- send("onLocationUpdate", "update", event) { self.sendUpdate() }
57
+ send(update, to: updates, as: "update") { self.sendUpdate() }
70
58
  }
71
59
 
72
60
  func transition(_ id: String, entered: Bool, at date: Date) {
73
- let event: [String: Any] = [
74
- "id": id, "transition": entered ? "enter" : "exit", "timestamp": Self.milliseconds(date),
75
- ]
76
- send("onRegionTransition", "region", event)
61
+ send(
62
+ TokamakLocationPlugin.RegionTransition(id: id, entered: entered, at: date), to: transitions,
63
+ as: "region")
77
64
  }
78
65
 
79
66
  func visit(_ visit: CLVisit) {
80
- let event: [String: Any] = [
81
- "latitude": visit.coordinate.latitude,
82
- "longitude": visit.coordinate.longitude,
83
- "accuracy": visit.horizontalAccuracy,
84
- "arrival": visit.arrivalDate == .distantPast
85
- ? NSNull() : Self.milliseconds(visit.arrivalDate),
86
- "departure": visit.departureDate == .distantFuture
87
- ? NSNull() : Self.milliseconds(visit.departureDate),
88
- ]
89
- send("onVisit", "visit", event)
67
+ send(TokamakLocationPlugin.Visit(visit), to: visits, as: "visit")
90
68
  }
91
69
 
92
70
  /// Reports each of `problems` that updates, or the region `id`, did not
@@ -99,20 +77,35 @@ final class TokamakLocationEvents {
99
77
  self.problems[id] = Set(problems.map(\.message))
100
78
  }
101
79
 
80
+ /// A main-actor task that runs `body`, reporting the failure that ends it
81
+ /// unless the task was cancelled.
82
+ @discardableResult
83
+ func task(_ body: @escaping @MainActor () async throws -> Void) -> Task<Void, Never> {
84
+ Task { @MainActor in
85
+ do {
86
+ try await body()
87
+ } catch {
88
+ guard !Task.isCancelled else { return }
89
+ self.fail(.locationUnavailable(error.localizedDescription))
90
+ }
91
+ }
92
+ }
93
+
102
94
  /// Reports a background service's failure, which concerns the region `id`
103
95
  /// when given.
104
96
  func fail(_ error: TokamakPluginError, region id: String? = nil) {
105
- var event: [String: Any] = ["name": error.name, "message": error.message]
106
- event["id"] = id
107
- send("onLocationError", "error", event)
97
+ send(
98
+ TokamakLocationPlugin.LocationErrorEvent(name: error.name, message: error.message, id: id),
99
+ to: errors, as: "error")
108
100
  }
109
101
 
110
- private func send(
111
- _ method: String, _ name: String, _ event: [String: Any], completion: @escaping () -> Void = {}
102
+ /// Delivers `event` to the page's `listeners` and the Worker's listeners of
103
+ /// `name`, then calls `completion`.
104
+ private func send<Event: Encodable>(
105
+ _ event: Event, to listeners: TokamakListeners<Event>, as name: String,
106
+ completion: @escaping () -> Void = {}
112
107
  ) {
113
- for reply in (listeners[method] ?? [:]).values {
114
- reply(.success(event))
115
- }
108
+ listeners.send(event)
116
109
  host.emit(name, event: event, timeout: eventDeadline) { result in
117
110
  if case .failure(let error) = result {
118
111
  print("location \(name) failed: \(error)")
@@ -120,31 +113,6 @@ final class TokamakLocationEvents {
120
113
  completion()
121
114
  }
122
115
  }
123
-
124
- /// `location` as the page's `Position`.
125
- static func position(_ location: CLLocation) -> [String: Any] {
126
- let coordinates = location.coordinate
127
- return [
128
- "coords": [
129
- "latitude": coordinates.latitude,
130
- "longitude": coordinates.longitude,
131
- "accuracy": location.horizontalAccuracy,
132
- "altitude": nullable(location.verticalAccuracy >= 0, location.altitude),
133
- "altitudeAccuracy": nullable(location.verticalAccuracy >= 0, location.verticalAccuracy),
134
- "heading": nullable(location.course >= 0, location.course),
135
- "speed": nullable(location.speed >= 0, location.speed),
136
- ],
137
- "timestamp": milliseconds(location.timestamp),
138
- ]
139
- }
140
-
141
- private static func nullable(_ available: Bool, _ value: Double) -> Any {
142
- available ? value : NSNull()
143
- }
144
-
145
- private static func milliseconds(_ date: Date) -> Double {
146
- date.timeIntervalSince1970 * 1000
147
- }
148
116
  }
149
117
 
150
118
  extension TokamakPluginError {