diff options
Diffstat (limited to 'android-app')
5 files changed, 62 insertions, 2 deletions
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 @@ <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> + <!-- application/gpx+xml: standard MIME type, recognised on Android 12+ --> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> <data android:mimeType="application/gpx+xml" /> </intent-filter> + <!-- application/gpx: older variant sent by some file managers and email clients --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:mimeType="application/gpx" /> + </intent-filter> + <!-- file:// URIs from legacy file managers; extension filtered in code --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="file" android:host="*" android:pathSuffix=".gpx" + android:mimeType="*/*" /> + </intent-filter> </activity> </application> </manifest> 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 @@ -118,6 +118,18 @@ class TrackRepository(context: Context) { } /** + * 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<List<TrackPoint>> = 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) } } |
