feat: added backup and restored functionality

This commit is contained in:
2026-06-14 20:21:38 +03:00
parent fba54e0243
commit 52c485af64
6 changed files with 193 additions and 4 deletions
@@ -0,0 +1,150 @@
package ru.omni_devel.cards
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import kotlinx.serialization.json.Json
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import kotlinx.serialization.decodeFromString
class BackupActivity : ComponentActivity() {
private lateinit var db: DbHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
db = DbHelper(this, null)
setContent {
OmniCardsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
BackupPage(innerPadding = innerPadding, getDbFun = {
return@BackupPage db
})
}
}
}
}
}
@Composable
fun BackupPage(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
val context = LocalContext.current
val activityContext = LocalActivity.current
val showImportDialog = rememberSaveable { mutableStateOf(false) }
var pendingUri by rememberSaveable { mutableStateOf<Uri?>(null) }
val createBackupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let {
try {
val jsonData = Json.encodeToString(getDbFun().getCards())
context.contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(jsonData.toByteArray())
Toast.makeText(context, "Backup successfully saved", Toast.LENGTH_SHORT).show()
activityContext!!.finish()
}
} catch (e: Exception) {
Toast.makeText(context, "Failed to save backup: $e", Toast.LENGTH_LONG).show()
}
}
}
val restoreFromBackupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
pendingUri = it
showImportDialog.value = true
}
}
if (showImportDialog.value) {
AlertDialog(
title = { Text("Restore backup") },
text = { Text("This will replace existing cards by data from backup file. Continue?") },
confirmButton = {
TextButton(
onClick = {
showImportDialog.value = false
pendingUri?.let { uri ->
try {
context.contentResolver.openInputStream(uri)?.use { stream ->
val json = stream.bufferedReader().use { it.readText() }
val cards = Json.decodeFromString<List<CardInfo>>(json)
val db = getDbFun()
db.clearDatabase()
db.addCards(cards)
Toast.makeText(context, "Data successfully restored from backup", Toast.LENGTH_SHORT).show()
activityContext!!.finish()
}
} catch (e: Exception) {
Toast.makeText(context, "Failed to restore data from backup: $e", Toast.LENGTH_LONG).show()
}
}
}
) {
Text("Yes")
}
},
dismissButton = {
TextButton(
onClick = {
showImportDialog.value = false
}
) {
Text("No")
}
},
onDismissRequest = { showImportDialog.value = false },
)
}
Page("Backup", innerPadding) {
FullWidthButton(
onClick = {
createBackupLauncher.launch("omnicards-backup.json")
}
) {
Text("Backup to file")
}
FullWidthButton(
onClick = {
restoreFromBackupLauncher.launch("application/json")
}
) {
Text("Restore from file")
}
}
}
@@ -106,4 +106,28 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
db.close()
}
fun clearDatabase() {
val db = this.writableDatabase
db.execSQL("DELETE FROM cards")
}
fun addCards(cards: List<CardInfo>) {
val db = this.writableDatabase
for (card in cards) {
val values = ContentValues()
values.put("id", card.id)
values.put("name", card.name)
values.put("codeValue", card.codeValue)
values.put("codeType", card.codeType.name)
values.put("color", card.color)
db.insert("cards", null, values)
}
db.close()
}
}
@@ -1,6 +1,5 @@
package ru.omni_devel.cards
import android.app.Activity
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
@@ -23,7 +22,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
@@ -32,12 +30,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import com.github.skydoves.colorpicker.compose.ColorEnvelope
@@ -145,9 +145,22 @@ fun MainPage(
) {
Text("Sync with WearOS")
}
FullWidthButton(
onClick = {
val intent = Intent(context, BackupActivity::class.java)
context.startActivity(intent)
isActionBarShowed = false
}
) {
Text("Backup")
}
FullWidthButton(
onClick = {
uriHandler.openUri("https://github.com/omni-devel/OmniCards")
isActionBarShowed = false
}
) {
Text("Source code")