summaryrefslogtreecommitdiff
path: root/android-app
diff options
context:
space:
mode:
Diffstat (limited to 'android-app')
-rw-r--r--android-app/app/src/main/AndroidManifest.xml16
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt9
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt12
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt8
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt19
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) }
}