From d3dc75f02cec03b04846b4be88902d6b4d6c89de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 15 May 2026 21:48:45 +0000 Subject: fix: GPX file handler coverage and track loading after reinstall - AndroidManifest: add application/gpx and file:// intent-filters so Nav appears in the chooser on devices that don't know application/gpx+xml - MainActivity: check file extension as fallback when MIME type is generic - TrackStorage: filter MediaStore fallback query by OWNER_PACKAGE_NAME so the system grants access to our own files without READ_EXTERNAL_STORAGE, which also survives app reinstall (same package name = same ownership) - TrackRepository: add reloadTracks() to force a fresh load from storage - MainViewModel: add reloadPastTracks() called after location permission grant so the first-launch race condition can self-heal https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G --- android-app/app/src/main/AndroidManifest.xml | 16 ++++++++++++++++ .../app/src/main/kotlin/org/terst/nav/MainActivity.kt | 9 +++++++++ .../kotlin/org/terst/nav/track/TrackRepository.kt | 12 ++++++++++++ .../main/kotlin/org/terst/nav/track/TrackStorage.kt | 8 ++++++-- .../src/main/kotlin/org/terst/nav/ui/MainViewModel.kt | 19 +++++++++++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) (limited to 'android-app') diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 7b9a9e4..35f8dd3 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -34,11 +34,27 @@ + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 1bd0b08..82c238e 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -141,6 +141,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private fun handleIncomingGpx(intent: Intent?) { if (intent?.action != Intent.ACTION_VIEW) return val uri = intent.data ?: return + // Accept if the MIME type is gpx-related OR if the URI's last path segment + // ends in .gpx — some Android versions / file managers send application/octet-stream + // for file types they don't recognise. + val mime = intent.type ?: contentResolver.getType(uri) ?: "" + val byExtension = uri.lastPathSegment?.lowercase()?.endsWith(".gpx") == true + if (!mime.contains("gpx", ignoreCase = true) && !byExtension) return lifecycleScope.launch { runCatching { val points = withContext(Dispatchers.IO) { @@ -483,6 +489,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true) { pendingServiceStart = true + // The MediaStore query in init{} may have thrown SecurityException while + // the permission dialog was active. Reload now that the system is settled. + viewModel.reloadPastTracks() } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index 20fc214..3c5c9d5 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -117,6 +117,18 @@ class TrackRepository(context: Context) { _pastTracks.toList() } + /** + * Forces a reload from storage regardless of whether it was loaded before. + * Called after the user grants permissions so the first-launch race condition + * (permission dialog active when init{} fires) doesn't permanently block loading. + */ + suspend fun reloadTracks(): List> = withContext(Dispatchers.IO) { + NavLogger.i("track", "reloadTracks: forcing reload from storage") + pastTracksLoaded = false + _pastTracks.clear() + getPastTracks() + } + /** * Returns all completed tracks as [SavedTrack] wrappers (summary + tacks pre-computed). * Internally delegates to [getPastTracks]; the first call loads from disk. diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt index 2eb8a87..0951a38 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt @@ -165,8 +165,12 @@ class TrackStorage(private val context: Context) { // Do NOT filter by RELATIVE_PATH — equality matching is fragile across // Android versions and devices (trailing-slash normalisation, volume prefix, // etc.). The display-name pattern is specific enough on its own. - val selection = "${MediaStore.Files.FileColumns.DISPLAY_NAME} LIKE ?" - val args = arrayOf("nav_%.gpx") + // Filtering by OWNER_PACKAGE_NAME tells the system we only want our own files. + // Android grants access to a package's own MediaStore entries without any + // storage permission, including after a reinstall with the same package name. + val selection = "${MediaStore.Files.FileColumns.DISPLAY_NAME} LIKE ? AND " + + "${MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME} = ?" + val args = arrayOf("nav_%.gpx", context.packageName) val cursor = runCatching { context.contentResolver.query(baseUri, projection, selection, args, null) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index 579ccdf..52344f2 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -419,6 +419,25 @@ class MainViewModel( } } + /** + * Re-runs the storage load unconditionally. Called after the user grants + * permissions so the first-launch SecurityException race doesn't permanently + * block past track loading. + */ + fun reloadPastTracks() { + viewModelScope.launch { + runCatching { trackRepository.reloadTracks() } + .onSuccess { tracks -> + _pastTracks.value = tracks + _savedTracks.value = trackRepository.getSavedTracks() + NavLogger.i("track", "reloadPastTracks: ${tracks.size} tracks loaded") + } + .onFailure { e -> + NavLogger.e("track", "reloadPastTracks: failed: ${e.javaClass.simpleName}: ${e.message}") + } + } + } + fun panToTrackPoint(lat: Double, lon: Double) { viewModelScope.launch { _panToPosition.emit(lat to lon) } } -- cgit v1.2.3