craft-native 0.0.238 → 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/dist/android/src/index.d.ts +17 -1
- package/dist/android/src/index.js +83 -6
- package/dist/android/src/native-map.d.ts +7 -0
- package/dist/android/templates/CraftBridge.kt.template +2 -1
- 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/LocationRecordingService.kt.template +3 -1
- 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/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +8 -0
- package/dist/ios/src/testing.d.ts +14 -2
- package/dist/ios/templates/CraftApp.swift +43 -6
- package/dist/ios/templates/CraftNativeScreen.swift +71 -13
- package/dist/ios/templates/testing/DriverTests.swift.template +37 -0
- package/dist/macos/templates/native_layout.zig +41 -5
- package/package.json +1 -1
|
@@ -110,17 +110,7 @@ import androidx.javascriptengine.Message
|
|
|
110
110
|
import androidx.javascriptengine.MessagePort
|
|
111
111
|
import androidx.javascriptengine.MessagePortClient
|
|
112
112
|
import com.craft.runtime.CraftNative
|
|
113
|
-
|
|
114
|
-
import com.google.android.gms.maps.GoogleMap
|
|
115
|
-
import com.google.android.gms.maps.MapView
|
|
116
|
-
import com.google.android.gms.maps.OnMapReadyCallback
|
|
117
|
-
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
118
|
-
import com.google.android.gms.maps.model.LatLng
|
|
119
|
-
import com.google.android.gms.maps.model.LatLngBounds
|
|
120
|
-
import com.google.android.gms.maps.model.Marker
|
|
121
|
-
import com.google.android.gms.maps.model.MarkerOptions
|
|
122
|
-
import com.google.android.gms.maps.model.Polyline
|
|
123
|
-
import com.google.android.gms.maps.model.PolylineOptions
|
|
113
|
+
{{NATIVE_MAP_IMPORTS}}
|
|
124
114
|
import org.json.JSONArray
|
|
125
115
|
import org.json.JSONObject
|
|
126
116
|
import java.io.File
|
|
@@ -597,422 +587,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
597
587
|
private data class Quadruple<T1, T2, T3, T4>(val first: T1, val second: T2, val third: T3, val fourth: T4)
|
|
598
588
|
}
|
|
599
589
|
|
|
600
|
-
|
|
601
|
-
private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
|
|
602
|
-
private val mapView = MapView(context)
|
|
603
|
-
private val scaleView = LinearLayout(context).apply {
|
|
604
|
-
orientation = LinearLayout.VERTICAL
|
|
605
|
-
setPadding(4, 2, 4, 2)
|
|
606
|
-
background = GradientDrawable().apply { setColor(Color.argb(230, 255, 255, 255)); cornerRadius = 4f }
|
|
607
|
-
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
608
|
-
}
|
|
609
|
-
private val scaleLabel = TextView(context).apply { textSize = 11f; setTextColor(Color.rgb(37, 51, 33)) }
|
|
610
|
-
private val scaleLine = View(context).apply { setBackgroundColor(Color.rgb(37, 51, 33)) }
|
|
611
|
-
private var googleMap: GoogleMap? = null
|
|
612
|
-
private var tileOverlay: com.google.android.gms.maps.model.TileOverlay? = null
|
|
613
|
-
private var tileTemplate: String? = null
|
|
614
|
-
private val tileAttribution = TextView(context).apply { textSize = 9f; setTextColor(Color.DKGRAY); setBackgroundColor(Color.argb(220, 255, 255, 255)); maxLines = 2; visibility = View.GONE }
|
|
615
|
-
private var pendingProps = JSONObject()
|
|
616
|
-
private var pendingChildren = JSONArray()
|
|
617
|
-
private val markers = mutableMapOf<String, Marker>()
|
|
618
|
-
private val polylines = mutableMapOf<String, Polyline>()
|
|
619
|
-
private val markerHandlers = mutableMapOf<String, String>()
|
|
620
|
-
private var onMarkerTap: ((String) -> Unit)? = null
|
|
621
|
-
var onMapPress: ((LatLng) -> Unit)? = null
|
|
622
|
-
var onRegionChangeComplete: ((JSONObject) -> Unit)? = null
|
|
623
|
-
private var userChangingCamera = false
|
|
624
|
-
private var markerBearings = mutableMapOf<String, Float>()
|
|
625
|
-
private var directionIcon: com.google.android.gms.maps.model.BitmapDescriptor? = null
|
|
626
|
-
private var lifecycleStarted = false
|
|
627
|
-
private var destroyed = false
|
|
628
|
-
private var initialCameraKey: String? = null
|
|
629
|
-
|
|
630
|
-
init {
|
|
631
|
-
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
632
|
-
scaleView.addView(scaleLabel)
|
|
633
|
-
scaleView.addView(scaleLine, LinearLayout.LayoutParams(1, 2))
|
|
634
|
-
addView(scaleView, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.START).apply {
|
|
635
|
-
leftMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
636
|
-
bottomMargin = (24 * resources.displayMetrics.density).roundToInt()
|
|
637
|
-
})
|
|
638
|
-
scaleView.visibility = View.GONE
|
|
639
|
-
addView(tileAttribution, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.END).apply { rightMargin = (8 * resources.displayMetrics.density).roundToInt(); bottomMargin = (8 * resources.displayMetrics.density).roundToInt() })
|
|
640
|
-
mapView.onCreate(null)
|
|
641
|
-
mapView.getMapAsync(this)
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
override fun onMapReady(map: GoogleMap) {
|
|
645
|
-
if (destroyed) return
|
|
646
|
-
googleMap = map
|
|
647
|
-
map.setOnMapClickListener { point -> onMapPress?.invoke(point) }
|
|
648
|
-
map.setOnCameraMoveStartedListener { reason ->
|
|
649
|
-
if (reason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE) userChangingCamera = true
|
|
650
|
-
}
|
|
651
|
-
map.setOnCameraMoveListener { updateScale() }
|
|
652
|
-
map.setOnCameraIdleListener {
|
|
653
|
-
updateScale()
|
|
654
|
-
if (userChangingCamera) {
|
|
655
|
-
userChangingCamera = false
|
|
656
|
-
val bounds = map.projection.visibleRegion.latLngBounds
|
|
657
|
-
val position = map.cameraPosition
|
|
658
|
-
val longitudeDelta = (bounds.northeast.longitude - bounds.southwest.longitude + 360) % 360
|
|
659
|
-
onRegionChangeComplete?.invoke(JSONObject()
|
|
660
|
-
.put("center", JSONObject().put("latitude", position.target.latitude).put("longitude", position.target.longitude))
|
|
661
|
-
.put("span", JSONObject().put("latitudeDelta", bounds.northeast.latitude - bounds.southwest.latitude).put("longitudeDelta", longitudeDelta))
|
|
662
|
-
.put("heading", position.bearing).put("pitch", position.tilt))
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
map.setOnMarkerClickListener { marker ->
|
|
666
|
-
val id = markers.entries.firstOrNull { it.value == marker }?.key ?: return@setOnMarkerClickListener false
|
|
667
|
-
onMarkerTap?.invoke(id)
|
|
668
|
-
// The native contract dispatches the marker event itself. Consume
|
|
669
|
-
// the click so Google Maps does not also show an unsolicited info
|
|
670
|
-
// window for the marker title.
|
|
671
|
-
true
|
|
672
|
-
}
|
|
673
|
-
applyPending()
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
fun configure(props: JSONObject, children: JSONArray, markerHandler: (String) -> Unit) {
|
|
677
|
-
pendingProps = props
|
|
678
|
-
pendingChildren = children
|
|
679
|
-
onMarkerTap = markerHandler
|
|
680
|
-
applyPending()
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
684
|
-
|
|
685
|
-
private fun applyPending() {
|
|
686
|
-
val map = googleMap ?: return
|
|
687
|
-
applyMapPadding()
|
|
688
|
-
map.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
|
|
689
|
-
// The toolbar is replaced by authored actions; the scale uses the native projection.
|
|
690
|
-
map.uiSettings.isMapToolbarEnabled = false
|
|
691
|
-
map.uiSettings.isScrollGesturesEnabled = craftNativeBoolean(pendingProps.opt("scrollEnabled"), true)
|
|
692
|
-
map.uiSettings.isZoomGesturesEnabled = craftNativeBoolean(pendingProps.opt("zoomEnabled"), true)
|
|
693
|
-
map.uiSettings.isRotateGesturesEnabled = craftNativeBoolean(pendingProps.opt("rotateEnabled"))
|
|
694
|
-
map.mapType = when (craftNativeOptionalString(pendingProps.opt("mapType")).orEmpty().lowercase()) {
|
|
695
|
-
"satellite" -> GoogleMap.MAP_TYPE_SATELLITE
|
|
696
|
-
"hybrid" -> GoogleMap.MAP_TYPE_HYBRID
|
|
697
|
-
"terrain" -> GoogleMap.MAP_TYPE_TERRAIN
|
|
698
|
-
else -> GoogleMap.MAP_TYPE_NORMAL
|
|
699
|
-
}
|
|
700
|
-
applyTileOverlay()
|
|
701
|
-
val showUserLocation = craftNativeBoolean(pendingProps.opt("showsUserLocation")) && hasLocationPermission()
|
|
702
|
-
runCatching { map.isMyLocationEnabled = showUserLocation }
|
|
703
|
-
val nextMarkers = mutableMapOf<String, Pair<LatLng, String?>>()
|
|
704
|
-
val nextBearings = mutableMapOf<String, Float>()
|
|
705
|
-
val nextHandlers = mutableMapOf<String, String>()
|
|
706
|
-
val nextPolylines = mutableMapOf<String, Triple<List<LatLng>, Int, Float>>()
|
|
707
|
-
for (index in 0 until pendingChildren.length()) {
|
|
708
|
-
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
709
|
-
val type = child.optString("type").lowercase()
|
|
710
|
-
val props = child.optJSONObject("props") ?: JSONObject()
|
|
711
|
-
val events = child.optJSONObject("events") ?: JSONObject()
|
|
712
|
-
val id = craftNativeOptionalString(props.opt("id"))
|
|
713
|
-
?: craftNativeOptionalString(child.opt("id"))
|
|
714
|
-
?: "overlay-$index"
|
|
715
|
-
val points = coordinates(props.optJSONArray("geometry"))
|
|
716
|
-
if (type == "mapmarker" || type == "marker") {
|
|
717
|
-
points.firstOrNull()?.let { point ->
|
|
718
|
-
nextMarkers[id] = point to (props.opt("title") as? String)
|
|
719
|
-
(props.opt("bearing") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.let { nextBearings[id] = (it % 360).toFloat() }
|
|
720
|
-
listOf(events.opt("onPress"), events.opt("onMarkerPress"))
|
|
721
|
-
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
722
|
-
?.let { nextHandlers[id] = it }
|
|
723
|
-
}
|
|
724
|
-
} else if (type == "mappolyline" || type == "polyline") {
|
|
725
|
-
if (points.size > 1) {
|
|
726
|
-
// Keep malformed authored widths inert, matching the iOS
|
|
727
|
-
// host. An omitted or null width still gets the shared
|
|
728
|
-
// default; an attacker-controlled NaN, infinity, zero,
|
|
729
|
-
// or negative value must not become a valid overlay.
|
|
730
|
-
val rawWidth = props.opt("strokeWidth")
|
|
731
|
-
val authoredWidth = when (rawWidth) {
|
|
732
|
-
null, JSONObject.NULL -> 3.0
|
|
733
|
-
is Number -> rawWidth.toDouble()
|
|
734
|
-
else -> 3.0
|
|
735
|
-
}
|
|
736
|
-
// MapKit's lineWidth is expressed in points, while the
|
|
737
|
-
// Google Maps SDK consumes screen pixels. Keep the
|
|
738
|
-
// protocol in logical points and convert only at this
|
|
739
|
-
// Android host boundary so dense displays stay in parity.
|
|
740
|
-
val width = (authoredWidth * resources.displayMetrics.density).toFloat()
|
|
741
|
-
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
742
|
-
nextPolylines[id] = Triple(points, color(craftNativeOptionalString(props.opt("strokeColor")).orEmpty()), width)
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
markerBearings = nextBearings
|
|
747
|
-
markers.keys.filter { nextMarkers[it] == null }.forEach { markers.remove(it)?.remove() }
|
|
748
|
-
for ((id, value) in nextMarkers) {
|
|
749
|
-
val marker = markers[id]
|
|
750
|
-
if (marker != null) {
|
|
751
|
-
marker.position = value.first
|
|
752
|
-
marker.title = value.second
|
|
753
|
-
marker.setIcon(if (markerBearings.containsKey(id)) directionMarkerIcon() else BitmapDescriptorFactory.defaultMarker())
|
|
754
|
-
marker.isFlat = markerBearings.containsKey(id)
|
|
755
|
-
marker.rotation = markerBearings[id] ?: 0f
|
|
756
|
-
marker.setAnchor(0.5f, if (markerBearings.containsKey(id)) 0.5f else 1f)
|
|
757
|
-
} else {
|
|
758
|
-
map.addMarker(MarkerOptions().position(value.first).title(value.second).icon(if (markerBearings.containsKey(id)) directionMarkerIcon() else BitmapDescriptorFactory.defaultMarker()).flat(markerBearings.containsKey(id)).rotation(markerBearings[id] ?: 0f).anchor(0.5f, if (markerBearings.containsKey(id)) 0.5f else 1f))?.let { markers[id] = it }
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
markerHandlers.clear()
|
|
762
|
-
markerHandlers.putAll(nextHandlers)
|
|
763
|
-
polylines.keys.filter { nextPolylines[it] == null }.forEach { id ->
|
|
764
|
-
polylines.remove(id)?.remove()
|
|
765
|
-
}
|
|
766
|
-
for ((id, value) in nextPolylines) {
|
|
767
|
-
val polyline = polylines[id]
|
|
768
|
-
if (polyline != null) {
|
|
769
|
-
polyline.points = value.first
|
|
770
|
-
polyline.color = value.second
|
|
771
|
-
polyline.width = value.third
|
|
772
|
-
} else {
|
|
773
|
-
map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(value.third))?.let { polylines[id] = it }
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
val explicitRegion = region(pendingProps.optJSONObject("region"))
|
|
777
|
-
val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
|
|
778
|
-
if (explicitRegion != null) {
|
|
779
|
-
// A controlled region is an exact camera request. The padding
|
|
780
|
-
// used for fitToBounds is framing chrome, not part of the region
|
|
781
|
-
// contract, and would otherwise zoom every Android screen out.
|
|
782
|
-
moveCamera(explicitRegion, padding = 0)
|
|
783
|
-
initialCameraKey = null
|
|
784
|
-
} else if (initialRegion != null) {
|
|
785
|
-
val key = "region:${pendingProps.optJSONObject("initialRegion")}"
|
|
786
|
-
if (initialCameraKey != key) {
|
|
787
|
-
moveCamera(initialRegion, padding = 0)
|
|
788
|
-
initialCameraKey = key
|
|
789
|
-
}
|
|
790
|
-
} else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
|
|
791
|
-
val key = "fit:${overlayKey(nextMarkers, nextPolylines)}:${pendingProps.optJSONObject("edgePadding")}"
|
|
792
|
-
if (initialCameraKey != key) {
|
|
793
|
-
if (fitToBounds()) initialCameraKey = key
|
|
794
|
-
}
|
|
795
|
-
} else {
|
|
796
|
-
initialCameraKey = null
|
|
797
|
-
}
|
|
798
|
-
applyCameraOrientation()
|
|
799
|
-
updateScale()
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
private fun applyTileOverlay() {
|
|
803
|
-
val map = googleMap ?: return
|
|
804
|
-
val options = pendingProps.optJSONObject("tileOverlay")
|
|
805
|
-
val template = options?.optString("urlTemplate").orEmpty()
|
|
806
|
-
val credit = options?.optString("attribution").orEmpty()
|
|
807
|
-
val next = template.takeIf { it.startsWith("https://") && listOf("{z}", "{x}", "{y}").all { key -> it.contains(key) } && credit.isNotBlank() }
|
|
808
|
-
if (next != tileTemplate) {
|
|
809
|
-
tileOverlay?.remove(); tileOverlay = null; tileTemplate = next
|
|
810
|
-
if (next != null) {
|
|
811
|
-
val provider = object : com.google.android.gms.maps.model.UrlTileProvider(256, 256) {
|
|
812
|
-
override fun getTileUrl(x: Int, y: Int, zoom: Int): java.net.URL? = runCatching { java.net.URL(next.replace("{x}", x.toString()).replace("{y}", y.toString()).replace("{z}", zoom.toString())) }.getOrNull()
|
|
813
|
-
}
|
|
814
|
-
tileOverlay = map.addTileOverlay(com.google.android.gms.maps.model.TileOverlayOptions().tileProvider(provider).zIndex(-1f))
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
if (next != null) map.mapType = GoogleMap.MAP_TYPE_NONE
|
|
818
|
-
tileAttribution.text = credit
|
|
819
|
-
tileAttribution.visibility = if (next == null) View.GONE else View.VISIBLE
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
private fun applyMapPadding() {
|
|
823
|
-
val authored = pendingProps.optJSONObject("edgePadding")
|
|
824
|
-
fun inset(name: String): Double = (authored?.opt(name) as? Number)?.toDouble()?.takeIf { it.isFinite() }?.coerceIn(0.0, 10_000.0)?.times(resources.displayMetrics.density) ?: 0.0
|
|
825
|
-
var left = inset("left"); var top = inset("top"); var right = inset("right"); var bottom = inset("bottom")
|
|
826
|
-
val horizontal = left + right
|
|
827
|
-
val vertical = top + bottom
|
|
828
|
-
val availableWidth = (width - 40 * resources.displayMetrics.density).coerceAtLeast(0f).toDouble()
|
|
829
|
-
val availableHeight = (height - 40 * resources.displayMetrics.density).coerceAtLeast(0f).toDouble()
|
|
830
|
-
if (horizontal > availableWidth && horizontal > 0) { left *= availableWidth / horizontal; right *= availableWidth / horizontal }
|
|
831
|
-
if (vertical > availableHeight && vertical > 0) { top *= availableHeight / vertical; bottom *= availableHeight / vertical }
|
|
832
|
-
googleMap?.setPadding(left.roundToInt(), top.roundToInt(), right.roundToInt(), bottom.roundToInt())
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
private fun updateScale() {
|
|
836
|
-
val map = googleMap ?: return
|
|
837
|
-
if (!craftNativeBoolean(pendingProps.opt("showsScale")) || width <= 0 || height <= 0) { scaleView.visibility = View.GONE; return }
|
|
838
|
-
val maximumWidth = (80 * resources.displayMetrics.density).roundToInt()
|
|
839
|
-
val origin = map.projection.fromScreenLocation(android.graphics.Point(width / 2, height / 2))
|
|
840
|
-
val end = map.projection.fromScreenLocation(android.graphics.Point(width / 2 + maximumWidth, height / 2))
|
|
841
|
-
val distance = FloatArray(1)
|
|
842
|
-
android.location.Location.distanceBetween(origin.latitude, origin.longitude, end.latitude, end.longitude, distance)
|
|
843
|
-
val metres = distance[0].toDouble()
|
|
844
|
-
if (!metres.isFinite() || metres <= 0) { scaleView.visibility = View.GONE; return }
|
|
845
|
-
val magnitude = Math.pow(10.0, kotlin.math.floor(kotlin.math.log10(metres)))
|
|
846
|
-
val length = listOf(5.0, 2.0, 1.0).firstOrNull { it * magnitude <= metres }?.times(magnitude) ?: magnitude
|
|
847
|
-
scaleLabel.text = if (length >= 1000) "${length / 1000} km" else "${length.roundToInt()} m"
|
|
848
|
-
scaleLine.layoutParams = LinearLayout.LayoutParams((maximumWidth * length / metres).roundToInt().coerceAtLeast(1), 2)
|
|
849
|
-
scaleView.visibility = View.VISIBLE
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
private fun applyCameraOrientation() {
|
|
853
|
-
val map = googleMap ?: return
|
|
854
|
-
val camera = map.cameraPosition
|
|
855
|
-
val pitch = (pendingProps.opt("cameraPitch") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.coerceIn(0.0, 60.0)?.toFloat() ?: camera.tilt
|
|
856
|
-
val heading = (pendingProps.opt("cameraHeading") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.toFloat() ?: camera.bearing
|
|
857
|
-
if (kotlin.math.abs(camera.tilt - pitch) > 0.01f || kotlin.math.abs(camera.bearing - heading) > 0.01f) {
|
|
858
|
-
map.moveCamera(CameraUpdateFactory.newCameraPosition(com.google.android.gms.maps.model.CameraPosition.Builder(camera).tilt(pitch).bearing(heading).build()))
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
private fun directionMarkerIcon(): com.google.android.gms.maps.model.BitmapDescriptor {
|
|
863
|
-
directionIcon?.let { return it }
|
|
864
|
-
val size = (26 * resources.displayMetrics.density).roundToInt().coerceAtLeast(24)
|
|
865
|
-
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
866
|
-
val canvas = Canvas(bitmap)
|
|
867
|
-
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
868
|
-
paint.color = Color.WHITE
|
|
869
|
-
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
|
|
870
|
-
paint.color = Color.rgb(36, 114, 75)
|
|
871
|
-
canvas.drawCircle(size / 2f, size / 2f, size * 0.43f, paint)
|
|
872
|
-
paint.color = Color.WHITE
|
|
873
|
-
paint.textSize = size * 0.76f
|
|
874
|
-
paint.textAlign = Paint.Align.CENTER
|
|
875
|
-
paint.typeface = Typeface.DEFAULT_BOLD
|
|
876
|
-
canvas.drawText("↑", size / 2f, size / 2f - (paint.ascent() + paint.descent()) / 2, paint)
|
|
877
|
-
return BitmapDescriptorFactory.fromBitmap(bitmap).also { directionIcon = it }
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
|
|
881
|
-
super.onSizeChanged(width, height, oldWidth, oldHeight)
|
|
882
|
-
applyMapPadding()
|
|
883
|
-
updateScale()
|
|
884
|
-
// MapView can become ready before its host has a usable viewport. Do
|
|
885
|
-
// not mark a fit as complete until the camera update can be issued.
|
|
886
|
-
if (width > 0 && height > 0 && craftNativeBoolean(pendingProps.opt("fitToBounds")) && initialCameraKey == null) {
|
|
887
|
-
applyPending()
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
private fun moveCamera(bounds: LatLngBounds, padding: Int = 24) {
|
|
892
|
-
post {
|
|
893
|
-
if (width > 0 && height > 0) {
|
|
894
|
-
googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding))
|
|
895
|
-
applyCameraOrientation()
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
private fun overlayKey(
|
|
901
|
-
markers: Map<String, Pair<LatLng, String?>>,
|
|
902
|
-
polylines: Map<String, Triple<List<LatLng>, Int, Float>>,
|
|
903
|
-
): String {
|
|
904
|
-
val markerKey = markers.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
905
|
-
"$id:${value.first.latitude},${value.first.longitude}"
|
|
906
|
-
}
|
|
907
|
-
val polylineKey = polylines.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
908
|
-
"$id:" + value.first.joinToString(";") { point -> "${point.latitude},${point.longitude}" }
|
|
909
|
-
}
|
|
910
|
-
return "m[$markerKey]p[$polylineKey]"
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
private fun fitToBounds(): Boolean {
|
|
914
|
-
val map = googleMap ?: return false
|
|
915
|
-
if (width <= 0 || height <= 0) return false
|
|
916
|
-
val builder = LatLngBounds.Builder()
|
|
917
|
-
var count = 0
|
|
918
|
-
markers.values.forEach { builder.include(it.position); count++ }
|
|
919
|
-
polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
920
|
-
if (count == 0) return false
|
|
921
|
-
post {
|
|
922
|
-
if (width > 0 && height > 0) {
|
|
923
|
-
applyMapPadding()
|
|
924
|
-
val padding = if (pendingProps.has("edgePadding")) 0 else (24 * resources.displayMetrics.density).roundToInt()
|
|
925
|
-
map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), padding))
|
|
926
|
-
applyCameraOrientation()
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
return true
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
override fun onAttachedToWindow() {
|
|
933
|
-
super.onAttachedToWindow()
|
|
934
|
-
if (destroyed) return
|
|
935
|
-
mapView.onStart()
|
|
936
|
-
mapView.onResume()
|
|
937
|
-
lifecycleStarted = true
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
override fun onDetachedFromWindow() {
|
|
941
|
-
if (lifecycleStarted) {
|
|
942
|
-
mapView.onPause()
|
|
943
|
-
mapView.onStop()
|
|
944
|
-
lifecycleStarted = false
|
|
945
|
-
}
|
|
946
|
-
super.onDetachedFromWindow()
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
fun destroy() {
|
|
950
|
-
if (destroyed) return
|
|
951
|
-
destroyed = true
|
|
952
|
-
if (lifecycleStarted) {
|
|
953
|
-
mapView.onPause()
|
|
954
|
-
mapView.onStop()
|
|
955
|
-
lifecycleStarted = false
|
|
956
|
-
}
|
|
957
|
-
onMarkerTap = null
|
|
958
|
-
onMapPress = null
|
|
959
|
-
onRegionChangeComplete = null
|
|
960
|
-
directionIcon = null
|
|
961
|
-
tileOverlay?.remove(); tileOverlay = null
|
|
962
|
-
markerHandlers.clear()
|
|
963
|
-
markers.values.forEach { it.remove() }
|
|
964
|
-
markers.clear()
|
|
965
|
-
polylines.values.forEach { it.remove() }
|
|
966
|
-
polylines.clear()
|
|
967
|
-
mapView.onDestroy()
|
|
968
|
-
googleMap = null
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
fun lowMemory() {
|
|
972
|
-
if (!destroyed) mapView.onLowMemory()
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private fun hasLocationPermission(): Boolean =
|
|
976
|
-
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
|
977
|
-
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
978
|
-
|
|
979
|
-
private fun coordinates(geometry: JSONArray?): List<LatLng> {
|
|
980
|
-
if (geometry == null) return emptyList()
|
|
981
|
-
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
982
|
-
val latitude = finiteNumber(geometry.opt(index)) ?: return@mapNotNull null
|
|
983
|
-
val longitude = finiteNumber(geometry.opt(index + 1)) ?: return@mapNotNull null
|
|
984
|
-
if (latitude.isFinite() && longitude.isFinite() &&
|
|
985
|
-
latitude >= -90.0 && latitude <= 90.0 &&
|
|
986
|
-
longitude >= -180.0 && longitude <= 180.0
|
|
987
|
-
) LatLng(latitude, longitude) else null
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
private fun region(value: JSONObject?): LatLngBounds? {
|
|
992
|
-
val center = value?.optJSONObject("center") ?: return null
|
|
993
|
-
val span = value.optJSONObject("span") ?: return null
|
|
994
|
-
val latitude = finiteNumber(center.opt("latitude")) ?: return null
|
|
995
|
-
val longitude = finiteNumber(center.opt("longitude")) ?: return null
|
|
996
|
-
val latitudeDelta = finiteNumber(span.opt("latitudeDelta"))
|
|
997
|
-
?: finiteNumber(span.opt("latitude_delta"))
|
|
998
|
-
?: return null
|
|
999
|
-
val longitudeDelta = finiteNumber(span.opt("longitudeDelta"))
|
|
1000
|
-
?: finiteNumber(span.opt("longitude_delta"))
|
|
1001
|
-
?: return null
|
|
1002
|
-
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite() ||
|
|
1003
|
-
latitude < -90.0 || latitude > 90.0 || longitude < -180.0 || longitude > 180.0 ||
|
|
1004
|
-
latitudeDelta <= 0.0 || latitudeDelta > 180.0 ||
|
|
1005
|
-
longitudeDelta <= 0.0 || longitudeDelta > 360.0 ||
|
|
1006
|
-
latitude - latitudeDelta / 2.0 < -90.0 || latitude + latitudeDelta / 2.0 > 90.0
|
|
1007
|
-
) return null
|
|
1008
|
-
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
private fun finiteNumber(value: Any?): Double? =
|
|
1012
|
-
(value as? Number)?.toDouble()?.takeIf { it.isFinite() }
|
|
1013
|
-
|
|
1014
|
-
private fun color(value: String): Int = runCatching { craftNativeColor(value) }.getOrDefault(Color.BLUE)
|
|
1015
|
-
}
|
|
590
|
+
{{NATIVE_MAP_HOST}}
|
|
1016
591
|
|
|
1017
592
|
/** A finite celebration owned by a View, never by the JavaScript frame loop. */
|
|
1018
593
|
internal class CraftNativeCelebration(private val view: View) {
|
|
@@ -1882,7 +1457,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1882
1457
|
putEnum(base, 16, if (root) sharedAlignItemsCode(alignItems) else 3)
|
|
1883
1458
|
putEnum(base, 17, if (root) 0 else sharedAlignSelfCode(style.alignSelf))
|
|
1884
1459
|
putEnum(base, 18, if (root && wrap) 1 else 0)
|
|
1885
|
-
putEnum(base, 19, if (root) alignContentCode() else
|
|
1460
|
+
putEnum(base, 19, if (root) alignContentCode() else 5)
|
|
1886
1461
|
put(base, 20, margins[2].toFloat())
|
|
1887
1462
|
put(base, 21, margins[1].toFloat())
|
|
1888
1463
|
put(base, 22, margins[3].toFloat())
|
|
@@ -7000,6 +6575,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7000
6575
|
val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
|
|
7001
6576
|
?: craftNativeOptionalString(props.opt("alt"))?.takeIf { type == "Image" || type == "ImageBackground" }
|
|
7002
6577
|
val value = craftNativeOptionalString(props.opt("accessibilityValue"))
|
|
6578
|
+
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
7003
6579
|
view.contentDescription = when {
|
|
7004
6580
|
label != null && value != null -> "$label, $value"
|
|
7005
6581
|
label != null -> label
|
|
@@ -7024,6 +6600,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
7024
6600
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
7025
6601
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
7026
6602
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
6603
|
+
// Expose STX test identities to Android's native automation
|
|
6604
|
+
// tree without replacing the label read by TalkBack.
|
|
6605
|
+
info.viewIdResourceName = testId
|
|
7027
6606
|
info.className = when (role) {
|
|
7028
6607
|
"button" -> Button::class.java.name
|
|
7029
6608
|
"image" -> ImageView::class.java.name
|
|
@@ -99,7 +99,7 @@ dependencies {
|
|
|
99
99
|
// Google Play Location Services (for geolocation)
|
|
100
100
|
implementation("com.google.android.gms:play-services-location:21.1.0")
|
|
101
101
|
|
|
102
|
-
{{
|
|
102
|
+
{{NATIVE_MAP_DEPENDENCY}}
|
|
103
103
|
|
|
104
104
|
// Google Play In-App Review
|
|
105
105
|
implementation("com.google.android.play:review:2.0.1")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
2
2
|
plugins {
|
|
3
3
|
id("com.android.application") version "8.9.1" apply false
|
|
4
|
-
id("org.jetbrains.kotlin.android") version "2.
|
|
4
|
+
id("org.jetbrains.kotlin.android") version "2.2.10" apply false
|
|
5
5
|
{{GOOGLE_SERVICES_PLUGIN}}
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.241";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.js
CHANGED
|
@@ -228,10 +228,18 @@ function serializeSimulatorSteps(steps) {
|
|
|
228
228
|
if (!Number.isInteger(number(step.index)))
|
|
229
229
|
throw new Error("field index must be an integer");
|
|
230
230
|
arg = `${step.index} ${step.text}`;
|
|
231
|
+
} else if ("position" in step) {
|
|
232
|
+
if (number(step.position) > 1)
|
|
233
|
+
throw new Error("Slider position must be between 0 and 1");
|
|
234
|
+
arg = `${token(step.id)} ${step.position}`;
|
|
231
235
|
} else if ("id" in step)
|
|
232
236
|
arg = `${token(step.id)} ${step.text}`;
|
|
233
237
|
else if ("text" in step) {
|
|
234
238
|
arg = step.text;
|
|
239
|
+
if (step.command === "longpressid")
|
|
240
|
+
token(arg);
|
|
241
|
+
if (step.command === "expectapp" && !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(arg))
|
|
242
|
+
throw new Error("Expected an application bundle identifier");
|
|
235
243
|
if ("seconds" in step && step.seconds !== undefined) {
|
|
236
244
|
if (arg.includes("|"))
|
|
237
245
|
throw new Error("A timed expectation cannot contain |");
|
|
@@ -66,15 +66,27 @@ export type SimulatorStep = {
|
|
|
66
66
|
index: number;
|
|
67
67
|
text: string;
|
|
68
68
|
} | {
|
|
69
|
-
command: 'typeid' | 'labelid';
|
|
69
|
+
command: 'typeid' | 'labelid' | 'valueid';
|
|
70
70
|
id: string;
|
|
71
71
|
text: string;
|
|
72
|
+
}
|
|
73
|
+
/** Adjust an actual native UISlider by identifier, using its normalized range. */
|
|
74
|
+
| {
|
|
75
|
+
command: 'sliderid';
|
|
76
|
+
id: string;
|
|
77
|
+
position: number;
|
|
78
|
+
}
|
|
79
|
+
/** Assert a handoff reached the foreground application without launching it. */
|
|
80
|
+
| {
|
|
81
|
+
command: 'expectapp';
|
|
82
|
+
text: string;
|
|
83
|
+
seconds?: number;
|
|
72
84
|
} | {
|
|
73
85
|
command: 'expect' | 'expectexact' | 'expectid' | 'systemexpect';
|
|
74
86
|
text: string;
|
|
75
87
|
seconds?: number;
|
|
76
88
|
} | {
|
|
77
|
-
command: 'tap' | 'tapif' | 'tapid' | 'tapexact' | 'type' | 'absent' | 'absentid' | 'selected' | 'tab' | 'scrollto' | 'dismiss' | 'log' | 'systemtap' | 'systemabsent';
|
|
89
|
+
command: 'tap' | 'tapif' | 'tapid' | 'longpressid' | 'tapexact' | 'type' | 'absent' | 'absentid' | 'selected' | 'tab' | 'scrollto' | 'dismiss' | 'log' | 'systemtap' | 'systemabsent';
|
|
78
90
|
text: string;
|
|
79
91
|
} | {
|
|
80
92
|
command: 'shot' | 'tree' | 'audit' | 'exec' | 'systemtree';
|