Skip to content

Merges to keep up to date with v1.0 and test fixes - #965

Merged
davecraig merged 44 commits into
Scottish-Tech-Army:mainfrom
davecraig:main
Aug 1, 2026
Merged

Merges to keep up to date with v1.0 and test fixes#965
davecraig merged 44 commits into
Scottish-Tech-Army:mainfrom
davecraig:main

Conversation

@davecraig

Copy link
Copy Markdown
Contributor

No description provided.

davecraig and others added 26 commits July 9, 2026 15:35
We moved the multiplatform branch to main and so we needed to rejig.
Ignoring the safe mode resulted in parts of the screen being drawn behind
camera slots etc.
TextToSpeech.getVoices() makes a synchronous Binder IPC to the system
TTS service, which can block for a long time on some OEM builds. It
was being called from viewModelScope's default Main dispatcher,
blocking the UI thread and triggering ANRs. Move it and
getAvailableSpeechEngines() to Dispatchers.IO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rror callback

Calling release() synchronously from within OnCompletionListener/OnErrorListener
re-enters MediaPlayer's native teardown while its EventHandler is still dispatching
that very callback, which can hang the main thread waiting on native locks. Posting
the release to the next message-loop iteration breaks the reentrancy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Our own worst-case splash hold time (maxSplashDelay=7000ms) exceeded
Android's 5s "no focused window" input-dispatch ANR timeout, so a tap
during a slow/never-completing splash sound could outlast the system's
patience for the window to become focusable. Cuts the safety net to
3000ms, comfortably under that threshold.

Also replaces the hand-rolled ViewTreeObserver.OnPreDrawListener that
held the splash open by suppressing the content view's first draw with
the platform's own SplashScreen.setKeepOnScreenCondition/
setOnExitAnimationListener APIs, which are coordinated with
WindowManagerService's window-focus bookkeeping rather than working
around it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TileSearch only ever resolved fuzzy hits back to a feature via the
literal "name" tag, so POIs whose matching text lived under name:hi,
name:pa etc. (very common in OSM/OpenMapTiles data) were found by the
fuzzy pass but silently dropped during feature resolution, leaving
non-English searches (e.g. Hindi) empty. Also surface the actual
matched name variant instead of always re-reading the Latin "name"
tag, and stop normalizeForSearch from stripping non-Latin combining
marks (Devanagari matras/virama etc.) that it was only meant to strip
for Latin/Greek/Cyrillic accents.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Photon server only supports en/fr/de + each location's local
language, and getPhotonLanguage() already correctly omits the lang
param for anything else so Photon falls back to local-language
matching - verified this against the live production server for an
Arabic query. There was no test coverage for this behavior at all, so
add a fast unit test for the fallback logic plus an instrumented
end-to-end check against the real server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bring in ANR fixes (splash input dispatch, MediaPlayer.release in
callback, TTS getVoices binder call), offline search fixes for
localized POI names, and doc/version updates from v1.0.

# Conflicts:
#	app/build.gradle.kts
#	app/src/androidTest/java/org/scottishtecharmy/soundscape/GeocoderTest.kt
#	app/src/main/java/org/scottishtecharmy/soundscape/MainActivity.kt
#	app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/SettingsViewModel.kt
#	app/src/test/java/org/scottishtecharmy/soundscape/SearchTest.kt
#	docs/documentationScreens/homeScreen.png
#	docs/documentationScreens/homeScreenWithRoute.png
#	docs/documentationScreens/routeDetails.png
#	docs/documentationScreens/routeEdit.png
#	shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/TileSearch.kt
The main change here is removing the audio beacons being a scrolling list within
the screen and instead just having the screen be bigger an containing the whole
list. This makes Maestro tests much easier as swipping upwards works the same at
any point on the screen.
Rows had been split into separate beacon+icon by mistake at some point. That's fixed.
# Conflicts:
#	shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/onboarding/audiobeacons/AudioBeaconItem.kt
#	shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/onboarding/audiobeacons/AudioBeacons.kt
TileSearch only ever resolved fuzzy hits back to a feature via the
literal "name" tag, so POIs whose matching text lived under name:hi,
name:pa etc. (very common in OSM/OpenMapTiles data) were found by the
fuzzy pass but silently dropped during feature resolution, leaving
non-English searches (e.g. Hindi) empty. Also surface the actual
matched name variant instead of always re-reading the Latin "name"
tag, and stop normalizeForSearch from stripping non-Latin combining
marks (Devanagari matras/virama etc.) that it was only meant to strip
for Latin/Greek/Cyrillic accents.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Photon server only supports en/fr/de + each location's local
language, and getPhotonLanguage() already correctly omits the lang
param for anything else so Photon falls back to local-language
matching - verified this against the live production server for an
Arabic query. There was no test coverage for this behavior at all, so
add a fast unit test for the fallback logic plus an instrumented
end-to-end check against the real server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The main change here is removing the audio beacons being a scrolling list within
the screen and instead just having the screen be bigger an containing the whole
list. This makes Maestro tests much easier as swipping upwards works the same at
any point on the screen.
Rows had been split into separate beacon+icon by mistake at some point. That's fixed.
StreetDescription.getInterpolateLocation() assumed every house number
feature had Point geometry, but building/POI polygons tagged with a
housenumber also get added to the street's house number map. Use
getCentralPointForFeature() (already used elsewhere for this) instead
of casting directly to Point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	app/build.gradle.kts
#	app/src/androidTest/java/org/scottishtecharmy/soundscape/GeocoderTest.kt
#	app/src/test/java/org/scottishtecharmy/soundscape/GeoEngineLanguageTest.kt
#	app/src/test/java/org/scottishtecharmy/soundscape/SearchTest.kt
#	docs/documentationScreens/homeScreen.png
#	docs/documentationScreens/homeScreenWithRoute.png
#	docs/documentationScreens/routeDetails.png
#	docs/documentationScreens/routeEdit.png
#	shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/StreetDescription.kt
#	shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/TileSearch.kt
#	shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/onboarding/audiobeacons/AudioBeacons.kt
davecraig and others added 3 commits July 31, 2026 11:58
AGP 9.2+ throws a ClassCastException on shared/'s classic
com.android.library + androidTarget() setup (sourceSets["main"] no
longer casts to AndroidLibrarySourceSet). Migrate shared/ to AGP 9's
native com.android.kotlin.multiplatform.library plugin with
kotlin.androidLibrary{}, matching the pattern already used by the
ComposePreference/preference submodule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…anded

protobuf-gradle-plugin 0.10.0 and KSP 2.3.10 both added AGP 9 support
since these flags were added. Also needed: bump com.jaredsburrows.license
0.9.8 -> 0.9.9 (was calling into the legacy AppExtension type removed by
android.newDsl's new default), and drop the now-redundant
org.jetbrains.kotlin.android plugin from app/ (AGP 9's built-in Kotlin
support refuses to coexist with it).

Regenerates open_source_licenses.json, which is more complete than
before (262 -> 333 entries).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bumps ~30 patch/minor dependencies plus three deliberate major-version
jumps (Wire, reorderable, JUnit Jupiter) and compose-bom. MapLibre and
kable are capped below their latest releases - see inline comments in
libs.versions.toml for why (MapLibre 13.4.0+ statically links its STL,
conflicting with this app's own native code; kable 0.43.1+ requires a
newer Kotlin KLIB ABI than this project's Kotlin version supports).

Also removes 10 unused libraries, 8 orphaned version keys, and 2 unused
plugins (kotlin-serialization, google-protobuf - the latter was never
actually applied anywhere; Wire already handles this project's protobuf
codegen for vector tiles).

shared's compileSdk bumped 36 -> 37 to satisfy lifecycle-viewmodel-compose-android 2.11.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
davecraig and others added 2 commits July 31, 2026 15:12
… version

The patch was pinning the submodule's android-gradlePlugin to 9.0.1 to
match what root used to be pinned to, before this session's AGP
migration bumped root to 9.3.0. On a fresh checkout the patch reapplies
unconditionally, so CI hit "Using multiple versions of the Android
Gradle plugin (9.0.1, 9.3.0) in the same build is not allowed" resolving
me.zhanghai.compose.preference:preference. Locally this was masked
because the submodule's .patched marker already existed, so the patch
never re-ran.

Regenerated the patch to upgrade the submodule's AGP to 9.3.0 instead.
Also drops the now-unnecessary kotlin downgrade hunk, since the
submodule's own upstream default (2.3.10) already matches root.

Verified: git apply --check passes, simulated a fresh checkout by
deleting .patched and resyncing, and directly resolved
:app:debugScreenshotTestCompileClasspath to confirm the dependency
substitution now works.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause: maplibre-compose#726 (upstream, still open) - MapLibre's
native multi-touch gesture detector and Compose's own scroll gesture
detection both race for the same touch stream on Android, causing
MotionEvents to get dropped/misrouted between them ("Some MotionEvents
were not passed to the library or events from different view trees are
merged").

Add an onInteractionChanged callback to MapContainerLibre (threaded
through PlatformMapContainer's expect/actual) that fires only on
genuine multi-touch (2+ pointers) over the map. The five screens that
embed the map inline inside a scrollable container use this to disable
that container's scrolling for the duration of the gesture, so only
one system owns the touch stream at a time. Single-finger drags are
deliberately excluded - the map's own pan is already off
(GestureOptions.ZoomOnly), so those fall through to the parent scroll
as before.

Verified pinch-to-zoom on a physical device; confirmed no regression to
single-finger scroll-through or map panning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real API migrations:
- TabRow -> PrimaryTabRow (SharedNavGraph.kt)
- invisibleToUser() -> hideFromAccessibility() (3 call sites)
- MarkdownParser/buildMarkdownTreeFromString -> non-deprecated
  CancellationToken/CharSequence overloads
- Locale(String) -> Locale.forLanguageTag(String) - the input is always
  a BCP-47 tag (getCurrentLocale().toLanguageTag())
- launch(context: Job) anti-pattern -> capture the Job launch() returns
  instead of pre-creating one and passing it in (2 call sites) - passing
  a Job breaks structured concurrency
- resourceConfigurations -> androidResources.localeFilters
- Compose Multiplatform compose.* accessors -> explicit version-catalog
  entries (materialIconsExtended stays pinned at 1.7.3 upstream; full
  migration to Material Symbols is a separate, larger effort)
- shared/build.gradle.kts: androidLibrary {} -> android {}. Empirically
  re-verified given this reverses an earlier finding this session -
  full clean build (Android + iOS + tests) confirms android {} is
  correct now and the deprecation warning is gone.

Deliberately not migrated: BackHandler -> NavigationEventHandler is
suppressed rather than migrated, since current Compose Multiplatform
only wires the dispatcher BackHandler needs internally - migrating can
break back handling on iOS, which this app targets.

Real gap found via the commonTest warning, not just cosmetic: the 8
test files in shared/src/commonTest (66 tests) were only ever compiled
in CI, never executed - no Android host test task existed, and the
iOS CI job only compiles its test target. Added withHostTest {} to
shared's android {} block, verified all 66 tests pass on first real
run, and updated run-tests.yaml to invoke :shared:testAndroidHostTest
so CI actually exercises them going forward.

Also silences two CI-environment-only warnings: disabled Kotlin/Native
targets on Linux runners (expected, not an error) and the
enableScreenshotTest experimental-flag notice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Listening onboarding screen's content lives in a verticalScroll
container. On CI's smaller emulator profile, "Continue" (at the
bottom) can end up scrolled out of the viewport, failing
assertIsDisplayed() even though the node exists - it's just not
currently visible. Reproduced by comparing against a physical device
(passes there) vs the CI failure log. Scroll each node into view via
performScrollTo() before asserting, which is a no-op when already
visible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@davecraig davecraig changed the title Merges to keep up to date with v1.0 Merges to keep up to date with v1.0 and test fixes Aug 1, 2026
@davecraig
davecraig merged commit 3b27621 into Scottish-Tech-Army:main Aug 1, 2026
7 checks passed
davecraig added a commit to davecraig/Soundscape-AndroidTest that referenced this pull request Aug 1, 2026
…y#965)

* Update docs on branches and github actions

We moved the multiplatform branch to main and so we needed to rejig.

* More docs updating for branch changes

* Reinstate safe mode for iOS screens

Ignoring the safe mode resulted in parts of the screen being drawn behind
camera slots etc.

* Debugging iOS runner

* Bump version to 1.1.2, version code 204

* New documentation screenshots

* Fix main-thread ANR from TTS getVoices() binder call

TextToSpeech.getVoices() makes a synchronous Binder IPC to the system
TTS service, which can block for a long time on some OEM builds. It
was being called from viewModelScope's default Main dispatcher,
blocking the UI thread and triggering ANRs. Move it and
getAvailableSpeechEngines() to Dispatchers.IO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix ANR from MediaPlayer.release() called inside its own completion/error callback

Calling release() synchronously from within OnCompletionListener/OnErrorListener
re-enters MediaPlayer's native teardown while its EventHandler is still dispatching
that very callback, which can hang the main thread waiting on native locks. Posting
the release to the next message-loop iteration breaks the reentrancy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix ANR: "Input dispatching timed out (No focused window)" during splash

Our own worst-case splash hold time (maxSplashDelay=7000ms) exceeded
Android's 5s "no focused window" input-dispatch ANR timeout, so a tap
during a slow/never-completing splash sound could outlast the system's
patience for the window to become focusable. Cuts the safety net to
3000ms, comfortably under that threshold.

Also replaces the hand-rolled ViewTreeObserver.OnPreDrawListener that
held the splash open by suppressing the content view's first draw with
the platform's own SplashScreen.setKeepOnScreenCondition/
setOnExitAnimationListener APIs, which are coordinated with
WindowManagerService's window-focus bookkeeping rather than working
around it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Bump version to 1.1.3, version code 205

* New documentation screenshots

* Fix offline search for localized (name:xx) POI names

TileSearch only ever resolved fuzzy hits back to a feature via the
literal "name" tag, so POIs whose matching text lived under name:hi,
name:pa etc. (very common in OSM/OpenMapTiles data) were found by the
fuzzy pass but silently dropped during feature resolution, leaving
non-English searches (e.g. Hindi) empty. Also surface the actual
matched name variant instead of always re-reading the Latin "name"
tag, and stop normalizeForSearch from stripping non-Latin combining
marks (Devanagari matras/virama etc.) that it was only meant to strip
for Latin/Greek/Cyrillic accents.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add test coverage for Photon search language fallback (Arabic etc.)

The Photon server only supports en/fr/de + each location's local
language, and getPhotonLanguage() already correctly omits the lang
param for anything else so Photon falls back to local-language
matching - verified this against the live production server for an
Arabic query. There was no test coverage for this behavior at all, so
add a fast unit test for the fallback logic plus an instrumented
end-to-end check against the real server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Improve audio beacons onboarding screen

The main change here is removing the audio beacons being a scrolling list within
the screen and instead just having the screen be bigger an containing the whole
list. This makes Maestro tests much easier as swipping upwards works the same at
any point on the screen.
Rows had been split into separate beacon+icon by mistake at some point. That's fixed.

* Fix offline search for localized (name:xx) POI names

TileSearch only ever resolved fuzzy hits back to a feature via the
literal "name" tag, so POIs whose matching text lived under name:hi,
name:pa etc. (very common in OSM/OpenMapTiles data) were found by the
fuzzy pass but silently dropped during feature resolution, leaving
non-English searches (e.g. Hindi) empty. Also surface the actual
matched name variant instead of always re-reading the Latin "name"
tag, and stop normalizeForSearch from stripping non-Latin combining
marks (Devanagari matras/virama etc.) that it was only meant to strip
for Latin/Greek/Cyrillic accents.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add test coverage for Photon search language fallback (Arabic etc.)

The Photon server only supports en/fr/de + each location's local
language, and getPhotonLanguage() already correctly omits the lang
param for anything else so Photon falls back to local-language
matching - verified this against the live production server for an
Arabic query. There was no test coverage for this behavior at all, so
add a fast unit test for the fallback logic plus an instrumented
end-to-end check against the real server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Improve audio beacons onboarding screen

The main change here is removing the audio beacons being a scrolling list within
the screen and instead just having the screen be bigger an containing the whole
list. This makes Maestro tests much easier as swipping upwards works the same at
any point on the screen.
Rows had been split into separate beacon+icon by mistake at some point. That's fixed.

* Bump version to 1.1.4, version code 206

* New documentation screenshots

* Bump version to 1.1.5, version code 207

* Fix ClassCastException when interpolating polygon-geometry house numbers

StreetDescription.getInterpolateLocation() assumed every house number
feature had Point geometry, but building/POI polygons tagged with a
housenumber also get added to the street's house number map. Use
getCentralPointForFeature() (already used elsewhere for this) instead
of casting directly to Point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Bump version to 1.1.6, version code 208

* New documentation screenshots

* Bump compileSdk and targetSdk to 37

* Bump version to 1.1.7, version code 209

* New documentation screenshots

* Give each offline map download a unique filename to avoid a MapLibre native crash

MapLibre's native PMTilesFileSource caches parsed header/directory data
per pmtiles://file://<path> URL for as long as the process runs, with no
API to invalidate it (platform/default/src/mbgl/storage/pmtiles_file_source.cpp
in maplibre-native - header_cache, metadata_cache, directory_cache are
all keyed by URL and never evicted). The previous OfflineDownloader
re-downloaded ("Update") an extract straight over its existing filename,
so once a live map had opened that URL, the next tile request read new
bytes through offsets cached from the old file - misreading garbage,
failing to decompress, and aborting the whole process with no try/catch
on MapLibre's worker thread. This crashed in production on release
1.1.7.

Fixed by downloading each extract to a versioned filename
("<region>.v<timestamp>.pmtiles") instead of overwriting the existing
one, so a re-download never reuses a URL MapLibre has cached anything
for. OfflineDownloader deletes the superseded version's .pmtiles file
and .geojson sidecar once the new one is published and validated - safe
to do immediately, since a reader either still has the old file open
(POSIX keeps it readable) or gets a clean "not found" on a fresh open,
both already handled gracefully by MapLibre, unlike reading
wrong-but-present bytes. delete() now matches on the region's logical
basename (anchored on the following '.') so it still removes every
version of an extract, including ones downloaded before this change.

Also drops the redundant finalFile.delete() before the final rename -
harmless either way now that the destination path is always new, but
one less non-atomic step.

PmtilesSwapCrashTest reproduces the original crash directly (confirmed
against a real emulator - SIGABRT in mbgl::util::decompress on the
PMTilesFileSource worker thread, "incorrect header check") and is
@ignore'd by default since a deliberate process crash can't be expressed
as a JUnit assertion; see its KDoc for how to run it manually.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Refresh the geoengine's tile grid immediately when offline maps change

Previously a newly downloaded (or deleted) offline map extract was only
picked up by ProtomapsGridState's next checkOfflineMaps() call, which
itself only runs when the user's location moves outside the current
grid's central area - so a completed download or deletion had no effect
until the user happened to walk far enough. GridState now exposes
refreshOfflineMaps(), which forces the next location update to recompute
the grid regardless of movement; this is wired through GeoEngine,
SoundscapeService and SoundscapeServiceConnection and called from
OfflineMapsViewModel whenever a download succeeds or an extract is
deleted, so both the main grid and the settlement grid start using
(or stop using) the affected extract within about a second.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Discard offline search's cached tile strings when offline maps change

TileSearch.stringCache caches each tile's normalized search strings
keyed only by tile (x, y), independent of which extract the data was
read from. Since the search spiral re-visits the same tiles on repeat
searches near the same location, a superseded or deleted extract's
strings for those tiles were never evicted, so search results could
stay stale indefinitely after an offline map was replaced or removed.
refreshOfflineMaps() now clears this cache too, wired in alongside the
grid refresh already triggered when a download succeeds or an extract
is deleted. stringCache is now a ConcurrentHashMap since it can be
cleared from a different thread than an in-progress search() call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Increase photon client timeout to 10 seconds

I was seeing Search timeout with the previous 5 seconds setting, so increase it
to 10.

* Retry adb root in Maestro CI to survive intermittent adbd restart race

adb root occasionally fails with "unable to connect for root: closed"
right after the emulator boots, which was killing the whole matrix leg
since each script line runs in its own shell.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Widen Maestro launch-retry to catch device-offline adbd race

launchApp's own adb call can hit the same adbd-restart race already
retried for adb root, but it surfaces as "device offline" instead of
"Unable to launch app" - the rf() helper's grep only matched the
latter, so this failure mode skipped the retry and failed the whole
run on the first flow (seen consistently on API 36 + small_phone +
en-US).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Bump Gradle version to support SDK 37

* Bump AGP to 9.3.0, Gradle to 9.6.1, Kotlin to 2.3.10

AGP 9.2+ throws a ClassCastException on shared/'s classic
com.android.library + androidTarget() setup (sourceSets["main"] no
longer casts to AndroidLibrarySourceSet). Migrate shared/ to AGP 9's
native com.android.kotlin.multiplatform.library plugin with
kotlin.androidLibrary{}, matching the pattern already used by the
ComposePreference/preference submodule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Drop android.newDsl/builtInKotlin workarounds now AGP 9 support has landed

protobuf-gradle-plugin 0.10.0 and KSP 2.3.10 both added AGP 9 support
since these flags were added. Also needed: bump com.jaredsburrows.license
0.9.8 -> 0.9.9 (was calling into the legacy AppExtension type removed by
android.newDsl's new default), and drop the now-redundant
org.jetbrains.kotlin.android plugin from app/ (AGP 9's built-in Kotlin
support refuses to coexist with it).

Regenerates open_source_licenses.json, which is more complete than
before (262 -> 333 entries).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Bump straightforward dependencies, remove unused version catalog entries

Bumps ~30 patch/minor dependencies plus three deliberate major-version
jumps (Wire, reorderable, JUnit Jupiter) and compose-bom. MapLibre and
kable are capped below their latest releases - see inline comments in
libs.versions.toml for why (MapLibre 13.4.0+ statically links its STL,
conflicting with this app's own native code; kable 0.43.1+ requires a
newer Kotlin KLIB ABI than this project's Kotlin version supports).

Also removes 10 unused libraries, 8 orphaned version keys, and 2 unused
plugins (kotlin-serialization, google-protobuf - the latter was never
actually applied anywhere; Wire already handles this project's protobuf
codegen for vector tiles).

shared's compileSdk bumped 36 -> 37 to satisfy lifecycle-viewmodel-compose-android 2.11.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix stale ComposePreference patch downgrading AGP below root's pinned version

The patch was pinning the submodule's android-gradlePlugin to 9.0.1 to
match what root used to be pinned to, before this session's AGP
migration bumped root to 9.3.0. On a fresh checkout the patch reapplies
unconditionally, so CI hit "Using multiple versions of the Android
Gradle plugin (9.0.1, 9.3.0) in the same build is not allowed" resolving
me.zhanghai.compose.preference:preference. Locally this was masked
because the submodule's .patched marker already existed, so the patch
never re-ran.

Regenerated the patch to upgrade the submodule's AGP to 9.3.0 instead.
Also drops the now-unnecessary kotlin downgrade hunk, since the
submodule's own upstream default (2.3.10) already matches root.

Verified: git apply --check passes, simulated a fresh checkout by
deleting .patched and resyncing, and directly resolved
:app:debugScreenshotTestCompileClasspath to confirm the dependency
substitution now works.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix unreliable pinch-to-zoom on maps embedded in scrollable screens

Root cause: maplibre-compose#726 (upstream, still open) - MapLibre's
native multi-touch gesture detector and Compose's own scroll gesture
detection both race for the same touch stream on Android, causing
MotionEvents to get dropped/misrouted between them ("Some MotionEvents
were not passed to the library or events from different view trees are
merged").

Add an onInteractionChanged callback to MapContainerLibre (threaded
through PlatformMapContainer's expect/actual) that fires only on
genuine multi-touch (2+ pointers) over the map. The five screens that
embed the map inline inside a scrollable container use this to disable
that container's scrolling for the duration of the gesture, so only
one system owns the touch stream at a time. Single-finger drags are
deliberately excluded - the map's own pan is already off
(GestureOptions.ZoomOnly), so those fall through to the parent scroll
as before.

Verified pinch-to-zoom on a physical device; confirmed no regression to
single-finger scroll-through or map panning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix deprecation warnings from the screenshot test build

Real API migrations:
- TabRow -> PrimaryTabRow (SharedNavGraph.kt)
- invisibleToUser() -> hideFromAccessibility() (3 call sites)
- MarkdownParser/buildMarkdownTreeFromString -> non-deprecated
  CancellationToken/CharSequence overloads
- Locale(String) -> Locale.forLanguageTag(String) - the input is always
  a BCP-47 tag (getCurrentLocale().toLanguageTag())
- launch(context: Job) anti-pattern -> capture the Job launch() returns
  instead of pre-creating one and passing it in (2 call sites) - passing
  a Job breaks structured concurrency
- resourceConfigurations -> androidResources.localeFilters
- Compose Multiplatform compose.* accessors -> explicit version-catalog
  entries (materialIconsExtended stays pinned at 1.7.3 upstream; full
  migration to Material Symbols is a separate, larger effort)
- shared/build.gradle.kts: androidLibrary {} -> android {}. Empirically
  re-verified given this reverses an earlier finding this session -
  full clean build (Android + iOS + tests) confirms android {} is
  correct now and the deprecation warning is gone.

Deliberately not migrated: BackHandler -> NavigationEventHandler is
suppressed rather than migrated, since current Compose Multiplatform
only wires the dispatcher BackHandler needs internally - migrating can
break back handling on iOS, which this app targets.

Real gap found via the commonTest warning, not just cosmetic: the 8
test files in shared/src/commonTest (66 tests) were only ever compiled
in CI, never executed - no Android host test task existed, and the
iOS CI job only compiles its test target. Added withHostTest {} to
shared's android {} block, verified all 66 tests pass on first real
run, and updated run-tests.yaml to invoke :shared:testAndroidHostTest
so CI actually exercises them going forward.

Also silences two CI-environment-only warnings: disabled Kotlin/Native
targets on Linux runners (expected, not an error) and the
enableScreenshotTest experimental-flag notice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix flaky ListeningScreenTest on small-screen CI emulators

The Listening onboarding screen's content lives in a verticalScroll
container. On CI's smaller emulator profile, "Continue" (at the
bottom) can end up scrolled out of the viewport, failing
assertIsDisplayed() even though the node exists - it's just not
currently visible. Reproduced by comparing against a physical device
(passes there) vs the CI failure log. Scroll each node into view via
performScrollTo() before asserting, which is a no-op when already
visible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: davecraig <8530624+davecraig@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant