diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt index 6acedc3dac..e16b2594d4 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -706,7 +706,11 @@ fun MapView( mapType = effectiveGoogleMapType, isMyLocationEnabled = isLocationTrackingEnabled && locationPermission.isGranted, ), - onMapLoaded = { isMapLoaded = true }, + onMapLoaded = { + isMapLoaded = true + // The store-screenshot capture waits for this tag instead of a fixed delay. + Logger.withTag("MapDrawn").d { "tiles drawn" } + }, onMapClick = { latLng -> if (isMainMode && boxAuthoringDraft != null) { val first = boxAuthoringFirstCorner diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt index 7445f3ea58..68bd482664 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt @@ -31,8 +31,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -51,6 +54,7 @@ import org.maplibre.compose.location.updateCamera import org.maplibre.compose.map.CameraConstraints import org.maplibre.compose.map.LocalMapState import org.maplibre.compose.map.LocalViewport +import org.maplibre.compose.map.MapEvent import org.maplibre.compose.map.MapState import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.rememberMapState @@ -228,6 +232,13 @@ fun MeshMap( // thrown by the time this is reached. if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) + // The store-screenshot capture waits for this tag instead of a fixed delay. + LaunchedEffect(mapState) { + // Idle can arrive before the first render session; only an idle after a drawn frame means tiles are on screen. + mapState.events.dropWhile { it !is MapEvent.FrameRendered }.filterIsInstance().first() + Logger.withTag("MapDrawn").d { "tiles drawn" } + } + val zoomRange = basemap.zoomRange() MaplibreMap( modifier = modifier, diff --git a/store-screenshots/src/main/kotlin/org/meshtastic/storescreenshots/StoreScreenshots.kt b/store-screenshots/src/main/kotlin/org/meshtastic/storescreenshots/StoreScreenshots.kt index e9547d95c2..94ddcd6197 100644 --- a/store-screenshots/src/main/kotlin/org/meshtastic/storescreenshots/StoreScreenshots.kt +++ b/store-screenshots/src/main/kotlin/org/meshtastic/storescreenshots/StoreScreenshots.kt @@ -132,7 +132,7 @@ class StoreScreenshots { open(Shot.Nodes.path) SystemClock.sleep(READ_WARM_UP_MS) } - open(shot.path) + if (shot.waitsForMapDrawn) openAndAwaitMapDrawn(shot) else open(shot.path) SystemClock.sleep(shot.minimumWaitMs) val stable = waitForStableInActiveWindow( @@ -150,6 +150,22 @@ class StoreScreenshots { save(bitmap, name) } + /** + * Waits for the map to log that its tiles are drawn: Google Maps' onMapLoaded, MapLibre's first idle. The stability + * check after it still covers the camera settling on the mesh. + */ + private fun UiAutomatorTestScope.openAndAwaitMapDrawn(shot: Shot) { + // A time boundary, not a line count: logcat is a ring buffer and older matches rotate out. + val since = System.currentTimeMillis().let { "%d.%03d".format(it / MILLIS_PER_SECOND, it % MILLIS_PER_SECOND) } + open(shot.path) + val deadline = SystemClock.uptimeMillis() + MAP_DRAWN_TIMEOUT_MS + while (SystemClock.uptimeMillis() < deadline) { + if (MAP_DRAWN_MESSAGE in shell("logcat -d -T $since -s $MAP_DRAWN_TAG")) return + SystemClock.sleep(POLL_MS) + } + Log.w(TAG, "${shot.fileName} never logged $MAP_DRAWN_TAG; capturing after the timeout") + } + /** Launches through the debug build's shell-only alias, the one launch the app honours the switches on. */ private fun UiAutomatorTestScope.open(path: String, clearTask: Boolean = false) { val flags = if (clearTask) "--activity-clear-task " else "" @@ -189,7 +205,7 @@ class StoreScreenshots { TenInch("tenInchScreenshots", 2560, 1440, 320), } - /** The five listing shots, named as fastlane lays them out. The map loads tiles for a while before it settles. */ + /** The five listing shots, named as fastlane lays them out. The map waits for its tiles before it settles. */ private enum class Shot( val fileName: String, val path: String, @@ -197,11 +213,12 @@ class StoreScreenshots { val stableTimeoutMs: Long = 30_000, val stableIntervalMs: Long = 2_000, val readFirst: Boolean = false, + val waitsForMapDrawn: Boolean = false, ) { // The primary channel's contact key, raw: `am start` takes it literally and Uri.parse accepts the caret. Messages("1_messages", "messages/0^all", readFirst = true), Nodes("2_nodes", "nodes"), - Map("3_map", "map", minimumWaitMs = 45_000, stableTimeoutMs = 120_000, stableIntervalMs = 8_000), + Map("3_map", "map", stableTimeoutMs = 120_000, stableIntervalMs = 8_000, waitsForMapDrawn = true), NodeDetail("4_node_detail", "nodes/$RIDGE_TOP_NUM"), Channels("5_channels", "channels"), } @@ -230,6 +247,12 @@ class StoreScreenshots { const val READ_WARM_UP_MS = 3_000L + /** Logged by both flavors' maps (MapView.kt, MeshMap.kt) once the tiles are drawn. */ + const val MAP_DRAWN_TAG = "MapDrawn" + const val MAP_DRAWN_MESSAGE = "tiles drawn" + const val MAP_DRAWN_TIMEOUT_MS = 45_000L + const val MILLIS_PER_SECOND = 1_000L + const val CONNECT_ATTEMPTS = 3 const val CONNECT_TIMEOUT_MS = 60_000L const val POLL_MS = 500L