Skip to content

fix(map): retain camera across tab navigation#6352

Merged
jamesarich merged 2 commits into
meshtastic:mainfrom
CatSu-OSM:bugfix/map-viewport-state
Jul 22, 2026
Merged

fix(map): retain camera across tab navigation#6352
jamesarich merged 2 commits into
meshtastic:mainfrom
CatSu-OSM:bugfix/map-viewport-state

Conversation

@CatSu-OSM

@CatSu-OSM CatSu-OSM commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Switching away from the map recreated provider camera state, causing the map to reopen at a default or unrelated viewport. This retains the last camera across tab navigation while preserving the first-use behavior of fitting discovered nodes.

  • Persist Google and F-Droid camera values atomically instead of treating temporary startup coordinates as saved state.
  • Wait for persisted camera loading before initializing the map, preventing the default ocean-centered camera from winning startup races.
  • Fit positioned nodes only when no camera has previously been saved.
  • Add regression coverage for absent camera state, atomic restoration, and valid zero-valued coordinates.

Testing Performed

  • ./gradlew spotlessCheck detekt assembleDebug :core:prefs:jvmTest --tests org.meshtastic.core.prefs.map.MapPrefsImplTest :androidApp:testFdroidDebugUnitTest :androidApp:testGoogleDebugUnitTest --tests org.meshtastic.app.map.prefs.map.GoogleMapsPrefsTest
  • Verified F-Droid behavior on a physical Android device: pan/zoom, change tabs, and return to the same viewport.
  • The repository-wide spotlessApply spotlessCheck detekt assembleDebug test allTests --continue invocation exceeded the 10-minute local command limit without reporting a map-related failure; the bounded cross-flavor matrix above completed successfully.

Summary by CodeRabbit

  • New Features
    • Map camera position is now restored when reopening the map (including target, zoom, tilt, and bearing where available).
    • When no saved camera exists, the map can automatically fit nodes after map data is ready.
  • Bug Fixes
    • Improved camera initialization timing to avoid applying node bounds before nodes/maps are ready.
    • Camera saving now occurs at the right lifecycle points, including on effect disposal.
  • Tests
    • Added/updated tests to verify camera position persistence and restore behavior, including the “absent before set” case.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d2ff7876-ea48-4bf2-a80f-ec6de3cb79df

📥 Commits

Reviewing files that changed from the base of the PR and between d149c31 and e876be4.

📒 Files selected for processing (4)
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
  • androidApp/src/testGoogle/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefsTest.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • androidApp/src/testGoogle/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefsTest.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt

📝 Walkthrough

Walkthrough

Changes

Map camera persistence

Layer / File(s) Summary
Camera preference contract and storage
core/repository/..., core/prefs/..., core/testing/..., androidApp/src/google/.../GoogleMapsPrefs.kt, androidApp/src/testGoogle/...
Map preferences now persist nullable camera position records atomically, with DataStore implementations, legacy-value handling, fake support, and persistence tests.
Google Maps restoration and fitting
androidApp/src/google/...
Google Maps restores saved camera state, fits node bounds when needed, and persists camera changes only after initialization and map loading complete.
F-Droid camera initialization and updates
androidApp/src/fdroid/...
The F-Droid map waits for camera loading, applies node bounds once, saves camera state during viewport changes, and captures the latest position on disposal.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MapView
  participant MapViewModel
  participant MapPrefs
  participant DataStore
  MapViewModel->>MapPrefs: load camera position
  MapPrefs->>DataStore: read camera preferences
  DataStore-->>MapPrefs: saved camera record or null
  MapPrefs-->>MapViewModel: camera position
  MapView->>MapViewModel: restore or fit node bounds
  MapView->>MapViewModel: save camera changes
  MapViewModel->>MapPrefs: persist camera position
  MapPrefs->>DataStore: atomically write camera preferences
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main user-visible change: preserving map camera state across tab navigation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bugfix PR tag label Jul 22, 2026
@CatSu-OSM
CatSu-OSM marked this pull request as ready for review July 22, 2026 05:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt (2)

125-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the float→double migration rationale.

getCameraCoordinate silently falls back to a floatPreferencesKey of the same name on ClassCastException. This is a legitimate workaround — AndroidX DataStore docs confirm "Using overlapping keys with different types can result in ClassCastException", which is exactly what happens when a previously floatPreferencesKey-backed value is now read via doublePreferencesKey. Without a comment, a future reader is likely to mistake this for dead defensive code and remove it, silently breaking migration for existing installs.

📝 Suggested comment
+    // camera_target_lat/lng were previously stored as Float; read as Double here and fall back to the
+    // legacy Float key so existing installs migrate without losing their saved camera position.
     private fun Preferences.getCameraCoordinate(key: Preferences.Key<Double>): Double? = try {
         this[key]
     } catch (_: ClassCastException) {
         this[floatPreferencesKey(key.name)]?.toDouble()
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt`
around lines 125 - 129, Document the migration rationale directly above
getCameraCoordinate: explain that the ClassCastException fallback reads the same
preference name through floatPreferencesKey to migrate values written by the
previous float-backed implementation, preserving compatibility with existing
installs. Keep the existing fallback behavior unchanged.

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two different classes named MapCameraPosition. This PR introduces org.meshtastic.core.repository.MapCameraPosition (latitude/longitude/zoom) and a same-named but differently-shaped org.meshtastic.app.map.prefs.map.MapCameraPosition (targetLat/targetLng/zoom/tilt/bearing) in the google flavor, which depends on core:repository. Both are simultaneously visible in the google module's classpath, risking autocomplete/import confusion between the two.

  • androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt#L53-59: rename this flavor-specific type (e.g. GoogleCameraPosition) to disambiguate from the shared MapPrefs contract type.
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt#L282-283: no change needed here; kept as the anchor reference for the canonical shared MapCameraPosition name that the google-flavor type collides with.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt`
at line 1, Rename the google-flavor MapCameraPosition type in GoogleMapsPrefs.kt
to a distinct name such as GoogleCameraPosition, and update all constructors,
properties, imports, and references in that flavor implementation accordingly.
Leave the canonical org.meshtastic.core.repository.MapCameraPosition contract
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt`:
- Around line 668-683: Update the boxOverlayListener callbacks in MapView so
saveCameraPosition is persisted only after scrolling or zooming has finished,
using OSMDroid’s movement-state signal (equivalent to gating on isMoving) rather
than saving on every intermediate callback. Keep the existing overlay generation
behavior in onScroll unchanged.

---

Nitpick comments:
In
`@androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt`:
- Around line 125-129: Document the migration rationale directly above
getCameraCoordinate: explain that the ClassCastException fallback reads the same
preference name through floatPreferencesKey to migrate values written by the
previous float-backed implementation, preserving compatibility with existing
installs. Keep the existing fallback behavior unchanged.
- Line 1: Rename the google-flavor MapCameraPosition type in GoogleMapsPrefs.kt
to a distinct name such as GoogleCameraPosition, and update all constructors,
properties, imports, and references in that flavor implementation accordingly.
Leave the canonical org.meshtastic.core.repository.MapCameraPosition contract
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d261360-0a62-4e33-86ad-637cddb7cbe8

📥 Commits

Reviewing files that changed from the base of the PR and between 12a8a38 and d149c31.

📒 Files selected for processing (11)
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
  • androidApp/src/testGoogle/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefsTest.kt
  • core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt
  • core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/map/MapPrefsImplTest.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt

Comment thread androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Solid camera-persistence refactor across both flavors.

The Loading → FitNodes/Restored state machine with the != Restored → return save guard cleanly prevents the initial node-fit from clobbering a restored camera, and gating node-fitting on map-ready (onMapLoaded / map.post {}) avoids the newLatLngBounds layout crash. Nice touch preserving existing installs via the legacy Float→Double ClassCastException fallback, and good coverage on both prefs impls.

Two non-blocking notes:

  • The fdroid MapView early-returns while InitialCameraState.Loading, so there is a blank frame until the camera loads from disk — fine in practice given DataStore is fast.
  • CI is still queued at review time; enabling auto-merge so it lands once checks pass.

@jamesarich
jamesarich enabled auto-merge July 22, 2026 10:54
@jamesarich
jamesarich disabled auto-merge July 22, 2026 12:10
@jamesarich
jamesarich merged commit 88e8e0f into meshtastic:main Jul 22, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants