jetzt mit Google Drive ZIP Backup

This commit is contained in:
Thorsten
2026-03-06 11:27:44 +01:00
parent a455be8db7
commit 8de7b4d886
@@ -20,6 +20,10 @@ import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class ActivitiesActivity : AppCompatActivity() { class ActivitiesActivity : AppCompatActivity() {
@@ -35,14 +39,16 @@ class ActivitiesActivity : AppCompatActivity() {
uri?.let { importGpxFile(it) } uri?.let { importGpxFile(it) }
} }
private val backupFolderLauncher = registerForActivityResult( // Backup: Wir erstellen eine ZIP-Datei
ActivityResultContracts.OpenDocumentTree() private val createBackupLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip")
) { uri -> ) { uri ->
uri?.let { performDriveBackup(it) } uri?.let { performDriveBackup(it) }
} }
private val restoreFolderLauncher = registerForActivityResult( // Restore: Wir wählen eine ZIP-Datei aus
ActivityResultContracts.OpenDocumentTree() private val restoreBackupLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri -> ) { uri ->
uri?.let { performDriveRestore(it) } uri?.let { performDriveRestore(it) }
} }
@@ -77,11 +83,12 @@ class ActivitiesActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_drive_backup -> { R.id.action_drive_backup -> {
backupFolderLauncher.launch(googleDriveUri()) val date = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()).format(java.util.Date())
createBackupLauncher.launch("gpstracker_backup_$date.zip")
true true
} }
R.id.action_drive_restore -> { R.id.action_drive_restore -> {
restoreFolderLauncher.launch(googleDriveUri()) restoreBackupLauncher.launch(arrayOf("application/zip", "application/x-zip-compressed"))
true true
} }
R.id.action_import_gpx -> { R.id.action_import_gpx -> {
@@ -92,10 +99,7 @@ class ActivitiesActivity : AppCompatActivity() {
} }
} }
private fun googleDriveUri(): Uri = private fun performDriveBackup(fileUri: Uri) {
Uri.parse("content://com.google.android.apps.docs.storage/")
private fun performDriveBackup(folderUri: Uri) {
val tracks = trackStorage.getAllTracks() val tracks = trackStorage.getAllTracks()
if (tracks.isEmpty()) { if (tracks.isEmpty()) {
Toast.makeText(this, "Keine Tracks zum Sichern", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Keine Tracks zum Sichern", Toast.LENGTH_SHORT).show()
@@ -104,39 +108,28 @@ class ActivitiesActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
val folder = DocumentFile.fromTreeUri(this@ActivitiesActivity, folderUri) contentResolver.openOutputStream(fileUri)?.use { os ->
if (folder == null || !folder.canWrite()) { ZipOutputStream(BufferedOutputStream(os)).use { zos ->
withContext(Dispatchers.Main) {
Toast.makeText(this@ActivitiesActivity, "Ordner nicht beschreibbar", Toast.LENGTH_LONG).show()
}
return@launch
}
var count = 0
tracks.forEach { track -> tracks.forEach { track ->
val safeName = track.name.replace(Regex("[^a-zA-Z0-9._-]"), "_") val safeName = trackStorage.sanitizeFileName(track.name)
val jsonName = "$safeName.json" // JSON Datei in ZIP
folder.findFile(jsonName)?.delete() val jsonEntry = ZipEntry("$safeName.json")
folder.createFile("application/json", jsonName)?.let { doc -> zos.putNextEntry(jsonEntry)
contentResolver.openOutputStream(doc.uri)?.use { os -> zos.write(gson.toJson(track).toByteArray(Charsets.UTF_8))
os.write(gson.toJson(track).toByteArray(Charsets.UTF_8)) zos.closeEntry()
}
}
val gpxName = "$safeName.gpx" // GPX Datei in ZIP
folder.findFile(gpxName)?.delete() val gpxEntry = ZipEntry("$safeName.gpx")
folder.createFile("application/gpx+xml", gpxName)?.let { doc -> zos.putNextEntry(gpxEntry)
contentResolver.openOutputStream(doc.uri)?.use { os -> zos.write(gpxExporter.generateGpxContent(track).toByteArray(Charsets.UTF_8))
os.write(gpxExporter.generateGpxContent(track).toByteArray(Charsets.UTF_8)) zos.closeEntry()
} }
} }
count++
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(this@ActivitiesActivity, "$count Track(s) gesichert (JSON + GPX)", Toast.LENGTH_LONG).show() Toast.makeText(this@ActivitiesActivity, "${tracks.size} Tracks in Backup gesichert", Toast.LENGTH_LONG).show()
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -146,39 +139,32 @@ class ActivitiesActivity : AppCompatActivity() {
} }
} }
private fun performDriveRestore(folderUri: Uri) { private fun performDriveRestore(fileUri: Uri) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
val folder = DocumentFile.fromTreeUri(this@ActivitiesActivity, folderUri)
if (folder == null) {
withContext(Dispatchers.Main) {
Toast.makeText(this@ActivitiesActivity, "Ordner nicht zugänglich", Toast.LENGTH_LONG).show()
}
return@launch
}
val files = folder.listFiles()
val jsonFiles = files.filter { it.name?.endsWith(".json") == true }
val gpxFiles = files.filter { it.name?.endsWith(".gpx") == true }
val jsonNames = jsonFiles.map { it.name!!.removeSuffix(".json") }.toSet()
var restored = 0 var restored = 0
var skipped = 0 var skipped = 0
jsonFiles.forEach { file -> contentResolver.openInputStream(fileUri)?.use { inputStream ->
contentResolver.openInputStream(file.uri)?.use { inputStream -> ZipInputStream(inputStream).use { zis ->
val content = inputStream.readBytes().toString(Charsets.UTF_8) var entry = zis.nextEntry
while (entry != null) {
if (!entry.isDirectory && entry.name.endsWith(".json")) {
val content = zis.readBytes().toString(Charsets.UTF_8)
val track = runCatching { gson.fromJson(content, Track::class.java) }.getOrNull() val track = runCatching { gson.fromJson(content, Track::class.java) }.getOrNull()
if (track != null) {
if (trackStorage.getTrack(track.name) == null) { trackStorage.saveTrack(track); restored++ } else skipped++
}
}
}
gpxFiles.filter { it.name?.removeSuffix(".gpx") !in jsonNames }.forEach { file ->
val track = gpxImporter.importFromUri(file.uri)
if (track != null) { if (track != null) {
if (trackStorage.getTrack(track.name) == null) { trackStorage.saveTrack(track); restored++ } else skipped++ if (trackStorage.getTrack(track.name) == null) {
trackStorage.saveTrack(track)
restored++
} else {
skipped++
}
}
}
zis.closeEntry()
entry = zis.nextEntry
}
} }
} }
@@ -206,7 +192,6 @@ class ActivitiesActivity : AppCompatActivity() {
private fun loadAndDisplayTracks() { private fun loadAndDisplayTracks() {
val tracks = trackStorage.getAllTracks() val tracks = trackStorage.getAllTracks()
binding.tracksContainer.removeAllViews() binding.tracksContainer.removeAllViews()
if (tracks.isEmpty()) { if (tracks.isEmpty()) {