diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-04-07 06:42:51 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-04-07 06:42:51 +0000 |
| commit | 5ee2dd8925afa858f466ae63db3f7df5c7516953 (patch) | |
| tree | 9a6e66a86746d58a4c283db45ee0401dff7945b8 /android-app/app/src/main/kotlin/org/terst/nav/track | |
| parent | d98b441f2f9ca8b11a04406240dd19ecc0cac7ab (diff) | |
fix(track): fix silent GPX save failure + add stop friction + quit buttonmain
TrackStorage: openOutputStream null returned true (file never written).
Added IS_PENDING flag to fix Android 10-11 race where insert succeeds
but file isn't physically created yet. Added storage-mounted guard.
TrackRepository now logs save failures.
Stop tracking now requires a long press (haptic feedback) — prevents
accidental mid-sail stops from a single tap.
Quit button (top-right, tonal X) stops LocationService and calls
finishAffinity(). Prompts if a track is in progress.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'android-app/app/src/main/kotlin/org/terst/nav/track')
| -rw-r--r-- | android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt | 4 | ||||
| -rw-r--r-- | android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt | 39 |
2 files changed, 39 insertions, 4 deletions
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 228a484..7ef67af 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 @@ -1,6 +1,7 @@ package org.terst.nav.track import android.content.Context +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -45,7 +46,8 @@ class TrackRepository(context: Context) { if (summary.durationMs < 2 * 60_000L) return@withContext null _pastTracks.add(0, points) - storage.saveTrack(points, trackStartMs) + val saved = storage.saveTrack(points, trackStartMs) + if (!saved) Log.e("TrackRepository", "GPX save failed — ${points.size} points will be lost on restart") summary } 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 620431c..08e1dc9 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 @@ -5,11 +5,14 @@ import android.content.Context import android.os.Build import android.os.Environment import android.provider.MediaStore +import android.util.Log import java.io.File import java.time.Instant import java.time.ZoneOffset import java.time.format.DateTimeFormatter +private const val TAG = "TrackStorage" + /** * Persists completed tracks as GPX files in the shared Documents/Nav/ folder. * @@ -56,19 +59,49 @@ class TrackStorage(private val context: Context) { // ── API 29+ ────────────────────────────────────────────────────────────── private fun saveViaMediaStore(fileName: String, gpx: String): Boolean { + // Guard: external storage must be mounted before touching MediaStore + val storageState = Environment.getExternalStorageState() + if (storageState != Environment.MEDIA_MOUNTED) { + Log.e(TAG, "External storage not mounted (state=$storageState) — cannot save $fileName") + return false + } + + // IS_PENDING marks the entry as in-progress, preventing a race condition on + // Android 10-11 where insert() succeeds but openOutputStream() returns null + // because the file hasn't been physically created on disk yet. val values = ContentValues().apply { put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName) put(MediaStore.Files.FileColumns.MIME_TYPE, "application/gpx+xml") put(MediaStore.Files.FileColumns.RELATIVE_PATH, "Documents/Nav/") + put(MediaStore.MediaColumns.IS_PENDING, 1) } val uri = context.contentResolver.insert( MediaStore.Files.getContentUri("external"), values - ) ?: return false + ) ?: run { + Log.e(TAG, "MediaStore insert returned null for $fileName") + return false + } return runCatching { - context.contentResolver.openOutputStream(uri)?.use { it.write(gpx.toByteArray()) } + val stream = context.contentResolver.openOutputStream(uri) + if (stream == null) { + context.contentResolver.delete(uri, null, null) + Log.e(TAG, "openOutputStream null for $fileName — deleted orphan entry") + return@runCatching false + } + stream.use { it.write(gpx.toByteArray()) } + + // Clear IS_PENDING so the file is visible to other apps and file managers + val update = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) } + context.contentResolver.update(uri, update, null, null) + + Log.d(TAG, "Saved $fileName (${gpx.length} bytes) → $uri") true - }.getOrDefault(false) + }.getOrElse { e -> + Log.e(TAG, "Write failed for $fileName: ${e.message}") + runCatching { context.contentResolver.delete(uri, null, null) } + false + } } private fun loadViaMediaStore(): List<List<TrackPoint>> { |
