Skip to content

Conversation

@torlando-tech
Copy link
Owner

@torlando-tech torlando-tech commented Jan 28, 2026

Summary

  • Offline maps now cache the style JSON to a local file during download and load it via Style.Builder().fromJson() when offline, instead of relying on MapLibre's HTTP cache which expires after days.
  • Verified on-device with full vector tile detail (roads, buildings, labels) rendering in offline mode.

Changes

DB Schema (Plan 01 — write path)

  • Migration 34→35: add localStylePath column to offline_map_regions
  • DAO: updateLocalStylePath(), getFirstCompletedRegionWithLocalStyle()
  • Repository: updateLocalStylePath(), getFirstCompletedRegionWithStyle()
  • fetchAndCacheStyleJson() in OfflineMapDownloadViewModel — fetches style JSON after download completes, saves to filesDir/offline_styles/{regionId}.json, non-blocking with 5s timeout

Style Loading (Plan 02 — read path)

  • Add MapStyleResult.OfflineWithLocalStyle sealed class variant
  • getMapStyle() checks for cached style file before falling back to HTTP URL
  • Both when(styleResult) blocks in MapScreen handle OfflineWithLocalStyle via fromJson()
  • Network connectivity disabled for offline local styles

Backward Compatibility

  • Existing regions without cached style JSON gracefully fall back to HTTP URL
  • Online mode completely unaffected
  • Safe nullable column migration (no data transformation)

Test plan

  • All 5,398 unit tests pass (no regressions)
  • On-device: downloaded offline region, disabled HTTP, confirmed full vector tile rendering
  • Logcat confirmed: "Cached style JSON for region" (write path)
  • Logcat confirmed: "Using offline maps with local style JSON" (read path)
  • Logcat confirmed: "Applying style: OfflineWithLocalStyle" and "Style loaded (from LaunchedEffect): OfflineWithLocalStyle"
  • Extended offline test: leave device in airplane mode for multiple days, recheck map

Closes #354

🤖 Generated with Claude Code

torlando-tech and others added 12 commits January 27, 2026 22:49
Phase 02.2: Offline Map Tile Rendering
- Identified root cause: HTTP style cache expiration after extended offline
- Current code uses same HTTP URL for online/offline, relies on MapLibre cache
- MapLibre respects HTTP cache headers, style JSON expires after days
- Solution: Generate and cache local style JSON during download
- Use Style.Builder().fromJson() for offline, not fromUri()
- Requires database schema update for localStylePath storage
- HIGH confidence on all findings
Phase 02.2: Offline Map Tile Rendering
- 2 plan(s) in 2 wave(s)
- 1 parallel (wave 1), 1 sequential (wave 2)
- Ready for execution
- Add explicit repository implementation for getFirstCompletedRegionWithStyle()
  wrapping offlineMapRegionDao.getFirstCompletedRegionWithLocalStyle()?.toOfflineMapRegion()
- Reframe must_haves.truths from implementation-focused to user-observable outcomes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2.2 inserted: Offline Map Tile Rendering (#354)
- Updated current focus to Phase 2.2
- Added roadmap evolution entry with root cause summary
- Updated next step pointer

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add localStylePath column to OfflineMapRegionEntity (nullable String)
- Create database migration 34->35 adding localStylePath column
- Add DAO methods: updateLocalStylePath() and getFirstCompletedRegionWithLocalStyle()
- Add repository methods wrapping DAO: updateLocalStylePath() and getFirstCompletedRegionWithStyle()
- Update OfflineMapRegion domain model with localStylePath field
- Add localStylePath to entity->domain mapping in toOfflineMapRegion()

This enables storing the path to locally cached style JSON files for offline map rendering.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add fetchAndCacheStyleJson() method to OfflineMapDownloadViewModel
- Fetch style JSON from MapTileSourceManager.DEFAULT_STYLE_URL after download completes
- Save style JSON to filesDir/offline_styles/{regionId}.json
- Persist local file path to database via updateLocalStylePath()
- Call fetchAndCacheStyleJson() in onComplete callback after markCompleteWithMaplibreId()
- Add withContext import for IO dispatcher context switching
- Style caching failure is non-fatal: logged as warning, download still marked complete

This ensures offline maps have persistent style JSON that survives HTTP cache expiration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move fetchAndCacheStyleJson() to separate coroutine (non-blocking)
- Add 5-second timeout to URL fetch to prevent infinite hangs
- Update state to isComplete before launching style fetch
- Add mock for updateLocalStylePath in OfflineMapDownloadViewModelTest
- Update test comment to reflect non-blocking behavior

This ensures the download completion UI update happens immediately while style
caching happens asynchronously in the background without blocking.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tasks completed: 2/2
- Add localStylePath to database schema and data layer
- Fetch and cache style JSON during offline map download

SUMMARY: .planning/phases/02.2-offline-map-tile-rendering/02.2-01-SUMMARY.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…olution

- Add MapStyleResult.OfflineWithLocalStyle variant with localStylePath
- Check for local style JSON in getMapStyle() hasOffline branch
- Verify file existence before using local style
- Fallback to HTTP URL if no local style exists (backward compatibility)
- Add OfflineWithLocalStyle handler to both when(styleResult) blocks
- Load style JSON from local file using Style.Builder().fromJson()
- Network connectivity automatically disabled (allowNetwork = false)
- Both initial style loading and style updates now support local style JSON
Tasks completed: 3/3
- Add OfflineWithLocalStyle variant and update style resolution
- Handle OfflineWithLocalStyle in MapScreen
- Human verification checkpoint (approved)

SUMMARY: .planning/phases/02.2-offline-map-tile-rendering/02.2-02-SUMMARY.md

Phase 02.2 (Offline Map Tile Rendering) is now COMPLETE.
Issue #354 resolved: offline maps render full detail indefinitely.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@torlando-tech torlando-tech changed the title fix: preserve contacts on clear announces + fix offline map rendering fix: offline map rendering after extended offline period (#354) Jan 28, 2026
@sentry
Copy link

sentry bot commented Jan 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 28, 2026

Greptile Overview

Greptile Summary

Fixes offline map rendering after extended offline periods by caching style JSON locally during download and loading it via Style.Builder().fromJson() instead of relying on MapLibre's HTTP cache which expires.

What Changed:

  • Write path: OfflineMapDownloadViewModel.fetchAndCacheStyleJson() downloads style JSON after map download completes, saves to filesDir/offline_styles/{regionId}.json, stores path in database
  • Read path: MapTileSourceManager checks for cached style file first, returns new MapStyleResult.OfflineWithLocalStyle variant
  • UI handling: MapScreen loads cached style via fromJson() with try-catch fallback to HTTP URL
  • Database: Migration 34→35 adds nullable localStylePath column to offline_map_regions table

Strengths:

  • Excellent backward compatibility: existing regions without cached style gracefully fall back to HTTP URL
  • Non-blocking style caching with 5s timeout prevents hanging downloads
  • Comprehensive error handling with try-catch blocks in MapScreen.kt:335 and MapScreen.kt:434 (previously flagged comments have been addressed)
  • All 5,398 unit tests pass with no regressions
  • Safe nullable column migration with no data transformation
  • Online mode completely unaffected

Issue (already flagged):

  • OfflineMapDownloadViewModel.kt:697 uses blocking java.net.URL.readText() on IO dispatcher (already noted in previous review threads, acceptable given 5s timeout and non-blocking launch)

Confidence Score: 4.5/5

  • This PR is safe to merge with minimal risk - the fix is well-architected with comprehensive error handling and backward compatibility
  • Score reflects excellent implementation quality: safe DB migration, comprehensive error handling, non-blocking async operations, and thorough testing (5,398 tests pass). Deducted 0.5 points for using blocking I/O in fetchAndCacheStyleJson(), though this is mitigated by the 5s timeout and non-blocking launch pattern.
  • No files require special attention - all previously flagged issues (try-catch blocks) have been addressed

Important Files Changed

Filename Overview
data/src/main/java/com/lxmf/messenger/data/di/DatabaseModule.kt Added migration 34→35 to add nullable localStylePath column to offline_map_regions table. Migration is safe and includes formatting cleanup.
app/src/main/java/com/lxmf/messenger/viewmodel/OfflineMapDownloadViewModel.kt Added fetchAndCacheStyleJson() to download style JSON after map download completes. Uses blocking I/O with 5s timeout (already flagged in previous threads).
app/src/main/java/com/lxmf/messenger/map/MapTileSourceManager.kt Added OfflineWithLocalStyle variant to MapStyleResult and updated getMapStyle() to check for cached style JSON before falling back to HTTP.
app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt Added handling for OfflineWithLocalStyle in both style loading blocks with try-catch error handling and HTTP URL fallback.

Sequence Diagram

sequenceDiagram
    participant User
    participant MapScreen
    participant MapTileSourceManager
    participant OfflineMapRegionRepository
    participant Database
    participant FileSystem
    
    Note over User,FileSystem: Write Path: During Offline Map Download
    User->>OfflineMapDownloadViewModel: Download offline map region
    OfflineMapDownloadViewModel->>MapLibre: Download tiles
    MapLibre-->>OfflineMapDownloadViewModel: Download complete
    OfflineMapDownloadViewModel->>Database: markCompleteWithMaplibreId()
    OfflineMapDownloadViewModel->>OfflineMapDownloadViewModel: fetchAndCacheStyleJson() (async)
    OfflineMapDownloadViewModel->>FileSystem: URL.readText() style JSON (5s timeout)
    FileSystem-->>OfflineMapDownloadViewModel: Style JSON content
    OfflineMapDownloadViewModel->>FileSystem: Write to filesDir/offline_styles/{regionId}.json
    OfflineMapDownloadViewModel->>Database: updateLocalStylePath()
    
    Note over User,FileSystem: Read Path: Viewing Map After Extended Offline Period
    User->>MapScreen: View map (offline, HTTP cache expired)
    MapScreen->>MapTileSourceManager: getMapStyle()
    MapTileSourceManager->>OfflineMapRegionRepository: getFirstCompletedRegionWithStyle()
    OfflineMapRegionRepository->>Database: Query for completed region with localStylePath
    Database-->>OfflineMapRegionRepository: OfflineMapRegion with localStylePath
    OfflineMapRegionRepository-->>MapTileSourceManager: Region with style path
    MapTileSourceManager->>FileSystem: Check if file exists
    FileSystem-->>MapTileSourceManager: File exists
    MapTileSourceManager-->>MapScreen: MapStyleResult.OfflineWithLocalStyle(path)
    MapScreen->>FileSystem: File(path).readText() in try-catch
    FileSystem-->>MapScreen: Style JSON content
    MapScreen->>MapLibre: Style.Builder().fromJson(styleJson)
    MapLibre-->>User: Map renders with full vector tile detail
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 335 to 338
is MapStyleResult.OfflineWithLocalStyle -> {
val styleJson = java.io.File(styleResult.localStylePath).readText()
Style.Builder().fromJson(styleJson)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

missing try-catch around File.readText() - if file is corrupted or deleted, app will crash

Suggested change
is MapStyleResult.OfflineWithLocalStyle -> {
val styleJson = java.io.File(styleResult.localStylePath).readText()
Style.Builder().fromJson(styleJson)
}
is MapStyleResult.OfflineWithLocalStyle -> {
try {
val styleJson = java.io.File(styleResult.localStylePath).readText()
Style.Builder().fromJson(styleJson)
} catch (e: Exception) {
Log.e("MapScreen", "Failed to read cached style JSON, falling back to HTTP", e)
Style.Builder().fromUri(MapTileSourceManager.DEFAULT_STYLE_URL)
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt
Line: 335:338

Comment:
missing try-catch around `File.readText()` - if file is corrupted or deleted, app will crash

```suggestion
                is MapStyleResult.OfflineWithLocalStyle -> {
                    try {
                        val styleJson = java.io.File(styleResult.localStylePath).readText()
                        Style.Builder().fromJson(styleJson)
                    } catch (e: Exception) {
                        Log.e("MapScreen", "Failed to read cached style JSON, falling back to HTTP", e)
                        Style.Builder().fromUri(MapTileSourceManager.DEFAULT_STYLE_URL)
                    }
                }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 429 to 432
is MapStyleResult.OfflineWithLocalStyle -> {
val styleJson = java.io.File(styleResult.localStylePath).readText()
Style.Builder().fromJson(styleJson)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

missing try-catch around File.readText() - same issue as line 335-338

Suggested change
is MapStyleResult.OfflineWithLocalStyle -> {
val styleJson = java.io.File(styleResult.localStylePath).readText()
Style.Builder().fromJson(styleJson)
}
is MapStyleResult.OfflineWithLocalStyle -> {
try {
val styleJson = java.io.File(styleResult.localStylePath).readText()
Style.Builder().fromJson(styleJson)
} catch (e: Exception) {
Log.e("MapScreen", "Failed to read cached style JSON, falling back to HTTP", e)
Style.Builder().fromUri(MapTileSourceManager.DEFAULT_STYLE_URL)
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt
Line: 429:432

Comment:
missing try-catch around `File.readText()` - same issue as line 335-338

```suggestion
                                is MapStyleResult.OfflineWithLocalStyle -> {
                                    try {
                                        val styleJson = java.io.File(styleResult.localStylePath).readText()
                                        Style.Builder().fromJson(styleJson)
                                    } catch (e: Exception) {
                                        Log.e("MapScreen", "Failed to read cached style JSON, falling back to HTTP", e)
                                        Style.Builder().fromUri(MapTileSourceManager.DEFAULT_STYLE_URL)
                                    }
                                }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +697 to +700
val styleJson =
kotlinx.coroutines.withTimeout(5000) {
java.net.URL(MapTileSourceManager.DEFAULT_STYLE_URL).readText()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

java.net.URL.readText() blocks the IO dispatcher - for large style JSONs, this could cause ANRs. Consider using OkHttp or similar for proper async I/O.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/viewmodel/OfflineMapDownloadViewModel.kt
Line: 697:700

Comment:
`java.net.URL.readText()` blocks the IO dispatcher - for large style JSONs, this could cause ANRs. Consider using OkHttp or similar for proper async I/O.

How can I resolve this? If you propose a fix, please make it concise.

Prevents crash if cached style file is corrupted or deleted between
the existence check in getMapStyle() and readText() in MapScreen.
Falls back to HTTP style URL on failure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@torlando-tech
Copy link
Owner Author

@greptileai

The app module has sentry/noSentry product flavors, so testDebugUnitTest
no longer matches any app task. Add testNoSentryDebugUnitTest to run
the ~5,398 app-level unit tests that were silently skipped.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@torlando-tech torlando-tech merged commit 16b710b into main Jan 28, 2026
9 checks passed
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.

Offline map do not show boundaries and itself after some offline days

2 participants