Add backup/restore feature

This commit is contained in:
vfsfitvnm 2022-06-07 19:13:40 +02:00
parent 5c9e10bf05
commit 55e8cfa7d5
7 changed files with 352 additions and 2 deletions

View file

@ -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)
}
}
}
}
}
}

View file

@ -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
)
}
}
}

View file

@ -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
)
}
}
}
}
}

View file

@ -10,3 +10,10 @@ fun rememberAppearanceRoute(): Route0 {
Route0("AppearanceRoute")
}
}
@Composable
fun rememberBackupAndRestoreRoute(): Route0 {
return remember {
Route0("BackupAndRestoreRoute")
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M376,160H272V313.37l52.69,-52.68a16,16 0,0 1,22.62 22.62l-80,80a16,16 0,0 1,-22.62 0l-80,-80a16,16 0,0 1,22.62 -22.62L240,313.37V160H136a56.06,56.06 0,0 0,-56 56V424a56.06,56.06 0,0 0,56 56H376a56.06,56.06 0,0 0,56 -56V216A56.06,56.06 0,0 0,376 160Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M272,48a16,16 0,0 0,-32 0V160h32Z"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M256,428C203.65,428 144.61,416.39 98.07,397 81,389.81 66.38,378.18 54.43,369A4,4 0,0 0,48 372.18v12.58c0,28.07 23.49,53.22 66.14,70.82C152.29,471.33 202.67,480 256,480s103.7,-8.67 141.86,-24.42C440.51,438 464,412.83 464,384.76V372.18a4,4 0,0 0,-6.43 -3.18C445.62,378.17 431,389.81 413.92,397 367.38,416.39 308.35,428 256,428Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M464,126.51c-0.81,-27.65 -24.18,-52.4 -66,-69.85C359.74,40.76 309.34,32 256,32S152.26,40.76 114.09,56.66c-41.78,17.41 -65.15,42.11 -66,69.69L48,144c0,6.41 5.2,16.48 14.63,24.73 11.13,9.73 27.65,19.33 47.78,27.73C153.24,214.36 207.67,225 256,225s102.76,-10.68 145.59,-28.58c20.13,-8.4 36.65,-18 47.78,-27.73C458.8,160.49 464,150.42 464,144Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M413.92,226C367.39,245.43 308.35,257 256,257S144.61,245.43 98.07,226C81,218.85 66.38,207.21 54.43,198A4,4 0,0 0,48 201.22V232c0,6.41 5.2,14.48 14.63,22.73 11.13,9.74 27.65,19.33 47.78,27.74C153.24,300.34 207.67,311 256,311s102.76,-10.68 145.59,-28.57c20.13,-8.41 36.65,-18 47.78,-27.74C458.8,246.47 464,238.41 464,232V201.22a4,4 0,0 0,-6.43 -3.18C445.62,207.21 431,218.85 413.92,226Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M413.92,312C367.38,331.41 308.35,343 256,343S144.61,331.41 98.07,312C81,304.83 66.38,293.19 54.43,284A4,4 0,0 0,48 287.2V317c0,6.41 5.2,14.47 14.62,22.71 11.13,9.74 27.66,19.33 47.79,27.74C153.24,385.32 207.66,396 256,396s102.76,-10.68 145.59,-28.57c20.13,-8.41 36.65,-18 47.78,-27.74C458.8,331.44 464,323.37 464,317V287.2a4,4 0,0 0,-6.43 -3.18C445.62,293.19 431,304.83 413.92,312Z"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M376,176H272V321a16,16 0,0 1,-32 0V176H136a56.06,56.06 0,0 0,-56 56V424a56.06,56.06 0,0 0,56 56H376a56.06,56.06 0,0 0,56 -56V232A56.06,56.06 0,0 0,376 176Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M272,86.63l52.69,52.68a16,16 0,0 0,22.62 -22.62l-80,-80a16,16 0,0 0,-22.62 0l-80,80a16,16 0,0 0,22.62 22.62L240,86.63V176h32Z"/>
</vector>