diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 1c583b2..1f57f40 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -8,6 +8,11 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.OmniCards"> + + 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(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>(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") + } + } +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt b/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt index 1eec773..0b67f0d 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt @@ -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) { + 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() + } } \ No newline at end of file diff --git a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt index ab4c21f..b439fd3 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt @@ -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 diff --git a/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt index f8c2d9f..8f57706 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt @@ -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") diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 05c113c..e54dca9 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -45,4 +45,5 @@ ShowCardActivity AddCardActivity EditCardActivity + BackupActivity \ No newline at end of file