Release GPS2Audio v2.5.48 TrackDraft backup fix

This commit is contained in:
Perplexity Computer
2026-05-30 22:19:04 +00:00
parent 990b66e24f
commit 2eeb086481
5 changed files with 86 additions and 33 deletions
+2 -2
View File
@@ -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")
@@ -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
@@ -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<String, TourCover> = 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<TrackDraft> = 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/... -----
@@ -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<String, TourRoute> = emptyMap(),
// v2.5.5 — Tour-Cover, die geschrieben werden sollen (Map endgültiger Tourname → Cover).
val tourCoversToWrite: Map<String, TourCover> = emptyMap(),
// v2.5.48 — Echte TrackDraft-Objekte, die als neue Drafts hinzugefügt werden sollen.
val trackDraftsToAdd: List<TrackDraft> = emptyList(),
val trackMinDistanceMeters: Int?,
val newTourSelection: String?,
val warnings: List<String>,
@@ -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<String, List<GpsTrackPoint>>()
val trackDraftsToAdd = mutableListOf<TrackDraft>()
var tracksImported = 0
if (selection.trackDrafts && draftsJson != null) {
val obj = runCatching {
JsonParser.parseString(draftsJson).asJsonObject
}.getOrNull()
val map: Map<String, List<GpsTrackPoint>> = if (obj != null) {
runCatching {
gson.fromJson<Map<String, List<GpsTrackPoint>>>(
obj.get("tracks"),
object : TypeToken<Map<String, List<GpsTrackPoint>>>() {}.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<TrackDraft> = runCatching {
gson.fromJson<List<TrackDraft>>(
obj.get("drafts"),
object : TypeToken<List<TrackDraft>>() {}.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,
@@ -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)
}