ReviewOS

also looking at this

craft-native/craft

feat(maps): add key-free native Android maps and shared map controls

#363
Merged chris wants to merge codex/wildloop-native-map-providers into main
28 files +1857 -481

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

.github/workflows/mobile-e2e.ymlmodified+1-1
Changes to .github/workflows/mobile-e2e.yml
@@ -607,7 +607,7 @@ jobs:
607607 api-level: ${{ fromJSON((inputs.scope != 'android-navigation' || inputs.android_suite == 'full') && '[34,35]' || inputs.android_suite == 'system-bars' && '[35]' || '[34]') }}
608608 env:
609609 CRAFT_ANDROID_NATIVE_SUITE: ${{ inputs.scope != 'android-navigation' && 'full' || inputs.android_suite || 'focused' }}
610 CRAFT_ANDROID_TEST_CLASS: ${{ inputs.scope == 'android-navigation' && inputs.android_suite == 'maps' && 'dev.craft.navigationtest.NativeMapTest#keyFreeBasemapsKeepTheRecordedRouteAndDirectionSignsVisible,dev.craft.navigationtest.NativeMapTest#nativeMapMutationsUpdateThePinAndCameraWithoutReloadingTheStyle,dev.craft.navigationtest.NativeMapTest#showingTheLocationDotDoesNotRequestPermissionDuringRendering,dev.craft.navigationtest.NativeMapTest#controlledRegionEchoPreservesTheCameraAfterANativeDrag' || inputs.scope == 'android-navigation' && inputs.android_suite == 'system-bars' && 'dev.craft.navigationtest.NativeNavigationTest#nativeScreenBackgroundOptionAppliesAndResets' || inputs.scope == 'android-navigation' && inputs.android_suite != 'full' && 'dev.craft.navigationtest.NativeNavigationTest#outgoingTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#thirdTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#cancelledSwipeTargetIsVisibleWhenSelectedLater,dev.craft.navigationtest.NativeNavigationTest#completedReverseSwipeKeepsRetainedTabVisible,dev.craft.navigationtest.NativeNavigationTest#nativePullToRefreshSurvivesViewReattachment,dev.craft.navigationtest.NativeNavigationTest#pushedScreenTabAccessoryOpensNativeRouteAndClearsOnBack,dev.craft.navigationtest.NativeFlatListTest#refreshAppearanceUsesTheScreenContrastFallback,dev.craft.navigationtest.NativeNavigationTest#nativeDatePickerKeepsFollowingControlsMounted' || '' }}
610 CRAFT_ANDROID_TEST_CLASS: ${{ inputs.scope == 'android-navigation' && inputs.android_suite == 'maps' && 'dev.craft.navigationtest.NativeMapTest#keyFreeBasemapsKeepTheRecordedRouteAndDirectionSignsVisible,dev.craft.navigationtest.NativeMapTest#nativeMapMutationsUpdateThePinAndCameraWithoutReloadingTheStyle,dev.craft.navigationtest.NativeMapTest#showingTheLocationDotDoesNotRequestPermissionDuringRendering,dev.craft.navigationtest.NativeMapTest#controlledRegionEchoPreservesTheCameraAfterANativeDrag,dev.craft.navigationtest.NativeMapTest#backgroundAndResumeKeepTheNativeMapAndCamera' || inputs.scope == 'android-navigation' && inputs.android_suite == 'system-bars' && 'dev.craft.navigationtest.NativeNavigationTest#nativeScreenBackgroundOptionAppliesAndResets' || inputs.scope == 'android-navigation' && inputs.android_suite != 'full' && 'dev.craft.navigationtest.NativeNavigationTest#outgoingTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#thirdTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#cancelledSwipeTargetIsVisibleWhenSelectedLater,dev.craft.navigationtest.NativeNavigationTest#completedReverseSwipeKeepsRetainedTabVisible,dev.craft.navigationtest.NativeNavigationTest#nativePullToRefreshSurvivesViewReattachment,dev.craft.navigationtest.NativeNavigationTest#pushedScreenTabAccessoryOpensNativeRouteAndClearsOnBack,dev.craft.navigationtest.NativeFlatListTest#refreshAppearanceUsesTheScreenContrastFallback,dev.craft.navigationtest.NativeNavigationTest#nativeDatePickerKeepsFollowingControlsMounted' || '' }}
611611 if: ${{ inputs.scope != 'ios-navigation' && inputs.scope != 'ios-render' }}
612612 runs-on: ubuntu-latest
613613 timeout-minutes: 35
packages/android/fixtures/native-navigation/NativeMapTest.ktmodified+24-0
Changes to packages/android/fixtures/native-navigation/NativeMapTest.kt
@@ -248,6 +248,30 @@ class NativeMapTest {
248248 }
249249 }
250250
251 @Test
252 fun backgroundAndResumeKeepTheNativeMapAndCamera() = withMap { activity, host, _, map, _ ->
253 awaitBasemapPixels(host, "before-background")
254 val camera = main { map.cameraPosition }
255 for (action in listOf(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.ACTION_UP)) {
256 assertTrue(instrumentation.uiAutomation.injectInputEvent(android.view.KeyEvent(action, android.view.KeyEvent.KEYCODE_HOME), true))
257 }
258 instrumentation.uiAutomation.waitForIdle(500, 5_000)
259 assertNotEquals("The Home action must actually background the app", activity.packageName, instrumentation.uiAutomation.rootInActiveWindow?.packageName?.toString())
260 val resumed = instrumentation.startActivitySync(Intent(instrumentation.targetContext, MainActivity::class.java)
261 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP))
262 assertSame("Returning must reuse the retained native map activity", activity, resumed)
263 awaitState("The resumed native map must render its retained route and arrows") {
264 features(map).size == 3 && map.queryRenderedFeatures(map.projection.toScreenLocation(LatLng(47.005, 11.005)), "craft-map-directions").isNotEmpty()
265 }
266 awaitBasemapPixels(host, "after-background")
267 main {
268 assertEquals(camera.zoom, map.cameraPosition.zoom, 0.001)
269 assertEquals(camera.target!!.latitude, map.cameraPosition.target!!.latitude, 0.000001)
270 assertEquals(camera.target!!.longitude, map.cameraPosition.target!!.longitude, 0.000001)
271 }
272 evidence("map-resumed-after-home")
273 }
274
251275 @Test
252276 fun showingTheLocationDotDoesNotRequestPermissionDuringRendering() = withMap { activity, host, _, map, _ ->
253277 assertEquals(PackageManager.PERMISSION_DENIED, activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION))
packages/android/templates/CraftNativeMapLibre.kt.templatemodified+37-3
Changes to packages/android/templates/CraftNativeMapLibre.kt.template
@@ -4,6 +4,14 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
44 private var map: MapLibreMap? = null
55 private var destroyed = false
66 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 }
715 private var pendingProps = JSONObject()
816 private var pendingChildren = JSONArray()
917 private var styleKey: String? = null
@@ -358,14 +366,38 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
358366
359367 override fun onAttachedToWindow() {
360368 super.onAttachedToWindow()
361 if (!destroyed && !lifecycleStarted) { mapView.onStart(); mapView.onResume(); lifecycleStarted = true }
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() }
362374 }
363375
364376 override fun onDetachedFromWindow() {
365 if (lifecycleStarted) { mapView.onPause(); mapView.onStop(); lifecycleStarted = false }
377 lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver)
378 lifecycleOwner = null
379 stopMap()
366380 super.onDetachedFromWindow()
367381 }
368382
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
369401 fun lowMemory() { if (!destroyed) mapView.onLowMemory() }
370402
371403 fun destroy() {
@@ -374,7 +406,9 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
374406 styleVersion++
375407 onMarkerTap = null; onMapPress = null; onRegionChangeComplete = null
376408 markerHandlers.clear(); geometryPoints = emptyList()
377 if (lifecycleStarted) { mapView.onPause(); mapView.onStop(); lifecycleStarted = false }
409 lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver)
410 lifecycleOwner = null
411 stopMap()
378412 mapView.onDestroy()
379413 readyStyle = null; map = null
380414 reportedRegion = null; reportedCamera = null; reportedViewport = null