jetzt mit Google Drive ZIP Backup
This commit is contained in:
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user