From 2eeb0864816e82a85d2d309ab7e7355b2abd1005 Mon Sep 17 00:00:00 2001 From: Perplexity Computer Date: Sat, 30 May 2026 22:19:04 +0000 Subject: [PATCH] Release GPS2Audio v2.5.48 TrackDraft backup fix --- app/build.gradle.kts | 4 +- .../de/waypointaudio/data/MapStyleSettings.kt | 2 +- .../waypointaudio/io/BackupExportManager.kt | 15 +++- .../waypointaudio/io/BackupImportManager.kt | 88 +++++++++++++------ .../viewmodel/WaypointViewModel.kt | 10 +++ 5 files changed, 86 insertions(+), 33 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3b5c4e..bacb296 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -157,8 +157,8 @@ android { // • Keine Regressionen: Phantom-Layer-Fix, Lizenzsystem, Multi-Clip, // PTT, Tour-Zähler, Backup/Cover, GPX-Import, Explorer/Routing, // Tour-/Waypoint-Kopien, Hintergrundwiedergabe. - versionCode = 100 - versionName = "2.5.46" + versionCode = 102 + versionName = "2.5.48" } val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE") diff --git a/app/src/main/kotlin/de/waypointaudio/data/MapStyleSettings.kt b/app/src/main/kotlin/de/waypointaudio/data/MapStyleSettings.kt index 3b9c4d9..e298ab7 100644 --- a/app/src/main/kotlin/de/waypointaudio/data/MapStyleSettings.kt +++ b/app/src/main/kotlin/de/waypointaudio/data/MapStyleSettings.kt @@ -55,7 +55,7 @@ enum class MapBaseStyle(val key: String) { PERSPECTIVE("perspective"); companion object { - val DEFAULT = MODERN_2D + val DEFAULT = CITY_3D fun fromKey(key: String?): MapBaseStyle { return when (key) { "modern_2d" -> MODERN_2D diff --git a/app/src/main/kotlin/de/waypointaudio/io/BackupExportManager.kt b/app/src/main/kotlin/de/waypointaudio/io/BackupExportManager.kt index 1230d2b..5bb886a 100644 --- a/app/src/main/kotlin/de/waypointaudio/io/BackupExportManager.kt +++ b/app/src/main/kotlin/de/waypointaudio/io/BackupExportManager.kt @@ -9,6 +9,7 @@ import de.waypointaudio.data.TourAudioSettings import de.waypointaudio.data.TourCover import de.waypointaudio.data.TourPlaybackDefaults import de.waypointaudio.data.TourRoute +import de.waypointaudio.data.TrackDraft import de.waypointaudio.data.Waypoint import de.waypointaudio.data.effectiveClips import java.io.BufferedOutputStream @@ -68,6 +69,10 @@ object BackupExportManager { // v2.5.5 — Tour-Cover (Icon + Palette) sind ein reines UI-Detail pro Tour // und wandern mit dem Tour-Export mit. val tourCovers: Map = emptyMap(), + // v2.5.48 — Echte TrackDraft-Objekte (Name, Notiz, Markierungen, Punkte). + // gpsTracks bleibt für die Live-Aufzeichnung weiterhin im Backup-Input, + // wird aber NICHT mehr in track_drafts.json geschrieben. + val trackDrafts: List = emptyList(), val appVersionName: String = "", val appVersionCode: Int = 0 ) @@ -197,13 +202,17 @@ object BackupExportManager { } // ----- data/track_drafts.json ----- + // v2.5.48 FIX: Exportiert die echten TrackDraft-Objekte (Name, Notiz, + // Markierungen, Punkte) statt der gpsTracks-Map. Altes Format + // ("tracks": { tourName: [GpsTrackPoint] }) wird beim Import + // weiterhin toleriert (Abwärtskompatibilität). if (selection.trackDrafts) { val trackPayload = mapOf( - "tracks" to input.gpsTracks + "drafts" to input.trackDrafts ) writeJsonEntry(zip, "data/track_drafts.json", trackPayload) - val pointCount = input.gpsTracks.values.sumOf { it.size } - sectionsWritten.add("Track-Entwürfe (${input.gpsTracks.size} Touren, $pointCount Punkte)") + val pointCount = input.trackDrafts.sumOf { it.points.size } + sectionsWritten.add("Track-Entwürfe (${input.trackDrafts.size} Entwürfe, $pointCount Punkte)") } // ----- data/music_settings.json + media/... ----- diff --git a/app/src/main/kotlin/de/waypointaudio/io/BackupImportManager.kt b/app/src/main/kotlin/de/waypointaudio/io/BackupImportManager.kt index 2a56f48..0276110 100644 --- a/app/src/main/kotlin/de/waypointaudio/io/BackupImportManager.kt +++ b/app/src/main/kotlin/de/waypointaudio/io/BackupImportManager.kt @@ -15,6 +15,7 @@ import de.waypointaudio.data.TourAudioSettings import de.waypointaudio.data.TourPlaybackDefaults import de.waypointaudio.data.TourCover import de.waypointaudio.data.TourRoute +import de.waypointaudio.data.TrackDraft import de.waypointaudio.data.Waypoint import de.waypointaudio.data.WaypointMusicBehavior import java.io.File @@ -102,6 +103,8 @@ object BackupImportManager { val tourRoutesToWrite: Map = emptyMap(), // v2.5.5 — Tour-Cover, die geschrieben werden sollen (Map endgültiger Tourname → Cover). val tourCoversToWrite: Map = emptyMap(), + // v2.5.48 — Echte TrackDraft-Objekte, die als neue Drafts hinzugefügt werden sollen. + val trackDraftsToAdd: List = emptyList(), val trackMinDistanceMeters: Int?, val newTourSelection: String?, val warnings: List, @@ -514,42 +517,71 @@ object BackupImportManager { } // ---- Track-Entwürfe ---- + // v2.5.48 FIX: Importiert echte TrackDraft-Objekte aus dem "drafts"-Array. + // Abwärtskompatibilität: Alte Backups schrieben fälschlicherweise die + // gpsTracks-Map unter "tracks" in track_drafts.json. Falls "drafts" fehlt + // aber "tracks" vorhanden ist, wird das alte Format erkannt und toleriert + // (kein Crash, keine Datenverluste an bestehenden Touren/Waypoints). val tracksToWrite = mutableMapOf>() + val trackDraftsToAdd = mutableListOf() var tracksImported = 0 if (selection.trackDrafts && draftsJson != null) { val obj = runCatching { JsonParser.parseString(draftsJson).asJsonObject }.getOrNull() - val map: Map> = if (obj != null) { - runCatching { - gson.fromJson>>( - obj.get("tracks"), - object : TypeToken>>() {}.type - ) ?: emptyMap() - }.getOrDefault(emptyMap()) - } else emptyMap() - map.forEach { (origTour, points) -> - val mappedTour = tourRename[origTour] - ?: run { - // Tour war nicht im Tour-Import (z. B. wenn nur Drafts importiert werden): - // prüfen, ob Name kollidiert. - if (origTour == Waypoint.DEFAULT_TOUR_NAME) { - origTour - } else if (origTour in resultTours) { - origTour - } else { - resultTours.add(origTour) - origTour - } + if (obj != null) { + // Neues Format (v2.5.48+): "drafts" enthält eine Liste echter TrackDraft-Objekte. + val hasDraftsKey = obj.has("drafts") && !obj.get("drafts").isJsonNull + + if (hasDraftsKey) { + // Neues Format: Liste von TrackDraft-Objekten + val importedDrafts: List = runCatching { + gson.fromJson>( + obj.get("drafts"), + object : TypeToken>() {}.type + ) ?: emptyList() + }.getOrDefault(emptyList()) + + importedDrafts.forEach { draft -> + // Tourname-Mapping anwenden (für den Fall, dass Tour umbenannt wurde). + val origTour = draft.sourceTourName + val mappedTour = tourRename[origTour] + ?: run { + if (origTour == Waypoint.DEFAULT_TOUR_NAME || + origTour in resultTours + ) { + origTour + } else { + resultTours.add(origTour) + origTour + } + } + // Neue UUID vergeben, damit importierte Drafts nie mit + // vorhandenen Drafts-IDs kollidieren. + val newDraft = draft.copy( + id = java.util.UUID.randomUUID().toString(), + sourceTourName = mappedTour, + updatedAt = System.currentTimeMillis() + ) + trackDraftsToAdd.add(newDraft) + tracksImported++ } - val existingPoints = existingTracks[mappedTour] ?: emptyList() - if (existingPoints.isEmpty()) { - // Bestehenden Track NICHT überschreiben. - tracksToWrite[mappedTour] = points - tracksImported++ } else { - warnings.add("Track-Entwurf für Tour „$mappedTour\" bereits vorhanden – nicht überschrieben.") + // Altes (fehlerhaftes) Format: "tracks" enthält gpsTracks-Map. + // Tolerieren ohne Crash, aber nicht in Tracks oder Drafts importieren, + // da der Inhalt GPS-Track-Daten (nicht Draft-Metadaten) sind. + // Hinweis in Warnings ausgeben. + val hasTracksKey = obj.has("tracks") && !obj.get("tracks").isJsonNull + if (hasTracksKey) { + warnings.add( + "Hinweis: track_drafts.json aus älterem Backup-Format erkannt " + + "(enthält GPS-Tracks statt Draft-Objekte). " + + "TrackDraft-Namen/Notizen können nicht wiederhergestellt werden. " + + "Bestehende Touren und Wegpunkte bleiben unverändert." + ) + } + // Keine Daten importieren aus altem, fehlerh. Format — kein Crash. } } } @@ -636,6 +668,8 @@ object BackupImportManager { musicToWrite = musicToWrite, tourRoutesToWrite = tourRoutesToWrite, tourCoversToWrite = tourCoversToWrite, + // v2.5.48 — Echte TrackDraft-Objekte weitergeben. + trackDraftsToAdd = trackDraftsToAdd, trackMinDistanceMeters = trackMinDistanceMeters, newTourSelection = newTourSelection, warnings = warnings, diff --git a/app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt b/app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt index 7ac70a5..896a9b5 100644 --- a/app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt +++ b/app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt @@ -1212,6 +1212,8 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application val tourRoutesMap = runCatching { repository.tourRoutes.first() }.getOrDefault(emptyMap()) val tourCoversMap = runCatching { repository.tourCovers.first() }.getOrDefault(emptyMap()) + // v2.5.48 — Echte TrackDraft-Objekte für Backup lesen (nicht gpsTracks). + val trackDraftsSnapshot = runCatching { repository.trackDrafts.first() }.getOrDefault(emptyList()) val input = de.waypointaudio.io.BackupExportManager.BackupInput( waypoints = _waypoints.value, @@ -1221,6 +1223,7 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application musicSettings = musicSettingsMap, tourRoutes = tourRoutesMap, tourCovers = tourCoversMap, + trackDrafts = trackDraftsSnapshot, appVersionName = pkgInfo?.versionName ?: "", appVersionCode = versionCode ) @@ -1366,6 +1369,13 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application } } if (trackDrafts) { + // v2.5.48 FIX: Echte TrackDraft-Objekte importieren statt gpsTracks-Map. + // trackDraftsToAdd enthält bereits neue UUIDs (vergeben im ImportManager). + payload.trackDraftsToAdd.forEach { draft -> + repository.addTrackDraft(draft) + } + // tracksToWrite ist für die Live-GPS-Track-Map (gpsTracksFlow). + // Bei neuem Format leer; bei altem Format auch leer (kein Import). payload.tracksToWrite.forEach { (tour, points) -> repository.saveGpsTrack(tour, points) }