@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.
package/README.md CHANGED
@@ -52,7 +52,7 @@ default. The timeout starts once the user allows location. Android takes
52
52
  positions from its fused provider at high accuracy, which combines GPS with
53
53
  Wi-Fi and cell signals.
54
54
 
55
- `watchPosition()` returns a byte stream of JSON positions. `decodeValues<Position>`
55
+ `watchPosition()` resolves to a byte stream of JSON positions. `decodeValues<Position>`
56
56
  decodes them. Cancelling the stream ends the native watch; errors terminate it.
57
57
 
58
58
  Both ask for in-use permission when the user has not answered. Off screen
@@ -134,7 +134,7 @@ event:
134
134
  |---|---|
135
135
  | `onLocationUpdate` | `{ positions, stationary? }` from updates and significant changes. |
136
136
  | `onRegionTransition` | `{ id, transition, timestamp }`, where `transition` is `"enter"` or `"exit"`. |
137
- | `onVisit` | Apple only: `{ latitude, longitude, accuracy, arrival, departure }`, times in milliseconds or null when unknown. |
137
+ | `onVisit` | Apple only: `{ latitude, longitude, accuracy, arrival, departure }`, times in milliseconds, absent when unknown. |
138
138
  | `onLocationError` | `{ name, message, id? }` when a background service fails, with the region's `id` when it concerns one. |
139
139
 
140
140
  In Doze, the idle state of an unplugged Android device left still with its
@@ -0,0 +1,264 @@
1
+ // Generated by `tok plugin generate android`. Do not edit.
2
+ package com.tokamak.plugins.location
3
+
4
+ import com.tokamak.runtime.TokamakDispatch
5
+ import com.tokamak.runtime.TokamakHost
6
+ import com.tokamak.runtime.TokamakPluginBytesReply
7
+ import com.tokamak.runtime.TokamakPluginError
8
+ import com.tokamak.runtime.TokamakValue
9
+ import kotlin.reflect.KClass
10
+ import org.json.JSONArray
11
+ import org.json.JSONObject
12
+
13
+ /** Routes the Worker's requests to the methods [TokamakLocationPlugin] exports, and encodes the events it emits. */
14
+ class TokamakLocationPluginDispatch(host: TokamakHost) : TokamakDispatch<TokamakLocationPlugin>(TokamakLocationPlugin(host), host) {
15
+ override fun callBytes(method: String, payload: ByteArray, reply: TokamakPluginBytesReply): (() -> Unit)? {
16
+ val arguments by lazy(LazyThreadSafetyMode.NONE) { TokamakValue.arguments(payload) }
17
+ return when (method) {
18
+ "setup" -> respond(reply) { plugin.setup(config = arguments["config"].asSetupConfig()) }
19
+ "recentPosition" -> respond(reply) {
20
+ val result = plugin.recentPosition(
21
+ maximumAge = arguments["maximumAge"].double(),
22
+ )
23
+ result?.let { json(it) }
24
+ }
25
+ "currentPosition" -> launch(method, reply) { plugin.currentPosition()?.let { json(it) } }
26
+ "permission" -> respond(reply) { json(plugin.permission()) }
27
+ "requestPermission" -> launch(method, reply) {
28
+ val result = plugin.requestPermission(
29
+ request = arguments["request"].nullable { it.asPermissionRequest() },
30
+ )
31
+ json(result)
32
+ }
33
+ "servicesEnabled" -> respond(reply) { plugin.servicesEnabled() }
34
+ "openSettings" -> respond(reply) { plugin.openSettings() }
35
+ "startUpdates" -> respond(reply) {
36
+ plugin.startUpdates(
37
+ options = arguments["options"].nullable { it.asUpdateOptions() },
38
+ )
39
+ }
40
+ "stopUpdates" -> respond(reply) { plugin.stopUpdates() }
41
+ "getUpdates" -> respond(reply) { plugin.getUpdates()?.let { json(it) } }
42
+ "addRegion" -> respond(reply) { plugin.addRegion(region = arguments["region"].asRegion()) }
43
+ "getRegions" -> respond(reply) { JSONArray(plugin.getRegions().map { json(it) }) }
44
+ "removeRegion" -> respond(reply) { plugin.removeRegion(id = arguments["id"].string()) }
45
+ else -> throw TokamakPluginError.notSupported("$id.$method is not supported")
46
+ }
47
+ }
48
+
49
+ override fun subscribeBytes(method: String, payload: ByteArray, reply: TokamakPluginBytesReply): () -> Unit {
50
+ return when (method) {
51
+ "watchPosition" -> collect(method, reply, plugin.watchPosition()) { json(it) }
52
+ "onLocationUpdate" -> collect(method, reply, plugin.onLocationUpdate()) { json(it) }
53
+ "onRegionTransition" -> collect(method, reply, plugin.onRegionTransition()) { json(it) }
54
+ "onLocationError" -> collect(method, reply, plugin.onLocationError()) { json(it) }
55
+ else -> throw TokamakPluginError.notSupported("$id.$method is not supported")
56
+ }
57
+ }
58
+
59
+ override fun eventPayload(name: String, event: Any): ByteArray =
60
+ when (name) {
61
+ "update" -> encodeEvent(name, event, LocationUpdate::class) { json(it) }
62
+ "region" -> encodeEvent(name, event, RegionTransition::class) { json(it) }
63
+ "error" -> encodeEvent(name, event, LocationErrorEvent::class) { json(it) }
64
+ else -> super.eventPayload(name, event)
65
+ }
66
+
67
+ override fun encodeValue(value: Any): Any =
68
+ when (value) {
69
+ is Coordinates -> json(value)
70
+ is LocationErrorEvent -> json(value)
71
+ is LocationPermission -> json(value)
72
+ is LocationUpdate -> json(value)
73
+ is MonitoredRegion -> json(value)
74
+ is PermissionRequest -> json(value)
75
+ is Position -> json(value)
76
+ is Region -> json(value)
77
+ is RegionTransition -> json(value)
78
+ is ServiceNotification -> json(value)
79
+ is SetupConfig -> json(value)
80
+ is UpdateOptions -> json(value)
81
+ else -> super.encodeValue(value)
82
+ }
83
+
84
+ override fun decodeValue(json: TokamakValue, type: KClass<*>): Any =
85
+ when (type) {
86
+ Coordinates::class -> json.asCoordinates()
87
+ LocationErrorEvent::class -> json.asLocationErrorEvent()
88
+ LocationPermission::class -> json.asLocationPermission()
89
+ LocationUpdate::class -> json.asLocationUpdate()
90
+ MonitoredRegion::class -> json.asMonitoredRegion()
91
+ PermissionRequest::class -> json.asPermissionRequest()
92
+ Position::class -> json.asPosition()
93
+ Region::class -> json.asRegion()
94
+ RegionTransition::class -> json.asRegionTransition()
95
+ ServiceNotification::class -> json.asServiceNotification()
96
+ SetupConfig::class -> json.asSetupConfig()
97
+ UpdateOptions::class -> json.asUpdateOptions()
98
+ else -> super.decodeValue(json, type)
99
+ }
100
+
101
+ private fun TokamakValue.asCoordinates(): Coordinates =
102
+ Coordinates(
103
+ latitude = this["latitude"].double(),
104
+ longitude = this["longitude"].double(),
105
+ accuracy = this["accuracy"].double(),
106
+ altitude = this["altitude"].nullable { it.double() },
107
+ altitudeAccuracy = this["altitudeAccuracy"].nullable { it.double() },
108
+ heading = this["heading"].nullable { it.double() },
109
+ speed = this["speed"].nullable { it.double() },
110
+ )
111
+
112
+ private fun json(value: Coordinates): JSONObject =
113
+ JSONObject()
114
+ .put("latitude", value.latitude)
115
+ .put("longitude", value.longitude)
116
+ .put("accuracy", value.accuracy)
117
+ .putOpt("altitude", value.altitude)
118
+ .putOpt("altitudeAccuracy", value.altitudeAccuracy)
119
+ .putOpt("heading", value.heading)
120
+ .putOpt("speed", value.speed)
121
+
122
+ private fun TokamakValue.asLocationErrorEvent(): LocationErrorEvent =
123
+ LocationErrorEvent(
124
+ name = this["name"].string(),
125
+ message = this["message"].string(),
126
+ )
127
+
128
+ private fun json(value: LocationErrorEvent): JSONObject =
129
+ JSONObject()
130
+ .put("name", value.name)
131
+ .put("message", value.message)
132
+
133
+ private fun TokamakValue.asLocationPermission(): LocationPermission =
134
+ LocationPermission(
135
+ inUse = this["inUse"].enum(Permission.entries),
136
+ background = this["background"].enum(Permission.entries),
137
+ precision = this["precision"].enum(Precision.entries),
138
+ )
139
+
140
+ private fun json(value: LocationPermission): JSONObject =
141
+ JSONObject()
142
+ .put("inUse", value.inUse.name)
143
+ .put("background", value.background.name)
144
+ .put("precision", value.precision.name)
145
+
146
+ private fun TokamakValue.asLocationUpdate(): LocationUpdate =
147
+ LocationUpdate(
148
+ positions = this["positions"].list { it.asPosition() },
149
+ )
150
+
151
+ private fun json(value: LocationUpdate): JSONObject =
152
+ JSONObject()
153
+ .put("positions", JSONArray(value.positions.map { json(it) }))
154
+
155
+ private fun TokamakValue.asMonitoredRegion(): MonitoredRegion =
156
+ MonitoredRegion(
157
+ id = this["id"].string(),
158
+ latitude = this["latitude"].double(),
159
+ longitude = this["longitude"].double(),
160
+ radius = this["radius"].double(),
161
+ onEntry = this["onEntry"].boolean(),
162
+ onExit = this["onExit"].boolean(),
163
+ )
164
+
165
+ private fun json(value: MonitoredRegion): JSONObject =
166
+ JSONObject()
167
+ .put("id", value.id)
168
+ .put("latitude", value.latitude)
169
+ .put("longitude", value.longitude)
170
+ .put("radius", value.radius)
171
+ .put("onEntry", value.onEntry)
172
+ .put("onExit", value.onExit)
173
+
174
+ private fun TokamakValue.asPermissionRequest(): PermissionRequest =
175
+ PermissionRequest(
176
+ background = this["background"].nullable { it.boolean() },
177
+ precision = this["precision"].nullable { it.enum(Precision.entries) },
178
+ )
179
+
180
+ private fun json(value: PermissionRequest): JSONObject =
181
+ JSONObject()
182
+ .putOpt("background", value.background)
183
+ .putOpt("precision", value.precision?.name)
184
+
185
+ private fun TokamakValue.asPosition(): Position =
186
+ Position(
187
+ coords = this["coords"].asCoordinates(),
188
+ timestamp = this["timestamp"].long(),
189
+ )
190
+
191
+ private fun json(value: Position): JSONObject =
192
+ JSONObject()
193
+ .put("coords", json(value.coords))
194
+ .put("timestamp", value.timestamp)
195
+
196
+ private fun TokamakValue.asRegion(): Region =
197
+ Region(
198
+ id = this["id"].string(),
199
+ latitude = this["latitude"].double(),
200
+ longitude = this["longitude"].double(),
201
+ radius = this["radius"].double(),
202
+ onEntry = this["onEntry"].nullable { it.boolean() },
203
+ onExit = this["onExit"].nullable { it.boolean() },
204
+ )
205
+
206
+ private fun json(value: Region): JSONObject =
207
+ JSONObject()
208
+ .put("id", value.id)
209
+ .put("latitude", value.latitude)
210
+ .put("longitude", value.longitude)
211
+ .put("radius", value.radius)
212
+ .putOpt("onEntry", value.onEntry)
213
+ .putOpt("onExit", value.onExit)
214
+
215
+ private fun TokamakValue.asRegionTransition(): RegionTransition =
216
+ RegionTransition(
217
+ id = this["id"].string(),
218
+ transition = this["transition"].enum(Transition.entries),
219
+ timestamp = this["timestamp"].long(),
220
+ )
221
+
222
+ private fun json(value: RegionTransition): JSONObject =
223
+ JSONObject()
224
+ .put("id", value.id)
225
+ .put("transition", value.transition.name)
226
+ .put("timestamp", value.timestamp)
227
+
228
+ private fun TokamakValue.asServiceNotification(): ServiceNotification =
229
+ ServiceNotification(
230
+ title = this["title"].string(),
231
+ body = this["body"].string(),
232
+ channel = this["channel"].string(),
233
+ )
234
+
235
+ private fun json(value: ServiceNotification): JSONObject =
236
+ JSONObject()
237
+ .put("title", value.title)
238
+ .put("body", value.body)
239
+ .put("channel", value.channel)
240
+
241
+ private fun TokamakValue.asSetupConfig(): SetupConfig =
242
+ SetupConfig(
243
+ significantChanges = this["significantChanges"].nullable { it.boolean() },
244
+ )
245
+
246
+ private fun json(value: SetupConfig): JSONObject =
247
+ JSONObject()
248
+ .putOpt("significantChanges", value.significantChanges)
249
+
250
+ private fun TokamakValue.asUpdateOptions(): UpdateOptions =
251
+ UpdateOptions(
252
+ accuracy = this["accuracy"].nullable { it.enum(Accuracy.entries) },
253
+ distance = this["distance"].nullable { it.double() },
254
+ interval = this["interval"].nullable { it.long() },
255
+ foregroundService = this["foregroundService"].nullable { it.asServiceNotification() },
256
+ )
257
+
258
+ private fun json(value: UpdateOptions): JSONObject =
259
+ JSONObject()
260
+ .putOpt("accuracy", value.accuracy?.name)
261
+ .putOpt("distance", value.distance)
262
+ .putOpt("interval", value.interval)
263
+ .putOpt("foregroundService", value.foregroundService?.let { json(it) })
264
+ }
@@ -0,0 +1,30 @@
1
+ package com.tokamak.plugins.location
2
+
3
+ import com.tokamak.runtime.TokamakExporter
4
+ import com.tokamak.runtime.TokamakExports
5
+ import com.tokamak.runtime.TokamakOverflow
6
+
7
+ object LocationExports : TokamakExports<TokamakLocationPlugin> {
8
+ override fun exports(e: TokamakExporter<TokamakLocationPlugin>) {
9
+ e.call(TokamakLocationPlugin::setup, doc = "Applies long-term location awareness, replacing the previous configuration.")
10
+ e.call(TokamakLocationPlugin::recentPosition, doc = "The provider's last known position, when it is no older than `maximumAge`.")
11
+ e.call(TokamakLocationPlugin::currentPosition, doc = "A new fix, or null when Android finds none. Cancelling the call stops locating.")
12
+ e.stream(TokamakLocationPlugin::watchPosition, overflow = TokamakOverflow.DROP_OLDEST, doc = "Positions as the device moves.")
13
+ e.call(TokamakLocationPlugin::permission, doc = "The app's in-use and background permission, and the precision it gets.")
14
+ e.call(TokamakLocationPlugin::requestPermission, doc = "Asks for the permission `request` names where the user has not answered.")
15
+ e.call(TokamakLocationPlugin::servicesEnabled, doc = "Whether location services are on for the device.")
16
+ e.call(TokamakLocationPlugin::openSettings, doc = "Opens the app's page in the system settings.")
17
+ e.call(TokamakLocationPlugin::startUpdates, doc = "Starts continuous updates, replacing any running, until `stopUpdates`.")
18
+ e.call(TokamakLocationPlugin::stopUpdates)
19
+ e.call(TokamakLocationPlugin::getUpdates, doc = "The options of the running updates, or null when none are running.")
20
+ e.call(TokamakLocationPlugin::addRegion, doc = "Monitors a circular region, replacing one with the same `id`.")
21
+ e.call(TokamakLocationPlugin::getRegions, doc = "The monitored regions.")
22
+ e.call(TokamakLocationPlugin::removeRegion)
23
+ e.stream(TokamakLocationPlugin::onLocationUpdate, overflow = TokamakOverflow.DROP_OLDEST, doc = "The updates the Worker's listeners receive.")
24
+ e.stream(TokamakLocationPlugin::onRegionTransition, doc = "The transitions the Worker's listeners receive.")
25
+ e.stream(TokamakLocationPlugin::onLocationError, doc = "The failures the Worker's listeners receive.")
26
+ e.event("update", LocationUpdate::class, doc = "Receives continuous updates and significant changes, including those that start the app.")
27
+ e.event("region", RegionTransition::class, doc = "Receives each region entry or exit, including one that starts the app.")
28
+ e.event("error", LocationErrorEvent::class, doc = "Receives failures of background services, such as a region Android cannot monitor.")
29
+ }
30
+ }
@@ -2,83 +2,69 @@ package com.tokamak.plugins.location
2
2
 
3
3
  import android.util.Log
4
4
  import com.tokamak.runtime.TokamakHost
5
+ import com.tokamak.runtime.TokamakListeners
5
6
  import com.tokamak.runtime.TokamakPluginError
6
- import com.tokamak.runtime.TokamakPluginReply
7
- import org.json.JSONArray
8
- import org.json.JSONObject
9
7
 
10
8
  /** How long the Worker's listeners have for an event, within the time Android gives a broadcast. */
11
9
  private const val EVENT_DEADLINE_MILLIS = 8_000L
12
10
 
13
- /** The listener methods, which receive the Worker's events. */
14
- private val LISTENERS = setOf("onLocationUpdate", "onRegionTransition", "onVisit", "onLocationError")
15
-
16
11
  /** Delivers the plugin's events to the Worker's listeners and the page's. Use it on the main thread. */
17
12
  internal class LocationEvents(private val host: TokamakHost) {
18
- private val listeners = mutableMapOf<String, MutableMap<Int, TokamakPluginReply>>()
19
- private var nextListener = 0
13
+ val updates = TokamakListeners<LocationUpdate>()
14
+ val transitions = TokamakListeners<RegionTransition>()
15
+ val errors = TokamakListeners<LocationErrorEvent>()
20
16
 
21
17
  /** Positions that arrived while the Worker handled the previous update, and the broadcasts to finish once it has them. */
22
- private val positions = mutableListOf<JSONObject>()
18
+ private val positions = mutableListOf<Position>()
23
19
  private val finishes = mutableListOf<() -> Unit>()
24
20
  private var updating = false
25
21
 
26
22
  /** The time of the newest position delivered, as requests that run together receive the same positions. */
27
23
  private var latest = Long.MIN_VALUE
28
24
 
29
- /** Adds a page's or Worker's listener of [method], returning its removal, or null for another method. */
30
- fun listen(method: String, reply: TokamakPluginReply): (() -> Unit)? {
31
- if (method !in LISTENERS) return null
32
- val key = nextListener++
33
- listeners.getOrPut(method, ::mutableMapOf)[key] = reply
34
- return { listeners[method]?.remove(key) }
35
- }
36
-
37
25
  /**
38
26
  * Delivers the [received] positions newer than those delivered, then calls [finish]. Positions
39
27
  * that arrive while the Worker handles an update reach it together as the next.
40
28
  */
41
- fun update(received: List<JSONObject>, finish: () -> Unit) {
42
- val newer = received.filter { it.getLong("timestamp") > latest }
29
+ fun update(received: List<Position>, finish: () -> Unit) {
30
+ val newer = received.filter { it.timestamp > latest }
43
31
  if (newer.isEmpty()) return finish()
44
- latest = newer.maxOf { it.getLong("timestamp") }
32
+ latest = newer.maxOf { it.timestamp }
45
33
  positions += newer
46
34
  finishes += finish
47
35
  if (!updating) sendUpdate()
48
36
  }
49
37
 
50
38
  fun transition(id: String, entered: Boolean, finish: () -> Unit) {
51
- val event =
52
- JSONObject()
53
- .put("id", id)
54
- .put("transition", if (entered) "enter" else "exit")
55
- .put("timestamp", System.currentTimeMillis())
56
- send("onRegionTransition", "region", event, finish)
39
+ val transition = RegionTransition(id, if (entered) Transition.enter else Transition.exit, System.currentTimeMillis())
40
+ transitions.send(transition)
41
+ send("region", transition, finish)
57
42
  }
58
43
 
59
44
  /** Reports a background service's failure, then calls [finish]. */
60
45
  fun fail(error: TokamakPluginError, finish: () -> Unit = {}) {
61
- val event = JSONObject().put("name", error.errorName).put("message", error.message)
62
- send("onLocationError", "error", event, finish)
46
+ val event = LocationErrorEvent(error.errorName, error.message.orEmpty())
47
+ errors.send(event)
48
+ send("error", event, finish)
63
49
  }
64
50
 
65
51
  /** Sends the waiting positions as one update, then the positions that arrive meanwhile as the next. */
66
52
  private fun sendUpdate() {
67
53
  updating = positions.isNotEmpty()
68
54
  if (!updating) return
69
- val event = JSONObject().put("positions", JSONArray(positions.toList()))
55
+ val update = LocationUpdate(positions.toList())
70
56
  val delivered = finishes.toList()
71
57
  positions.clear()
72
58
  finishes.clear()
73
- send("onLocationUpdate", "update", event) {
59
+ updates.send(update)
60
+ send("update", update) {
74
61
  delivered.forEach { it() }
75
62
  sendUpdate()
76
63
  }
77
64
  }
78
65
 
79
- /** Delivers [event] to the page's [method] listeners and the Worker's [name] listeners, then calls [finish]. */
80
- private fun send(method: String, name: String, event: JSONObject, finish: () -> Unit) {
81
- listeners[method].orEmpty().values.toList().forEach { it(Result.success(event)) }
66
+ /** Delivers [event] to the Worker's [name] listeners, then calls [finish]. */
67
+ private fun send(name: String, event: Any, finish: () -> Unit) {
82
68
  Thread {
83
69
  runCatching { host.emit(name, event, EVENT_DEADLINE_MILLIS) }
84
70
  .onFailure { Log.w("tokamak", "location $name failed: ${it.message}") }