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.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) {
|
tracks.forEach { track ->
|
||||||
Toast.makeText(this@ActivitiesActivity, "Ordner nicht beschreibbar", Toast.LENGTH_LONG).show()
|
val safeName = trackStorage.sanitizeFileName(track.name)
|
||||||
}
|
|
||||||
return@launch
|
// 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
|
// GPX Datei in ZIP
|
||||||
tracks.forEach { track ->
|
val gpxEntry = ZipEntry("$safeName.gpx")
|
||||||
val safeName = track.name.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
zos.putNextEntry(gpxEntry)
|
||||||
|
zos.write(gpxExporter.generateGpxContent(track).toByteArray(Charsets.UTF_8))
|
||||||
val jsonName = "$safeName.json"
|
zos.closeEntry()
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
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,42 +139,35 @@ 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
|
||||||
val track = runCatching { gson.fromJson(content, Track::class.java) }.getOrNull()
|
while (entry != null) {
|
||||||
if (track != null) {
|
if (!entry.isDirectory && entry.name.endsWith(".json")) {
|
||||||
if (trackStorage.getTrack(track.name) == null) { trackStorage.saveTrack(track); restored++ } else skipped++
|
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) {
|
withContext(Dispatchers.Main) {
|
||||||
val msg = buildString {
|
val msg = buildString {
|
||||||
append("$restored Track(s) wiederhergestellt")
|
append("$restored Track(s) wiederhergestellt")
|
||||||
@@ -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()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user