craft-native 0.0.237 → 0.0.241
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 +71 -0
- package/dist/android/src/emulator.d.ts +23 -0
- package/dist/android/src/index.d.ts +24 -2
- package/dist/android/src/index.js +362 -103
- package/dist/android/src/native-map.d.ts +7 -0
- package/dist/android/templates/CraftBridge.kt.template +67 -55
- package/dist/android/templates/CraftNative.kt.template +27 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +36 -5
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +440 -0
- package/dist/android/templates/CraftNativeMapLibre.kt.template +438 -0
- package/dist/android/templates/CraftNativeMessagingService.kt.template +3 -1
- package/dist/android/templates/CraftNativeNotificationReceiver.kt.template +2 -1
- package/dist/android/templates/CraftNativeServices.kt.template +45 -35
- package/dist/android/templates/CraftNativeShell.kt.template +103 -20
- package/dist/android/templates/LocationRecordingService.kt.template +255 -47
- package/dist/android/templates/MainActivityNative.kt.template +7 -428
- package/dist/android/templates/build.gradle.kts.app.template +1 -1
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/android-adb.d.ts +2 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/mobile.d.ts +49 -14
- package/dist/browser/storage.d.ts +3 -0
- package/dist/browser.js +163 -51
- package/dist/cli.js +1 -1
- package/dist/index.cjs +126 -12
- package/dist/index.js +126 -12
- package/dist/ios/src/index.d.ts +5 -0
- package/dist/ios/src/index.js +17 -1
- package/dist/ios/src/testing.d.ts +16 -2
- package/dist/ios/templates/CraftApp.swift +418 -93
- package/dist/ios/templates/CraftHeartRate.swift +141 -44
- package/dist/ios/templates/CraftNativeActions.swift +46 -48
- package/dist/ios/templates/CraftNativeScreen.swift +82 -17
- package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
- package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +45 -1
- package/dist/macos/templates/native_layout.zig +41 -5
- package/dist/mobile-testing.js +30 -4
- package/dist/mobile.js +169 -17
- package/package.json +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/** A key-free native Android map. STX geometry never enters a WebView. */
|
|
2
|
+
internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
|
|
3
|
+
private val mapView: MapView
|
|
4
|
+
private var map: MapLibreMap? = null
|
|
5
|
+
private var destroyed = false
|
|
6
|
+
private var lifecycleStarted = false
|
|
7
|
+
private var lifecycleResumed = false
|
|
8
|
+
private var lifecycleOwner: androidx.lifecycle.LifecycleOwner? = null
|
|
9
|
+
private val lifecycleObserver = object : androidx.lifecycle.DefaultLifecycleObserver {
|
|
10
|
+
override fun onStart(owner: androidx.lifecycle.LifecycleOwner) { startMap() }
|
|
11
|
+
override fun onResume(owner: androidx.lifecycle.LifecycleOwner) { resumeMap() }
|
|
12
|
+
override fun onPause(owner: androidx.lifecycle.LifecycleOwner) { pauseMap() }
|
|
13
|
+
override fun onStop(owner: androidx.lifecycle.LifecycleOwner) { stopMap() }
|
|
14
|
+
}
|
|
15
|
+
private var pendingProps = JSONObject()
|
|
16
|
+
private var pendingChildren = JSONArray()
|
|
17
|
+
private var styleKey: String? = null
|
|
18
|
+
private var styleVersion = 0
|
|
19
|
+
private var readyStyle: Style? = null
|
|
20
|
+
private var initialCameraKey: String? = null
|
|
21
|
+
private var reportedRegion: LatLngBounds? = null
|
|
22
|
+
private var reportedCamera: CameraPosition? = null
|
|
23
|
+
private var reportedViewport: Pair<Int, Int>? = null
|
|
24
|
+
private var userChangingCamera = false
|
|
25
|
+
private var geometryPoints = emptyList<LatLng>()
|
|
26
|
+
private val markerHandlers = mutableMapOf<String, String>()
|
|
27
|
+
private var onMarkerTap: ((String) -> Unit)? = null
|
|
28
|
+
var onMapPress: ((LatLng) -> Unit)? = null
|
|
29
|
+
var onRegionChangeComplete: ((JSONObject) -> Unit)? = null
|
|
30
|
+
private val attribution = TextView(context).apply {
|
|
31
|
+
textSize = 9f
|
|
32
|
+
setTextColor(Color.DKGRAY)
|
|
33
|
+
setBackgroundColor(Color.argb(230, 255, 255, 255))
|
|
34
|
+
maxLines = 2
|
|
35
|
+
}
|
|
36
|
+
private val scale = LinearLayout(context).apply {
|
|
37
|
+
orientation = LinearLayout.VERTICAL
|
|
38
|
+
setPadding(4, 2, 4, 2)
|
|
39
|
+
setBackgroundColor(Color.argb(230, 255, 255, 255))
|
|
40
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
41
|
+
visibility = View.GONE
|
|
42
|
+
}
|
|
43
|
+
private val scaleLabel = TextView(context).apply { textSize = 11f; setTextColor(Color.rgb(37, 51, 33)) }
|
|
44
|
+
private val scaleLine = View(context).apply { setBackgroundColor(Color.rgb(37, 51, 33)) }
|
|
45
|
+
|
|
46
|
+
init {
|
|
47
|
+
MapLibre.getInstance(context)
|
|
48
|
+
mapView = MapView(context)
|
|
49
|
+
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
50
|
+
scale.addView(scaleLabel)
|
|
51
|
+
scale.addView(scaleLine, LinearLayout.LayoutParams(1, 2))
|
|
52
|
+
addView(scale, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.START).apply {
|
|
53
|
+
leftMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
54
|
+
bottomMargin = (24 * resources.displayMetrics.density).roundToInt()
|
|
55
|
+
})
|
|
56
|
+
addView(attribution, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.END).apply {
|
|
57
|
+
rightMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
58
|
+
bottomMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
59
|
+
})
|
|
60
|
+
mapView.onCreate(null)
|
|
61
|
+
mapView.addOnDidFinishLoadingMapListener { if (!destroyed && initialCameraKey == null) applyPending() }
|
|
62
|
+
mapView.getMapAsync(this)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun onMapReady(nativeMap: MapLibreMap) {
|
|
66
|
+
if (destroyed) return
|
|
67
|
+
map = nativeMap
|
|
68
|
+
nativeMap.addOnMapClickListener { point ->
|
|
69
|
+
val screen = nativeMap.projection.toScreenLocation(point)
|
|
70
|
+
val radius = 18 * resources.displayMetrics.density
|
|
71
|
+
val hit = nativeMap.queryRenderedFeatures(android.graphics.RectF(screen.x - radius, screen.y - radius, screen.x + radius, screen.y + radius), "craft-map-markers", "craft-map-directions").firstOrNull()
|
|
72
|
+
val id = hit?.takeIf { it.hasProperty("craftMarkerId") }?.getStringProperty("craftMarkerId")
|
|
73
|
+
if (id != null) onMarkerTap?.invoke(id) else onMapPress?.invoke(point)
|
|
74
|
+
true
|
|
75
|
+
}
|
|
76
|
+
nativeMap.addOnCameraMoveStartedListener { reason ->
|
|
77
|
+
if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) userChangingCamera = true
|
|
78
|
+
}
|
|
79
|
+
nativeMap.addOnCameraMoveListener { updateScale() }
|
|
80
|
+
nativeMap.addOnCameraIdleListener {
|
|
81
|
+
updateScale()
|
|
82
|
+
if (userChangingCamera) {
|
|
83
|
+
userChangingCamera = false
|
|
84
|
+
val bounds = nativeMap.projection.visibleRegion.latLngBounds
|
|
85
|
+
val camera = nativeMap.cameraPosition
|
|
86
|
+
val center = camera.target
|
|
87
|
+
if (center != null) {
|
|
88
|
+
val event = JSONObject()
|
|
89
|
+
.put("center", JSONObject().put("latitude", center.latitude).put("longitude", center.longitude))
|
|
90
|
+
.put("span", JSONObject().put("latitudeDelta", bounds.latitudeNorth - bounds.latitudeSouth).put("longitudeDelta", bounds.longitudeEast - bounds.longitudeWest))
|
|
91
|
+
.put("heading", camera.bearing).put("pitch", camera.tilt)
|
|
92
|
+
reportedRegion = region(event)
|
|
93
|
+
reportedCamera = camera
|
|
94
|
+
reportedViewport = width to height
|
|
95
|
+
onRegionChangeComplete?.invoke(event)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
applyPending()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun configure(props: JSONObject, children: JSONArray, markerHandler: (String) -> Unit) {
|
|
103
|
+
pendingProps = props
|
|
104
|
+
pendingChildren = children
|
|
105
|
+
onMarkerTap = markerHandler
|
|
106
|
+
applyPending()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
110
|
+
|
|
111
|
+
private fun applyPending() {
|
|
112
|
+
val nativeMap = map ?: return
|
|
113
|
+
if (destroyed) return
|
|
114
|
+
nativeMap.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
|
|
115
|
+
nativeMap.uiSettings.isScrollGesturesEnabled = craftNativeBoolean(pendingProps.opt("scrollEnabled"), true)
|
|
116
|
+
nativeMap.uiSettings.isZoomGesturesEnabled = craftNativeBoolean(pendingProps.opt("zoomEnabled"), true)
|
|
117
|
+
nativeMap.uiSettings.isRotateGesturesEnabled = craftNativeBoolean(pendingProps.opt("rotateEnabled"))
|
|
118
|
+
nativeMap.uiSettings.isTiltGesturesEnabled = craftNativeBoolean(pendingProps.opt("pitchEnabled"), true)
|
|
119
|
+
// The authored attribution is kept visible rather than hidden in a menu.
|
|
120
|
+
nativeMap.uiSettings.isAttributionEnabled = false
|
|
121
|
+
nativeMap.uiSettings.isLogoEnabled = false
|
|
122
|
+
applyPadding()
|
|
123
|
+
val nextStyle = basemapStyle()
|
|
124
|
+
if (styleKey != nextStyle) {
|
|
125
|
+
styleKey = nextStyle
|
|
126
|
+
readyStyle = null
|
|
127
|
+
val version = ++styleVersion
|
|
128
|
+
nativeMap.setStyle(Style.Builder().fromJson(nextStyle)) { loaded ->
|
|
129
|
+
if (!destroyed && version == styleVersion) {
|
|
130
|
+
readyStyle = loaded
|
|
131
|
+
installOverlays(loaded)
|
|
132
|
+
applyPending()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
val loaded = readyStyle ?: return
|
|
138
|
+
updateOverlays(loaded)
|
|
139
|
+
applyLocation(nativeMap, loaded)
|
|
140
|
+
val explicit = region(pendingProps.optJSONObject("region"))
|
|
141
|
+
val initial = region(pendingProps.optJSONObject("initialRegion"))
|
|
142
|
+
if (explicit != null) {
|
|
143
|
+
// A measured region is a bounding envelope, especially with
|
|
144
|
+
// padding or rotation. Refitting its unchanged echo would zoom
|
|
145
|
+
// out after every gesture. New requests still move the camera.
|
|
146
|
+
if (!isCurrentReportedRegion(explicit)) moveCamera(explicit, 0)
|
|
147
|
+
initialCameraKey = null
|
|
148
|
+
} else if (initial != null) {
|
|
149
|
+
val key = "region:${pendingProps.optJSONObject("initialRegion")}"
|
|
150
|
+
if (initialCameraKey != key) { moveCamera(initial, 0); initialCameraKey = key }
|
|
151
|
+
} else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
|
|
152
|
+
val key = "$width,$height:${pendingProps.optJSONObject("edgePadding")}:${pendingProps.opt("cameraPitch")}:${pendingProps.opt("cameraHeading")}:" + geometryPoints.joinToString(";") { "${it.latitude},${it.longitude}" }
|
|
153
|
+
if (initialCameraKey != key && geometryPoints.isNotEmpty() && width > 0 && height > 0) {
|
|
154
|
+
val distinct = geometryPoints.distinctBy { it.latitude to it.longitude }
|
|
155
|
+
if (distinct.size == 1) nativeMap.moveCamera(CameraUpdateFactory.newLatLngZoom(distinct.first(), 15.0))
|
|
156
|
+
else moveCamera(LatLngBounds.Builder().includes(geometryPoints).build(), if (pendingProps.has("edgePadding")) 0 else (24 * resources.displayMetrics.density).roundToInt())
|
|
157
|
+
initialCameraKey = key
|
|
158
|
+
}
|
|
159
|
+
} else initialCameraKey = null
|
|
160
|
+
applyOrientation()
|
|
161
|
+
updateScale()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fun basemapStyle(): String {
|
|
165
|
+
val custom = pendingProps.optJSONObject("tileOverlay")
|
|
166
|
+
val template = custom?.optString("urlTemplate").orEmpty()
|
|
167
|
+
val credit = custom?.optString("attribution").orEmpty()
|
|
168
|
+
val customValid = template.startsWith("https://") && listOf("{z}", "{x}", "{y}").all { template.contains(it) } && credit.isNotBlank()
|
|
169
|
+
val type = pendingProps.optString("mapType", "standard").lowercase()
|
|
170
|
+
val satellite = type == "satellite" || type == "hybrid"
|
|
171
|
+
val url = when {
|
|
172
|
+
customValid -> template
|
|
173
|
+
satellite -> "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
|
|
174
|
+
type == "terrain" -> "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}"
|
|
175
|
+
else -> "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
176
|
+
}
|
|
177
|
+
val attributionText = when {
|
|
178
|
+
customValid -> credit
|
|
179
|
+
satellite -> "Imagery © Esri, Maxar, Earthstar Geographics, and the GIS User Community"
|
|
180
|
+
type == "terrain" -> "Topography: Esri, USGS, NOAA, OpenStreetMap contributors"
|
|
181
|
+
else -> "© OpenStreetMap contributors"
|
|
182
|
+
}
|
|
183
|
+
attribution.text = attributionText
|
|
184
|
+
val sources = JSONObject().put("basemap", rasterSource(url, attributionText))
|
|
185
|
+
val layers = JSONArray().put(JSONObject().put("id", "basemap").put("type", "raster").put("source", "basemap"))
|
|
186
|
+
if (!customValid && type == "hybrid") {
|
|
187
|
+
sources.put("labels", rasterSource("https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}", "Labels © Esri"))
|
|
188
|
+
layers.put(JSONObject().put("id", "labels").put("type", "raster").put("source", "labels"))
|
|
189
|
+
attribution.text = "$attributionText · Labels © Esri"
|
|
190
|
+
}
|
|
191
|
+
return JSONObject().put("version", 8).put("sources", sources).put("layers", layers).toString()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private fun rasterSource(url: String, credit: String): JSONObject = JSONObject()
|
|
195
|
+
.put("type", "raster").put("tiles", JSONArray().put(url)).put("tileSize", 256).put("maxzoom", 19).put("attribution", credit)
|
|
196
|
+
|
|
197
|
+
private fun installOverlays(style: Style) {
|
|
198
|
+
style.addSource(GeoJsonSource("craft-map-overlays"))
|
|
199
|
+
style.addImage("craft-map-direction", directionBitmap())
|
|
200
|
+
style.addLayer(LineLayer("craft-map-routes", "craft-map-overlays")
|
|
201
|
+
.withFilter(Expression.eq(Expression.get("kind"), "line"))
|
|
202
|
+
.withProperties(lineColor(Expression.toColor(Expression.get("color"))), lineWidth(Expression.get("width")), lineCap("round"), lineJoin("round")))
|
|
203
|
+
style.addLayer(CircleLayer("craft-map-markers", "craft-map-overlays")
|
|
204
|
+
.withFilter(Expression.eq(Expression.get("kind"), "marker"))
|
|
205
|
+
.withProperties(circleColor(Expression.toColor(Expression.get("color"))), circleRadius(7f), circleStrokeColor(Color.WHITE), circleStrokeWidth(2f)))
|
|
206
|
+
style.addLayer(SymbolLayer("craft-map-directions", "craft-map-overlays")
|
|
207
|
+
.withFilter(Expression.eq(Expression.get("kind"), "direction"))
|
|
208
|
+
.withProperties(iconImage("craft-map-direction"), iconRotate(Expression.get("bearing")), iconRotationAlignment("map"), iconAllowOverlap(true), iconIgnorePlacement(true), iconAnchor("center")))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun updateOverlays(style: Style) {
|
|
212
|
+
val features = JSONArray()
|
|
213
|
+
val pointsForFit = mutableListOf<LatLng>()
|
|
214
|
+
markerHandlers.clear()
|
|
215
|
+
for (index in 0 until pendingChildren.length()) {
|
|
216
|
+
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
217
|
+
val type = child.optString("type").lowercase()
|
|
218
|
+
val props = child.optJSONObject("props") ?: JSONObject()
|
|
219
|
+
val events = child.optJSONObject("events") ?: JSONObject()
|
|
220
|
+
val id = craftNativeOptionalString(props.opt("id")) ?: craftNativeOptionalString(child.opt("id")) ?: "overlay-$index"
|
|
221
|
+
val points = coordinates(props.optJSONArray("geometry"))
|
|
222
|
+
val properties = JSONObject().put("craftOverlayId", id)
|
|
223
|
+
val geometry = JSONObject()
|
|
224
|
+
if ((type == "mapmarker" || type == "marker") && points.isNotEmpty()) {
|
|
225
|
+
val point = points.first()
|
|
226
|
+
val bearing = finiteNumber(props.opt("bearing"))
|
|
227
|
+
properties.put("kind", if (bearing == null) "marker" else "direction").put("craftMarkerId", id)
|
|
228
|
+
properties.put("bearing", (bearing ?: 0.0) % 360).put("color", cssColor(props.opt("pinColor"), Color.rgb(36, 114, 75)))
|
|
229
|
+
geometry.put("type", "Point").put("coordinates", JSONArray().put(point.longitude).put(point.latitude))
|
|
230
|
+
listOf(events.opt("onPress"), events.opt("onMarkerPress")).firstNotNullOfOrNull(::craftNativeOptionalString)?.let { markerHandlers[id] = it }
|
|
231
|
+
pointsForFit.add(point)
|
|
232
|
+
} else if ((type == "mappolyline" || type == "polyline") && points.size > 1) {
|
|
233
|
+
val authoredWidth = props.opt("strokeWidth")
|
|
234
|
+
val width = if (authoredWidth == null || authoredWidth == JSONObject.NULL) 3.0 else finiteNumber(authoredWidth) ?: continue
|
|
235
|
+
if (width <= 0 || !width.toFloat().isFinite()) continue
|
|
236
|
+
properties.put("kind", "line").put("color", cssColor(props.opt("strokeColor"), Color.BLUE)).put("width", width)
|
|
237
|
+
geometry.put("type", "LineString").put("coordinates", JSONArray().apply { points.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) } })
|
|
238
|
+
pointsForFit.addAll(points)
|
|
239
|
+
} else continue
|
|
240
|
+
features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
|
|
241
|
+
}
|
|
242
|
+
geometryPoints = pointsForFit
|
|
243
|
+
style.getSourceAs<GeoJsonSource>("craft-map-overlays")?.setGeoJson(JSONObject().put("type", "FeatureCollection").put("features", features).toString())
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private fun cssColor(value: Any?, fallback: Int): String {
|
|
247
|
+
val color = (value as? String)?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
|
|
248
|
+
return "rgba(${Color.red(color)},${Color.green(color)},${Color.blue(color)},${Color.alpha(color) / 255.0})"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private fun directionBitmap(): Bitmap {
|
|
252
|
+
val size = (26 * resources.displayMetrics.density).roundToInt().coerceAtLeast(24)
|
|
253
|
+
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
254
|
+
bitmap.density = resources.displayMetrics.densityDpi
|
|
255
|
+
val canvas = Canvas(bitmap)
|
|
256
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
257
|
+
paint.color = Color.WHITE
|
|
258
|
+
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
|
|
259
|
+
paint.color = Color.rgb(36, 114, 75)
|
|
260
|
+
canvas.drawCircle(size / 2f, size / 2f, size * 0.43f, paint)
|
|
261
|
+
paint.color = Color.WHITE
|
|
262
|
+
paint.textSize = size * 0.76f
|
|
263
|
+
paint.textAlign = Paint.Align.CENTER
|
|
264
|
+
paint.typeface = Typeface.DEFAULT_BOLD
|
|
265
|
+
canvas.drawText("↑", size / 2f, size / 2f - (paint.ascent() + paint.descent()) / 2, paint)
|
|
266
|
+
return bitmap
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private fun applyLocation(nativeMap: MapLibreMap, style: Style) {
|
|
270
|
+
val permitted = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
|
271
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
272
|
+
val enabled = permitted && craftNativeBoolean(pendingProps.opt("showsUserLocation"))
|
|
273
|
+
val component = nativeMap.locationComponent
|
|
274
|
+
if (!enabled && !component.isLocationComponentActivated) return
|
|
275
|
+
try {
|
|
276
|
+
if (!component.isLocationComponentActivated) component.activateLocationComponent(LocationComponentActivationOptions.builder(context, style).useDefaultLocationEngine(true).build())
|
|
277
|
+
component.isLocationComponentEnabled = enabled
|
|
278
|
+
} catch (error: SecurityException) {
|
|
279
|
+
// Permission may be revoked between the check and native activation.
|
|
280
|
+
if (component.isLocationComponentActivated) component.isLocationComponentEnabled = false
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private fun applyPadding() {
|
|
285
|
+
val value = pendingProps.optJSONObject("edgePadding")
|
|
286
|
+
fun inset(key: String): Double = (finiteNumber(value?.opt(key)) ?: 0.0).coerceIn(0.0, 10_000.0) * resources.displayMetrics.density
|
|
287
|
+
var left = inset("left"); var right = inset("right"); var top = inset("top"); var bottom = inset("bottom")
|
|
288
|
+
val availableWidth = (width - 40 * resources.displayMetrics.density).coerceAtLeast(0f)
|
|
289
|
+
val availableHeight = (height - 40 * resources.displayMetrics.density).coerceAtLeast(0f)
|
|
290
|
+
val horizontal = left + right; val vertical = top + bottom
|
|
291
|
+
if (horizontal > availableWidth && horizontal > 0) { left *= availableWidth / horizontal; right *= availableWidth / horizontal }
|
|
292
|
+
if (vertical > availableHeight && vertical > 0) { top *= availableHeight / vertical; bottom *= availableHeight / vertical }
|
|
293
|
+
map?.setPadding(left.roundToInt(), top.roundToInt(), right.roundToInt(), bottom.roundToInt())
|
|
294
|
+
// Floating STX controls reserve the same space for credits and scale
|
|
295
|
+
// as for route fitting, so a profile never covers either utility.
|
|
296
|
+
(attribution.layoutParams as LayoutParams).let { params ->
|
|
297
|
+
val margin = bottom.roundToInt() + (8 * resources.displayMetrics.density).roundToInt()
|
|
298
|
+
if (params.bottomMargin != margin) { params.bottomMargin = margin; attribution.layoutParams = params }
|
|
299
|
+
}
|
|
300
|
+
(scale.layoutParams as LayoutParams).let { params ->
|
|
301
|
+
val margin = bottom.roundToInt() + (32 * resources.displayMetrics.density).roundToInt()
|
|
302
|
+
if (params.bottomMargin != margin) { params.bottomMargin = margin; scale.layoutParams = params }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private fun moveCamera(bounds: LatLngBounds, padding: Int) {
|
|
307
|
+
post {
|
|
308
|
+
val nativeMap = map
|
|
309
|
+
if (!destroyed && nativeMap != null && width > 0 && height > 0) {
|
|
310
|
+
val camera = nativeMap.cameraPosition
|
|
311
|
+
val bearing = finiteNumber(pendingProps.opt("cameraHeading")) ?: camera.bearing
|
|
312
|
+
val tilt = finiteNumber(pendingProps.opt("cameraPitch"))?.coerceIn(0.0, 60.0) ?: camera.tilt
|
|
313
|
+
val insets = nativeMap.padding.map { it + padding }.toIntArray()
|
|
314
|
+
val target = nativeMap.getCameraForLatLngBounds(bounds, insets, bearing, tilt)
|
|
315
|
+
if (target == null) initialCameraKey = null
|
|
316
|
+
else nativeMap.moveCamera(CameraUpdateFactory.newCameraPosition(target))
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private fun isCurrentReportedRegion(requested: LatLngBounds): Boolean {
|
|
322
|
+
val reported = reportedRegion ?: return false
|
|
323
|
+
val measured = reportedCamera ?: return false
|
|
324
|
+
val current = map?.cameraPosition ?: return false
|
|
325
|
+
val measuredTarget = measured.target ?: return false
|
|
326
|
+
val currentTarget = current.target ?: return false
|
|
327
|
+
fun same(a: Double, b: Double): Boolean = abs(a - b) < 0.000000001
|
|
328
|
+
return reportedViewport == (width to height) &&
|
|
329
|
+
same(requested.latitudeNorth, reported.latitudeNorth) && same(requested.latitudeSouth, reported.latitudeSouth) &&
|
|
330
|
+
same(requested.longitudeEast, reported.longitudeEast) && same(requested.longitudeWest, reported.longitudeWest) &&
|
|
331
|
+
abs(current.zoom - measured.zoom) < 0.000001 &&
|
|
332
|
+
same(currentTarget.latitude, measuredTarget.latitude) && same(currentTarget.longitude, measuredTarget.longitude)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private fun applyOrientation() {
|
|
336
|
+
val nativeMap = map ?: return
|
|
337
|
+
val camera = nativeMap.cameraPosition
|
|
338
|
+
val pitch = finiteNumber(pendingProps.opt("cameraPitch"))?.coerceIn(0.0, 60.0) ?: camera.tilt
|
|
339
|
+
val heading = finiteNumber(pendingProps.opt("cameraHeading")) ?: camera.bearing
|
|
340
|
+
if (abs(camera.tilt - pitch) > 0.01 || abs(camera.bearing - heading) > 0.01) {
|
|
341
|
+
nativeMap.moveCamera(CameraUpdateFactory.newCameraPosition(CameraPosition.Builder(camera).tilt(pitch).bearing(heading).build()))
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private fun updateScale() {
|
|
346
|
+
val nativeMap = map ?: return
|
|
347
|
+
if (!craftNativeBoolean(pendingProps.opt("showsScale")) || width <= 0 || height <= 0) { scale.visibility = View.GONE; return }
|
|
348
|
+
val pixels = 80 * resources.displayMetrics.density
|
|
349
|
+
val start = nativeMap.projection.fromScreenLocation(android.graphics.PointF(width / 2f, height / 2f))
|
|
350
|
+
val end = nativeMap.projection.fromScreenLocation(android.graphics.PointF(width / 2f + pixels, height / 2f))
|
|
351
|
+
val measured = FloatArray(1)
|
|
352
|
+
android.location.Location.distanceBetween(start.latitude, start.longitude, end.latitude, end.longitude, measured)
|
|
353
|
+
val meters = measured[0].toDouble()
|
|
354
|
+
if (!meters.isFinite() || meters <= 0) { scale.visibility = View.GONE; return }
|
|
355
|
+
val magnitude = Math.pow(10.0, kotlin.math.floor(kotlin.math.log10(meters)))
|
|
356
|
+
val length = listOf(5.0, 2.0, 1.0).firstOrNull { it * magnitude <= meters }?.times(magnitude) ?: magnitude
|
|
357
|
+
scaleLabel.text = if (length >= 1000) "${length / 1000} km" else "${length.roundToInt()} m"
|
|
358
|
+
scaleLine.layoutParams = LinearLayout.LayoutParams((pixels * length / meters).roundToInt().coerceAtLeast(1), 2)
|
|
359
|
+
scale.visibility = View.VISIBLE
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
|
|
363
|
+
super.onSizeChanged(width, height, oldWidth, oldHeight)
|
|
364
|
+
if (width > 0 && height > 0) applyPending()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
override fun onAttachedToWindow() {
|
|
368
|
+
super.onAttachedToWindow()
|
|
369
|
+
if (destroyed) return
|
|
370
|
+
lifecycleOwner = context as? androidx.lifecycle.LifecycleOwner
|
|
371
|
+
val owner = lifecycleOwner
|
|
372
|
+
if (owner != null) owner.lifecycle.addObserver(lifecycleObserver)
|
|
373
|
+
else { startMap(); resumeMap() }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
override fun onDetachedFromWindow() {
|
|
377
|
+
lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver)
|
|
378
|
+
lifecycleOwner = null
|
|
379
|
+
stopMap()
|
|
380
|
+
super.onDetachedFromWindow()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private fun startMap() {
|
|
384
|
+
if (!destroyed && !lifecycleStarted) { mapView.onStart(); lifecycleStarted = true }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private fun resumeMap() {
|
|
388
|
+
startMap()
|
|
389
|
+
if (!destroyed && !lifecycleResumed) { mapView.onResume(); lifecycleResumed = true }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private fun pauseMap() {
|
|
393
|
+
if (lifecycleResumed) { mapView.onPause(); lifecycleResumed = false }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private fun stopMap() {
|
|
397
|
+
pauseMap()
|
|
398
|
+
if (lifecycleStarted) { mapView.onStop(); lifecycleStarted = false }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fun lowMemory() { if (!destroyed) mapView.onLowMemory() }
|
|
402
|
+
|
|
403
|
+
fun destroy() {
|
|
404
|
+
if (destroyed) return
|
|
405
|
+
destroyed = true
|
|
406
|
+
styleVersion++
|
|
407
|
+
onMarkerTap = null; onMapPress = null; onRegionChangeComplete = null
|
|
408
|
+
markerHandlers.clear(); geometryPoints = emptyList()
|
|
409
|
+
lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver)
|
|
410
|
+
lifecycleOwner = null
|
|
411
|
+
stopMap()
|
|
412
|
+
mapView.onDestroy()
|
|
413
|
+
readyStyle = null; map = null
|
|
414
|
+
reportedRegion = null; reportedCamera = null; reportedViewport = null
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private fun coordinates(value: JSONArray?): List<LatLng> {
|
|
418
|
+
if (value == null) return emptyList()
|
|
419
|
+
return (0 until value.length() - 1 step 2).mapNotNull { index ->
|
|
420
|
+
val latitude = finiteNumber(value.opt(index)) ?: return@mapNotNull null
|
|
421
|
+
val longitude = finiteNumber(value.opt(index + 1)) ?: return@mapNotNull null
|
|
422
|
+
if (latitude in -90.0..90.0 && longitude in -180.0..180.0) LatLng(latitude, longitude) else null
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private fun region(value: JSONObject?): LatLngBounds? {
|
|
427
|
+
val center = value?.optJSONObject("center") ?: return null
|
|
428
|
+
val span = value.optJSONObject("span") ?: return null
|
|
429
|
+
val latitude = finiteNumber(center.opt("latitude")) ?: return null
|
|
430
|
+
val longitude = finiteNumber(center.opt("longitude")) ?: return null
|
|
431
|
+
val height = finiteNumber(span.opt("latitudeDelta")) ?: finiteNumber(span.opt("latitude_delta")) ?: return null
|
|
432
|
+
val width = finiteNumber(span.opt("longitudeDelta")) ?: finiteNumber(span.opt("longitude_delta")) ?: return null
|
|
433
|
+
if (latitude !in -90.0..90.0 || longitude !in -180.0..180.0 || height <= 0 || height > 180 || width <= 0 || width > 360 || latitude - height / 2 < -90 || latitude + height / 2 > 90) return null
|
|
434
|
+
return LatLngBounds.from(latitude + height / 2, longitude + width / 2, latitude - height / 2, longitude - width / 2)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private fun finiteNumber(value: Any?): Double? = (value as? Number)?.toDouble()?.takeIf { it.isFinite() }
|
|
438
|
+
}
|
|
@@ -8,6 +8,7 @@ import android.content.Intent
|
|
|
8
8
|
import android.os.Build
|
|
9
9
|
import androidx.core.app.NotificationCompat
|
|
10
10
|
import androidx.core.app.NotificationManagerCompat
|
|
11
|
+
import androidx.core.content.ContextCompat
|
|
11
12
|
import com.google.firebase.messaging.FirebaseMessagingService
|
|
12
13
|
import com.google.firebase.messaging.RemoteMessage
|
|
13
14
|
import org.json.JSONObject
|
|
@@ -59,7 +60,8 @@ class CraftNativeMessagingService : FirebaseMessagingService() {
|
|
|
59
60
|
val id = (data["notification_id"]?.toIntOrNull() ?: System.currentTimeMillis().toInt())
|
|
60
61
|
val tap = PendingIntent.getActivity(this, id, open, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
61
62
|
val built = NotificationCompat.Builder(this, CHANNEL)
|
|
62
|
-
.setSmallIcon(
|
|
63
|
+
.setSmallIcon(R.drawable.craft_notification_icon)
|
|
64
|
+
.setColor(ContextCompat.getColor(this, R.color.craft_notification_color))
|
|
63
65
|
.setContentTitle(title)
|
|
64
66
|
.setContentText(body)
|
|
65
67
|
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
|
@@ -32,7 +32,8 @@ internal class CraftNativeNotificationReceiver : BroadcastReceiver() {
|
|
|
32
32
|
))
|
|
33
33
|
}
|
|
34
34
|
val notification = NotificationCompat.Builder(context, "craft_notifications")
|
|
35
|
-
.setSmallIcon(
|
|
35
|
+
.setSmallIcon(R.drawable.craft_notification_icon)
|
|
36
|
+
.setColor(ContextCompat.getColor(context, R.color.craft_notification_color))
|
|
36
37
|
.setContentTitle(value.optString("title"))
|
|
37
38
|
.setContentText(value.optString("body"))
|
|
38
39
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
|
+
import com.craft.runtime.CraftNative
|
|
4
|
+
|
|
3
5
|
import android.Manifest
|
|
4
6
|
import android.app.Activity
|
|
5
7
|
import android.content.ActivityNotFoundException
|
|
@@ -29,6 +31,15 @@ import java.util.Locale
|
|
|
29
31
|
import java.util.UUID
|
|
30
32
|
{{NATIVE_PUSH_IMPORTS}}
|
|
31
33
|
|
|
34
|
+
private fun craftLocationFix(location: android.location.Location): JSONObject = JSONObject()
|
|
35
|
+
.put("latitude", location.latitude)
|
|
36
|
+
.put("longitude", location.longitude)
|
|
37
|
+
.put("altitude", location.altitude)
|
|
38
|
+
.put("accuracy", location.accuracy.toDouble())
|
|
39
|
+
.put("speed", location.speed.toDouble())
|
|
40
|
+
.put("heading", location.bearing.toDouble())
|
|
41
|
+
.put("timestamp", location.time.toDouble())
|
|
42
|
+
|
|
32
43
|
/**
|
|
33
44
|
* The capabilities a native screen asks for beyond Craft's core set
|
|
34
45
|
* (CraftNativeCapabilities): the system's dialogs, the in-app browser, the
|
|
@@ -59,6 +70,16 @@ internal class CraftNativeServices(
|
|
|
59
70
|
private val speechQueue = mutableListOf<() -> Unit>()
|
|
60
71
|
private val speeches = mutableMapOf<String, (CraftNativeCapabilityResult) -> Unit>()
|
|
61
72
|
|
|
73
|
+
init {
|
|
74
|
+
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation")) {
|
|
75
|
+
// Native screens must also hear a recording started by the page.
|
|
76
|
+
// Keep this process sink independent from the page's listener.
|
|
77
|
+
com.craft.runtime.CraftLocationRecordingStore.nativeListener = { location, id ->
|
|
78
|
+
CraftNativeHub.broadcast("CRAFT_EVENT", JSONObject().put("event", "craftLocationUpdate").put("detail", craftLocationFix(location).put("recordingId", id)))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
/** `background.complete(ok, id)`: who is waiting for a background refresh's answer. */
|
|
63
84
|
var onBackgroundComplete: ((Boolean, String?) -> Unit)? = null
|
|
64
85
|
|
|
@@ -84,7 +105,11 @@ internal class CraftNativeServices(
|
|
|
84
105
|
add("healthAvailability")
|
|
85
106
|
}
|
|
86
107
|
if (config.optBoolean("enableKeepAwake")) add("keepAwake")
|
|
87
|
-
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation"))
|
|
108
|
+
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation")) {
|
|
109
|
+
add("geolocation")
|
|
110
|
+
if (CraftNative.supportsRecordingOwnership()) add("locationRecordingOwnership")
|
|
111
|
+
if (CraftNative.supportsRecordingIntentIdentity()) add("locationRecordingIntentIdentity")
|
|
112
|
+
}
|
|
88
113
|
if (config.optJSONObject("backgroundRefresh")?.optBoolean("enabled") == true || config.optBoolean("enablePushNotifications")) add("background")
|
|
89
114
|
// craft.crashReporter: the app's crash and error reporter (bughq).
|
|
90
115
|
if (CraftCrashReporter.enabled) add("crashReporter")
|
|
@@ -130,6 +155,8 @@ internal class CraftNativeServices(
|
|
|
130
155
|
"CrashReporter" -> crashReporter(method, args, done)
|
|
131
156
|
else -> done(failure("UNKNOWN_ACTION", "Unsupported native API"))
|
|
132
157
|
}
|
|
158
|
+
} catch (error: com.craft.runtime.CraftRecordingException) {
|
|
159
|
+
done(failure(error.code, error.message ?: "Recording failed"))
|
|
133
160
|
} catch (error: Exception) {
|
|
134
161
|
Log.e("CraftNativeAndroid", "$module.$method failed", error)
|
|
135
162
|
done(failure("CRAFT_ERROR", error.message ?: "$module.$method failed"))
|
|
@@ -359,14 +386,6 @@ internal class CraftNativeServices(
|
|
|
359
386
|
private var watchCallback: com.google.android.gms.location.LocationCallback? = null
|
|
360
387
|
private var pendingLocationPermission: ((Boolean) -> Unit)? = null
|
|
361
388
|
|
|
362
|
-
private fun fix(location: android.location.Location): JSONObject = JSONObject()
|
|
363
|
-
.put("latitude", location.latitude)
|
|
364
|
-
.put("longitude", location.longitude)
|
|
365
|
-
.put("altitude", location.altitude)
|
|
366
|
-
.put("accuracy", location.accuracy.toDouble())
|
|
367
|
-
.put("speed", location.speed.toDouble())
|
|
368
|
-
.put("heading", location.bearing.toDouble())
|
|
369
|
-
.put("timestamp", location.time.toDouble())
|
|
370
389
|
|
|
371
390
|
private fun hasLocation(): Boolean =
|
|
372
391
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
@@ -374,10 +393,15 @@ internal class CraftNativeServices(
|
|
|
374
393
|
|
|
375
394
|
/** Location, asked for once a person starts something that needs it. */
|
|
376
395
|
private fun withLocation(done: (CraftNativeCapabilityResult) -> Unit, then: () -> Unit) {
|
|
377
|
-
|
|
396
|
+
val authorized = {
|
|
397
|
+
try { then() }
|
|
398
|
+
catch (error: com.craft.runtime.CraftRecordingException) { done(failure(error.code, error.message ?: "Recording failed")) }
|
|
399
|
+
catch (error: Exception) { done(failure("CRAFT_ERROR", error.message ?: "Location failed")) }
|
|
400
|
+
}
|
|
401
|
+
if (hasLocation()) return authorized()
|
|
378
402
|
val host = activity() ?: return done(failure("PERMISSION_DENIED", "Location is not allowed"))
|
|
379
403
|
pendingLocationPermission?.invoke(false)
|
|
380
|
-
pendingLocationPermission = { granted -> if (granted)
|
|
404
|
+
pendingLocationPermission = { granted -> if (granted) authorized() else done(failure("PERMISSION_DENIED", "Location is not allowed")) }
|
|
381
405
|
ActivityCompat.requestPermissions(host, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), LOCATION_PERMISSION_REQUEST)
|
|
382
406
|
}
|
|
383
407
|
|
|
@@ -396,7 +420,7 @@ internal class CraftNativeServices(
|
|
|
396
420
|
val priority = if (options.optBoolean("enableHighAccuracy", true)) com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY
|
|
397
421
|
else com.google.android.gms.location.Priority.PRIORITY_BALANCED_POWER_ACCURACY
|
|
398
422
|
locationClient.getCurrentLocation(priority, null)
|
|
399
|
-
.addOnSuccessListener { location -> done(if (location != null) success(
|
|
423
|
+
.addOnSuccessListener { location -> done(if (location != null) success(craftLocationFix(location)) else failure("POSITION_UNAVAILABLE", "No location fix")) }
|
|
400
424
|
.addOnFailureListener { error -> done(failure("POSITION_UNAVAILABLE", error.message ?: "No location fix")) }
|
|
401
425
|
}
|
|
402
426
|
"watchPosition" -> withLocation(done) {
|
|
@@ -404,7 +428,7 @@ internal class CraftNativeServices(
|
|
|
404
428
|
val callback = object : com.google.android.gms.location.LocationCallback() {
|
|
405
429
|
override fun onLocationResult(result: com.google.android.gms.location.LocationResult) {
|
|
406
430
|
for (location in result.locations) {
|
|
407
|
-
CraftNativeHub.broadcast("CRAFT_EVENT", JSONObject().put("event", "craftLocationUpdate").put("detail",
|
|
431
|
+
CraftNativeHub.broadcast("CRAFT_EVENT", JSONObject().put("event", "craftLocationUpdate").put("detail", craftLocationFix(location)))
|
|
408
432
|
}
|
|
409
433
|
}
|
|
410
434
|
}
|
|
@@ -418,29 +442,15 @@ internal class CraftNativeServices(
|
|
|
418
442
|
watchCallback = null
|
|
419
443
|
done(success(true))
|
|
420
444
|
}
|
|
421
|
-
"startRecording" ->
|
|
422
|
-
val
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
done(success(store.state(context)))
|
|
430
|
-
}
|
|
431
|
-
"pauseRecording", "resumeRecording" -> {
|
|
432
|
-
store.setPaused(context, method == "pauseRecording")
|
|
433
|
-
done(success(store.state(context)))
|
|
434
|
-
}
|
|
435
|
-
"stopRecording" -> {
|
|
436
|
-
val state = store.state(context, includeLocations = true)
|
|
437
|
-
store.stop(context)
|
|
438
|
-
store.listener = null
|
|
439
|
-
runCatching { context.startService(Intent(context, com.craft.runtime.LocationRecordingService::class.java).setAction(com.craft.runtime.LocationRecordingService.ACTION_STOP)) }
|
|
440
|
-
done(success(state.put("active", false).put("paused", false)))
|
|
445
|
+
"startRecording", "pauseRecording", "resumeRecording", "stopRecording", "getRecordingState", "readRecording", "acknowledgeRecording" -> {
|
|
446
|
+
val invoke = {
|
|
447
|
+
val answer = JSONObject(store.perform(context, method, (args.optJSONObject(0) ?: JSONObject()).toString()))
|
|
448
|
+
if (answer.optBoolean("ok")) {
|
|
449
|
+
done(success(answer.opt("data")))
|
|
450
|
+
} else done(failure(answer.optString("code"), answer.optString("message")))
|
|
451
|
+
}
|
|
452
|
+
if (method == "startRecording") withLocation(done, invoke) else invoke()
|
|
441
453
|
}
|
|
442
|
-
"getRecordingState" -> done(success(store.state(context)))
|
|
443
|
-
"readRecording" -> done(success(store.locations(context)))
|
|
444
454
|
else -> done(failure("UNKNOWN_ACTION", "Unsupported Location method"))
|
|
445
455
|
}
|
|
446
456
|
}
|