From 55e8cfa7d50758df219ea1b4e51b017aaa6b686d Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 7 Jun 2022 19:13:40 +0200 Subject: [PATCH] Add backup/restore feature --- .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 19 ++ .../vimusic/ui/screens/SettingsScreen.kt | 17 +- .../settings/BackupAndRestoreScreen.kt | 269 ++++++++++++++++++ .../vimusic/ui/screens/settings/routes.kt | 7 + app/src/main/res/drawable/download.xml | 12 + app/src/main/res/drawable/server.xml | 18 ++ app/src/main/res/drawable/share.xml | 12 + 7 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt create mode 100644 app/src/main/res/drawable/download.xml create mode 100644 app/src/main/res/drawable/server.xml create mode 100644 app/src/main/res/drawable/share.xml diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 5123052..c03d026 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -1,10 +1,13 @@ package it.vfsfitvnm.vimusic import android.content.Context +import android.database.Cursor import androidx.room.* +import androidx.sqlite.db.SupportSQLiteQuery import it.vfsfitvnm.vimusic.models.* import kotlinx.coroutines.flow.Flow + @Dao interface Database { companion object : Database by DatabaseInitializer.Instance.database @@ -138,3 +141,19 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() { val Database.internal: RoomDatabase get() = DatabaseInitializer.Instance + +fun Database.checkpoint() { + internal.openHelper.writableDatabase.run { + query("PRAGMA journal_mode").use { cursor -> + if (cursor.moveToFirst()) { + when (cursor.getString(0).lowercase()) { + "wal" -> { + query("PRAGMA wal_checkpoint").use(Cursor::moveToFirst) + query("PRAGMA wal_checkpoint(TRUNCATE)").use(Cursor::moveToFirst) + query("PRAGMA wal_checkpoint").use(Cursor::moveToFirst) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt index 08995fb..8996852 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt @@ -39,6 +39,7 @@ fun SettingsScreen() { val albumRoute = rememberPlaylistOrAlbumRoute() val artistRoute = rememberArtistRoute() val appearanceRoute = rememberAppearanceRoute() + val backupAndRestoreRoute = rememberBackupAndRestoreRoute() val scrollState = rememberScrollState() @@ -46,11 +47,11 @@ fun SettingsScreen() { listenToGlobalEmitter = true, transitionSpec = { when (targetState.route) { - appearanceRoute -> + appearanceRoute, backupAndRestoreRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Left) with slideOutOfContainer(AnimatedContentScope.SlideDirection.Left) else -> when (initialState.route) { - appearanceRoute -> + appearanceRoute, backupAndRestoreRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Right) with slideOutOfContainer(AnimatedContentScope.SlideDirection.Right) else -> fastFade @@ -74,6 +75,10 @@ fun SettingsScreen() { AppearanceScreen() } + backupAndRestoreRoute { + BackupAndRestoreScreen() + } + host { val colorPalette = LocalColorPalette.current val typography = LocalTypography.current @@ -172,6 +177,14 @@ fun SettingsScreen() { description = "Change the colors and shapes of the app", route = appearanceRoute, ) + + Entry( + color = colorPalette.orange, + icon = R.drawable.server, + title = "Backup & Restore", + description = "Backup and restore the app database", + route = backupAndRestoreRoute + ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt new file mode 100644 index 0000000..e04cca1 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt @@ -0,0 +1,269 @@ +package it.vfsfitvnm.vimusic.ui.screens.settings + +import android.annotation.SuppressLint +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.checkpoint +import it.vfsfitvnm.vimusic.internal +import it.vfsfitvnm.vimusic.ui.components.TopAppBar +import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog +import it.vfsfitvnm.vimusic.ui.screens.ArtistScreen +import it.vfsfitvnm.vimusic.ui.screens.PlaylistOrAlbumScreen +import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute +import it.vfsfitvnm.vimusic.ui.screens.rememberPlaylistOrAlbumRoute +import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette +import it.vfsfitvnm.vimusic.ui.styling.LocalTypography +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.FileInputStream +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.* +import kotlin.system.exitProcess + + +@ExperimentalAnimationApi +@Composable +fun BackupAndRestoreScreen() { + val albumRoute = rememberPlaylistOrAlbumRoute() + val artistRoute = rememberArtistRoute() + + val scrollState = rememberScrollState() + + RouteHandler(listenToGlobalEmitter = true) { + albumRoute { browseId -> + PlaylistOrAlbumScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + artistRoute { browseId -> + ArtistScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + host { + val colorPalette = LocalColorPalette.current + val typography = LocalTypography.current + val context = LocalContext.current + + val coroutineScope = rememberCoroutineScope() + + val backupLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/vnd.sqlite3")) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + + coroutineScope.launch(Dispatchers.IO) { + Database.checkpoint() + context.applicationContext.contentResolver.openOutputStream(uri) + ?.use { outputStream -> + FileInputStream(Database.internal.openHelper.writableDatabase.path).use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + } + + val restoreLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + + coroutineScope.launch(Dispatchers.IO) { + Database.internal.close() + + FileOutputStream(Database.internal.openHelper.writableDatabase.path).use { outputStream -> + context.applicationContext.contentResolver.openInputStream(uri) + ?.use { inputStream -> + inputStream.copyTo(outputStream) + } + } + + exitProcess(0) + } + } + + var isShowingRestoreDialog by rememberSaveable { + mutableStateOf(false) + } + + if (isShowingRestoreDialog) { + ConfirmationDialog( + text = "The application will automatically close itself to avoid problems after restoring the database.", + onDismiss = { + isShowingRestoreDialog = false + }, + onConfirm = { + restoreLauncher.launch( + arrayOf("application/x-sqlite3", "application/vnd.sqlite3", "application/octet-stream") + ) + }, + confirmText = "Ok" + ) + } + + Column( + modifier = Modifier + .background(colorPalette.background) + .fillMaxSize() + .verticalScroll(scrollState) + .padding(bottom = 72.dp) + ) { + TopAppBar( + modifier = Modifier + .height(52.dp) + ) { + Image( + painter = painterResource(R.drawable.chevron_back), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable(onClick = pop) + .padding(horizontal = 16.dp, vertical = 8.dp) + .size(24.dp) + ) + + BasicText( + text = "Backup & Restore", + style = typography.m.semiBold + ) + + Spacer( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .size(24.dp) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(vertical = 16.dp, horizontal = 32.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .clickable { + @SuppressLint("SimpleDateFormat") + val dateFormat = SimpleDateFormat("yyyyMMddHHmmss") + backupLauncher.launch("vimusic_${dateFormat.format(Date())}.db") + } + .shadow(elevation = 8.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .size(92.dp) + ) { + Image( + painter = painterResource(R.drawable.share), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.blue), + modifier = Modifier + .size(32.dp) + ) + } + + BasicText( + text = "Backup", + style = typography.xs.semiBold, + modifier = Modifier + ) + } + + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(vertical = 16.dp, horizontal = 32.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .clickable { + isShowingRestoreDialog = true + } + .shadow(elevation = 8.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .size(92.dp) + ) { + Image( + painter = painterResource(R.drawable.download), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.orange), + modifier = Modifier + .size(32.dp) + ) + } + + BasicText( + text = "Restore", + style = typography.xs.semiBold, + modifier = Modifier + ) + } + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + BasicText( + text = "Backup", + style = typography.xxs.semiBold.secondary + ) + + BasicText( + text = "The backup consists in exporting the application database to your device storage.\nThis means playlists, song history, favorites songs will exported.\nThis operation excludes personal preferences such as the theme mode and everything you can set in the Settings page.", + style = typography.xxs.secondary + ) + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + BasicText( + text = "Restore", + style = typography.xxs.semiBold.secondary + ) + + BasicText( + text = "The restore replaces the existing application database with the selected - previously exported - one.\nThis means every currently existing data will be wiped: THE TWO DATABASES WON'T BE MERGED.\nIt is recommended to restore the database immediately after a the application is installed on a new device.", + style = typography.xxs.secondary + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/routes.kt index d43e8d7..54b3c76 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/routes.kt @@ -10,3 +10,10 @@ fun rememberAppearanceRoute(): Route0 { Route0("AppearanceRoute") } } + +@Composable +fun rememberBackupAndRestoreRoute(): Route0 { + return remember { + Route0("BackupAndRestoreRoute") + } +} diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml new file mode 100644 index 0000000..34f9394 --- /dev/null +++ b/app/src/main/res/drawable/download.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/server.xml b/app/src/main/res/drawable/server.xml new file mode 100644 index 0000000..b481fd9 --- /dev/null +++ b/app/src/main/res/drawable/server.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/share.xml b/app/src/main/res/drawable/share.xml new file mode 100644 index 0000000..f5eaa4c --- /dev/null +++ b/app/src/main/res/drawable/share.xml @@ -0,0 +1,12 @@ + + + +