diff --git a/mobile/src/main/java/de/tstratmann/gpstracker/ActivitiesActivity.kt b/mobile/src/main/java/de/tstratmann/gpstracker/ActivitiesActivity.kt index 0c14fea..67b7639 100644 --- a/mobile/src/main/java/de/tstratmann/gpstracker/ActivitiesActivity.kt +++ b/mobile/src/main/java/de/tstratmann/gpstracker/ActivitiesActivity.kt @@ -20,6 +20,10 @@ import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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() { @@ -35,14 +39,16 @@ class ActivitiesActivity : AppCompatActivity() { uri?.let { importGpxFile(it) } } - private val backupFolderLauncher = registerForActivityResult( - ActivityResultContracts.OpenDocumentTree() + // Backup: Wir erstellen eine ZIP-Datei + private val createBackupLauncher = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/zip") ) { uri -> uri?.let { performDriveBackup(it) } } - private val restoreFolderLauncher = registerForActivityResult( - ActivityResultContracts.OpenDocumentTree() + // Restore: Wir wählen eine ZIP-Datei aus + private val restoreBackupLauncher = registerForActivityResult( + ActivityResultContracts.OpenDocument() ) { uri -> uri?.let { performDriveRestore(it) } } @@ -77,11 +83,12 @@ class ActivitiesActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { 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 } R.id.action_drive_restore -> { - restoreFolderLauncher.launch(googleDriveUri()) + restoreBackupLauncher.launch(arrayOf("application/zip", "application/x-zip-compressed")) true } R.id.action_import_gpx -> { @@ -92,10 +99,7 @@ class ActivitiesActivity : AppCompatActivity() { } } - private fun googleDriveUri(): Uri = - Uri.parse("content://com.google.android.apps.docs.storage/") - - private fun performDriveBackup(folderUri: Uri) { + private fun performDriveBackup(fileUri: Uri) { val tracks = trackStorage.getAllTracks() if (tracks.isEmpty()) { Toast.makeText(this, "Keine Tracks zum Sichern", Toast.LENGTH_SHORT).show() @@ -104,39 +108,28 @@ class ActivitiesActivity : AppCompatActivity() { lifecycleScope.launch(Dispatchers.IO) { try { - val folder = DocumentFile.fromTreeUri(this@ActivitiesActivity, folderUri) - if (folder == null || !folder.canWrite()) { - withContext(Dispatchers.Main) { - Toast.makeText(this@ActivitiesActivity, "Ordner nicht beschreibbar", Toast.LENGTH_LONG).show() - } - return@launch - } + contentResolver.openOutputStream(fileUri)?.use { os -> + ZipOutputStream(BufferedOutputStream(os)).use { zos -> + tracks.forEach { track -> + val safeName = trackStorage.sanitizeFileName(track.name) + + // JSON Datei in ZIP + val jsonEntry = ZipEntry("$safeName.json") + zos.putNextEntry(jsonEntry) + zos.write(gson.toJson(track).toByteArray(Charsets.UTF_8)) + zos.closeEntry() - var count = 0 - tracks.forEach { track -> - val safeName = track.name.replace(Regex("[^a-zA-Z0-9._-]"), "_") - - val jsonName = "$safeName.json" - folder.findFile(jsonName)?.delete() - folder.createFile("application/json", jsonName)?.let { doc -> - contentResolver.openOutputStream(doc.uri)?.use { os -> - os.write(gson.toJson(track).toByteArray(Charsets.UTF_8)) + // GPX Datei in ZIP + val gpxEntry = ZipEntry("$safeName.gpx") + zos.putNextEntry(gpxEntry) + zos.write(gpxExporter.generateGpxContent(track).toByteArray(Charsets.UTF_8)) + zos.closeEntry() } } - - val gpxName = "$safeName.gpx" - folder.findFile(gpxName)?.delete() - folder.createFile("application/gpx+xml", gpxName)?.let { doc -> - contentResolver.openOutputStream(doc.uri)?.use { os -> - os.write(gpxExporter.generateGpxContent(track).toByteArray(Charsets.UTF_8)) - } - } - - count++ } 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) { withContext(Dispatchers.Main) { @@ -146,42 +139,35 @@ class ActivitiesActivity : AppCompatActivity() { } } - private fun performDriveRestore(folderUri: Uri) { + private fun performDriveRestore(fileUri: Uri) { lifecycleScope.launch(Dispatchers.IO) { 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 skipped = 0 - jsonFiles.forEach { file -> - contentResolver.openInputStream(file.uri)?.use { inputStream -> - val content = inputStream.readBytes().toString(Charsets.UTF_8) - 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++ + contentResolver.openInputStream(fileUri)?.use { inputStream -> + ZipInputStream(inputStream).use { zis -> + 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() + + if (track != null) { + if (trackStorage.getTrack(track.name) == null) { + trackStorage.saveTrack(track) + restored++ + } else { + skipped++ + } + } + } + zis.closeEntry() + entry = zis.nextEntry } } } - gpxFiles.filter { it.name?.removeSuffix(".gpx") !in jsonNames }.forEach { file -> - val track = gpxImporter.importFromUri(file.uri) - if (track != null) { - if (trackStorage.getTrack(track.name) == null) { trackStorage.saveTrack(track); restored++ } else skipped++ - } - } - withContext(Dispatchers.Main) { val msg = buildString { append("$restored Track(s) wiederhergestellt") @@ -206,7 +192,6 @@ class ActivitiesActivity : AppCompatActivity() { private fun loadAndDisplayTracks() { val tracks = trackStorage.getAllTracks() - binding.tracksContainer.removeAllViews() if (tracks.isEmpty()) {